2026-06-29 14:26:06 +08:00

332 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package registrationreward
import (
"context"
"database/sql"
"strconv"
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/activityclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
activityv1 "hyapp.local/api/proto/activity/v1"
"github.com/gin-gonic/gin"
)
type Handler struct {
activity activityclient.Client
userDB *sql.DB
audit shared.OperationLogger
}
func New(activity activityclient.Client, userDB *sql.DB, audit shared.OperationLogger) *Handler {
return &Handler{activity: activity, userDB: userDB, audit: audit}
}
type configRequest struct {
Enabled bool `json:"enabled"`
RewardType string `json:"reward_type"`
CoinAmount int64 `json:"coin_amount"`
ResourceGroupID int64 `json:"resource_group_id"`
DailyLimit int64 `json:"daily_limit"`
}
type configDTO struct {
AppCode string `json:"app_code"`
Enabled bool `json:"enabled"`
RewardType string `json:"reward_type"`
CoinAmount int64 `json:"coin_amount"`
ResourceGroupID int64 `json:"resource_group_id"`
DailyLimit int64 `json:"daily_limit"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type claimDTO struct {
ClaimID string `json:"claim_id"`
CommandID string `json:"command_id"`
UserID int64 `json:"user_id,string"`
User *userDTO `json:"user,omitempty"`
RewardDay string `json:"reward_day"`
RewardType string `json:"reward_type"`
CoinAmount int64 `json:"coin_amount"`
ResourceGroupID int64 `json:"resource_group_id"`
Status string `json:"status"`
WalletCommandID string `json:"wallet_command_id"`
WalletTransactionID string `json:"wallet_transaction_id"`
ResourceGrantID string `json:"resource_grant_id"`
FailureReason string `json:"failure_reason"`
ClaimedAtMS int64 `json:"claimed_at_ms"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type userDTO struct {
UserID int64 `json:"user_id,string"`
DisplayUserID string `json:"display_user_id"`
Username string `json:"username"`
Avatar string `json:"avatar"`
}
type claimsPageDTO struct {
Items []claimDTO `json:"items"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
Total int64 `json:"total"`
TodayClaimedCount int64 `json:"today_claimed_count"`
}
func (h *Handler) GetRegistrationRewardConfig(c *gin.Context) {
resp, err := h.activity.GetRegistrationRewardConfig(c.Request.Context(), &activityv1.GetRegistrationRewardConfigRequest{Meta: h.meta(c)})
if err != nil {
response.ServerError(c, "获取注册奖励配置失败")
return
}
response.OK(c, configFromProto(resp.GetConfig()))
}
func (h *Handler) UpdateRegistrationRewardConfig(c *gin.Context) {
var req configRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "注册奖励配置参数不正确")
return
}
resp, err := h.activity.UpdateRegistrationRewardConfig(c.Request.Context(), &activityv1.UpdateRegistrationRewardConfigRequest{
Meta: h.meta(c),
Enabled: req.Enabled,
RewardType: strings.TrimSpace(req.RewardType),
CoinAmount: req.CoinAmount,
ResourceGroupId: req.ResourceGroupID,
DailyLimit: req.DailyLimit,
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := configFromProto(resp.GetConfig())
shared.OperationLogWithResourceID(c, h.audit, "update-registration-reward", "registration_reward_configs", item.AppCode, "success", item.RewardType)
response.OK(c, item)
}
func (h *Handler) ListRegistrationRewardClaims(c *gin.Context) {
options := shared.ListOptions(c)
claimedStartMS := queryInt64(c, "claimed_start_ms", "claimedStartMs", "start_ms", "startMs")
claimedEndMS := queryInt64(c, "claimed_end_ms", "claimedEndMs", "end_ms", "endMs")
userFilter := shared.UserIdentityFilterFromQuery(c, "")
userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword), userFilter)
if !ok {
response.ServerError(c, "查询用户信息失败")
return
}
if (options.Keyword != "" || !userFilter.IsEmpty()) && !matched {
todayClaimedCount, ok := h.loadTodayClaimedCount(c)
if !ok {
return
}
response.OK(c, claimsPageDTO{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0, TodayClaimedCount: todayClaimedCount})
return
}
resp, err := h.activity.ListRegistrationRewardClaims(c.Request.Context(), &activityv1.ListRegistrationRewardClaimsRequest{
Meta: h.meta(c),
Status: strings.TrimSpace(options.Status),
UserId: userID,
ClaimedStartMs: claimedStartMS,
ClaimedEndMs: claimedEndMS,
Page: int32(options.Page),
PageSize: int32(options.PageSize),
})
if err != nil {
response.ServerError(c, "获取注册奖励领取记录失败")
return
}
items := make([]claimDTO, 0, len(resp.GetClaims()))
for _, item := range resp.GetClaims() {
items = append(items, claimFromProto(item))
}
if err := h.fillClaimUsers(c.Request.Context(), items); err != nil {
response.ServerError(c, "补全用户信息失败")
return
}
response.OK(c, claimsPageDTO{
Items: items,
Page: options.Page,
PageSize: options.PageSize,
Total: resp.GetTotal(),
TodayClaimedCount: resp.GetTodayClaimedCount(),
})
}
func (h *Handler) loadTodayClaimedCount(c *gin.Context) (int64, bool) {
resp, err := h.activity.ListRegistrationRewardClaims(c.Request.Context(), &activityv1.ListRegistrationRewardClaimsRequest{
Meta: h.meta(c),
Page: 1,
PageSize: 1,
})
if err != nil {
response.ServerError(c, "获取注册奖励今日领取数量失败")
return 0, false
}
return resp.GetTodayClaimedCount(), true
}
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
return &activityv1.RequestMeta{
RequestId: middleware.CurrentRequestID(c),
Caller: "admin-server",
AppCode: appctx.FromContext(c.Request.Context()),
SentAtMs: time.Now().UnixMilli(),
}
}
func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if filter.IsEmpty() && keyword == "" {
return 0, false, true
}
if !filter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, h.userDB, appctx.FromContext(ctx), filter, time.Now().UnixMilli())
return userID, matched, err == nil
}
// 注册奖励领取记录的 owner 是 activity-service列表关键词先解析成内部 user_id再保持下游分页和 total 同源。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {
return 0, false, false
}
return userID, matched, true
}
func (h *Handler) fillClaimUsers(ctx context.Context, claims []claimDTO) error {
if h.userDB == nil || len(claims) == 0 {
return nil
}
ids := collectClaimUserIDs(claims)
if len(ids) == 0 {
return nil
}
args := append([]any{appctx.FromContext(ctx)}, int64Args(ids)...)
rows, err := h.userDB.QueryContext(ctx, `
SELECT user_id, current_display_user_id, COALESCE(username, ''), COALESCE(avatar, '')
FROM users
WHERE app_code = ? AND user_id IN (`+placeholders(len(ids))+`)`,
args...,
)
if err != nil {
return err
}
defer rows.Close()
users := make(map[int64]userDTO, len(ids))
for rows.Next() {
var user userDTO
if err := rows.Scan(&user.UserID, &user.DisplayUserID, &user.Username, &user.Avatar); err != nil {
return err
}
users[user.UserID] = user
}
if err := rows.Err(); err != nil {
return err
}
for index := range claims {
if user, ok := users[claims[index].UserID]; ok {
claims[index].User = &user
continue
}
// 领取事实必须可追踪;用户资料缺失时仍返回 user_id 让后台能定位数据问题。
claims[index].User = &userDTO{UserID: claims[index].UserID}
}
return nil
}
func configFromProto(item *activityv1.RegistrationRewardConfig) configDTO {
if item == nil {
return configDTO{RewardType: "coin"}
}
return configDTO{
AppCode: item.GetAppCode(),
Enabled: item.GetEnabled(),
RewardType: item.GetRewardType(),
CoinAmount: item.GetCoinAmount(),
ResourceGroupID: item.GetResourceGroupId(),
DailyLimit: item.GetDailyLimit(),
UpdatedByAdminID: item.GetUpdatedByAdminId(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func claimFromProto(item *activityv1.RegistrationRewardClaim) claimDTO {
if item == nil {
return claimDTO{}
}
return claimDTO{
ClaimID: item.GetClaimId(),
CommandID: item.GetCommandId(),
UserID: item.GetUserId(),
RewardDay: item.GetRewardDay(),
RewardType: item.GetRewardType(),
CoinAmount: item.GetCoinAmount(),
ResourceGroupID: item.GetResourceGroupId(),
Status: item.GetStatus(),
WalletCommandID: item.GetWalletCommandId(),
WalletTransactionID: item.GetWalletTransactionId(),
ResourceGrantID: item.GetResourceGrantId(),
FailureReason: item.GetFailureReason(),
ClaimedAtMS: item.GetClaimedAtMs(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func collectClaimUserIDs(claims []claimDTO) []int64 {
seen := make(map[int64]struct{}, len(claims))
ids := make([]int64, 0, len(claims))
for _, claim := range claims {
if claim.UserID <= 0 {
continue
}
if _, ok := seen[claim.UserID]; ok {
continue
}
seen[claim.UserID] = struct{}{}
ids = append(ids, claim.UserID)
}
return ids
}
func placeholders(count int) string {
if count <= 0 {
return ""
}
items := make([]string, count)
for index := range items {
items[index] = "?"
}
return strings.Join(items, ",")
}
func int64Args(values []int64) []any {
args := make([]any, 0, len(values))
for _, value := range values {
args = append(args, value)
}
return args
}
func queryInt64(c *gin.Context, names ...string) int64 {
for _, name := range names {
value := strings.TrimSpace(c.Query(name))
if value == "" {
continue
}
parsed, err := strconv.ParseInt(value, 10, 64)
if err == nil && parsed > 0 {
return parsed
}
}
return 0
}