320 lines
11 KiB
Go
320 lines
11 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
userv1 "hyapp.local/api/proto/user/v1"
|
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
|
"hyapp/pkg/appcode"
|
|
"hyapp/pkg/logx"
|
|
"hyapp/services/gateway-service/internal/auth"
|
|
)
|
|
|
|
const managerResourceGrantCapability = "manager_resource_grant"
|
|
|
|
type managerGrantResourceData struct {
|
|
ResourceID string `json:"resource_id"`
|
|
ResourceType string `json:"resource_type"`
|
|
Name string `json:"name"`
|
|
AssetURL string `json:"asset_url"`
|
|
PreviewURL string `json:"preview_url"`
|
|
WalletAssetType string `json:"wallet_asset_type"`
|
|
WalletAssetAmount int64 `json:"wallet_asset_amount"`
|
|
SortOrder int32 `json:"sort_order"`
|
|
}
|
|
|
|
type businessUserLookupData struct {
|
|
UserID string `json:"user_id"`
|
|
DisplayUserID string `json:"display_user_id"`
|
|
Username string `json:"username"`
|
|
Avatar string `json:"avatar"`
|
|
Status string `json:"status"`
|
|
RegionID int64 `json:"region_id,omitempty"`
|
|
RegionCode string `json:"region_code,omitempty"`
|
|
RegionName string `json:"region_name,omitempty"`
|
|
}
|
|
|
|
type managerResourceGrantData struct {
|
|
GrantID string `json:"grant_id"`
|
|
CommandID string `json:"command_id"`
|
|
TargetUserID string `json:"target_user_id"`
|
|
ResourceID string `json:"resource_id"`
|
|
Status string `json:"status"`
|
|
GrantSource string `json:"grant_source"`
|
|
OperatorUserID string `json:"operator_user_id"`
|
|
CreatedAtMS int64 `json:"created_at_ms"`
|
|
}
|
|
|
|
func (h *Handler) listManagerGrantResources(writer http.ResponseWriter, request *http.Request) {
|
|
if h.walletClient == nil || h.userHostClient == nil {
|
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
|
return
|
|
}
|
|
if !h.requireManagerResourceGrantCapability(writer, request) {
|
|
return
|
|
}
|
|
page, pageSize, ok := managerPage(request)
|
|
if !ok {
|
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
|
return
|
|
}
|
|
resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{
|
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
|
AppCode: appcode.FromContext(request.Context()),
|
|
ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
ActiveOnly: true,
|
|
ManagerGrantOnly: true,
|
|
})
|
|
if err != nil {
|
|
httpkit.WriteRPCError(writer, request, err)
|
|
return
|
|
}
|
|
items := make([]managerGrantResourceData, 0, len(resp.GetResources()))
|
|
for _, resource := range resp.GetResources() {
|
|
items = append(items, managerGrantResourceFromProto(resource))
|
|
}
|
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
|
}
|
|
|
|
func (h *Handler) lookupBusinessUsers(writer http.ResponseWriter, request *http.Request) {
|
|
if h.userProfileClient == nil {
|
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
|
return
|
|
}
|
|
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
|
if !ok {
|
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
|
return
|
|
}
|
|
if pageSize > 20 {
|
|
pageSize = 20
|
|
}
|
|
resp, err := h.userProfileClient.BusinessUserLookup(request.Context(), &userv1.BusinessUserLookupRequest{
|
|
Meta: authRequestMeta(request, ""),
|
|
ActorUserId: auth.UserIDFromContext(request.Context()),
|
|
Scene: strings.TrimSpace(request.URL.Query().Get("scene")),
|
|
Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")),
|
|
PageSize: pageSize,
|
|
})
|
|
if err != nil {
|
|
httpkit.WriteRPCError(writer, request, err)
|
|
return
|
|
}
|
|
items := make([]businessUserLookupData, 0, len(resp.GetUsers()))
|
|
for _, user := range resp.GetUsers() {
|
|
items = append(items, businessUserLookupFromProto(user))
|
|
}
|
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items), "page_size": pageSize})
|
|
}
|
|
|
|
func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http.Request) {
|
|
if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil {
|
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
|
return
|
|
}
|
|
var body struct {
|
|
CommandID string `json:"command_id"`
|
|
CommandIDAlt string `json:"commandId"`
|
|
TargetUserID json.RawMessage `json:"target_user_id"`
|
|
TargetUserIDAlt json.RawMessage `json:"targetUserId"`
|
|
ResourceID json.RawMessage `json:"resource_id"`
|
|
ResourceIDAlt json.RawMessage `json:"resourceId"`
|
|
Reason string `json:"reason"`
|
|
Quantity *int64 `json:"quantity"`
|
|
DurationMS *int64 `json:"duration_ms"`
|
|
DurationMSAlt *int64 `json:"durationMs"`
|
|
}
|
|
if !decode(writer, request, &body) {
|
|
return
|
|
}
|
|
if body.Quantity != nil || body.DurationMS != nil || body.DurationMSAlt != nil {
|
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
|
return
|
|
}
|
|
if !h.requireManagerResourceGrantCapability(writer, request) {
|
|
return
|
|
}
|
|
commandID := strings.TrimSpace(body.CommandID)
|
|
if commandID == "" {
|
|
commandID = strings.TrimSpace(body.CommandIDAlt)
|
|
}
|
|
targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
|
|
resourceID, resourceOK := rawInt64(body.ResourceID, body.ResourceIDAlt)
|
|
if commandID == "" || !targetOK || targetUserID <= 0 || !resourceOK || resourceID <= 0 {
|
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
|
return
|
|
}
|
|
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
|
Meta: authRequestMeta(request, ""),
|
|
UserId: targetUserID,
|
|
})
|
|
if err != nil {
|
|
httpkit.WriteRPCError(writer, request, err)
|
|
return
|
|
}
|
|
if target := targetResp.GetUser(); target == nil || target.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE {
|
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
|
|
return
|
|
}
|
|
reason := strings.TrimSpace(body.Reason)
|
|
if reason == "" {
|
|
reason = "manager_center"
|
|
}
|
|
actorUserID := auth.UserIDFromContext(request.Context())
|
|
resp, err := h.walletClient.GrantResource(request.Context(), &walletv1.GrantResourceRequest{
|
|
CommandId: commandID,
|
|
AppCode: appcode.FromContext(request.Context()),
|
|
TargetUserId: targetUserID,
|
|
ResourceId: resourceID,
|
|
Quantity: 1,
|
|
DurationMs: 0,
|
|
Reason: reason,
|
|
OperatorUserId: actorUserID,
|
|
GrantSource: "manager_center",
|
|
})
|
|
if err != nil {
|
|
logx.Warn(request.Context(), "manager_resource_grant_failed",
|
|
slog.Int64("actor_user_id", actorUserID),
|
|
slog.Int64("target_user_id", targetUserID),
|
|
slog.Int64("resource_id", resourceID),
|
|
slog.String("command_id", commandID),
|
|
)
|
|
httpkit.WriteRPCError(writer, request, err)
|
|
return
|
|
}
|
|
grant := managerResourceGrantFromProto(resp.GetGrant())
|
|
logx.Info(request.Context(), "manager_resource_grant_succeeded",
|
|
slog.Int64("actor_user_id", actorUserID),
|
|
slog.Int64("target_user_id", targetUserID),
|
|
slog.Int64("resource_id", resourceID),
|
|
slog.String("grant_id", grant.GrantID),
|
|
)
|
|
httpkit.WriteOK(writer, request, grant)
|
|
}
|
|
|
|
func (h *Handler) requireManagerResourceGrantCapability(writer http.ResponseWriter, request *http.Request) bool {
|
|
resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
|
Meta: authRequestMeta(request, ""),
|
|
ActorUserId: auth.UserIDFromContext(request.Context()),
|
|
Capability: managerResourceGrantCapability,
|
|
})
|
|
if err != nil {
|
|
httpkit.WriteRPCError(writer, request, err)
|
|
return false
|
|
}
|
|
if !resp.GetAllowed() {
|
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func managerGrantResourceFromProto(resource *walletv1.Resource) managerGrantResourceData {
|
|
if resource == nil {
|
|
return managerGrantResourceData{}
|
|
}
|
|
return managerGrantResourceData{
|
|
ResourceID: userIDString(resource.GetResourceId()),
|
|
ResourceType: resource.GetResourceType(),
|
|
Name: resource.GetName(),
|
|
AssetURL: resource.GetAssetUrl(),
|
|
PreviewURL: resource.GetPreviewUrl(),
|
|
WalletAssetType: resource.GetWalletAssetType(),
|
|
WalletAssetAmount: resource.GetWalletAssetAmount(),
|
|
SortOrder: resource.GetSortOrder(),
|
|
}
|
|
}
|
|
|
|
func businessUserLookupFromProto(user *userv1.BusinessUserLookupItem) businessUserLookupData {
|
|
if user == nil {
|
|
return businessUserLookupData{}
|
|
}
|
|
return businessUserLookupData{
|
|
UserID: userIDString(user.GetUserId()),
|
|
DisplayUserID: user.GetDisplayUserId(),
|
|
Username: user.GetUsername(),
|
|
Avatar: user.GetAvatar(),
|
|
Status: userStatusString(user.GetStatus()),
|
|
RegionID: user.GetRegionId(),
|
|
RegionCode: user.GetRegionCode(),
|
|
RegionName: user.GetRegionName(),
|
|
}
|
|
}
|
|
|
|
func managerResourceGrantFromProto(grant *walletv1.ResourceGrant) managerResourceGrantData {
|
|
if grant == nil {
|
|
return managerResourceGrantData{}
|
|
}
|
|
resourceID := grant.GetGrantSubjectId()
|
|
if len(grant.GetItems()) > 0 && grant.GetItems()[0].GetResourceId() > 0 {
|
|
resourceID = userIDString(grant.GetItems()[0].GetResourceId())
|
|
}
|
|
return managerResourceGrantData{
|
|
GrantID: grant.GetGrantId(),
|
|
CommandID: grant.GetCommandId(),
|
|
TargetUserID: userIDString(grant.GetTargetUserId()),
|
|
ResourceID: resourceID,
|
|
Status: grant.GetStatus(),
|
|
GrantSource: grant.GetGrantSource(),
|
|
OperatorUserID: userIDString(grant.GetOperatorUserId()),
|
|
CreatedAtMS: grant.GetCreatedAtMs(),
|
|
}
|
|
}
|
|
|
|
func managerPage(request *http.Request) (int32, int32, bool) {
|
|
page, pageSize, ok := resourcePage(request)
|
|
if !ok {
|
|
return 0, 0, false
|
|
}
|
|
if pageSize > 20 {
|
|
pageSize = 20
|
|
}
|
|
return page, pageSize, true
|
|
}
|
|
|
|
func rawInt64(values ...json.RawMessage) (int64, bool) {
|
|
for _, raw := range values {
|
|
value, ok := parseRawInt64(raw)
|
|
if ok {
|
|
return value, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func parseRawInt64(raw json.RawMessage) (int64, bool) {
|
|
text := strings.TrimSpace(string(raw))
|
|
if text == "" || text == "null" {
|
|
return 0, false
|
|
}
|
|
if strings.HasPrefix(text, `"`) {
|
|
var value string
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
return 0, false
|
|
}
|
|
text = strings.TrimSpace(value)
|
|
}
|
|
value, err := strconv.ParseInt(text, 10, 64)
|
|
return value, err == nil
|
|
}
|
|
|
|
func userStatusString(status userv1.UserStatus) string {
|
|
switch status {
|
|
case userv1.UserStatus_USER_STATUS_ACTIVE:
|
|
return "active"
|
|
case userv1.UserStatus_USER_STATUS_DISABLED:
|
|
return "disabled"
|
|
case userv1.UserStatus_USER_STATUS_BANNED:
|
|
return "banned"
|
|
default:
|
|
return "unspecified"
|
|
}
|
|
}
|