2026-05-12 09:53:20 +08:00

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