2026-06-12 12:08:01 +08:00

345 lines
11 KiB
Go

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
}