Add admin endpoints for affiliate overview, invites, rebates, transfers, user overrides, batch rate updates, and global rebate config.
335 lines
8.4 KiB
Go
335 lines
8.4 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
maxAffiliateRatePercent = 100
|
|
maxAffiliateFrozenDays = 3650
|
|
maxAffiliateBatchUsers = 1000
|
|
)
|
|
|
|
type affiliateUserSettingsRequest struct {
|
|
AffRebateRatePercent int `json:"aff_rebate_rate_percent"`
|
|
AffRebateFrozenDays int `json:"aff_rebate_frozen_days"`
|
|
}
|
|
|
|
type affiliateBatchRateRequest struct {
|
|
UserIds []int `json:"user_ids"`
|
|
AffRebateRatePercent int `json:"aff_rebate_rate_percent"`
|
|
}
|
|
|
|
type affiliateConfigRequest struct {
|
|
AffRebateRatePercent *int `json:"aff_rebate_rate_percent"`
|
|
AffRebateFrozenDays *int `json:"aff_rebate_frozen_days"`
|
|
AffRebateEnabled *bool `json:"aff_rebate_enabled"`
|
|
}
|
|
|
|
func AdminGetAffiliateOverview(c *gin.Context) {
|
|
overview, err := model.GetAffiliateOverview()
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, overview)
|
|
}
|
|
|
|
func AdminGetAffiliateRebates(c *gin.Context) {
|
|
pageInfo := common.GetPageQuery(c)
|
|
status := strings.TrimSpace(c.Query("status"))
|
|
if status != "" && status != "frozen" && status != "released" {
|
|
common.ApiErrorMsg(c, "status 只能是 frozen 或 released")
|
|
return
|
|
}
|
|
|
|
userId := 0
|
|
if userIdText := strings.TrimSpace(c.Query("user_id")); userIdText != "" {
|
|
parsed, err := strconv.Atoi(userIdText)
|
|
if err != nil || parsed <= 0 {
|
|
common.ApiErrorMsg(c, "user_id 参数格式不正确")
|
|
return
|
|
}
|
|
userId = parsed
|
|
}
|
|
|
|
records, total, err := model.GetRebateRecords(pageInfo.GetPage(), pageInfo.GetPageSize(), status, userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
pageInfo.SetTotal(int(total))
|
|
pageInfo.SetItems(records)
|
|
common.ApiSuccess(c, pageInfo)
|
|
}
|
|
|
|
func AdminGetAffiliateInvites(c *gin.Context) {
|
|
pageInfo := common.GetPageQuery(c)
|
|
userId, ok := parseOptionalPositiveQuery(c, "user_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
startTimestamp, ok := parseOptionalTimestampQuery(c, "start_timestamp")
|
|
if !ok {
|
|
return
|
|
}
|
|
endTimestamp, ok := parseOptionalTimestampQuery(c, "end_timestamp")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
invites, total, err := model.GetAffiliateInvites(pageInfo.GetPage(), pageInfo.GetPageSize(), userId, startTimestamp, endTimestamp, c.Query("keyword"))
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
pageInfo.SetTotal(int(total))
|
|
pageInfo.SetItems(invites)
|
|
common.ApiSuccess(c, pageInfo)
|
|
}
|
|
|
|
func AdminGetAffiliateTransfers(c *gin.Context) {
|
|
pageInfo := common.GetPageQuery(c)
|
|
userId, ok := parseOptionalPositiveQuery(c, "user_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
startTimestamp, ok := parseOptionalTimestampQuery(c, "start_timestamp")
|
|
if !ok {
|
|
return
|
|
}
|
|
endTimestamp, ok := parseOptionalTimestampQuery(c, "end_timestamp")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
logs, total, err := model.GetAffiliateTransferLogs(pageInfo.GetPage(), pageInfo.GetPageSize(), userId, startTimestamp, endTimestamp)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
pageInfo.SetTotal(int(total))
|
|
pageInfo.SetItems(logs)
|
|
common.ApiSuccess(c, pageInfo)
|
|
}
|
|
|
|
func AdminGetAffiliateUsers(c *gin.Context) {
|
|
pageInfo := common.GetPageQuery(c)
|
|
users, total, err := model.GetAffiliateUsers(pageInfo.GetPage(), pageInfo.GetPageSize(), c.Query("keyword"))
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
pageInfo.SetTotal(int(total))
|
|
pageInfo.SetItems(users)
|
|
common.ApiSuccess(c, pageInfo)
|
|
}
|
|
|
|
func AdminGetAffiliateUserOverview(c *gin.Context) {
|
|
userId, ok := getAffiliateUserIdParam(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
overview, err := model.GetAffiliateUserOverview(userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, overview)
|
|
}
|
|
|
|
func AdminUpdateAffiliateUserSettings(c *gin.Context) {
|
|
userId, ok := getAffiliateUserIdParam(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req affiliateUserSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if err := validateAffiliateRate(req.AffRebateRatePercent); err != nil {
|
|
common.ApiErrorMsg(c, err.Error())
|
|
return
|
|
}
|
|
if err := validateAffiliateFrozenDays(req.AffRebateFrozenDays); err != nil {
|
|
common.ApiErrorMsg(c, err.Error())
|
|
return
|
|
}
|
|
|
|
user, err := model.UpdateUserAffiliateSettings(userId, req.AffRebateRatePercent, req.AffRebateFrozenDays)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, user)
|
|
}
|
|
|
|
func AdminBatchUpdateAffiliateRate(c *gin.Context) {
|
|
var req affiliateBatchRateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if len(req.UserIds) == 0 {
|
|
common.ApiErrorMsg(c, "user_ids 不能为空")
|
|
return
|
|
}
|
|
if len(req.UserIds) > maxAffiliateBatchUsers {
|
|
common.ApiErrorMsg(c, "user_ids 一次最多 1000 个")
|
|
return
|
|
}
|
|
if err := validateAffiliateRate(req.AffRebateRatePercent); err != nil {
|
|
common.ApiErrorMsg(c, err.Error())
|
|
return
|
|
}
|
|
userIds := uniquePositiveUserIds(req.UserIds)
|
|
if len(userIds) != len(req.UserIds) {
|
|
common.ApiErrorMsg(c, "user_ids 必须是正整数且不能重复")
|
|
return
|
|
}
|
|
|
|
affected, err := model.BatchUpdateUserAffiliateRate(userIds, req.AffRebateRatePercent)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, gin.H{"affected": affected})
|
|
}
|
|
|
|
func AdminClearAffiliateUserSettings(c *gin.Context) {
|
|
userId, ok := getAffiliateUserIdParam(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
user, err := model.ClearUserAffiliateSettings(userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, user)
|
|
}
|
|
|
|
func AdminUpdateAffiliateConfig(c *gin.Context) {
|
|
var req affiliateConfigRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if req.AffRebateRatePercent != nil {
|
|
if err := validateAffiliateRate(*req.AffRebateRatePercent); err != nil {
|
|
common.ApiErrorMsg(c, err.Error())
|
|
return
|
|
}
|
|
}
|
|
if req.AffRebateFrozenDays != nil {
|
|
if err := validateAffiliateFrozenDays(*req.AffRebateFrozenDays); err != nil {
|
|
common.ApiErrorMsg(c, err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
if req.AffRebateRatePercent != nil {
|
|
if err := model.UpdateOption(model.AffRebateRatePercentKey, strconv.Itoa(*req.AffRebateRatePercent)); err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
}
|
|
if req.AffRebateFrozenDays != nil {
|
|
if err := model.UpdateOption(model.AffRebateFrozenDaysKey, strconv.Itoa(*req.AffRebateFrozenDays)); err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
}
|
|
if req.AffRebateEnabled != nil {
|
|
if err := model.UpdateOption(model.AffRebateEnabledKey, strconv.FormatBool(*req.AffRebateEnabled)); err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
common.ApiSuccess(c, getAffiliateConfigSnapshot())
|
|
}
|
|
|
|
func getAffiliateUserIdParam(c *gin.Context) (int, bool) {
|
|
userId, err := strconv.Atoi(c.Param("user_id"))
|
|
if err != nil || userId <= 0 {
|
|
common.ApiErrorMsg(c, "user_id 参数格式不正确")
|
|
return 0, false
|
|
}
|
|
return userId, true
|
|
}
|
|
|
|
func parseOptionalPositiveQuery(c *gin.Context, key string) (int, bool) {
|
|
value := strings.TrimSpace(c.Query(key))
|
|
if value == "" {
|
|
return 0, true
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil || parsed <= 0 {
|
|
common.ApiErrorMsg(c, key+" 参数格式不正确")
|
|
return 0, false
|
|
}
|
|
return parsed, true
|
|
}
|
|
|
|
func parseOptionalTimestampQuery(c *gin.Context, key string) (int64, bool) {
|
|
value := strings.TrimSpace(c.Query(key))
|
|
if value == "" {
|
|
return 0, true
|
|
}
|
|
parsed, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil || parsed < 0 {
|
|
common.ApiErrorMsg(c, key+" 参数格式不正确")
|
|
return 0, false
|
|
}
|
|
return parsed, true
|
|
}
|
|
|
|
func validateAffiliateRate(rate int) error {
|
|
if rate < 0 || rate > maxAffiliateRatePercent {
|
|
return errors.New("返利比例必须在 0-100 之间")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateAffiliateFrozenDays(days int) error {
|
|
if days < 0 || days > maxAffiliateFrozenDays {
|
|
return errors.New("冻结天数必须在 0-3650 之间")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func uniquePositiveUserIds(userIds []int) []int {
|
|
seen := make(map[int]struct{}, len(userIds))
|
|
uniqueIds := make([]int, 0, len(userIds))
|
|
for _, userId := range userIds {
|
|
if userId <= 0 {
|
|
return uniqueIds
|
|
}
|
|
if _, ok := seen[userId]; ok {
|
|
return uniqueIds
|
|
}
|
|
seen[userId] = struct{}{}
|
|
uniqueIds = append(uniqueIds, userId)
|
|
}
|
|
return uniqueIds
|
|
}
|
|
|
|
func getAffiliateConfigSnapshot() gin.H {
|
|
common.OptionMapRWMutex.RLock()
|
|
defer common.OptionMapRWMutex.RUnlock()
|
|
return gin.H{
|
|
"aff_rebate_rate_percent": common.OptionMap[model.AffRebateRatePercentKey],
|
|
"aff_rebate_frozen_days": common.OptionMap[model.AffRebateFrozenDaysKey],
|
|
"aff_rebate_enabled": common.OptionMap[model.AffRebateEnabledKey],
|
|
}
|
|
}
|