332 lines
10 KiB
Go
332 lines
10 KiB
Go
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
|
||
}
|