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" } }