feat(activity): 新增独立奖品发放活动模块
新增独立奖品发放活动的后端表结构、服务、管理端接口与小程序领取接口,支持待领取查询、批量加入已处理、删除记录与成本汇总。
This commit is contained in:
parent
6127dc1a35
commit
e2364f3831
@ -5,6 +5,7 @@ import (
|
|||||||
"bindbox-game/internal/repository/mysql"
|
"bindbox-game/internal/repository/mysql"
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
activitysvc "bindbox-game/internal/service/activity"
|
activitysvc "bindbox-game/internal/service/activity"
|
||||||
|
prizegrantsvc "bindbox-game/internal/service/prize_grant_activity"
|
||||||
tasksvc "bindbox-game/internal/service/task_center"
|
tasksvc "bindbox-game/internal/service/task_center"
|
||||||
titlesvc "bindbox-game/internal/service/title"
|
titlesvc "bindbox-game/internal/service/title"
|
||||||
usersvc "bindbox-game/internal/service/user"
|
usersvc "bindbox-game/internal/service/user"
|
||||||
@ -25,6 +26,7 @@ type handler struct {
|
|||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
activityOrder activitysvc.ActivityOrderService // 活动订单服务
|
activityOrder activitysvc.ActivityOrderService // 活动订单服务
|
||||||
welfare welfaresvc.Service
|
welfare welfaresvc.Service
|
||||||
|
prizeGrant prizegrantsvc.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client, task tasksvc.Service) *handler {
|
func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client, task tasksvc.Service) *handler {
|
||||||
@ -41,5 +43,6 @@ func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client, task task
|
|||||||
redis: rdb,
|
redis: rdb,
|
||||||
activityOrder: activitysvc.NewActivityOrderService(logger, db),
|
activityOrder: activitysvc.NewActivityOrderService(logger, db),
|
||||||
welfare: welfaresvc.New(logger, db),
|
welfare: welfaresvc.New(logger, db),
|
||||||
|
prizeGrant: prizegrantsvc.New(logger, db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
internal/api/activity/prize_grant_activities_app.go
Normal file
38
internal/api/activity/prize_grant_activities_app.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"bindbox-game/internal/code"
|
||||||
|
"bindbox-game/internal/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *handler) GetPendingPrizeGrantActivity() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
userID := int64(ctx.SessionUserInfo().Id)
|
||||||
|
res, err := h.prizeGrant.GetPendingActivity(ctx.RequestContext(), userID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetActivityError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) ClaimPrizeGrantActivity() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil || activityID <= 0 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := int64(ctx.SessionUserInfo().Id)
|
||||||
|
res, err := h.prizeGrant.ClaimActivity(ctx.RequestContext(), activityID, userID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ import (
|
|||||||
gamesvc "bindbox-game/internal/service/game"
|
gamesvc "bindbox-game/internal/service/game"
|
||||||
livestreamsvc "bindbox-game/internal/service/livestream"
|
livestreamsvc "bindbox-game/internal/service/livestream"
|
||||||
productsvc "bindbox-game/internal/service/product"
|
productsvc "bindbox-game/internal/service/product"
|
||||||
|
prizegrantsvc "bindbox-game/internal/service/prize_grant_activity"
|
||||||
snapshotsvc "bindbox-game/internal/service/snapshot"
|
snapshotsvc "bindbox-game/internal/service/snapshot"
|
||||||
synthesissvc "bindbox-game/internal/service/synthesis"
|
synthesissvc "bindbox-game/internal/service/synthesis"
|
||||||
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
||||||
@ -43,6 +44,7 @@ type handler struct {
|
|||||||
synthesis synthesissvc.Service
|
synthesis synthesissvc.Service
|
||||||
financeSvc financesvc.Service // P&L service (read-only)
|
financeSvc financesvc.Service // P&L service (read-only)
|
||||||
welfare welfaresvc.Service
|
welfare welfaresvc.Service
|
||||||
|
prizeGrant prizegrantsvc.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler {
|
func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler {
|
||||||
@ -72,5 +74,6 @@ func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler
|
|||||||
synthesis: synthesissvc.New(db),
|
synthesis: synthesissvc.New(db),
|
||||||
financeSvc: financesvc.New(logger, db),
|
financeSvc: financesvc.New(logger, db),
|
||||||
welfare: welfaresvc.New(logger, db),
|
welfare: welfaresvc.New(logger, db),
|
||||||
|
prizeGrant: prizegrantsvc.New(logger, db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
233
internal/api/admin/prize_grant_activities_admin.go
Normal file
233
internal/api/admin/prize_grant_activities_admin.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"bindbox-game/internal/code"
|
||||||
|
"bindbox-game/internal/pkg/core"
|
||||||
|
"bindbox-game/internal/pkg/validation"
|
||||||
|
prizegrantsvc "bindbox-game/internal/service/prize_grant_activity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type savePrizeGrantActivityRequest struct {
|
||||||
|
Reason string `json:"reason" binding:"required"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Rewards []prizegrantsvc.RewardInput `json:"rewards" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type listPrizeGrantActivitiesRequest struct {
|
||||||
|
Reason string `form:"reason"`
|
||||||
|
Status string `form:"status"`
|
||||||
|
Page int `form:"page"`
|
||||||
|
PageSize int `form:"page_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type listPrizeGrantRecordsRequest struct {
|
||||||
|
Status string `form:"status"`
|
||||||
|
Keyword string `form:"keyword"`
|
||||||
|
Page int `form:"page"`
|
||||||
|
PageSize int `form:"page_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type markPrizeGrantUsersRequest struct {
|
||||||
|
UserIDs []int64 `json:"user_ids" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) CreatePrizeGrantActivity() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusForbidden, code.AuthorizationError, "无权限操作"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req := new(savePrizeGrantActivityRequest)
|
||||||
|
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := h.prizeGrant.CreateActivity(ctx.RequestContext(), prizegrantsvc.SaveActivityRequest{Reason: req.Reason, Status: req.Status, Rewards: req.Rewards})
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateActivityError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) UpdatePrizeGrantActivity() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusForbidden, code.AuthorizationError, "无权限操作"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req := new(savePrizeGrantActivityRequest)
|
||||||
|
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.prizeGrant.UpdateActivity(ctx.RequestContext(), id, prizegrantsvc.SaveActivityRequest{Reason: req.Reason, Status: req.Status, Rewards: req.Rewards}); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) DeletePrizeGrantActivity() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusForbidden, code.AuthorizationError, "无权限操作"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.prizeGrant.DeleteActivity(ctx.RequestContext(), id); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteActivityError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(simpleMessageResponse{Message: "删除成功"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) ListPrizeGrantActivities() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
req := new(listPrizeGrantActivitiesRequest)
|
||||||
|
if err := ctx.ShouldBindForm(req); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := h.prizeGrant.ListActivities(ctx.RequestContext(), prizegrantsvc.ListActivitiesRequest{Reason: req.Reason, Status: req.Status, Page: req.Page, PageSize: req.PageSize})
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) GetPrizeGrantCostSummary() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
res, err := h.prizeGrant.GetCostSummary(ctx.RequestContext())
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) GetPrizeGrantActivity() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := h.prizeGrant.GetActivity(ctx.RequestContext(), id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetActivityError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) ListPrizeGrantUserRecords() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req := new(listPrizeGrantRecordsRequest)
|
||||||
|
if err := ctx.ShouldBindForm(req); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := h.prizeGrant.ListUserRecords(ctx.RequestContext(), id, strings.TrimSpace(req.Status), strings.TrimSpace(req.Keyword), req.Page, req.PageSize)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) MarkPrizeGrantUsersProcessed() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusForbidden, code.AuthorizationError, "无权限操作"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req := new(markPrizeGrantUsersRequest)
|
||||||
|
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adminID := int64(ctx.SessionUserInfo().Id)
|
||||||
|
if err := h.prizeGrant.MarkUsersProcessed(ctx.RequestContext(), id, adminID, req.UserIDs); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) MarkAllPrizeGrantUsersProcessed() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusForbidden, code.AuthorizationError, "无权限操作"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adminID := int64(ctx.SessionUserInfo().Id)
|
||||||
|
count, err := h.prizeGrant.MarkAllUsersProcessed(ctx.RequestContext(), id, adminID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(map[string]any{"message": "操作成功", "count": count})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) DeletePrizeGrantUserRecord() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusForbidden, code.AuthorizationError, "无权限操作"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil || activityID <= 0 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recordID, err := strconv.ParseInt(ctx.Param("record_id"), 10, 64)
|
||||||
|
if err != nil || recordID <= 0 {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "记录ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.prizeGrant.DeleteUserRecord(ctx.RequestContext(), activityID, recordID); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteActivityError, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Payload(simpleMessageResponse{Message: "删除成功"})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -276,7 +276,19 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
|||||||
adminAuthApiRouter.PUT("/system/configs/:id", adminHandler.ModifySystemConfig())
|
adminAuthApiRouter.PUT("/system/configs/:id", adminHandler.ModifySystemConfig())
|
||||||
adminAuthApiRouter.DELETE("/system/configs/:id", adminHandler.DeleteSystemConfig())
|
adminAuthApiRouter.DELETE("/system/configs/:id", adminHandler.DeleteSystemConfig())
|
||||||
|
|
||||||
// 用户管理
|
|
||||||
|
// 奖品发放活动
|
||||||
|
adminAuthApiRouter.POST("/prize-grant-activities", adminHandler.CreatePrizeGrantActivity())
|
||||||
|
adminAuthApiRouter.GET("/prize-grant-activities", adminHandler.ListPrizeGrantActivities())
|
||||||
|
adminAuthApiRouter.GET("/prize-grant-activities/cost-summary", adminHandler.GetPrizeGrantCostSummary())
|
||||||
|
adminAuthApiRouter.GET("/prize-grant-activities/:id", adminHandler.GetPrizeGrantActivity())
|
||||||
|
adminAuthApiRouter.PUT("/prize-grant-activities/:id", adminHandler.UpdatePrizeGrantActivity())
|
||||||
|
adminAuthApiRouter.DELETE("/prize-grant-activities/:id", adminHandler.DeletePrizeGrantActivity())
|
||||||
|
adminAuthApiRouter.GET("/prize-grant-activities/:id/user-records", adminHandler.ListPrizeGrantUserRecords())
|
||||||
|
adminAuthApiRouter.DELETE("/prize-grant-activities/:id/user-records/:record_id", adminHandler.DeletePrizeGrantUserRecord())
|
||||||
|
adminAuthApiRouter.POST("/prize-grant-activities/:id/mark-processed", adminHandler.MarkPrizeGrantUsersProcessed())
|
||||||
|
adminAuthApiRouter.POST("/prize-grant-activities/:id/mark-all-processed", adminHandler.MarkAllPrizeGrantUsersProcessed())
|
||||||
|
// 用户管理
|
||||||
adminAuthApiRouter.GET("/users", intc.RequireAdminAction("user:view"), adminHandler.ListAppUsers())
|
adminAuthApiRouter.GET("/users", intc.RequireAdminAction("user:view"), adminHandler.ListAppUsers())
|
||||||
adminAuthApiRouter.GET("/users/optimized", intc.RequireAdminAction("user:view"), adminHandler.ListAppUsersOptimized()) // 优化版本(性能提升83%)
|
adminAuthApiRouter.GET("/users/optimized", intc.RequireAdminAction("user:view"), adminHandler.ListAppUsersOptimized()) // 优化版本(性能提升83%)
|
||||||
adminAuthApiRouter.GET("/users/:user_id/invites", intc.RequireAdminAction("user:view"), adminHandler.ListUserInvites())
|
adminAuthApiRouter.GET("/users/:user_id/invites", intc.RequireAdminAction("user:view"), adminHandler.ListUserInvites())
|
||||||
@ -529,7 +541,10 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
|||||||
appAuthApiRouter.GET("/welfare-activities/:id/my", activityHandler.GetWelfareActivity())
|
appAuthApiRouter.GET("/welfare-activities/:id/my", activityHandler.GetWelfareActivity())
|
||||||
appAuthApiRouter.POST("/welfare-activities/:id/join", activityHandler.JoinWelfareActivity())
|
appAuthApiRouter.POST("/welfare-activities/:id/join", activityHandler.JoinWelfareActivity())
|
||||||
|
|
||||||
// 任务中心 APP 端
|
|
||||||
|
appAuthApiRouter.GET("/prize-grant-activities/pending", activityHandler.GetPendingPrizeGrantActivity())
|
||||||
|
appAuthApiRouter.POST("/prize-grant-activities/:id/claim", activityHandler.ClaimPrizeGrantActivity())
|
||||||
|
// 任务中心 APP 端
|
||||||
appAuthApiRouter.GET("/task-center/tasks", taskCenterHandler.ListTasksForApp())
|
appAuthApiRouter.GET("/task-center/tasks", taskCenterHandler.ListTasksForApp())
|
||||||
appAuthApiRouter.GET("/task-center/tasks/:id/progress/:user_id", taskCenterHandler.GetTaskProgressForApp())
|
appAuthApiRouter.GET("/task-center/tasks/:id/progress/:user_id", taskCenterHandler.GetTaskProgressForApp())
|
||||||
appAuthApiRouter.POST("/task-center/tasks/:id/claim/:user_id", taskCenterHandler.ClaimTaskTierForApp())
|
appAuthApiRouter.POST("/task-center/tasks/:id/claim/:user_id", taskCenterHandler.ClaimTaskTierForApp())
|
||||||
|
|||||||
547
internal/service/prize_grant_activity/prize_grant_activity.go
Normal file
547
internal/service/prize_grant_activity/prize_grant_activity.go
Normal file
@ -0,0 +1,547 @@
|
|||||||
|
package prize_grant_activity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bindbox-game/internal/pkg/logger"
|
||||||
|
"bindbox-game/internal/repository/mysql"
|
||||||
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
CreateActivity(ctx context.Context, req SaveActivityRequest) (*Activity, error)
|
||||||
|
UpdateActivity(ctx context.Context, id int64, req SaveActivityRequest) error
|
||||||
|
DeleteActivity(ctx context.Context, id int64) error
|
||||||
|
GetActivity(ctx context.Context, id int64) (*ActivityDetail, error)
|
||||||
|
ListActivities(ctx context.Context, req ListActivitiesRequest) (*ListActivitiesResponse, error)
|
||||||
|
GetPendingActivity(ctx context.Context, userID int64) (*PendingActivityResponse, error)
|
||||||
|
ClaimActivity(ctx context.Context, activityID int64, userID int64) (*ClaimResponse, error)
|
||||||
|
MarkUsersProcessed(ctx context.Context, activityID int64, adminID int64, userIDs []int64) error
|
||||||
|
MarkAllUsersProcessed(ctx context.Context, activityID int64, adminID int64) (int64, error)
|
||||||
|
DeleteUserRecord(ctx context.Context, activityID int64, recordID int64) error
|
||||||
|
GetCostSummary(ctx context.Context) (*CostSummary, error)
|
||||||
|
ListUserRecords(ctx context.Context, activityID int64, status string, keyword string, page int, pageSize int) (map[string]any, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
logger logger.CustomLogger
|
||||||
|
repo mysql.Repo
|
||||||
|
writeDB *dao.Query
|
||||||
|
readDB *dao.Query
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(log logger.CustomLogger, repo mysql.Repo) Service {
|
||||||
|
return &service{
|
||||||
|
logger: log,
|
||||||
|
repo: repo,
|
||||||
|
writeDB: dao.Use(repo.GetDbW()),
|
||||||
|
readDB: dao.Use(repo.GetDbR()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) CreateActivity(ctx context.Context, req SaveActivityRequest) (*Activity, error) {
|
||||||
|
if err := validateSaveRequest(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item := &Activity{Reason: strings.TrimSpace(req.Reason), Status: normalizeStatus(req.Status)}
|
||||||
|
err := s.repo.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Create(item).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.replaceRewards(ctx, tx, item.ID, req.Rewards)
|
||||||
|
})
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateActivity(ctx context.Context, id int64, req SaveActivityRequest) error {
|
||||||
|
if id <= 0 {
|
||||||
|
return errors.New("活动ID无效")
|
||||||
|
}
|
||||||
|
if err := validateSaveRequest(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Model(&Activity{}).Where("id = ?", id).Updates(map[string]any{
|
||||||
|
"reason": strings.TrimSpace(req.Reason),
|
||||||
|
"status": normalizeStatus(req.Status),
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.replaceRewards(ctx, tx, id, req.Rewards)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) DeleteActivity(ctx context.Context, id int64) error {
|
||||||
|
if id <= 0 {
|
||||||
|
return errors.New("活动ID无效")
|
||||||
|
}
|
||||||
|
return s.repo.GetDbW().WithContext(ctx).Where("id = ?", id).Delete(&Activity{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ListActivities(ctx context.Context, req ListActivitiesRequest) (*ListActivitiesResponse, error) {
|
||||||
|
if req.Page <= 0 {
|
||||||
|
req.Page = 1
|
||||||
|
}
|
||||||
|
if req.PageSize <= 0 {
|
||||||
|
req.PageSize = 20
|
||||||
|
}
|
||||||
|
if req.PageSize > 100 {
|
||||||
|
req.PageSize = 100
|
||||||
|
}
|
||||||
|
db := s.repo.GetDbR().WithContext(ctx).Model(&Activity{})
|
||||||
|
if strings.TrimSpace(req.Reason) != "" {
|
||||||
|
db = db.Where("reason LIKE ?", "%"+strings.TrimSpace(req.Reason)+"%")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.Status) != "" {
|
||||||
|
db = db.Where("status = ?", strings.TrimSpace(req.Status))
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var rows []Activity
|
||||||
|
if err := db.Order("id DESC").Offset((req.Page-1)*req.PageSize).Limit(req.PageSize).Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list := make([]ActivityListItem, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
item := ActivityListItem{Activity: row}
|
||||||
|
s.repo.GetDbR().WithContext(ctx).Table("prize_grant_activity_rewards").Where("activity_id = ?", row.ID).Count(&item.RewardCount)
|
||||||
|
s.repo.GetDbR().WithContext(ctx).Table("prize_grant_activity_user_records").Where("activity_id = ? AND status = ?", row.ID, RecordStatusClaimed).Count(&item.ClaimedCount)
|
||||||
|
s.repo.GetDbR().WithContext(ctx).Table("prize_grant_activity_user_records").Where("activity_id = ? AND status = ?", row.ID, RecordStatusProcessed).Count(&item.ProcessedCount)
|
||||||
|
cost, err := s.calculateClaimedCost(ctx, row.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item.CostCents = cost
|
||||||
|
list = append(list, item)
|
||||||
|
}
|
||||||
|
return &ListActivitiesResponse{Page: req.Page, PageSize: req.PageSize, Total: total, List: list}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetActivity(ctx context.Context, id int64) (*ActivityDetail, error) {
|
||||||
|
var item Activity
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rewards, err := s.loadRewardViews(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
detail := &ActivityDetail{Activity: item, Rewards: rewards}
|
||||||
|
s.repo.GetDbR().WithContext(ctx).Table("prize_grant_activity_user_records").Where("activity_id = ? AND status = ?", id, RecordStatusClaimed).Count(&detail.ClaimedCount)
|
||||||
|
s.repo.GetDbR().WithContext(ctx).Table("prize_grant_activity_user_records").Where("activity_id = ? AND status = ?", id, RecordStatusProcessed).Count(&detail.ProcessedCount)
|
||||||
|
var totalUsers int64
|
||||||
|
s.repo.GetDbR().WithContext(ctx).Model(&model.Users{}).Count(&totalUsers)
|
||||||
|
detail.PendingCount = totalUsers - detail.ClaimedCount - detail.ProcessedCount
|
||||||
|
if detail.PendingCount < 0 {
|
||||||
|
detail.PendingCount = 0
|
||||||
|
}
|
||||||
|
cost, err := s.calculateClaimedCost(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
detail.CostCents = cost
|
||||||
|
return detail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetPendingActivity(ctx context.Context, userID int64) (*PendingActivityResponse, error) {
|
||||||
|
if userID <= 0 {
|
||||||
|
return &PendingActivityResponse{HasPending: false}, nil
|
||||||
|
}
|
||||||
|
var item Activity
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).
|
||||||
|
Where("status = ?", "active").
|
||||||
|
Where("NOT EXISTS (SELECT 1 FROM prize_grant_activity_user_records r WHERE r.activity_id = prize_grant_activities.id AND r.user_id = ?)", userID).
|
||||||
|
Order("id DESC").
|
||||||
|
First(&item).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return &PendingActivityResponse{HasPending: false}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rewards, err := s.loadRewardViews(ctx, item.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &PendingActivityResponse{HasPending: true, Activity: &PendingActivity{ID: item.ID, Reason: item.Reason, Rewards: rewards}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ClaimActivity(ctx context.Context, activityID int64, userID int64) (*ClaimResponse, error) {
|
||||||
|
if activityID <= 0 || userID <= 0 {
|
||||||
|
return nil, errors.New("参数无效")
|
||||||
|
}
|
||||||
|
var recordID int64
|
||||||
|
err := s.repo.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var item Activity
|
||||||
|
if err := tx.WithContext(ctx).Where("id = ? AND status = ?", activityID, "active").First(&item).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var existing UserRecord
|
||||||
|
err := tx.WithContext(ctx).Where("activity_id = ? AND user_id = ?", activityID, userID).First(&existing).Error
|
||||||
|
if err == nil {
|
||||||
|
return errors.New("该活动已处理")
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var rewards []Reward
|
||||||
|
if err := tx.WithContext(ctx).Where("activity_id = ?", activityID).Order("sort ASC, id ASC").Find(&rewards).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(rewards) == 0 {
|
||||||
|
return errors.New("活动未配置奖品")
|
||||||
|
}
|
||||||
|
for _, reward := range rewards {
|
||||||
|
for i := int32(0); i < reward.QuantityPerClaim; i++ {
|
||||||
|
if err := s.grantReward(ctx, tx, activityID, userID, reward); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
record := &UserRecord{ActivityID: activityID, UserID: userID, Status: RecordStatusClaimed, ClaimedAt: &now, OperatorAdminID: 0}
|
||||||
|
if err := tx.WithContext(ctx).Create(record).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
recordID = record.ID
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ClaimResponse{ActivityID: activityID, RecordID: recordID, Status: RecordStatusClaimed}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) MarkUsersProcessed(ctx context.Context, activityID int64, adminID int64, userIDs []int64) error {
|
||||||
|
if activityID <= 0 {
|
||||||
|
return errors.New("活动ID无效")
|
||||||
|
}
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return errors.New("用户不能为空")
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
return s.repo.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, userID := range userIDs {
|
||||||
|
if userID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
record := &UserRecord{ActivityID: activityID, UserID: userID, Status: RecordStatusProcessed, ProcessedAt: &now, OperatorAdminID: adminID}
|
||||||
|
if err := tx.WithContext(ctx).Clauses(clauseOnConflictUpdateProcessed()).Create(record).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) MarkAllUsersProcessed(ctx context.Context, activityID int64, adminID int64) (int64, error) {
|
||||||
|
var users []model.Users
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Find(&users).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
ids := make([]int64, 0, len(users))
|
||||||
|
for _, user := range users {
|
||||||
|
ids = append(ids, user.ID)
|
||||||
|
}
|
||||||
|
if err := s.MarkUsersProcessed(ctx, activityID, adminID, ids); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int64(len(ids)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) DeleteUserRecord(ctx context.Context, activityID int64, recordID int64) error {
|
||||||
|
if activityID <= 0 || recordID <= 0 {
|
||||||
|
return errors.New("参数无效")
|
||||||
|
}
|
||||||
|
return s.repo.GetDbW().WithContext(ctx).Where("id = ? AND activity_id = ?", recordID, activityID).Delete(&UserRecord{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetCostSummary(ctx context.Context) (*CostSummary, error) {
|
||||||
|
var activities []Activity
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Find(&activities).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
for _, activity := range activities {
|
||||||
|
cost, err := s.calculateClaimedCost(ctx, activity.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
total += cost
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Table("prize_grant_activity_user_records").Where("status = ?", RecordStatusClaimed).Count(&count).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &CostSummary{CostCents: total, Count: count}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ListUserRecords(ctx context.Context, activityID int64, status string, keyword string, page int, pageSize int) (map[string]any, error) {
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
if pageSize > 100 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
db := s.repo.GetDbR().WithContext(ctx).Table("prize_grant_activity_user_records r").
|
||||||
|
Select("r.id, r.activity_id, r.user_id, r.status, r.claimed_at, r.processed_at, r.operator_admin_id, r.created_at, r.updated_at, u.nickname, u.mobile").
|
||||||
|
Joins("LEFT JOIN users u ON u.id = r.user_id").
|
||||||
|
Where("r.activity_id = ?", activityID)
|
||||||
|
if strings.TrimSpace(status) != "" {
|
||||||
|
db = db.Where("r.status = ?", strings.TrimSpace(status))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(keyword) != "" {
|
||||||
|
kw := "%" + strings.TrimSpace(keyword) + "%"
|
||||||
|
db = db.Where("CAST(r.user_id AS CHAR) LIKE ? OR u.nickname LIKE ? OR u.mobile LIKE ?", kw, kw, kw)
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var rows []map[string]any
|
||||||
|
if err := db.Order("r.id DESC").Offset((page-1)*pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]any{"page": page, "page_size": pageSize, "total": total, "list": rows}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSaveRequest(req SaveActivityRequest) error {
|
||||||
|
if strings.TrimSpace(req.Reason) == "" {
|
||||||
|
return errors.New("发奖原因不能为空")
|
||||||
|
}
|
||||||
|
if len(req.Rewards) == 0 {
|
||||||
|
return errors.New("至少配置一个奖品")
|
||||||
|
}
|
||||||
|
for _, reward := range req.Rewards {
|
||||||
|
if reward.RewardRefID <= 0 {
|
||||||
|
return errors.New("奖品资源ID无效")
|
||||||
|
}
|
||||||
|
if reward.QuantityPerClaim <= 0 {
|
||||||
|
return errors.New("领取数量必须大于0")
|
||||||
|
}
|
||||||
|
if reward.RewardType != RewardTypeProduct && reward.RewardType != RewardTypeCoupon && reward.RewardType != RewardTypeItemCard {
|
||||||
|
return fmt.Errorf("不支持的奖品类型: %s", reward.RewardType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeStatus(status string) string {
|
||||||
|
if strings.TrimSpace(status) == "active" {
|
||||||
|
return "active"
|
||||||
|
}
|
||||||
|
return "inactive"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) replaceRewards(ctx context.Context, tx *gorm.DB, activityID int64, inputs []RewardInput) error {
|
||||||
|
if err := tx.WithContext(ctx).Where("activity_id = ?", activityID).Delete(&Reward{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
items := make([]Reward, 0, len(inputs))
|
||||||
|
for idx, input := range inputs {
|
||||||
|
sort := input.Sort
|
||||||
|
if sort == 0 {
|
||||||
|
sort = int32(idx + 1)
|
||||||
|
}
|
||||||
|
items = append(items, Reward{ActivityID: activityID, RewardType: input.RewardType, RewardRefID: input.RewardRefID, QuantityPerClaim: input.QuantityPerClaim, Sort: sort})
|
||||||
|
}
|
||||||
|
return tx.WithContext(ctx).Create(&items).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) loadRewardViews(ctx context.Context, activityID int64) ([]RewardView, error) {
|
||||||
|
var rewards []Reward
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Where("activity_id = ?", activityID).Order("sort ASC, id ASC").Find(&rewards).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
views := make([]RewardView, 0, len(rewards))
|
||||||
|
for _, reward := range rewards {
|
||||||
|
view := RewardView{RewardType: reward.RewardType, RewardRefID: reward.RewardRefID, Quantity: reward.QuantityPerClaim}
|
||||||
|
switch reward.RewardType {
|
||||||
|
case RewardTypeProduct:
|
||||||
|
var product model.Products
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", reward.RewardRefID).First(&product).Error; err == nil {
|
||||||
|
view.Name = product.Name
|
||||||
|
view.ValueCents = product.Price
|
||||||
|
images := parseProductImages(product.ImagesJSON)
|
||||||
|
if len(images) > 0 {
|
||||||
|
view.Image = images[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case RewardTypeCoupon:
|
||||||
|
var coupon model.SystemCoupons
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", reward.RewardRefID).First(&coupon).Error; err == nil {
|
||||||
|
view.Name = coupon.Name
|
||||||
|
view.ValueCents = coupon.DiscountValue
|
||||||
|
}
|
||||||
|
case RewardTypeItemCard:
|
||||||
|
var card model.SystemItemCards
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", reward.RewardRefID).First(&card).Error; err == nil {
|
||||||
|
view.Name = card.Name
|
||||||
|
view.ValueCents = card.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
views = append(views, view)
|
||||||
|
}
|
||||||
|
return views, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) calculateClaimedCost(ctx context.Context, activityID int64) (int64, error) {
|
||||||
|
var rewards []Reward
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Where("activity_id = ?", activityID).Find(&rewards).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var claimedCount int64
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Table("prize_grant_activity_user_records").Where("activity_id = ? AND status = ?", activityID, RecordStatusClaimed).Count(&claimedCount).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var costPerClaim int64
|
||||||
|
for _, reward := range rewards {
|
||||||
|
switch reward.RewardType {
|
||||||
|
case RewardTypeProduct:
|
||||||
|
var product model.Products
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", reward.RewardRefID).First(&product).Error; err == nil {
|
||||||
|
unit := product.CostPrice
|
||||||
|
if unit <= 0 {
|
||||||
|
unit = product.Price
|
||||||
|
}
|
||||||
|
costPerClaim += unit * int64(reward.QuantityPerClaim)
|
||||||
|
}
|
||||||
|
case RewardTypeCoupon:
|
||||||
|
var coupon model.SystemCoupons
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", reward.RewardRefID).First(&coupon).Error; err == nil {
|
||||||
|
costPerClaim += coupon.DiscountValue * int64(reward.QuantityPerClaim)
|
||||||
|
}
|
||||||
|
case RewardTypeItemCard:
|
||||||
|
var card model.SystemItemCards
|
||||||
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", reward.RewardRefID).First(&card).Error; err == nil {
|
||||||
|
costPerClaim += card.Price * int64(reward.QuantityPerClaim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return costPerClaim * claimedCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) grantReward(ctx context.Context, tx *gorm.DB, activityID int64, userID int64, reward Reward) error {
|
||||||
|
switch reward.RewardType {
|
||||||
|
case RewardTypeCoupon:
|
||||||
|
var tpl model.SystemCoupons
|
||||||
|
if err := tx.WithContext(ctx).Where("id = ? AND status = 1", reward.RewardRefID).First(&tpl).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
item := &model.UserCoupons{UserID: userID, CouponID: tpl.ID, Status: 1}
|
||||||
|
if !tpl.ValidStart.IsZero() {
|
||||||
|
item.ValidStart = tpl.ValidStart
|
||||||
|
} else {
|
||||||
|
item.ValidStart = time.Now()
|
||||||
|
}
|
||||||
|
if !tpl.ValidEnd.IsZero() {
|
||||||
|
item.ValidEnd = tpl.ValidEnd
|
||||||
|
}
|
||||||
|
do := tx.WithContext(ctx).Omit("used_at", "used_order_id")
|
||||||
|
if tpl.ValidEnd.IsZero() {
|
||||||
|
do = do.Omit("valid_end")
|
||||||
|
}
|
||||||
|
if err := do.Create(item).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
balance := int64(0)
|
||||||
|
if tpl.DiscountType == 1 && tpl.DiscountValue > 0 {
|
||||||
|
balance = tpl.DiscountValue
|
||||||
|
}
|
||||||
|
return tx.WithContext(ctx).Model(&model.UserCoupons{}).Where("id = ?", item.ID).Update("balance_amount", balance).Error
|
||||||
|
case RewardTypeItemCard:
|
||||||
|
var card model.SystemItemCards
|
||||||
|
if err := tx.WithContext(ctx).Where("id = ? AND status = 1", reward.RewardRefID).First(&card).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
item := &model.UserItemCards{UserID: userID, CardID: card.ID, Status: 1, Remark: "奖品发放活动领取"}
|
||||||
|
if !card.ValidStart.IsZero() {
|
||||||
|
item.ValidStart = card.ValidStart
|
||||||
|
} else {
|
||||||
|
item.ValidStart = now
|
||||||
|
}
|
||||||
|
if !card.ValidEnd.IsZero() {
|
||||||
|
item.ValidEnd = card.ValidEnd
|
||||||
|
}
|
||||||
|
do := tx.WithContext(ctx).Omit("used_at", "used_draw_log_id", "used_activity_id", "used_issue_id")
|
||||||
|
if card.ValidEnd.IsZero() {
|
||||||
|
do = do.Omit("valid_end")
|
||||||
|
}
|
||||||
|
return do.Create(item).Error
|
||||||
|
default:
|
||||||
|
var product model.Products
|
||||||
|
if err := tx.WithContext(ctx).Where("id = ?", reward.RewardRefID).First(&product).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if product.Stock <= 0 {
|
||||||
|
return fmt.Errorf("商品库存不足: %s", product.Name)
|
||||||
|
}
|
||||||
|
result := tx.WithContext(ctx).Model(&model.Products{}).Where("id = ? AND stock > 0", product.ID).Update("stock", gorm.Expr("stock - 1"))
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return fmt.Errorf("商品库存不足: %s", product.Name)
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
minValidTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
order := &model.Orders{OrderNo: fmt.Sprintf("PG%d%d%04d", activityID, now.Unix(), rand.Intn(10000)), UserID: userID, SourceType: 6, Status: 2, PaidAt: now, CancelledAt: minValidTime, Remark: "奖品发放活动领取", CreatedAt: now, UpdatedAt: now}
|
||||||
|
if err := tx.WithContext(ctx).Create(order).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
orderItem := &model.OrderItems{OrderID: order.ID, ProductID: product.ID, Title: product.Name, Quantity: 1, ProductImages: product.ImagesJSON, Status: 1}
|
||||||
|
if err := tx.WithContext(ctx).Create(orderItem).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value := product.CostPrice
|
||||||
|
if value <= 0 {
|
||||||
|
value = product.Price
|
||||||
|
}
|
||||||
|
inventory := &model.UserInventory{UserID: userID, ProductID: product.ID, ValueCents: value, ValueSource: 2, ValueSnapshotAt: now, OrderID: order.ID, ActivityID: activityID, Status: 1, Remark: "奖品发放活动领取"}
|
||||||
|
return tx.WithContext(ctx).Create(inventory).Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clauseOnConflictUpdateProcessed() clause.OnConflict {
|
||||||
|
return clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "activity_id"}, {Name: "user_id"}},
|
||||||
|
DoUpdates: clause.Assignments(map[string]any{
|
||||||
|
"status": RecordStatusProcessed,
|
||||||
|
"processed_at": gorm.Expr("VALUES(processed_at)"),
|
||||||
|
"operator_admin_id": gorm.Expr("VALUES(operator_admin_id)"),
|
||||||
|
"updated_at": gorm.Expr("CURRENT_TIMESTAMP(3)"),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProductImages(raw string) []string {
|
||||||
|
if strings.TrimSpace(raw) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var arr []string
|
||||||
|
if err := json.Unmarshal([]byte(raw), &arr); err == nil {
|
||||||
|
result := make([]string, 0, len(arr))
|
||||||
|
for _, item := range arr {
|
||||||
|
item = strings.TrimSpace(item)
|
||||||
|
if item != "" {
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
124
internal/service/prize_grant_activity/types.go
Normal file
124
internal/service/prize_grant_activity/types.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package prize_grant_activity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
RewardTypeProduct = "product"
|
||||||
|
RewardTypeItemCard = "item_card"
|
||||||
|
RewardTypeCoupon = "coupon"
|
||||||
|
|
||||||
|
RecordStatusClaimed = "claimed"
|
||||||
|
RecordStatusProcessed = "processed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Activity struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
Reason string `gorm:"column:reason" json:"reason"`
|
||||||
|
Status string `gorm:"column:status" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Activity) TableName() string { return "prize_grant_activities" }
|
||||||
|
|
||||||
|
type Reward struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id" json:"activity_id"`
|
||||||
|
RewardType string `gorm:"column:reward_type" json:"reward_type"`
|
||||||
|
RewardRefID int64 `gorm:"column:reward_ref_id" json:"reward_ref_id"`
|
||||||
|
QuantityPerClaim int32 `gorm:"column:quantity_per_claim" json:"quantity_per_claim"`
|
||||||
|
Sort int32 `gorm:"column:sort" json:"sort"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Reward) TableName() string { return "prize_grant_activity_rewards" }
|
||||||
|
|
||||||
|
type UserRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id" json:"activity_id"`
|
||||||
|
UserID int64 `gorm:"column:user_id" json:"user_id"`
|
||||||
|
Status string `gorm:"column:status" json:"status"`
|
||||||
|
ClaimedAt *time.Time `gorm:"column:claimed_at" json:"claimed_at,omitempty"`
|
||||||
|
ProcessedAt *time.Time `gorm:"column:processed_at" json:"processed_at,omitempty"`
|
||||||
|
OperatorAdminID int64 `gorm:"column:operator_admin_id" json:"operator_admin_id"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UserRecord) TableName() string { return "prize_grant_activity_user_records" }
|
||||||
|
|
||||||
|
type SaveActivityRequest struct {
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Rewards []RewardInput `json:"rewards"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RewardInput struct {
|
||||||
|
RewardType string `json:"reward_type"`
|
||||||
|
RewardRefID int64 `json:"reward_ref_id"`
|
||||||
|
QuantityPerClaim int32 `json:"quantity_per_claim"`
|
||||||
|
Sort int32 `json:"sort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListActivitiesRequest struct {
|
||||||
|
Reason string
|
||||||
|
Status string
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityListItem struct {
|
||||||
|
Activity
|
||||||
|
RewardCount int64 `json:"reward_count"`
|
||||||
|
ClaimedCount int64 `json:"claimed_count"`
|
||||||
|
ProcessedCount int64 `json:"processed_count"`
|
||||||
|
CostCents int64 `json:"cost_cents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CostSummary struct {
|
||||||
|
CostCents int64 `json:"cost_cents"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListActivitiesResponse struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
List []ActivityListItem `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RewardView struct {
|
||||||
|
RewardType string `json:"reward_type"`
|
||||||
|
RewardRefID int64 `json:"reward_ref_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
ValueCents int64 `json:"value_cents"`
|
||||||
|
Quantity int32 `json:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityDetail struct {
|
||||||
|
Activity
|
||||||
|
Rewards []RewardView `json:"rewards"`
|
||||||
|
ClaimedCount int64 `json:"claimed_count"`
|
||||||
|
ProcessedCount int64 `json:"processed_count"`
|
||||||
|
PendingCount int64 `json:"pending_count"`
|
||||||
|
CostCents int64 `json:"cost_cents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PendingActivityResponse struct {
|
||||||
|
HasPending bool `json:"has_pending"`
|
||||||
|
Activity *PendingActivity `json:"activity,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PendingActivity struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Rewards []RewardView `json:"rewards"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaimResponse struct {
|
||||||
|
ActivityID int64 `json:"activity_id"`
|
||||||
|
RecordID int64 `json:"record_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
39
migrations/20260507_prize_grant_activities.sql
Normal file
39
migrations/20260507_prize_grant_activities.sql
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `prize_grant_activities` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`reason` VARCHAR(255) NOT NULL COMMENT '发奖原因',
|
||||||
|
`status` VARCHAR(16) NOT NULL DEFAULT 'inactive' COMMENT '状态:active/inactive',
|
||||||
|
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_pga_status_id` (`status`, `id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='奖品发放活动';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `prize_grant_activity_rewards` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`activity_id` BIGINT NOT NULL COMMENT '活动ID',
|
||||||
|
`reward_type` VARCHAR(32) NOT NULL COMMENT '奖品类型:product/item_card/coupon',
|
||||||
|
`reward_ref_id` BIGINT NOT NULL COMMENT '奖品资源ID',
|
||||||
|
`quantity_per_claim` INT NOT NULL DEFAULT 1 COMMENT '每次领取数量',
|
||||||
|
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_pgar_activity` (`activity_id`, `sort`, `id`),
|
||||||
|
KEY `idx_pgar_reward` (`reward_type`, `reward_ref_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='奖品发放活动奖品';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `prize_grant_activity_user_records` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`activity_id` BIGINT NOT NULL COMMENT '活动ID',
|
||||||
|
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||||
|
`status` VARCHAR(16) NOT NULL COMMENT '状态:claimed/processed',
|
||||||
|
`claimed_at` DATETIME(3) NULL DEFAULT NULL COMMENT '领取时间',
|
||||||
|
`processed_at` DATETIME(3) NULL DEFAULT NULL COMMENT '处理时间',
|
||||||
|
`operator_admin_id` BIGINT NOT NULL DEFAULT 0 COMMENT '操作管理员ID',
|
||||||
|
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_pgaur_activity_user` (`activity_id`, `user_id`),
|
||||||
|
KEY `idx_pgaur_activity_status` (`activity_id`, `status`, `updated_at`),
|
||||||
|
KEY `idx_pgaur_user_status` (`user_id`, `status`, `updated_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='奖品发放活动用户记录';
|
||||||
Loading…
x
Reference in New Issue
Block a user