1176 lines
38 KiB
Go
1176 lines
38 KiB
Go
package resource
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"fmt"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/integration/walletclient"
|
||
"hyapp-admin-server/internal/middleware"
|
||
"hyapp-admin-server/internal/modules/shared"
|
||
"hyapp-admin-server/internal/repository"
|
||
"hyapp-admin-server/internal/response"
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type Handler struct {
|
||
wallet walletclient.Client
|
||
store *repository.Store
|
||
userDB *sql.DB
|
||
requestTimeout time.Duration
|
||
audit shared.OperationLogger
|
||
}
|
||
|
||
func New(wallet walletclient.Client, store *repository.Store, userDB *sql.DB, requestTimeout time.Duration, audit shared.OperationLogger) *Handler {
|
||
if requestTimeout <= 0 {
|
||
requestTimeout = 3 * time.Second
|
||
}
|
||
return &Handler{wallet: wallet, store: store, userDB: userDB, requestTimeout: requestTimeout, audit: audit}
|
||
}
|
||
|
||
func (h *Handler) walletRequestContext(c *gin.Context) (context.Context, context.CancelFunc) {
|
||
// 资源后台在广州 admin-server 跨区访问 Saudi wallet-service;每次 RPC 必须继承 HTTP 取消信号并设置硬上限,避免坏连接拖到前置 504。
|
||
return context.WithTimeout(c.Request.Context(), h.requestTimeout)
|
||
}
|
||
|
||
func (h *Handler) ListResources(c *gin.Context) {
|
||
options := shared.ListOptions(c)
|
||
ctx, cancel := h.walletRequestContext(c)
|
||
defer cancel()
|
||
resp, err := h.wallet.ListResources(ctx, &walletv1.ListResourcesRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceType: strings.TrimSpace(c.Query("resource_type")),
|
||
Status: options.Status,
|
||
Keyword: options.Keyword,
|
||
Page: int32(options.Page),
|
||
PageSize: int32(options.PageSize),
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "获取资源列表失败")
|
||
return
|
||
}
|
||
items := make([]resourceDTO, 0, len(resp.GetResources()))
|
||
for _, item := range resp.GetResources() {
|
||
items = append(items, resourceFromProto(item))
|
||
}
|
||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||
}
|
||
|
||
func (h *Handler) GetResource(c *gin.Context) {
|
||
resourceID, ok := parseID(c, "resource_id")
|
||
if !ok {
|
||
return
|
||
}
|
||
resp, err := h.wallet.GetResource(c.Request.Context(), &walletv1.GetResourceRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceId: resourceID,
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
response.OK(c, resourceFromProto(resp.GetResource()))
|
||
}
|
||
|
||
func (h *Handler) CreateResource(c *gin.Context) {
|
||
var req resourceRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "资源参数不正确")
|
||
return
|
||
}
|
||
if err := validateResourcePricing(req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
if err := validateResourceBadgeForm(req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
if err := validateResourceAvatarFrameKind(req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
if err := validateResourceMetadata(req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
resp, err := h.wallet.CreateResource(c.Request.Context(), req.createProto(c))
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
resource := resourceFromProto(resp.GetResource())
|
||
h.auditLog(c, "create-resource", "resources", fmt.Sprintf("%d", resource.ResourceID), "success", resource.ResourceCode)
|
||
response.Created(c, resource)
|
||
}
|
||
|
||
func (h *Handler) ListEmojiPacks(c *gin.Context) {
|
||
options := shared.ListOptions(c)
|
||
ctx, cancel := h.walletRequestContext(c)
|
||
defer cancel()
|
||
resp, err := h.wallet.ListResources(ctx, &walletv1.ListResourcesRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceType: resourceTypeEmojiPack,
|
||
Status: options.Status,
|
||
Keyword: options.Keyword,
|
||
Page: int32(options.Page),
|
||
PageSize: int32(options.PageSize),
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "获取表情包列表失败")
|
||
return
|
||
}
|
||
items := make([]emojiPackDTO, 0, len(resp.GetResources()))
|
||
for _, item := range resp.GetResources() {
|
||
items = append(items, emojiPackFromProto(item))
|
||
}
|
||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||
}
|
||
|
||
func (h *Handler) ListEmojiPackCategories(c *gin.Context) {
|
||
const pageSize int32 = 100
|
||
categories := map[string]struct{}{}
|
||
ctx, cancel := h.walletRequestContext(c)
|
||
defer cancel()
|
||
for page := int32(1); ; page++ {
|
||
resp, err := h.wallet.ListResources(ctx, &walletv1.ListResourcesRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceType: resourceTypeEmojiPack,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "获取表情包分类失败")
|
||
return
|
||
}
|
||
for _, item := range resp.GetResources() {
|
||
category := emojiPackMetadataFromJSON(item.GetMetadataJson()).Category
|
||
if category != "" {
|
||
categories[category] = struct{}{}
|
||
}
|
||
}
|
||
if len(resp.GetResources()) == 0 || int64(page)*int64(pageSize) >= resp.GetTotal() {
|
||
break
|
||
}
|
||
}
|
||
items := make([]string, 0, len(categories))
|
||
for category := range categories {
|
||
items = append(items, category)
|
||
}
|
||
sort.Strings(items)
|
||
response.OK(c, items)
|
||
}
|
||
|
||
func (h *Handler) CreateEmojiPack(c *gin.Context) {
|
||
var req emojiPackRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "表情包参数不正确")
|
||
return
|
||
}
|
||
protoReq, err := req.createProto(c)
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
resp, err := h.wallet.CreateResource(c.Request.Context(), protoReq)
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
item := emojiPackFromProto(resp.GetResource())
|
||
h.auditLog(c, "create-emoji-pack", "resources", fmt.Sprintf("%d", item.ResourceID), "success", item.ResourceCode)
|
||
response.Created(c, item)
|
||
}
|
||
|
||
func (h *Handler) UpdateResource(c *gin.Context) {
|
||
resourceID, ok := parseID(c, "resource_id")
|
||
if !ok {
|
||
return
|
||
}
|
||
var req resourceRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "资源参数不正确")
|
||
return
|
||
}
|
||
if err := validateResourcePricing(req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
if err := validateResourceBadgeForm(req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
if err := validateResourceAvatarFrameKind(req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
if err := validateResourceMetadata(req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
protoReq := req.updateProto(c, resourceID)
|
||
resp, err := h.wallet.UpdateResource(c.Request.Context(), protoReq)
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
resource := resourceFromProto(resp.GetResource())
|
||
h.auditLog(c, "update-resource", "resources", fmt.Sprintf("%d", resource.ResourceID), "success", resource.ResourceCode)
|
||
response.OK(c, resource)
|
||
}
|
||
|
||
func (h *Handler) DeleteResource(c *gin.Context) {
|
||
resourceID, ok := parseID(c, "resource_id")
|
||
if !ok {
|
||
return
|
||
}
|
||
resp, err := h.wallet.DeleteResource(c.Request.Context(), &walletv1.DeleteResourceRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceId: resourceID,
|
||
OperatorUserId: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
resource := resourceFromProto(resp.GetResource())
|
||
h.auditLog(c, "delete-resource", "resources", fmt.Sprintf("%d", resource.ResourceID), "success", resource.ResourceCode)
|
||
response.OK(c, resource)
|
||
}
|
||
|
||
func (h *Handler) BatchDeleteResources(c *gin.Context) {
|
||
var req resourceDeleteBatchRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil || len(req.ResourceIDs) == 0 {
|
||
response.BadRequest(c, "资源参数不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.BatchDeleteResources(c.Request.Context(), &walletv1.BatchDeleteResourcesRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceIds: req.ResourceIDs,
|
||
OperatorUserId: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
items := make([]resourceDTO, 0, len(resp.GetResources()))
|
||
deletedIDs := make([]string, 0, len(resp.GetResources()))
|
||
for _, item := range resp.GetResources() {
|
||
resource := resourceFromProto(item)
|
||
items = append(items, resource)
|
||
deletedIDs = append(deletedIDs, fmt.Sprintf("%d", resource.ResourceID))
|
||
}
|
||
h.auditLog(c, "delete-resources", "resources", "batch", "success", strings.Join(deletedIDs, ","))
|
||
response.OK(c, items)
|
||
}
|
||
|
||
func (h *Handler) UpdateMP4ResourceLayouts(c *gin.Context) {
|
||
var req resourceMP4LayoutBatchRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "MP4 布局参数不正确")
|
||
return
|
||
}
|
||
items, err := normalizeMP4LayoutBatchItems(req.Items)
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
ctx, cancel := h.walletRequestContext(c)
|
||
defer cancel()
|
||
type preparedMP4LayoutUpdate struct {
|
||
resource *walletv1.Resource
|
||
metadataJSON string
|
||
}
|
||
prepared := make([]preparedMP4LayoutUpdate, 0, len(items))
|
||
for _, item := range items {
|
||
resp, err := h.wallet.GetResource(ctx, &walletv1.GetResourceRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceId: item.ResourceID,
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
current := resp.GetResource()
|
||
if current.GetResourceId() == 0 {
|
||
response.BadRequest(c, "资源不存在")
|
||
return
|
||
}
|
||
// 批量接口只更新真实 MP4 动效资源;非 MP4 行按产品要求自动跳过,
|
||
// 这样前端全选资源后提交到后端,也不会把图片、SVGA、PAG 的 metadata 写坏。
|
||
if !resourceAnimationSourceIsMP4(current) {
|
||
continue
|
||
}
|
||
metadataJSON, err := mp4LayoutMetadataForResource(current, item.MetadataJSON)
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
prepared = append(prepared, preparedMP4LayoutUpdate{resource: current, metadataJSON: metadataJSON})
|
||
}
|
||
|
||
updated := make([]resourceDTO, 0, len(prepared))
|
||
updatedIDs := make([]string, 0, len(prepared))
|
||
for _, item := range prepared {
|
||
// wallet-service 只有全量资源更新 RPC,所以这里用当前资源还原所有业务字段,
|
||
// 只替换 metadata_json,避免批量重算 MP4 时顺手覆盖价格、状态、资产或经理赠送配置。
|
||
resp, err := h.wallet.UpdateResource(ctx, updateResourceMP4MetadataProto(c, item.resource, item.metadataJSON))
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
resource := resourceFromProto(resp.GetResource())
|
||
updated = append(updated, resource)
|
||
updatedIDs = append(updatedIDs, fmt.Sprintf("%d", resource.ResourceID))
|
||
}
|
||
h.auditLog(c, "update-resource-mp4-layouts", "resources", "batch", "success", strings.Join(updatedIDs, ","))
|
||
response.OK(c, updated)
|
||
}
|
||
|
||
func (h *Handler) EnableResource(c *gin.Context) {
|
||
h.setResourceStatus(c, "active")
|
||
}
|
||
|
||
func (h *Handler) DisableResource(c *gin.Context) {
|
||
h.setResourceStatus(c, "disabled")
|
||
}
|
||
|
||
func (h *Handler) ListResourceGroups(c *gin.Context) {
|
||
options := shared.ListOptions(c)
|
||
ctx, cancel := h.walletRequestContext(c)
|
||
defer cancel()
|
||
resp, err := h.wallet.ListResourceGroups(ctx, &walletv1.ListResourceGroupsRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
Status: options.Status,
|
||
Keyword: options.Keyword,
|
||
Page: int32(options.Page),
|
||
PageSize: int32(options.PageSize),
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "获取资源组列表失败")
|
||
return
|
||
}
|
||
items := make([]resourceGroupDTO, 0, len(resp.GetGroups()))
|
||
for _, item := range resp.GetGroups() {
|
||
items = append(items, resourceGroupFromProto(item))
|
||
}
|
||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||
}
|
||
|
||
func (h *Handler) GetResourceGroup(c *gin.Context) {
|
||
groupID, ok := parseID(c, "group_id")
|
||
if !ok {
|
||
return
|
||
}
|
||
resp, err := h.wallet.GetResourceGroup(c.Request.Context(), &walletv1.GetResourceGroupRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
GroupId: groupID,
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
response.OK(c, resourceGroupFromProto(resp.GetGroup()))
|
||
}
|
||
|
||
func (h *Handler) CreateResourceGroup(c *gin.Context) {
|
||
var req resourceGroupRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "资源组参数不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.CreateResourceGroup(c.Request.Context(), req.createProto(c))
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
group := resourceGroupFromProto(resp.GetGroup())
|
||
h.auditLog(c, "create-resource-group", "resource_groups", fmt.Sprintf("%d", group.GroupID), "success", group.GroupCode)
|
||
response.Created(c, group)
|
||
}
|
||
|
||
func (h *Handler) UpdateResourceGroup(c *gin.Context) {
|
||
groupID, ok := parseID(c, "group_id")
|
||
if !ok {
|
||
return
|
||
}
|
||
var req resourceGroupRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "资源组参数不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.UpdateResourceGroup(c.Request.Context(), req.updateProto(c, groupID))
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
group := resourceGroupFromProto(resp.GetGroup())
|
||
h.auditLog(c, "update-resource-group", "resource_groups", fmt.Sprintf("%d", group.GroupID), "success", group.GroupCode)
|
||
response.OK(c, group)
|
||
}
|
||
|
||
func (h *Handler) EnableResourceGroup(c *gin.Context) {
|
||
h.setResourceGroupStatus(c, "active")
|
||
}
|
||
|
||
func (h *Handler) DisableResourceGroup(c *gin.Context) {
|
||
h.setResourceGroupStatus(c, "disabled")
|
||
}
|
||
|
||
func (h *Handler) ListGifts(c *gin.Context) {
|
||
options := shared.ListOptions(c)
|
||
regionID, filterRegion, ok := optionalInt64QueryWithPresence(c, "regionId", "region_id")
|
||
if !ok {
|
||
return
|
||
}
|
||
resp, err := h.wallet.ListGiftConfigs(c.Request.Context(), &walletv1.ListGiftConfigsRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
Status: options.Status,
|
||
Keyword: options.Keyword,
|
||
Page: int32(options.Page),
|
||
PageSize: int32(options.PageSize),
|
||
RegionId: regionID,
|
||
FilterRegion: filterRegion,
|
||
GiftTypeCode: giftTypeCodeQuery(c),
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "获取礼物列表失败")
|
||
return
|
||
}
|
||
items := make([]giftDTO, 0, len(resp.GetGifts()))
|
||
for _, item := range resp.GetGifts() {
|
||
items = append(items, giftFromProto(item))
|
||
}
|
||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||
}
|
||
|
||
func (h *Handler) ListGiftTypes(c *gin.Context) {
|
||
resp, err := h.wallet.ListGiftTypeConfigs(c.Request.Context(), &walletv1.ListGiftTypeConfigsRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "获取礼物类型失败")
|
||
return
|
||
}
|
||
items := make([]giftTypeDTO, 0, len(resp.GetGiftTypes()))
|
||
for _, item := range resp.GetGiftTypes() {
|
||
items = append(items, giftTypeFromProto(item))
|
||
}
|
||
response.OK(c, items)
|
||
}
|
||
|
||
func (h *Handler) UpdateGiftType(c *gin.Context) {
|
||
typeCode := strings.TrimSpace(c.Param("type_code"))
|
||
if typeCode == "" {
|
||
response.BadRequest(c, "类型编码不正确")
|
||
return
|
||
}
|
||
var req giftTypeRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "礼物类型参数不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.UpsertGiftTypeConfig(c.Request.Context(), req.upsertProto(c, typeCode))
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
item := giftTypeFromProto(resp.GetGiftType())
|
||
h.auditLog(c, "update-gift-type", "gift_type_configs", item.TabKey, "success", item.TabName)
|
||
response.OK(c, item)
|
||
}
|
||
|
||
func (h *Handler) UpdateGiftTypes(c *gin.Context) {
|
||
var req giftTypesRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "礼物类型参数不正确")
|
||
return
|
||
}
|
||
if len(req.Items) == 0 {
|
||
response.BadRequest(c, "礼物类型不能为空")
|
||
return
|
||
}
|
||
items := make([]giftTypeDTO, 0, len(req.Items))
|
||
tabKeys := make([]string, 0, len(req.Items))
|
||
for _, requestItem := range req.Items {
|
||
tabKey := strings.TrimSpace(requestItem.TabKey)
|
||
if tabKey == "" {
|
||
response.BadRequest(c, "类型编码不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.UpsertGiftTypeConfig(c.Request.Context(), requestItem.upsertProto(c))
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
item := giftTypeFromProto(resp.GetGiftType())
|
||
items = append(items, item)
|
||
tabKeys = append(tabKeys, item.TabKey)
|
||
}
|
||
h.auditLog(c, "update-gift-types", "gift_type_configs", "batch", "success", strings.Join(tabKeys, ","))
|
||
response.OK(c, items)
|
||
}
|
||
|
||
func (h *Handler) CreateGift(c *gin.Context) {
|
||
var req giftRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "礼物参数不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.CreateGiftConfig(c.Request.Context(), req.createProto(c))
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
gift := giftFromProto(resp.GetGift())
|
||
h.auditLog(c, "create-gift", "gift_configs", gift.GiftID, "success", gift.Name)
|
||
response.Created(c, gift)
|
||
}
|
||
|
||
func (h *Handler) BatchCreateGifts(c *gin.Context) {
|
||
var req giftBatchRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil || len(req.Items) == 0 {
|
||
response.BadRequest(c, "礼物参数不正确")
|
||
return
|
||
}
|
||
items := make([]*walletv1.CreateGiftConfigRequest, 0, len(req.Items))
|
||
for _, item := range req.Items {
|
||
items = append(items, item.createProto(c))
|
||
}
|
||
resp, err := h.wallet.BatchCreateGiftConfigs(c.Request.Context(), &walletv1.BatchCreateGiftConfigsRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
Items: items,
|
||
OperatorUserId: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
gifts := make([]giftDTO, 0, len(resp.GetGifts()))
|
||
giftIDs := make([]string, 0, len(resp.GetGifts()))
|
||
for _, item := range resp.GetGifts() {
|
||
gift := giftFromProto(item)
|
||
gifts = append(gifts, gift)
|
||
giftIDs = append(giftIDs, gift.GiftID)
|
||
}
|
||
h.auditLog(c, "create-gifts", "gift_configs", "batch", "success", strings.Join(giftIDs, ","))
|
||
response.Created(c, gifts)
|
||
}
|
||
|
||
func (h *Handler) UpdateGift(c *gin.Context) {
|
||
giftID := strings.TrimSpace(c.Param("gift_id"))
|
||
if giftID == "" {
|
||
response.BadRequest(c, "ID 参数不正确")
|
||
return
|
||
}
|
||
var req giftRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "礼物参数不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.UpdateGiftConfig(c.Request.Context(), req.updateProto(c, giftID))
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
gift := giftFromProto(resp.GetGift())
|
||
h.auditLog(c, "update-gift", "gift_configs", gift.GiftID, "success", gift.Name)
|
||
response.OK(c, gift)
|
||
}
|
||
|
||
func (h *Handler) DeleteGift(c *gin.Context) {
|
||
giftID := strings.TrimSpace(c.Param("gift_id"))
|
||
if giftID == "" {
|
||
response.BadRequest(c, "ID 参数不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.DeleteGiftConfig(c.Request.Context(), &walletv1.DeleteGiftConfigRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
GiftId: giftID,
|
||
OperatorUserId: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
gift := giftFromProto(resp.GetGift())
|
||
h.auditLog(c, "delete-gift", "gift_configs", gift.GiftID, "success", gift.Name)
|
||
response.OK(c, gift)
|
||
}
|
||
|
||
func (h *Handler) EnableGift(c *gin.Context) {
|
||
h.setGiftStatus(c, "active")
|
||
}
|
||
|
||
func (h *Handler) DisableGift(c *gin.Context) {
|
||
h.setGiftStatus(c, "disabled")
|
||
}
|
||
|
||
func (h *Handler) GrantResource(c *gin.Context) {
|
||
var req grantResourceRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "资源赠送参数不正确")
|
||
return
|
||
}
|
||
targetUserID, err := parseGrantTargetUserID(req.TargetUserID)
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
resp, err := h.wallet.GrantResource(c.Request.Context(), &walletv1.GrantResourceRequest{
|
||
CommandId: strings.TrimSpace(req.CommandID),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
TargetUserId: targetUserID,
|
||
ResourceId: req.ResourceID,
|
||
Quantity: req.Quantity,
|
||
DurationMs: req.DurationMS,
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
OperatorUserId: actorID(c),
|
||
GrantSource: "admin",
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
grant := grantFromProto(resp.GetGrant())
|
||
h.auditLog(c, "grant-resource", "resource_grants", grant.GrantID, "success", fmt.Sprintf("target_user_id=%d", grant.TargetUserID))
|
||
response.Created(c, grant)
|
||
}
|
||
|
||
func (h *Handler) GrantResourceGroup(c *gin.Context) {
|
||
var req grantGroupRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "资源组赠送参数不正确")
|
||
return
|
||
}
|
||
targetUserID, err := parseGrantTargetUserID(req.TargetUserID)
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
resp, err := h.wallet.GrantResourceGroup(c.Request.Context(), &walletv1.GrantResourceGroupRequest{
|
||
CommandId: strings.TrimSpace(req.CommandID),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
TargetUserId: targetUserID,
|
||
GroupId: req.GroupID,
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
OperatorUserId: actorID(c),
|
||
GrantSource: "admin",
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
grant := grantFromProto(resp.GetGrant())
|
||
h.auditLog(c, "grant-resource-group", "resource_grants", grant.GrantID, "success", fmt.Sprintf("target_user_id=%d", grant.TargetUserID))
|
||
response.Created(c, grant)
|
||
}
|
||
|
||
func (h *Handler) RevokeResourceGrant(c *gin.Context) {
|
||
grantID := strings.TrimSpace(c.Param("grant_id"))
|
||
if grantID == "" {
|
||
response.BadRequest(c, "资源赠送记录不存在")
|
||
return
|
||
}
|
||
resp, err := h.wallet.RevokeResourceGrant(c.Request.Context(), &walletv1.RevokeResourceGrantRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
GrantId: grantID,
|
||
Reason: "admin revoke resource grant",
|
||
OperatorUserId: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
grant := grantFromProto(resp.GetGrant())
|
||
h.auditLog(c, "revoke-resource-grant", "resource_grants", grant.GrantID, "success", fmt.Sprintf("target_user_id=%d", grant.TargetUserID))
|
||
response.OK(c, grant)
|
||
}
|
||
|
||
func (h *Handler) LookupResourceGrantTarget(c *gin.Context) {
|
||
keyword := strings.TrimSpace(firstQuery(c, "user_id", "userId", "keyword"))
|
||
if keyword == "" {
|
||
response.BadRequest(c, "请输入用户 ID 或短号")
|
||
return
|
||
}
|
||
if h == nil || h.userDB == nil {
|
||
response.ServerError(c, "用户数据库未配置")
|
||
return
|
||
}
|
||
// 资源赠送页允许输入短号,但 wallet 只认内部 user_id;这里在 admin 侧先解析,
|
||
// 避免前端把短号直接发给 wallet,也避免 JS number 对长 user_id 造成精度损失。
|
||
nowMs := time.Now().UnixMilli()
|
||
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
|
||
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
|
||
args := append([]any{appctx.FromContext(c.Request.Context())}, matchArgs...)
|
||
args = append(args, orderArgs...)
|
||
row := h.userDB.QueryRowContext(c.Request.Context(), `
|
||
SELECT u.user_id, u.current_display_user_id, COALESCE(u.username, ''), COALESCE(u.avatar, '')
|
||
FROM users u
|
||
WHERE u.app_code = ? AND `+matchSQL+`
|
||
ORDER BY
|
||
`+orderSQL+`,
|
||
u.user_id DESC
|
||
LIMIT 1`,
|
||
args...,
|
||
)
|
||
user := grantUserDTO{}
|
||
if err := row.Scan(&user.UserID, &user.DisplayUserID, &user.Username, &user.Avatar); err != nil {
|
||
if err == sql.ErrNoRows {
|
||
response.BadRequest(c, "用户不存在")
|
||
return
|
||
}
|
||
response.ServerError(c, "查询用户失败")
|
||
return
|
||
}
|
||
response.OK(c, user)
|
||
}
|
||
|
||
func (h *Handler) ListResourceGrants(c *gin.Context) {
|
||
options := shared.ListOptions(c)
|
||
appCode := appctx.FromContext(c.Request.Context())
|
||
targetFilter := shared.UserIdentityFilterFromQuery(c, "target")
|
||
targetUserID, _, err := shared.ResolveExactUserIDOrNumericFallback(
|
||
c.Request.Context(),
|
||
h.userDB,
|
||
appCode,
|
||
firstQuery(c, "target_user_id", "targetUserId"),
|
||
time.Now().UnixMilli(),
|
||
)
|
||
if err != nil {
|
||
if errors.Is(err, shared.ErrInvalidUserIdentityFilter) {
|
||
response.BadRequest(c, "target_user_id 参数不正确")
|
||
return
|
||
}
|
||
response.ServerError(c, "查询赠送用户失败")
|
||
return
|
||
}
|
||
if targetUserID == 0 && !targetFilter.IsEmpty() {
|
||
var matched bool
|
||
targetUserID, matched, err = shared.ResolveUserIdentityFilter(c.Request.Context(), h.userDB, appCode, targetFilter, time.Now().UnixMilli())
|
||
if err != nil {
|
||
response.ServerError(c, "查询赠送用户失败")
|
||
return
|
||
}
|
||
if !matched {
|
||
response.OK(c, response.Page{Items: []grantDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
|
||
return
|
||
}
|
||
}
|
||
resp, err := h.wallet.ListResourceGrants(c.Request.Context(), &walletv1.ListResourceGrantsRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appCode,
|
||
TargetUserId: targetUserID,
|
||
Status: options.Status,
|
||
Page: int32(options.Page),
|
||
PageSize: int32(options.PageSize),
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "获取资源赠送记录失败")
|
||
return
|
||
}
|
||
items := make([]grantDTO, 0, len(resp.GetGrants()))
|
||
for _, item := range resp.GetGrants() {
|
||
items = append(items, grantFromProto(item))
|
||
}
|
||
if err := h.enrichGrantOperators(c.Request.Context(), items); err != nil {
|
||
response.ServerError(c, "获取资源赠送操作人失败")
|
||
return
|
||
}
|
||
if err := h.enrichGrantTargets(c.Request.Context(), items); err != nil {
|
||
response.ServerError(c, "获取资源赠送用户信息失败")
|
||
return
|
||
}
|
||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||
}
|
||
|
||
func (h *Handler) ListResourceShopItems(c *gin.Context) {
|
||
options := shared.ListOptions(c)
|
||
resp, err := h.wallet.ListResourceShopItems(c.Request.Context(), &walletv1.ListResourceShopItemsRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceType: strings.TrimSpace(c.Query("resource_type")),
|
||
Status: options.Status,
|
||
Keyword: options.Keyword,
|
||
Page: int32(options.Page),
|
||
PageSize: int32(options.PageSize),
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "获取道具商店列表失败")
|
||
return
|
||
}
|
||
items := make([]resourceShopItemDTO, 0, len(resp.GetItems()))
|
||
for _, item := range resp.GetItems() {
|
||
items = append(items, resourceShopItemFromProto(item))
|
||
}
|
||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||
}
|
||
|
||
func (h *Handler) ListResourceShopPurchaseOrders(c *gin.Context) {
|
||
options := shared.ListOptions(c)
|
||
appCode := appctx.FromContext(c.Request.Context())
|
||
userID, ok := optionalInt64Query(c, "user_id", "userId")
|
||
if !ok {
|
||
return
|
||
}
|
||
userFilter := shared.UserIdentityFilterFromQuery(c, "")
|
||
userKeyword := strings.TrimSpace(firstQuery(c, "user_keyword", "userKeyword", "user"))
|
||
if userID == 0 && !userFilter.IsEmpty() {
|
||
resolvedUserID, matched, err := shared.ResolveUserIdentityFilter(c.Request.Context(), h.userDB, appCode, userFilter, time.Now().UnixMilli())
|
||
if err != nil {
|
||
response.ServerError(c, "查询购买用户失败")
|
||
return
|
||
}
|
||
if !matched {
|
||
response.OK(c, response.Page{Items: []resourceShopPurchaseOrderDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
|
||
return
|
||
}
|
||
userID = resolvedUserID
|
||
}
|
||
if userID == 0 && userKeyword != "" {
|
||
resolvedUserID, matched, err := h.resolveResourceShopPurchaseUserID(c.Request.Context(), appCode, userKeyword)
|
||
if err != nil {
|
||
response.ServerError(c, "查询购买用户失败")
|
||
return
|
||
}
|
||
if !matched {
|
||
response.OK(c, response.Page{Items: []resourceShopPurchaseOrderDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
|
||
return
|
||
}
|
||
userID = resolvedUserID
|
||
}
|
||
ctx, cancel := h.walletRequestContext(c)
|
||
defer cancel()
|
||
resp, err := h.wallet.ListResourceShopPurchaseOrders(ctx, &walletv1.ListResourceShopPurchaseOrdersRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appCode,
|
||
UserId: userID,
|
||
ResourceType: strings.TrimSpace(c.Query("resource_type")),
|
||
Status: options.Status,
|
||
Keyword: options.Keyword,
|
||
Page: int32(options.Page),
|
||
PageSize: int32(options.PageSize),
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "获取售卖记录失败")
|
||
return
|
||
}
|
||
items := make([]resourceShopPurchaseOrderDTO, 0, len(resp.GetOrders()))
|
||
for _, item := range resp.GetOrders() {
|
||
items = append(items, resourceShopPurchaseOrderFromProto(item))
|
||
}
|
||
if err := h.enrichResourceShopPurchaseUsers(c.Request.Context(), items); err != nil {
|
||
response.ServerError(c, "获取购买用户信息失败")
|
||
return
|
||
}
|
||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||
}
|
||
|
||
func (h *Handler) UpsertResourceShopItems(c *gin.Context) {
|
||
var req resourceShopItemsRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "道具商店参数不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.UpsertResourceShopItems(c.Request.Context(), req.upsertProto(c))
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
items := make([]resourceShopItemDTO, 0, len(resp.GetItems()))
|
||
resourceIDs := make([]string, 0, len(resp.GetItems()))
|
||
for _, item := range resp.GetItems() {
|
||
dto := resourceShopItemFromProto(item)
|
||
items = append(items, dto)
|
||
resourceIDs = append(resourceIDs, fmt.Sprintf("%d", dto.ResourceID))
|
||
}
|
||
h.auditLog(c, "upsert-resource-shop-items", "resource_shop_items", "batch", "success", strings.Join(resourceIDs, ","))
|
||
response.OK(c, items)
|
||
}
|
||
|
||
func (h *Handler) EnableResourceShopItem(c *gin.Context) {
|
||
h.setResourceShopItemStatus(c, "active")
|
||
}
|
||
|
||
func (h *Handler) DisableResourceShopItem(c *gin.Context) {
|
||
h.setResourceShopItemStatus(c, "disabled")
|
||
}
|
||
|
||
func (h *Handler) setResourceStatus(c *gin.Context, status string) {
|
||
resourceID, ok := parseID(c, "resource_id")
|
||
if !ok {
|
||
return
|
||
}
|
||
resp, err := h.wallet.SetResourceStatus(c.Request.Context(), &walletv1.SetResourceStatusRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceId: resourceID,
|
||
Status: status,
|
||
OperatorUserId: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
resource := resourceFromProto(resp.GetResource())
|
||
h.auditLog(c, "set-resource-status", "resources", fmt.Sprintf("%d", resource.ResourceID), "success", status)
|
||
response.OK(c, resource)
|
||
}
|
||
|
||
func (h *Handler) setResourceGroupStatus(c *gin.Context, status string) {
|
||
groupID, ok := parseID(c, "group_id")
|
||
if !ok {
|
||
return
|
||
}
|
||
resp, err := h.wallet.SetResourceGroupStatus(c.Request.Context(), &walletv1.SetResourceGroupStatusRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
GroupId: groupID,
|
||
Status: status,
|
||
OperatorUserId: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
group := resourceGroupFromProto(resp.GetGroup())
|
||
h.auditLog(c, "set-resource-group-status", "resource_groups", fmt.Sprintf("%d", group.GroupID), "success", status)
|
||
response.OK(c, group)
|
||
}
|
||
|
||
func (h *Handler) setGiftStatus(c *gin.Context, status string) {
|
||
giftID := strings.TrimSpace(c.Param("gift_id"))
|
||
if giftID == "" {
|
||
response.BadRequest(c, "ID 参数不正确")
|
||
return
|
||
}
|
||
resp, err := h.wallet.SetGiftConfigStatus(c.Request.Context(), &walletv1.SetGiftConfigStatusRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
GiftId: giftID,
|
||
Status: status,
|
||
OperatorUserId: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
gift := giftFromProto(resp.GetGift())
|
||
h.auditLog(c, "set-gift-status", "gift_configs", gift.GiftID, "success", status)
|
||
response.OK(c, gift)
|
||
}
|
||
|
||
func (h *Handler) setResourceShopItemStatus(c *gin.Context, status string) {
|
||
shopItemID, ok := parseID(c, "shop_item_id")
|
||
if !ok {
|
||
return
|
||
}
|
||
resp, err := h.wallet.SetResourceShopItemStatus(c.Request.Context(), &walletv1.SetResourceShopItemStatusRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ShopItemId: shopItemID,
|
||
Status: status,
|
||
OperatorUserId: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
item := resourceShopItemFromProto(resp.GetItem())
|
||
h.auditLog(c, "set-resource-shop-item-status", "resource_shop_items", fmt.Sprintf("%d", item.ShopItemID), "success", status)
|
||
response.OK(c, item)
|
||
}
|
||
|
||
func (h *Handler) auditLog(c *gin.Context, action string, resource string, resourceID string, status string, detail string) {
|
||
shared.OperationLogWithResourceID(c, h.audit, action, resource, resourceID, status, detail)
|
||
}
|
||
|
||
func actorID(c *gin.Context) int64 {
|
||
return int64(shared.ActorFromContext(c).UserID)
|
||
}
|
||
|
||
func normalizeMP4LayoutBatchItems(items []resourceMP4LayoutUpdateRequest) ([]resourceMP4LayoutUpdateRequest, error) {
|
||
const maxMP4LayoutBatchSize = 50
|
||
if len(items) == 0 {
|
||
return nil, fmt.Errorf("请选择要更新的 MP4 资源")
|
||
}
|
||
if len(items) > maxMP4LayoutBatchSize {
|
||
return nil, fmt.Errorf("单次最多更新 %d 个 MP4 资源", maxMP4LayoutBatchSize)
|
||
}
|
||
seen := map[int64]struct{}{}
|
||
normalized := make([]resourceMP4LayoutUpdateRequest, 0, len(items))
|
||
for _, item := range items {
|
||
if item.ResourceID <= 0 {
|
||
return nil, fmt.Errorf("资源 ID 不正确")
|
||
}
|
||
if _, ok := seen[item.ResourceID]; ok {
|
||
continue
|
||
}
|
||
seen[item.ResourceID] = struct{}{}
|
||
normalized = append(normalized, resourceMP4LayoutUpdateRequest{
|
||
ResourceID: item.ResourceID,
|
||
MetadataJSON: strings.TrimSpace(item.MetadataJSON),
|
||
})
|
||
}
|
||
if len(normalized) == 0 {
|
||
return nil, fmt.Errorf("请选择要更新的 MP4 资源")
|
||
}
|
||
return normalized, nil
|
||
}
|
||
|
||
func resourceAnimationSourceIsMP4(item *walletv1.Resource) bool {
|
||
if item == nil {
|
||
return false
|
||
}
|
||
return resourceSourceIsMP4(item.GetAnimationUrl()) || resourceSourceIsMP4(item.GetAssetUrl())
|
||
}
|
||
|
||
func resourceSourceIsMP4(value string) bool {
|
||
source := strings.TrimSpace(value)
|
||
if source == "" {
|
||
return false
|
||
}
|
||
if before, _, ok := strings.Cut(source, "?"); ok {
|
||
source = before
|
||
}
|
||
if before, _, ok := strings.Cut(source, "#"); ok {
|
||
source = before
|
||
}
|
||
return strings.HasSuffix(strings.ToLower(source), ".mp4")
|
||
}
|
||
|
||
func mp4LayoutMetadataForResource(current *walletv1.Resource, metadataJSON string) (string, error) {
|
||
incoming, fields, err := parseResourceMetadataPayload(metadataJSON)
|
||
if err != nil {
|
||
return "", fmt.Errorf("资源元数据格式不正确")
|
||
}
|
||
if incoming == nil {
|
||
return "", fmt.Errorf("MP4 透明布局参数不完整")
|
||
}
|
||
if _, ok := fields["mp4_alpha_layout"]; !ok {
|
||
return "", fmt.Errorf("MP4 透明布局参数不完整")
|
||
}
|
||
if err := validateResourceMetadata(resourceRequest{
|
||
ResourceType: current.GetResourceType(),
|
||
MetadataJSON: metadataJSON,
|
||
}); err != nil {
|
||
return "", err
|
||
}
|
||
layout, err := sanitizeMP4AlphaLayoutMetadata(incoming.MP4AlphaLayout)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
// 这个接口是“只更新 MP4 布局”的窄接口,所以其它 metadata 不信任前端请求;
|
||
// 资料卡布局从当前资源保留,MP4 布局使用本次重算结果,最终再走统一 marshal 过滤空字段。
|
||
merged := resourceMetadataPayload{
|
||
AnimationFormat: resourceAnimationFormatMP4,
|
||
MP4AlphaLayout: layout,
|
||
}
|
||
if current.GetResourceType() == resourceTypeProfileCard {
|
||
currentPayload, _, _ := parseResourceMetadataPayload(current.GetMetadataJson())
|
||
if currentPayload != nil {
|
||
merged.ProfileCardLayout = sanitizeProfileCardLayoutMetadata(currentPayload.ProfileCardLayout)
|
||
}
|
||
}
|
||
return marshalResourceMetadataPayload(merged), nil
|
||
}
|
||
|
||
func updateResourceMP4MetadataProto(c *gin.Context, current *walletv1.Resource, metadataJSON string) *walletv1.UpdateResourceRequest {
|
||
managerGrantEnabled := current.GetManagerGrantEnabled()
|
||
return &walletv1.UpdateResourceRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
ResourceId: current.GetResourceId(),
|
||
ResourceCode: current.GetResourceCode(),
|
||
ResourceType: current.GetResourceType(),
|
||
Name: current.GetName(),
|
||
Status: current.GetStatus(),
|
||
Grantable: current.GetGrantable(),
|
||
GrantStrategy: current.GetGrantStrategy(),
|
||
WalletAssetType: current.GetWalletAssetType(),
|
||
WalletAssetAmount: current.GetWalletAssetAmount(),
|
||
UsageScopes: append([]string(nil), current.GetUsageScopes()...),
|
||
AssetUrl: current.GetAssetUrl(),
|
||
PreviewUrl: current.GetPreviewUrl(),
|
||
AnimationUrl: current.GetAnimationUrl(),
|
||
MetadataJson: metadataJSON,
|
||
SortOrder: current.GetSortOrder(),
|
||
OperatorUserId: actorID(c),
|
||
ManagerGrantEnabled: &managerGrantEnabled,
|
||
PriceType: current.GetPriceType(),
|
||
CoinPrice: current.GetCoinPrice(),
|
||
}
|
||
}
|
||
|
||
func parseID(c *gin.Context, name string) (int64, bool) {
|
||
raw := strings.TrimSpace(c.Param(name))
|
||
value, err := strconv.ParseInt(raw, 10, 64)
|
||
if err != nil || value <= 0 {
|
||
response.BadRequest(c, "ID 参数不正确")
|
||
return 0, false
|
||
}
|
||
return value, true
|
||
}
|
||
|
||
func optionalInt64Query(c *gin.Context, primary string, fallback string) (int64, bool) {
|
||
value, _, ok := optionalInt64QueryWithPresence(c, primary, fallback)
|
||
return value, ok
|
||
}
|
||
|
||
func giftTypeCodeQuery(c *gin.Context) string {
|
||
value := strings.TrimSpace(c.Query("giftTypeCode"))
|
||
if value == "" {
|
||
value = strings.TrimSpace(c.Query("gift_type_code"))
|
||
}
|
||
return value
|
||
}
|
||
|
||
func optionalInt64QueryWithPresence(c *gin.Context, primary string, fallback string) (int64, bool, bool) {
|
||
raw := strings.TrimSpace(c.Query(primary))
|
||
if raw == "" {
|
||
raw = strings.TrimSpace(c.Query(fallback))
|
||
}
|
||
if raw == "" {
|
||
return 0, false, true
|
||
}
|
||
value, err := strconv.ParseInt(raw, 10, 64)
|
||
if err != nil || value < 0 {
|
||
response.BadRequest(c, primary+" 参数不正确")
|
||
return 0, false, false
|
||
}
|
||
return value, true, true
|
||
}
|
||
|
||
func firstQuery(c *gin.Context, keys ...string) string {
|
||
for _, key := range keys {
|
||
value := strings.TrimSpace(c.Query(key))
|
||
if value != "" {
|
||
return value
|
||
}
|
||
}
|
||
return ""
|
||
}
|