x
This commit is contained in:
parent
029ed489bc
commit
fbdaf77eda
81
internal/api/app/coupon_transfer.go
Normal file
81
internal/api/app/coupon_transfer.go
Normal file
@ -0,0 +1,81 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
usersvc "bindbox-game/internal/service/user"
|
||||
)
|
||||
|
||||
type couponTransferHandler struct {
|
||||
logger logger.CustomLogger
|
||||
repo mysql.Repo
|
||||
userSvc usersvc.Service
|
||||
}
|
||||
|
||||
func NewCouponTransfer(l logger.CustomLogger, db mysql.Repo, userSvc usersvc.Service) *couponTransferHandler {
|
||||
return &couponTransferHandler{logger: l, repo: db, userSvc: userSvc}
|
||||
}
|
||||
|
||||
type TransferCouponRequest struct {
|
||||
ReceiverID int64 `json:"receiver_id" binding:"required"`
|
||||
}
|
||||
|
||||
// TransferCouponHandler 转赠优惠券
|
||||
// @Summary 转赠优惠券给其他用户
|
||||
// @Description 将自己持有的优惠券转赠给其他用户
|
||||
// @Tags APP端.优惠券
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security LoginVerifyToken
|
||||
// @Param user_id path int true "发送方用户ID"
|
||||
// @Param user_coupon_id path int true "优惠券记录ID"
|
||||
// @Param body body TransferCouponRequest true "接收方信息"
|
||||
// @Success 200 {object} map[string]bool
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/app/users/{user_id}/coupons/{user_coupon_id}/transfer [post]
|
||||
func (h *couponTransferHandler) TransferCouponHandler() core.HandlerFunc {
|
||||
return func(c core.Context) {
|
||||
userID, err := strconv.ParseInt(c.Param("user_id"), 10, 64)
|
||||
if err != nil {
|
||||
c.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "user_id 无效"))
|
||||
return
|
||||
}
|
||||
|
||||
userCouponID, err := strconv.ParseInt(c.Param("user_coupon_id"), 10, 64)
|
||||
if err != nil {
|
||||
c.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "user_coupon_id 无效"))
|
||||
return
|
||||
}
|
||||
|
||||
var req TransferCouponRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userSvc.TransferCoupon(c.RequestContext(), userID, req.ReceiverID, userCouponID); err != nil {
|
||||
switch err.Error() {
|
||||
case "invalid_params":
|
||||
c.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "参数无效"))
|
||||
case "cannot_transfer_to_self":
|
||||
c.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "不能转赠给自己"))
|
||||
case "coupon_not_found":
|
||||
c.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "优惠券不存在"))
|
||||
case "coupon_not_available":
|
||||
c.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "优惠券状态不可用"))
|
||||
case "receiver_not_found":
|
||||
c.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "接收用户不存在"))
|
||||
default:
|
||||
c.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.Payload(map[string]any{"success": true})
|
||||
}
|
||||
}
|
||||
60
internal/api/app/product_category.go
Normal file
60
internal/api/app/product_category.go
Normal file
@ -0,0 +1,60 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
)
|
||||
|
||||
type productCategoryHandler struct {
|
||||
logger logger.CustomLogger
|
||||
readDB *dao.Query
|
||||
}
|
||||
|
||||
func NewProductCategory(logger logger.CustomLogger, db mysql.Repo) *productCategoryHandler {
|
||||
return &productCategoryHandler{logger: logger, readDB: dao.Use(db.GetDbR())}
|
||||
}
|
||||
|
||||
type productCategoryItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type listProductCategoriesResponse struct {
|
||||
Items []productCategoryItem `json:"items"`
|
||||
}
|
||||
|
||||
// ListProductCategoriesForApp 商品分类列表
|
||||
// @Summary 获取商品分类列表
|
||||
// @Description 返回所有启用状态的商品分类
|
||||
// @Tags APP端.商品
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security LoginVerifyToken
|
||||
// @Success 200 {object} listProductCategoriesResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/app/product_categories [get]
|
||||
func (h *productCategoryHandler) ListProductCategoriesForApp() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
rows, err := h.readDB.ProductCategories.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.ProductCategories.Status.Eq(1)).
|
||||
Order(h.readDB.ProductCategories.ID.Asc()).
|
||||
Find()
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]productCategoryItem, len(rows))
|
||||
for i, row := range rows {
|
||||
items[i] = productCategoryItem{ID: row.ID, Name: row.Name}
|
||||
}
|
||||
|
||||
ctx.Payload(&listProductCategoriesResponse{Items: items})
|
||||
}
|
||||
}
|
||||
@ -12,18 +12,20 @@ const TableNameTaskCenterTaskTiers = "task_center_task_tiers"
|
||||
|
||||
// TaskCenterTaskTiers 任务中心-档位配置
|
||||
type TaskCenterTaskTiers struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||
TaskID int64 `gorm:"column:task_id;not null;comment:关联任务ID(task_center_tasks.id)" json:"task_id"` // 关联任务ID(task_center_tasks.id)
|
||||
Metric string `gorm:"column:metric;not null;comment:指标:first_order|order_count|invite_count" json:"metric"` // 指标:first_order|order_count|invite_count
|
||||
Operator string `gorm:"column:operator;not null;comment:比较符:>= 或 ==" json:"operator"` // 比较符:>= 或 ==
|
||||
Threshold int64 `gorm:"column:threshold;not null;comment:阈值(数量或布尔首单)" json:"threshold"` // 阈值(数量或布尔首单)
|
||||
Window string `gorm:"column:window;not null;comment:时间窗口:activity_period|since_registration" json:"window"` // 时间窗口:activity_period|since_registration
|
||||
Repeatable int32 `gorm:"column:repeatable;not null;default:1;comment:是否每档一次:0否 1是" json:"repeatable"` // 是否每档一次:0否 1是
|
||||
Priority int32 `gorm:"column:priority;not null;comment:匹配优先级(数值越小越先匹配)" json:"priority"` // 匹配优先级(数值越小越先匹配)
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
|
||||
ExtraParams string `gorm:"column:extra_params;comment:额外参数配置(如消费门槛)" json:"extra_params"` // 额外参数配置(如消费门槛)
|
||||
ActivityID int32 `gorm:"column:activity_id;comment:活动ID" json:"activity_id"` // 活动ID
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||
TaskID int64 `gorm:"column:task_id;not null;comment:关联任务ID(task_center_tasks.id)" json:"task_id"` // 关联任务ID(task_center_tasks.id)
|
||||
Metric string `gorm:"column:metric;not null;comment:指标:first_order|order_count|invite_count" json:"metric"` // 指标:first_order|order_count|invite_count
|
||||
Operator string `gorm:"column:operator;not null;comment:比较符:>= 或 ==" json:"operator"` // 比较符:>= 或 ==
|
||||
Threshold int64 `gorm:"column:threshold;not null;comment:阈值(数量或布尔首单)" json:"threshold"` // 阈值(数量或布尔首单)
|
||||
Window string `gorm:"column:window;not null;comment:时间窗口:activity_period|since_registration" json:"window"` // 时间窗口:activity_period|since_registration
|
||||
Repeatable int32 `gorm:"column:repeatable;not null;default:1;comment:是否每档一次:0否 1是" json:"repeatable"` // 是否每档一次:0否 1是
|
||||
Priority int32 `gorm:"column:priority;not null;comment:匹配优先级(数值越小越先匹配)" json:"priority"` // 匹配优先级(数值越小越先匹配)
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
|
||||
ExtraParams string `gorm:"column:extra_params;comment:额外参数配置(如消费门槛)" json:"extra_params"` // 额外参数配置(如消费门槛)
|
||||
ActivityID int32 `gorm:"column:activity_id;comment:活动ID" json:"activity_id"` // 活动ID
|
||||
Quota int32 `gorm:"column:quota;default:0;comment:总限额,0表示不限" json:"quota"` // 总限额,0表示不限
|
||||
ClaimedCount int32 `gorm:"column:claimed_count;default:0;comment:已领取数" json:"claimed_count"` // 已领取数
|
||||
}
|
||||
|
||||
// TableName TaskCenterTaskTiers's table name
|
||||
|
||||
@ -25,18 +25,20 @@ type Task struct {
|
||||
func (Task) TableName() string { return "task_center_tasks" }
|
||||
|
||||
type TaskTier struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||
TaskID int64 `gorm:"index;not null"`
|
||||
Metric string `gorm:"size:32;not null"`
|
||||
Operator string `gorm:"size:8;not null"`
|
||||
Threshold int64 `gorm:"not null"`
|
||||
Window string `gorm:"size:32;not null"`
|
||||
Repeatable int32 `gorm:"not null"`
|
||||
Priority int32 `gorm:"not null"`
|
||||
ActivityID int64 `gorm:"not null;default:0;index"` // 关联活动ID,0为全局
|
||||
ExtraParams datatypes.JSON `gorm:"type:json"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||
TaskID int64 `gorm:"index;not null"`
|
||||
Metric string `gorm:"size:32;not null"`
|
||||
Operator string `gorm:"size:8;not null"`
|
||||
Threshold int64 `gorm:"not null"`
|
||||
Window string `gorm:"size:32;not null"`
|
||||
Repeatable int32 `gorm:"not null"`
|
||||
Priority int32 `gorm:"not null"`
|
||||
ActivityID int64 `gorm:"not null;default:0;index"` // 关联活动ID,0为全局
|
||||
ExtraParams datatypes.JSON `gorm:"type:json"`
|
||||
Quota int32 `gorm:"not null;default:0"` // 总限额,0表示不限
|
||||
ClaimedCount int32 `gorm:"not null;default:0"` // 已领取数
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (TaskTier) TableName() string { return "task_center_task_tiers" }
|
||||
|
||||
@ -457,6 +457,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
appAuthApiRouter.GET("/users/:user_id/coupons", userHandler.ListUserCoupons())
|
||||
appAuthApiRouter.GET("/users/:user_id/coupons/stats", userHandler.GetUserCouponStats())
|
||||
appAuthApiRouter.GET("/users/:user_id/coupons/:user_coupon_id/usage", userHandler.ListUserCouponUsage())
|
||||
appAuthApiRouter.POST("/users/:user_id/coupons/:user_coupon_id/transfer", appapi.NewCouponTransfer(logger, db, userSvc).TransferCouponHandler())
|
||||
appAuthApiRouter.GET("/users/:user_id/points", userHandler.ListUserPoints())
|
||||
appAuthApiRouter.GET("/users/:user_id/points/balance", userHandler.GetUserPointsBalance())
|
||||
appAuthApiRouter.GET("/users/:user_id/stats", userHandler.GetUserStats())
|
||||
@ -486,6 +487,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
appAuthApiRouter.POST("/orders/:order_id/cancel", userHandler.CancelOrder())
|
||||
appAuthApiRouter.GET("/products", appapi.NewProduct(logger, db, userSvc).ListProductsForApp())
|
||||
appAuthApiRouter.GET("/products/:id", appapi.NewProduct(logger, db, userSvc).GetProductDetailForApp())
|
||||
appAuthApiRouter.GET("/product_categories", appapi.NewProductCategory(logger, db).ListProductCategoriesForApp())
|
||||
appAuthApiRouter.GET("/store/items", appapi.NewStore(logger, db, userSvc).ListStoreItemsForApp())
|
||||
appAuthApiRouter.GET("/lottery/result", activityHandler.LotteryResultByOrder())
|
||||
|
||||
|
||||
@ -126,15 +126,18 @@ type TaskTierInput struct {
|
||||
}
|
||||
|
||||
type TaskTierItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Metric string `json:"metric"`
|
||||
Operator string `json:"operator"`
|
||||
Threshold int64 `json:"threshold"`
|
||||
Window string `json:"window"`
|
||||
Repeatable int32 `json:"repeatable"`
|
||||
Priority int32 `json:"priority"`
|
||||
ActivityID int64 `json:"activity_id"`
|
||||
ExtraParams datatypes.JSON `json:"extra_params"`
|
||||
ID int64 `json:"id"`
|
||||
Metric string `json:"metric"`
|
||||
Operator string `json:"operator"`
|
||||
Threshold int64 `json:"threshold"`
|
||||
Window string `json:"window"`
|
||||
Repeatable int32 `json:"repeatable"`
|
||||
Priority int32 `json:"priority"`
|
||||
ActivityID int64 `json:"activity_id"`
|
||||
ExtraParams datatypes.JSON `json:"extra_params"`
|
||||
Quota int32 `json:"quota"` // 总限额,0表示不限
|
||||
ClaimedCount int32 `json:"claimed_count"` // 已领取数
|
||||
Remaining int32 `json:"remaining"` // 剩余可领,-1表示不限
|
||||
}
|
||||
|
||||
type TaskRewardInput struct {
|
||||
@ -251,7 +254,14 @@ func (s *service) ListTasks(ctx context.Context, in ListTasksInput) (items []Tas
|
||||
// 填充 Tiers
|
||||
out[i].Tiers = make([]TaskTierItem, len(v.Tiers))
|
||||
for j, t := range v.Tiers {
|
||||
out[i].Tiers[j] = TaskTierItem{ID: t.ID, Metric: t.Metric, Operator: t.Operator, Threshold: t.Threshold, Window: t.Window, Repeatable: t.Repeatable, Priority: t.Priority, ActivityID: t.ActivityID, ExtraParams: t.ExtraParams}
|
||||
remaining := int32(-1) // -1 表示不限
|
||||
if t.Quota > 0 {
|
||||
remaining = t.Quota - t.ClaimedCount
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
}
|
||||
out[i].Tiers[j] = TaskTierItem{ID: t.ID, Metric: t.Metric, Operator: t.Operator, Threshold: t.Threshold, Window: t.Window, Repeatable: t.Repeatable, Priority: t.Priority, ActivityID: t.ActivityID, ExtraParams: t.ExtraParams, Quota: t.Quota, ClaimedCount: t.ClaimedCount, Remaining: remaining}
|
||||
}
|
||||
// 填充 Rewards
|
||||
out[i].Rewards = make([]TaskRewardItem, len(v.Rewards))
|
||||
@ -442,7 +452,21 @@ func (s *service) ClaimTier(ctx context.Context, userID int64, taskID int64, tie
|
||||
return errors.New("任务条件未达成,无法领取")
|
||||
}
|
||||
|
||||
// 1. 先尝试发放奖励 (grantTierRewards 内部有幂等校验)
|
||||
// 2. 限额校验:如果设置了限额(quota > 0),需要原子性地增加 claimed_count
|
||||
if tier.Quota > 0 {
|
||||
result := s.repo.GetDbW().Model(&tcmodel.TaskTier{}).
|
||||
Where("id = ? AND claimed_count < quota", tierID).
|
||||
Update("claimed_count", gorm.Expr("claimed_count + 1"))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("奖励已领完")
|
||||
}
|
||||
s.logger.Info("ClaimTier: Quota check passed", zap.Int64("tier_id", tierID), zap.Int32("quota", tier.Quota))
|
||||
}
|
||||
|
||||
// 3. 先尝试发放奖励 (grantTierRewards 内部有幂等校验)
|
||||
// IDK logic inside grantTierRewards ensures we don't double grant.
|
||||
// We use "manual_claim" as source type.
|
||||
// IMPORTANT: Call this BEFORE updating the progress status to avoid "Claimed but not received" state if grant fails.
|
||||
@ -528,7 +552,14 @@ func (s *service) ListTaskTiers(ctx context.Context, taskID int64) ([]TaskTierIt
|
||||
}
|
||||
out := make([]TaskTierItem, len(rows))
|
||||
for i, v := range rows {
|
||||
out[i] = TaskTierItem{ID: v.ID, Metric: v.Metric, Operator: v.Operator, Threshold: v.Threshold, Window: v.Window, Repeatable: v.Repeatable, Priority: v.Priority, ActivityID: v.ActivityID, ExtraParams: v.ExtraParams}
|
||||
remaining := int32(-1) // -1 表示不限
|
||||
if v.Quota > 0 {
|
||||
remaining = v.Quota - v.ClaimedCount
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
}
|
||||
out[i] = TaskTierItem{ID: v.ID, Metric: v.Metric, Operator: v.Operator, Threshold: v.Threshold, Window: v.Window, Repeatable: v.Repeatable, Priority: v.Priority, ActivityID: v.ActivityID, ExtraParams: v.ExtraParams, Quota: v.Quota, ClaimedCount: v.ClaimedCount, Remaining: remaining}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
99
internal/service/user/coupon_transfer.go
Normal file
99
internal/service/user/coupon_transfer.go
Normal file
@ -0,0 +1,99 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TransferCoupon 转赠优惠券给另一个用户
|
||||
// fromUserID: 发送方用户ID
|
||||
// toUserID: 接收方用户ID
|
||||
// userCouponID: 用户持有的优惠券记录ID
|
||||
func (s *service) TransferCoupon(ctx context.Context, fromUserID, toUserID, userCouponID int64) error {
|
||||
if fromUserID <= 0 || toUserID <= 0 || userCouponID <= 0 {
|
||||
return fmt.Errorf("invalid_params")
|
||||
}
|
||||
if fromUserID == toUserID {
|
||||
return fmt.Errorf("cannot_transfer_to_self")
|
||||
}
|
||||
|
||||
// 1. 校验发送方持有该优惠券
|
||||
uc, err := s.readDB.UserCoupons.WithContext(ctx).
|
||||
Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).
|
||||
Where(s.readDB.UserCoupons.UserID.Eq(fromUserID)).
|
||||
First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("coupon_not_found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 校验优惠券状态为可用
|
||||
if uc.Status != 1 {
|
||||
return fmt.Errorf("coupon_not_available")
|
||||
}
|
||||
|
||||
// 3. 校验接收方用户存在
|
||||
_, err = s.readDB.Users.WithContext(ctx).Where(s.readDB.Users.ID.Eq(toUserID)).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("receiver_not_found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 执行转赠:更新 user_id
|
||||
db := s.repo.GetDbW()
|
||||
err = db.Transaction(func(tx *gorm.DB) error {
|
||||
// 更新优惠券归属
|
||||
if err := tx.Model(&model.UserCoupons{}).
|
||||
Where("id = ? AND user_id = ? AND status = 1", userCouponID, fromUserID).
|
||||
Update("user_id", toUserID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录转出日志
|
||||
transferOutLog := &model.UserCouponLedger{
|
||||
UserID: fromUserID,
|
||||
UserCouponID: userCouponID,
|
||||
ChangeAmount: 0, // 转赠不涉及金额变动
|
||||
BalanceAfter: uc.BalanceAmount,
|
||||
OrderID: 0,
|
||||
Action: "transfer_out",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := tx.Create(transferOutLog).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录转入日志
|
||||
transferInLog := &model.UserCouponLedger{
|
||||
UserID: toUserID,
|
||||
UserCouponID: userCouponID,
|
||||
ChangeAmount: 0,
|
||||
BalanceAfter: uc.BalanceAmount,
|
||||
OrderID: 0,
|
||||
Action: "transfer_in",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := tx.Create(transferInLog).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("[优惠券转赠] 成功: coupon_id=%d from=%d to=%d\n", userCouponID, fromUserID, toUserID)
|
||||
return nil
|
||||
}
|
||||
@ -82,6 +82,8 @@ type Service interface {
|
||||
GrantGamePass(ctx context.Context, userID int64, packageID int64, count int32, orderNo string) error
|
||||
// 邀请人绑定
|
||||
BindInviter(ctx context.Context, userID int64, in BindInviterInput) (*BindInviterOutput, error)
|
||||
// 优惠券转赠
|
||||
TransferCoupon(ctx context.Context, fromUserID, toUserID, userCouponID int64) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
|
||||
6
migrations/20260206_add_task_tier_quota.sql
Normal file
6
migrations/20260206_add_task_tier_quota.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- 任务限量领取功能 - 数据库迁移脚本
|
||||
-- 为 task_center_task_tiers 表添加限额字段
|
||||
|
||||
ALTER TABLE task_center_task_tiers
|
||||
ADD COLUMN quota INT DEFAULT 0 COMMENT '总限额,0表示不限' AFTER activity_id,
|
||||
ADD COLUMN claimed_count INT DEFAULT 0 COMMENT '已领取数' AFTER quota;
|
||||
Loading…
x
Reference in New Issue
Block a user