package redpacket import ( "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/response" walletv1 "hyapp.local/api/proto/wallet/v1" "github.com/gin-gonic/gin" ) type Handler struct { wallet walletclient.Client audit shared.OperationLogger } func New(wallet walletclient.Client, audit shared.OperationLogger) *Handler { return &Handler{wallet: wallet, audit: audit} } type configRequest struct { Enabled bool `json:"enabled"` CountTiers []int32 `json:"count_tiers"` CountOptions []int32 `json:"countOptions"` AmountTiers []int64 `json:"amount_tiers"` AmountOptions []int64 `json:"amountOptions"` DelayedOpenSeconds int32 `json:"delayed_open_seconds"` DelaySeconds int32 `json:"delaySeconds"` ExpireSeconds int32 `json:"expire_seconds"` DailySendLimit int32 `json:"daily_send_limit"` DailySendLimitV2 int32 `json:"dailySendLimit"` RuleURL string `json:"rule_url"` RuleURLV2 string `json:"ruleUrl"` } type configDTO struct { AppCode string `json:"app_code"` Enabled bool `json:"enabled"` CountTiers []int32 `json:"count_tiers"` AmountTiers []int64 `json:"amount_tiers"` DelayedOpenSeconds int32 `json:"delayed_open_seconds"` ExpireSeconds int32 `json:"expire_seconds"` DailySendLimit int32 `json:"daily_send_limit"` RuleURL string `json:"rule_url"` UpdatedByAdminID int64 `json:"updated_by_admin_id"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type packetDTO struct { AppCode string `json:"app_code"` PacketID string `json:"packet_id"` CommandID string `json:"command_id"` SenderUserID int64 `json:"sender_user_id,string"` RoomID string `json:"room_id"` RegionID int64 `json:"region_id"` PacketType string `json:"packet_type"` TotalAmount int64 `json:"total_amount"` PacketCount int32 `json:"packet_count"` RemainingAmount int64 `json:"remaining_amount"` RemainingCount int32 `json:"remaining_count"` Status string `json:"status"` OpenAtMS int64 `json:"open_at_ms"` ExpiresAtMS int64 `json:"expires_at_ms"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` RefundAmount int64 `json:"refund_amount"` RefundStatus string `json:"refund_status"` RefundedAtMS int64 `json:"refunded_at_ms"` Claims []claimDTO `json:"claims,omitempty"` } type refundRetryRequest struct { PacketNo string `json:"packetNo"` PacketID string `json:"packet_id"` } type claimDTO struct { AppCode string `json:"app_code"` ClaimID string `json:"claim_id"` CommandID string `json:"command_id"` PacketID string `json:"packet_id"` UserID int64 `json:"user_id,string"` Amount int64 `json:"amount"` WalletTransactionID string `json:"wallet_transaction_id"` Status string `json:"status"` FailureReason string `json:"failure_reason"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } func (h *Handler) GetConfig(c *gin.Context) { if h.wallet == nil { response.ServerError(c, "钱包服务未配置") return } resp, err := h.wallet.GetRedPacketConfig(c.Request.Context(), &walletv1.GetRedPacketConfigRequest{ AppCode: appctx.FromContext(c.Request.Context()), }) if err != nil { response.ServerError(c, "获取红包配置失败") return } response.OK(c, configFromProto(resp.GetConfig())) } func (h *Handler) UpdateConfig(c *gin.Context) { if h.wallet == nil { response.ServerError(c, "钱包服务未配置") return } var req configRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "红包配置参数不正确") return } countTiers := req.CountTiers if len(countTiers) == 0 { countTiers = req.CountOptions } amountTiers := req.AmountTiers if len(amountTiers) == 0 { amountTiers = req.AmountOptions } delaySeconds := req.DelayedOpenSeconds if delaySeconds == 0 { delaySeconds = req.DelaySeconds } dailyLimit := req.DailySendLimit if dailyLimit == 0 { dailyLimit = req.DailySendLimitV2 } ruleURL := strings.TrimSpace(req.RuleURL) if ruleURL == "" { ruleURL = strings.TrimSpace(req.RuleURLV2) } resp, err := h.wallet.UpdateRedPacketConfig(c.Request.Context(), &walletv1.UpdateRedPacketConfigRequest{ AppCode: appctx.FromContext(c.Request.Context()), Enabled: req.Enabled, CountTiers: countTiers, AmountTiers: amountTiers, DelayedOpenSeconds: delaySeconds, ExpireSeconds: 86400, DailySendLimit: dailyLimit, OperatorUserId: int64(middleware.CurrentUserID(c)), RuleUrl: ruleURL, }) if err != nil { response.BadRequest(c, err.Error()) return } item := configFromProto(resp.GetConfig()) shared.OperationLogWithResourceID(c, h.audit, "update-red-packet-config", "red_packet_configs", item.AppCode, "success", "") response.OK(c, item) } func (h *Handler) ListPackets(c *gin.Context) { if h.wallet == nil { response.ServerError(c, "钱包服务未配置") return } options := shared.ListOptions(c) req := &walletv1.ListRedPacketsRequest{ AppCode: appctx.FromContext(c.Request.Context()), RoomId: strings.TrimSpace(c.Query("room_id")), PacketType: strings.TrimSpace(c.Query("packet_type")), Status: strings.TrimSpace(options.Status), Page: int32(options.Page), PageSize: int32(options.PageSize), SenderUserId: firstInt64Query(c, "senderUserId", "sender_user_id"), RegionId: parseInt64Query(c, "region_id"), StartAtMs: firstInt64Query(c, "startTime", "start_at_ms"), EndAtMs: firstInt64Query(c, "endTime", "end_at_ms"), } if roomID := strings.TrimSpace(c.Query("roomId")); roomID != "" { req.RoomId = roomID } if packetMode := strings.TrimSpace(c.Query("packetMode")); packetMode != "" { req.PacketType = packetMode } resp, err := h.wallet.ListRedPackets(c.Request.Context(), req) if err != nil { response.ServerError(c, "获取红包列表失败") return } items := make([]packetDTO, 0, len(resp.GetPackets())) for _, packet := range resp.GetPackets() { items = append(items, packetFromProto(packet)) } response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()}) } func (h *Handler) GetPacket(c *gin.Context) { if h.wallet == nil { response.ServerError(c, "钱包服务未配置") return } packetID := strings.TrimSpace(c.Param("packet_id")) if packetID == "" { response.BadRequest(c, "红包 ID 不正确") return } resp, err := h.wallet.GetRedPacket(c.Request.Context(), &walletv1.GetRedPacketRequest{ AppCode: appctx.FromContext(c.Request.Context()), PacketId: packetID, IncludeClaims: true, }) if err != nil { response.ServerError(c, "获取红包详情失败") return } response.OK(c, packetFromProto(resp.GetPacket())) } func (h *Handler) RetryRefund(c *gin.Context) { if h.wallet == nil { response.ServerError(c, "钱包服务未配置") return } var req refundRetryRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "红包退款参数不正确") return } packetID := strings.TrimSpace(req.PacketNo) if packetID == "" { packetID = strings.TrimSpace(req.PacketID) } if packetID == "" { response.BadRequest(c, "红包 ID 不正确") return } resp, err := h.wallet.RetryRedPacketRefund(c.Request.Context(), &walletv1.RetryRedPacketRefundRequest{ AppCode: appctx.FromContext(c.Request.Context()), PacketId: packetID, OperatorUserId: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "retry-red-packet-refund", "red_packets", packetID, "success", "") response.OK(c, gin.H{ "packet": packetFromProto(resp.GetPacket()), "refundedAmount": resp.GetRefundedAmount(), "serverTime": resp.GetServerTimeMs(), }) } func configFromProto(config *walletv1.RedPacketConfig) configDTO { if config == nil { return configDTO{} } return configDTO{ AppCode: config.GetAppCode(), Enabled: config.GetEnabled(), CountTiers: append([]int32(nil), config.GetCountTiers()...), AmountTiers: append([]int64(nil), config.GetAmountTiers()...), DelayedOpenSeconds: config.GetDelayedOpenSeconds(), ExpireSeconds: config.GetExpireSeconds(), DailySendLimit: config.GetDailySendLimit(), RuleURL: config.GetRuleUrl(), UpdatedByAdminID: config.GetUpdatedByAdminId(), CreatedAtMS: config.GetCreatedAtMs(), UpdatedAtMS: config.GetUpdatedAtMs(), } } func packetFromProto(packet *walletv1.RedPacket) packetDTO { if packet == nil { return packetDTO{} } claims := make([]claimDTO, 0, len(packet.GetClaims())) for _, claim := range packet.GetClaims() { claims = append(claims, claimFromProto(claim)) } return packetDTO{ AppCode: packet.GetAppCode(), PacketID: packet.GetPacketId(), CommandID: packet.GetCommandId(), SenderUserID: packet.GetSenderUserId(), RoomID: packet.GetRoomId(), RegionID: packet.GetRegionId(), PacketType: packet.GetPacketType(), TotalAmount: packet.GetTotalAmount(), PacketCount: packet.GetPacketCount(), RemainingAmount: packet.GetRemainingAmount(), RemainingCount: packet.GetRemainingCount(), Status: packet.GetStatus(), OpenAtMS: packet.GetOpenAtMs(), ExpiresAtMS: packet.GetExpiresAtMs(), CreatedAtMS: packet.GetCreatedAtMs(), UpdatedAtMS: packet.GetUpdatedAtMs(), RefundAmount: packet.GetRefundAmount(), RefundStatus: packet.GetRefundStatus(), RefundedAtMS: packet.GetRefundedAtMs(), Claims: claims, } } func claimFromProto(claim *walletv1.RedPacketClaim) claimDTO { if claim == nil { return claimDTO{} } return claimDTO{ AppCode: claim.GetAppCode(), ClaimID: claim.GetClaimId(), CommandID: claim.GetCommandId(), PacketID: claim.GetPacketId(), UserID: claim.GetUserId(), Amount: claim.GetAmount(), WalletTransactionID: claim.GetWalletTransactionId(), Status: claim.GetStatus(), FailureReason: claim.GetFailureReason(), CreatedAtMS: claim.GetCreatedAtMs(), UpdatedAtMS: claim.GetUpdatedAtMs(), } } func firstInt64Query(c *gin.Context, keys ...string) int64 { for _, key := range keys { if value := parseInt64Query(c, key); value > 0 { return value } } return 0 } func parseInt64Query(c *gin.Context, key string) int64 { value := strings.TrimSpace(c.Query(key)) if value == "" { return 0 } parsed, _ := strconv.ParseInt(value, 10, 64) if parsed < 0 { return 0 } return parsed }