2026-06-11 18:14:04 +08:00

365 lines
12 KiB
Go

package inviteactivityreward
import (
"context"
"database/sql"
"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"`
PerInviteInviterRewardCoinAmount int64 `json:"per_invite_inviter_reward_coin_amount"`
PerInviteInviteeRewardCoinAmount int64 `json:"per_invite_invitee_reward_coin_amount"`
Tiers []tierDTO `json:"tiers"`
}
type configDTO struct {
AppCode string `json:"app_code"`
Enabled bool `json:"enabled"`
PerInviteInviterRewardCoinAmount int64 `json:"per_invite_inviter_reward_coin_amount"`
PerInviteInviteeRewardCoinAmount int64 `json:"per_invite_invitee_reward_coin_amount"`
Tiers []tierDTO `json:"tiers"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type tierDTO struct {
TierID int64 `json:"tier_id"`
RewardType string `json:"reward_type"`
TierCode string `json:"tier_code"`
TierName string `json:"tier_name"`
ThresholdCoinAmount int64 `json:"threshold_coin_amount"`
ThresholdValidInviteCount int64 `json:"threshold_valid_invite_count"`
RewardCoinAmount int64 `json:"reward_coin_amount"`
Status string `json:"status"`
SortOrder int32 `json:"sort_order"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type claimDTO struct {
ClaimID string `json:"claim_id"`
AppCode string `json:"app_code"`
CycleKey string `json:"cycle_key"`
UserID int64 `json:"user_id"`
User *userDTO `json:"user,omitempty"`
RewardType string `json:"reward_type"`
CommandID string `json:"command_id"`
TierID int64 `json:"tier_id"`
TierCode string `json:"tier_code"`
TierName string `json:"tier_name"`
ThresholdCoinAmount int64 `json:"threshold_coin_amount"`
ThresholdValidInviteCount int64 `json:"threshold_valid_invite_count"`
ReachedValue int64 `json:"reached_value"`
RewardCoinAmount int64 `json:"reward_coin_amount"`
Status string `json:"status"`
WalletCommandID string `json:"wallet_command_id"`
WalletTransactionID string `json:"wallet_transaction_id"`
FailureReason string `json:"failure_reason"`
GrantedAtMS int64 `json:"granted_at_ms"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type userDTO struct {
UserID int64 `json:"user_id"`
DisplayUserID string `json:"display_user_id"`
Username string `json:"username"`
Avatar string `json:"avatar"`
}
func (h *Handler) GetConfig(c *gin.Context) {
resp, err := h.activity.GetInviteActivityRewardConfig(c.Request.Context(), &activityv1.GetInviteActivityRewardConfigRequest{Meta: h.meta(c)})
if err != nil {
response.ServerError(c, "获取邀请活动配置失败")
return
}
response.OK(c, configFromProto(resp.GetConfig()))
}
func (h *Handler) UpdateConfig(c *gin.Context) {
var req configRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "邀请活动配置参数不正确")
return
}
tiers := make([]*activityv1.InviteActivityRewardTier, 0, len(req.Tiers))
for _, tier := range req.Tiers {
// admin-server 只裁剪字符串和注入操作人;档位门槛、奖励金额、最高档领取语义都由 activity-service 统一校验。
tiers = append(tiers, &activityv1.InviteActivityRewardTier{
TierId: tier.TierID,
RewardType: strings.TrimSpace(tier.RewardType),
TierCode: strings.TrimSpace(tier.TierCode),
TierName: strings.TrimSpace(tier.TierName),
ThresholdCoinAmount: tier.ThresholdCoinAmount,
ThresholdValidInviteCount: tier.ThresholdValidInviteCount,
RewardCoinAmount: tier.RewardCoinAmount,
Status: strings.TrimSpace(tier.Status),
SortOrder: tier.SortOrder,
})
}
resp, err := h.activity.UpdateInviteActivityRewardConfig(c.Request.Context(), &activityv1.UpdateInviteActivityRewardConfigRequest{
Meta: h.meta(c),
Enabled: req.Enabled,
PerInviteInviterRewardCoinAmount: req.PerInviteInviterRewardCoinAmount,
PerInviteInviteeRewardCoinAmount: req.PerInviteInviteeRewardCoinAmount,
Tiers: tiers,
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := configFromProto(resp.GetConfig())
shared.OperationLogWithResourceID(c, h.audit, "update-invite-activity-reward", "invite_activity_reward_configs", item.AppCode, "success", "")
response.OK(c, item)
}
func (h *Handler) ListClaims(c *gin.Context) {
options := shared.ListOptions(c)
userID, matched, ok := h.resolveUserID(c.Request.Context(), strings.TrimSpace(options.Keyword))
if !ok {
response.ServerError(c, "查询用户信息失败")
return
}
if options.Keyword != "" && !matched {
response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return
}
resp, err := h.activity.ListInviteActivityRewardClaims(c.Request.Context(), &activityv1.ListInviteActivityRewardClaimsRequest{
Meta: h.meta(c),
Status: strings.TrimSpace(options.Status),
RewardType: strings.TrimSpace(c.Query("reward_type")),
UserId: userID,
CycleKey: strings.TrimSpace(c.Query("cycle_key")),
Page: int32(options.Page),
PageSize: int32(options.PageSize),
})
if err != nil {
response.ServerError(c, "获取邀请活动领取记录失败")
return
}
items := make([]claimDTO, 0, len(resp.GetClaims()))
for _, claim := range resp.GetClaims() {
items = append(items, claimFromProto(claim))
}
if err := h.fillUsers(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) 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) resolveUserID(ctx context.Context, keyword string) (int64, bool, bool) {
if keyword == "" {
return 0, false, true
}
if h.userDB == nil {
return 0, false, true
}
appCode := appctx.FromContext(ctx)
rows, err := h.userDB.QueryContext(ctx, `
SELECT user_id
FROM users
WHERE app_code = ?
AND (
CAST(user_id AS CHAR) = ?
OR current_display_user_id = ?
OR username LIKE ?
)
ORDER BY
CASE
WHEN CAST(user_id AS CHAR) = ? THEN 0
WHEN current_display_user_id = ? THEN 1
ELSE 2
END,
user_id DESC
LIMIT 1`,
appCode, keyword, keyword, "%"+keyword+"%", keyword, keyword,
)
if err != nil {
return 0, false, false
}
defer rows.Close()
if !rows.Next() {
return 0, false, rows.Err() == nil
}
var userID int64
if err := rows.Scan(&userID); err != nil {
return 0, false, false
}
return userID, true, rows.Err() == nil
}
func (h *Handler) fillUsers(ctx context.Context, claims []claimDTO) error {
if h.userDB == nil || len(claims) == 0 {
return nil
}
ids := collectUserIDs(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
}
claims[index].User = &userDTO{UserID: claims[index].UserID}
}
return nil
}
func configFromProto(config *activityv1.InviteActivityRewardConfig) configDTO {
if config == nil {
return configDTO{Tiers: []tierDTO{}}
}
tiers := make([]tierDTO, 0, len(config.GetTiers()))
for _, tier := range config.GetTiers() {
tiers = append(tiers, tierFromProto(tier))
}
return configDTO{
AppCode: config.GetAppCode(),
Enabled: config.GetEnabled(),
PerInviteInviterRewardCoinAmount: config.GetPerInviteInviterRewardCoinAmount(),
PerInviteInviteeRewardCoinAmount: config.GetPerInviteInviteeRewardCoinAmount(),
Tiers: tiers,
UpdatedByAdminID: config.GetUpdatedByAdminId(),
CreatedAtMS: config.GetCreatedAtMs(),
UpdatedAtMS: config.GetUpdatedAtMs(),
}
}
func tierFromProto(tier *activityv1.InviteActivityRewardTier) tierDTO {
if tier == nil {
return tierDTO{}
}
return tierDTO{
TierID: tier.GetTierId(),
RewardType: tier.GetRewardType(),
TierCode: tier.GetTierCode(),
TierName: tier.GetTierName(),
ThresholdCoinAmount: tier.GetThresholdCoinAmount(),
ThresholdValidInviteCount: tier.GetThresholdValidInviteCount(),
RewardCoinAmount: tier.GetRewardCoinAmount(),
Status: tier.GetStatus(),
SortOrder: tier.GetSortOrder(),
CreatedAtMS: tier.GetCreatedAtMs(),
UpdatedAtMS: tier.GetUpdatedAtMs(),
}
}
func claimFromProto(claim *activityv1.InviteActivityRewardClaim) claimDTO {
if claim == nil {
return claimDTO{}
}
return claimDTO{
ClaimID: claim.GetClaimId(),
AppCode: claim.GetAppCode(),
CycleKey: claim.GetCycleKey(),
UserID: claim.GetUserId(),
RewardType: claim.GetRewardType(),
CommandID: claim.GetCommandId(),
TierID: claim.GetTierId(),
TierCode: claim.GetTierCode(),
TierName: claim.GetTierName(),
ThresholdCoinAmount: claim.GetThresholdCoinAmount(),
ThresholdValidInviteCount: claim.GetThresholdValidInviteCount(),
ReachedValue: claim.GetReachedValue(),
RewardCoinAmount: claim.GetRewardCoinAmount(),
Status: claim.GetStatus(),
WalletCommandID: claim.GetWalletCommandId(),
WalletTransactionID: claim.GetWalletTransactionId(),
FailureReason: claim.GetFailureReason(),
GrantedAtMS: claim.GetGrantedAtMs(),
CreatedAtMS: claim.GetCreatedAtMs(),
UpdatedAtMS: claim.GetUpdatedAtMs(),
}
}
func collectUserIDs(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 int64Args(ids []int64) []any {
args := make([]any, 0, len(ids))
for _, id := range ids {
args = append(args, id)
}
return args
}
func placeholders(count int) string {
if count <= 0 {
return ""
}
parts := make([]string, count)
for i := range parts {
parts[i] = "?"
}
return strings.Join(parts, ",")
}