2026-05-26 01:02:12 +08:00

615 lines
19 KiB
Go

package resource
import (
"database/sql"
"fmt"
"sort"
"strconv"
"strings"
"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
audit shared.OperationLogger
}
func New(wallet walletclient.Client, store *repository.Store, userDB *sql.DB, audit shared.OperationLogger) *Handler {
return &Handler{wallet: wallet, store: store, userDB: userDB, audit: audit}
}
func (h *Handler) ListResources(c *gin.Context) {
options := shared.ListOptions(c)
resp, err := h.wallet.ListResources(c.Request.Context(), &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
}
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)
resp, err := h.wallet.ListResources(c.Request.Context(), &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{}{}
for page := int32(1); ; page++ {
resp, err := h.wallet.ListResources(c.Request.Context(), &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
}
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) 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)
resp, err := h.wallet.ListResourceGroups(c.Request.Context(), &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,
})
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) 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) 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
}
resp, err := h.wallet.GrantResource(c.Request.Context(), &walletv1.GrantResourceRequest{
CommandId: strings.TrimSpace(req.CommandID),
AppCode: appctx.FromContext(c.Request.Context()),
TargetUserId: req.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
}
resp, err := h.wallet.GrantResourceGroup(c.Request.Context(), &walletv1.GrantResourceGroupRequest{
CommandId: strings.TrimSpace(req.CommandID),
AppCode: appctx.FromContext(c.Request.Context()),
TargetUserId: req.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) ListResourceGrants(c *gin.Context) {
options := shared.ListOptions(c)
targetUserID, ok := optionalInt64Query(c, "target_user_id", "targetUserId")
if !ok {
return
}
resp, err := h.wallet.ListResourceGrants(c.Request.Context(), &walletv1.ListResourceGrantsRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
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) 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) 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 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 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
}