add: 仪表盘
This commit is contained in:
parent
9eea272d69
commit
571cb2f4db
@ -1,9 +1,11 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
@ -274,6 +276,25 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询下线(被邀请人)累计消费金额
|
||||
// 从 user_invites.accumulated_amount 聚合,该字段在被邀请人支付订单时自动更新
|
||||
inviteeTotalConsumes := make(map[int64]int64)
|
||||
if len(userIDs) > 0 {
|
||||
type consumeResult struct {
|
||||
InviterID int64
|
||||
Total int64
|
||||
}
|
||||
var consumes []consumeResult
|
||||
h.readDB.UserInvites.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.UserInvites.InviterID, h.readDB.UserInvites.AccumulatedAmount.Sum().As("total")).
|
||||
Where(h.readDB.UserInvites.InviterID.In(userIDs...)).
|
||||
Group(h.readDB.UserInvites.InviterID).
|
||||
Scan(&consumes)
|
||||
for _, c := range consumes {
|
||||
inviteeTotalConsumes[c.InviterID] = c.Total
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询次数卡数量
|
||||
gamePassCounts := make(map[int64]int64)
|
||||
if len(userIDs) > 0 {
|
||||
@ -404,32 +425,33 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
assetVal := pointsBal*100 + invVal + cpVal + icVal + gpCount*200
|
||||
|
||||
rsp.List[i] = adminUserItem{
|
||||
ID: v.ID,
|
||||
Nickname: v.Nickname,
|
||||
Avatar: v.Avatar,
|
||||
InviteCode: v.InviteCode,
|
||||
InviterID: v.InviterID,
|
||||
InviterNickname: inviterNicknames[v.InviterID],
|
||||
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
DouyinID: v.DouyinID,
|
||||
DouyinUserID: v.DouyinUserID,
|
||||
Mobile: v.Mobile,
|
||||
Remark: v.Remark,
|
||||
ChannelName: v.ChannelName,
|
||||
ChannelCode: v.ChannelCode,
|
||||
PointsBalance: pointsBal,
|
||||
CouponsCount: couponCounts[v.ID],
|
||||
ItemCardsCount: cardCounts[v.ID],
|
||||
TodayConsume: todayConsume[v.ID],
|
||||
SevenDayConsume: sevenDayConsume[v.ID],
|
||||
ThirtyDayConsume: thirtyDayConsume[v.ID],
|
||||
TotalConsume: totalConsume[v.ID],
|
||||
InviteCount: inviteCounts[v.ID],
|
||||
GamePassCount: gpCount,
|
||||
GameTicketCount: gtCount,
|
||||
InventoryValue: invVal,
|
||||
TotalAssetValue: assetVal,
|
||||
Status: v.Status,
|
||||
ID: v.ID,
|
||||
Nickname: v.Nickname,
|
||||
Avatar: v.Avatar,
|
||||
InviteCode: v.InviteCode,
|
||||
InviterID: v.InviterID,
|
||||
InviterNickname: inviterNicknames[v.InviterID],
|
||||
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
DouyinID: v.DouyinID,
|
||||
DouyinUserID: v.DouyinUserID,
|
||||
Mobile: v.Mobile,
|
||||
Remark: v.Remark,
|
||||
ChannelName: v.ChannelName,
|
||||
ChannelCode: v.ChannelCode,
|
||||
PointsBalance: pointsBal,
|
||||
CouponsCount: couponCounts[v.ID],
|
||||
ItemCardsCount: cardCounts[v.ID],
|
||||
TodayConsume: todayConsume[v.ID],
|
||||
SevenDayConsume: sevenDayConsume[v.ID],
|
||||
ThirtyDayConsume: thirtyDayConsume[v.ID],
|
||||
TotalConsume: totalConsume[v.ID],
|
||||
InviteCount: inviteCounts[v.ID],
|
||||
InviteeTotalConsume: inviteeTotalConsumes[v.ID],
|
||||
GamePassCount: gpCount,
|
||||
GameTicketCount: gtCount,
|
||||
InventoryValue: invVal,
|
||||
TotalAssetValue: assetVal,
|
||||
Status: v.Status,
|
||||
}
|
||||
}
|
||||
ctx.Payload(rsp)
|
||||
@ -445,11 +467,16 @@ type listInvitesResponse struct {
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
List []adminUserItem `json:"list"`
|
||||
Summary struct {
|
||||
TotalConsume int64 `json:"total_consume"` // 所有下线累计消费
|
||||
TotalAsset int64 `json:"total_asset"` // 所有下线资产价值
|
||||
TotalProfit int64 `json:"total_profit"` // 所有下线总盈亏
|
||||
} `json:"summary"`
|
||||
}
|
||||
|
||||
// ListUserInvites 查看用户邀请列表
|
||||
// @Summary 查看用户邀请列表
|
||||
// @Description 查看指定用户邀请的用户列表
|
||||
// @Description 查看指定用户邀请的用户列表,包含每个被邀请人的累计消费金额
|
||||
// @Tags 管理端.用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@ -478,13 +505,96 @@ func (h *handler) ListUserInvites() core.HandlerFunc {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20103, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取被邀请人ID列表,批量查询消费金额
|
||||
inviteeIDs := make([]int64, len(rows))
|
||||
for i, v := range rows {
|
||||
inviteeIDs[i] = v.ID
|
||||
}
|
||||
|
||||
// 从 user_invites 表查询每个被邀请人的累计消费
|
||||
inviteeConsumes := make(map[int64]int64)
|
||||
if len(inviteeIDs) > 0 {
|
||||
type consumeResult struct {
|
||||
InviteeID int64
|
||||
Amount int64
|
||||
}
|
||||
var consumes []consumeResult
|
||||
h.readDB.UserInvites.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.UserInvites.InviteeID, h.readDB.UserInvites.AccumulatedAmount.As("amount")).
|
||||
Where(h.readDB.UserInvites.InviterID.Eq(userID)).
|
||||
Where(h.readDB.UserInvites.InviteeID.In(inviteeIDs...)).
|
||||
Scan(&consumes)
|
||||
for _, c := range consumes {
|
||||
inviteeConsumes[c.InviteeID] = c.Amount
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询被邀请人的累计获得商品价值
|
||||
// 真实盈亏 = 商品价值(持有+发货,排除兑换)- 累计消费
|
||||
// 注:已兑换(status=2)的商品不计入,因为兑换=转成积分,积分是用户的现金等价物
|
||||
inviteeAssets := make(map[int64]int64)
|
||||
if len(inviteeIDs) > 0 {
|
||||
type assetResult struct {
|
||||
UserID int64
|
||||
Value int64
|
||||
}
|
||||
|
||||
// 商品价值:排除已兑换(status=2)
|
||||
var invRes []assetResult
|
||||
h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
|
||||
LeftJoin(h.readDB.Products, h.readDB.Products.ID.EqCol(h.readDB.UserInventory.ProductID)).
|
||||
Select(h.readDB.UserInventory.UserID, h.readDB.Products.Price.Sum().As("value")).
|
||||
Where(h.readDB.UserInventory.UserID.In(inviteeIDs...)).
|
||||
Where(h.readDB.UserInventory.Status.Neq(2)). // 排除已兑换
|
||||
Group(h.readDB.UserInventory.UserID).
|
||||
Scan(&invRes)
|
||||
for _, r := range invRes {
|
||||
inviteeAssets[r.UserID] = r.Value
|
||||
}
|
||||
}
|
||||
|
||||
// 查询所有下线的汇总数据(不分页)
|
||||
var summaryConsume, summaryAsset int64
|
||||
// 累计消费汇总
|
||||
_ = h.repo.GetDbR().Raw(`
|
||||
SELECT COALESCE(SUM(accumulated_amount), 0)
|
||||
FROM user_invites
|
||||
WHERE inviter_id = ?
|
||||
`, userID).Scan(&summaryConsume).Error
|
||||
// 资产价值汇总(不包含已兑换的商品)
|
||||
_ = h.repo.GetDbR().Raw(`
|
||||
SELECT COALESCE(SUM(p.price), 0)
|
||||
FROM user_inventory ui
|
||||
LEFT JOIN products p ON p.id = ui.product_id
|
||||
WHERE ui.user_id IN (SELECT invitee_id FROM user_invites WHERE inviter_id = ?)
|
||||
AND ui.status != 2
|
||||
`, userID).Scan(&summaryAsset).Error
|
||||
|
||||
rsp.Page = req.Page
|
||||
rsp.PageSize = req.PageSize
|
||||
rsp.Total = total
|
||||
rsp.List = make([]adminUserItem, len(rows))
|
||||
for i, v := range rows {
|
||||
rsp.List[i] = adminUserItem{ID: v.ID, Nickname: v.Nickname, Avatar: v.Avatar, InviteCode: v.InviteCode, InviterID: v.InviterID, CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05")}
|
||||
consume := inviteeConsumes[v.ID]
|
||||
asset := inviteeAssets[v.ID]
|
||||
rsp.List[i] = adminUserItem{
|
||||
ID: v.ID,
|
||||
Nickname: v.Nickname,
|
||||
Avatar: v.Avatar,
|
||||
InviteCode: v.InviteCode,
|
||||
InviterID: v.InviterID,
|
||||
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
TotalConsume: consume, // 被邀请人的累计消费
|
||||
TotalAssetValue: asset, // 被邀请人的资产价值
|
||||
}
|
||||
}
|
||||
|
||||
// 填充汇总数据
|
||||
rsp.Summary.TotalConsume = summaryConsume
|
||||
rsp.Summary.TotalAsset = summaryAsset
|
||||
rsp.Summary.TotalProfit = summaryAsset - summaryConsume
|
||||
|
||||
ctx.Payload(rsp)
|
||||
}
|
||||
}
|
||||
@ -1031,6 +1141,23 @@ func (h *handler) ListUserAuditLogs() core.HandlerFunc {
|
||||
if t, err := time.Parse(time.RFC3339, logs[i].CreatedAt); err == nil {
|
||||
logs[i].CreatedAt = t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// 处理金额显示 (分 -> 元)
|
||||
if logs[i].Category == "points" || logs[i].Category == "order" {
|
||||
if amount, err := strconv.ParseFloat(logs[i].AmountStr, 64); err == nil {
|
||||
val := amount / 100.0
|
||||
if logs[i].Category == "order" {
|
||||
val = -math.Abs(val) // 订单固定为支出
|
||||
}
|
||||
// 积分正数加+号
|
||||
if logs[i].Category == "points" && val > 0 {
|
||||
logs[i].AmountStr = fmt.Sprintf("+%.2f", val)
|
||||
} else {
|
||||
logs[i].AmountStr = fmt.Sprintf("%.2f", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 翻译 Shipping Status 等 (可选项,也可以前端做)
|
||||
// 翻译 Shipping Status 等 (可选项,也可以前端做)
|
||||
if logs[i].Category == "shipping" {
|
||||
switch logs[i].SubType {
|
||||
@ -1044,6 +1171,54 @@ func (h *handler) ListUserAuditLogs() core.HandlerFunc {
|
||||
logs[i].SubType = "异常"
|
||||
}
|
||||
}
|
||||
// 翻译积分变动
|
||||
if logs[i].Category == "points" {
|
||||
switch logs[i].SubType {
|
||||
case "redeem_coupon":
|
||||
logs[i].SubType = "兑换优惠券"
|
||||
case "redeem_product":
|
||||
logs[i].SubType = "兑换商品"
|
||||
case "redeem_item_card":
|
||||
logs[i].SubType = "兑换道具卡"
|
||||
case "minesweeper_settle":
|
||||
logs[i].SubType = "扫雷游戏奖励"
|
||||
case "game_reward":
|
||||
logs[i].SubType = "游戏奖励"
|
||||
case "redeem_reward":
|
||||
logs[i].SubType = "奖品分解"
|
||||
case "pay_reward":
|
||||
logs[i].SubType = "支付奖励"
|
||||
case "manual", "manual_add":
|
||||
logs[i].SubType = "管理员调整"
|
||||
case "task_reward":
|
||||
logs[i].SubType = "任务中心奖励"
|
||||
case "order_reward":
|
||||
logs[i].SubType = "订单奖励"
|
||||
case "douyin_product_reward":
|
||||
logs[i].SubType = "抖店商品奖励"
|
||||
case "signin":
|
||||
logs[i].SubType = "签到奖励"
|
||||
case "draw_cost":
|
||||
logs[i].SubType = "抽奖消耗"
|
||||
}
|
||||
// 翻译 RefInfo
|
||||
if strings.HasPrefix(logs[i].RefInfo, "system_coupons:") {
|
||||
logs[i].RefInfo = strings.Replace(logs[i].RefInfo, "system_coupons:", "系统优惠券ID: ", 1)
|
||||
}
|
||||
if strings.HasPrefix(logs[i].RefInfo, "user_inventory:") {
|
||||
logs[i].RefInfo = strings.Replace(logs[i].RefInfo, "user_inventory:", "用户背包ID: ", 1)
|
||||
}
|
||||
if strings.HasPrefix(logs[i].RefInfo, "orders:") {
|
||||
logs[i].RefInfo = strings.Replace(logs[i].RefInfo, "orders:", "关联订单号: ", 1)
|
||||
}
|
||||
}
|
||||
// 翻译订单状态
|
||||
if logs[i].Category == "order" && logs[i].SubType == "paid" {
|
||||
logs[i].SubType = "支付成功"
|
||||
}
|
||||
if logs[i].Category == "draw" && logs[i].SubType == "win" {
|
||||
logs[i].SubType = "中奖"
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Payload(&listAuditLogsResponse{
|
||||
@ -1141,32 +1316,33 @@ type pointsBalanceResponse struct {
|
||||
}
|
||||
|
||||
type adminUserItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
InviterID int64 `json:"inviter_id"`
|
||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
||||
CreatedAt string `json:"created_at"`
|
||||
DouyinID string `json:"douyin_id"`
|
||||
DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号ID
|
||||
Mobile string `json:"mobile"` // 手机号
|
||||
Remark string `json:"remark"` // 备注
|
||||
ChannelName string `json:"channel_name"`
|
||||
ChannelCode string `json:"channel_code"`
|
||||
PointsBalance int64 `json:"points_balance"`
|
||||
CouponsCount int64 `json:"coupons_count"`
|
||||
ItemCardsCount int64 `json:"item_cards_count"`
|
||||
TodayConsume int64 `json:"today_consume"`
|
||||
SevenDayConsume int64 `json:"seven_day_consume"`
|
||||
ThirtyDayConsume int64 `json:"thirty_day_consume"` // 近30天消费
|
||||
TotalConsume int64 `json:"total_consume"` // 累计消费
|
||||
InviteCount int64 `json:"invite_count"` // 邀请人数
|
||||
GamePassCount int64 `json:"game_pass_count"` // 次数卡数量
|
||||
GameTicketCount int64 `json:"game_ticket_count"` // 游戏资格数量
|
||||
InventoryValue int64 `json:"inventory_value"` // 持有商品总价值
|
||||
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
|
||||
Status int32 `json:"status"` // 用户状态:1正常 2禁用 3黑名单
|
||||
ID int64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
InviterID int64 `json:"inviter_id"`
|
||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
||||
CreatedAt string `json:"created_at"`
|
||||
DouyinID string `json:"douyin_id"`
|
||||
DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号ID
|
||||
Mobile string `json:"mobile"` // 手机号
|
||||
Remark string `json:"remark"` // 备注
|
||||
ChannelName string `json:"channel_name"`
|
||||
ChannelCode string `json:"channel_code"`
|
||||
PointsBalance int64 `json:"points_balance"`
|
||||
CouponsCount int64 `json:"coupons_count"`
|
||||
ItemCardsCount int64 `json:"item_cards_count"`
|
||||
TodayConsume int64 `json:"today_consume"`
|
||||
SevenDayConsume int64 `json:"seven_day_consume"`
|
||||
ThirtyDayConsume int64 `json:"thirty_day_consume"` // 近30天消费
|
||||
TotalConsume int64 `json:"total_consume"` // 累计消费
|
||||
InviteCount int64 `json:"invite_count"` // 邀请人数
|
||||
InviteeTotalConsume int64 `json:"invitee_total_consume"` // 下线(被邀请人)累计消费
|
||||
GamePassCount int64 `json:"game_pass_count"` // 次数卡数量
|
||||
GameTicketCount int64 `json:"game_ticket_count"` // 游戏资格数量
|
||||
InventoryValue int64 `json:"inventory_value"` // 持有商品总价值
|
||||
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
|
||||
Status int32 `json:"status"` // 用户状态:1正常 2禁用 3黑名单
|
||||
}
|
||||
|
||||
// ListAppUsers 管理端用户列表GetUserPointsBalance 查看用户积分余额
|
||||
@ -1706,3 +1882,17 @@ func (h *handler) UpdateUserStatus() core.HandlerFunc {
|
||||
ctx.Payload(map[string]any{"success": true})
|
||||
}
|
||||
}
|
||||
|
||||
type listAuditLogsRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
}
|
||||
|
||||
type auditLogItem struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
Category string `json:"category"` // points, order, shipping, draw
|
||||
SubType string `json:"sub_type"` // 具体类型
|
||||
AmountStr string `json:"amount_str"` // 变动金额/数量
|
||||
RefInfo string `json:"ref_info"` // 关联信息
|
||||
DetailInfo string `json:"detail_info"` // 详细信息
|
||||
}
|
||||
|
||||
73
internal/api/user/bind_inviter_app.go
Normal file
73
internal/api/user/bind_inviter_app.go
Normal file
@ -0,0 +1,73 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
usersvc "bindbox-game/internal/service/user"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type bindInviterRequest struct {
|
||||
InviteCode string `json:"invite_code" binding:"required"`
|
||||
}
|
||||
|
||||
type bindInviterResponse struct {
|
||||
InviterID int64 `json:"inviter_id"`
|
||||
InviterNickname string `json:"inviter_nickname"`
|
||||
}
|
||||
|
||||
// BindInviter 用户绑定邀请人
|
||||
// @Summary 用户绑定邀请人
|
||||
// @Description 未绑定过邀请人的用户可主动填写邀请码绑定邀请人(绑定后不可更改)
|
||||
// @Tags APP端.用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body bindInviterRequest true "邀请码"
|
||||
// @Success 200 {object} bindInviterResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/app/users/inviter/bind [post]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) BindInviter() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(bindInviterRequest)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
userID := int64(ctx.SessionUserInfo().Id)
|
||||
|
||||
result, err := h.user.BindInviter(ctx.RequestContext(), userID, usersvc.BindInviterInput{
|
||||
InviteCode: req.InviteCode,
|
||||
})
|
||||
if err != nil {
|
||||
switch err {
|
||||
case usersvc.ErrAlreadyBound:
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10100, "您已绑定过邀请人,无法重复绑定"))
|
||||
case usersvc.ErrInvalidCode:
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10101, "邀请码不存在"))
|
||||
case usersvc.ErrCannotInviteSelf:
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10102, "不能邀请自己"))
|
||||
default:
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10103, err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 触发任务中心的邀请成功逻辑(给邀请人发放奖励)
|
||||
if result.InviterID > 0 {
|
||||
if err := h.task.OnInviteSuccess(ctx.RequestContext(), result.InviterID, userID); err != nil {
|
||||
h.logger.Error("触发邀请任务失败", zap.Error(err), zap.Int64("inviter_id", result.InviterID), zap.Int64("invitee_id", userID))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Payload(&bindInviterResponse{
|
||||
InviterID: result.InviterID,
|
||||
InviterNickname: result.InviterNickname,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -464,6 +464,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
appAuthApiRouter.POST("/users/:user_id/douyin/phone/bind", userHandler.DouyinBindPhone())
|
||||
appAuthApiRouter.POST("/users/douyin/bind", userHandler.BindDouyinOrder())
|
||||
appAuthApiRouter.GET("/users/:user_id/invites", userHandler.ListUserInvites())
|
||||
appAuthApiRouter.POST("/users/inviter/bind", userHandler.BindInviter())
|
||||
appAuthApiRouter.GET("/users/:user_id/inventory", userHandler.ListUserInventory())
|
||||
appAuthApiRouter.GET("/users/:user_id/shipments", userHandler.ListUserShipments())
|
||||
appAuthApiRouter.GET("/users/:user_id/item_cards", userHandler.ListUserItemCards())
|
||||
|
||||
91
internal/service/user/bind_inviter.go
Normal file
91
internal/service/user/bind_inviter.go
Normal file
@ -0,0 +1,91 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// BindInviterInput 绑定邀请人输入
|
||||
type BindInviterInput struct {
|
||||
InviteCode string // 邀请人的邀请码
|
||||
}
|
||||
|
||||
// BindInviterOutput 绑定邀请人输出
|
||||
type BindInviterOutput struct {
|
||||
InviterID int64 `json:"inviter_id"`
|
||||
InviterNickname string `json:"inviter_nickname"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrAlreadyBound = errors.New("already_bound")
|
||||
ErrInvalidCode = errors.New("invalid_code")
|
||||
ErrCannotInviteSelf = errors.New("cannot_invite_self")
|
||||
)
|
||||
|
||||
// BindInviter 用户主动绑定邀请人(仅限未绑定过的用户)
|
||||
func (s *service) BindInviter(ctx context.Context, userID int64, in BindInviterInput) (*BindInviterOutput, error) {
|
||||
var result *BindInviterOutput
|
||||
|
||||
err := s.writeDB.Transaction(func(tx *dao.Query) error {
|
||||
// 1. 获取当前用户,加行锁
|
||||
user, err := tx.Users.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where(tx.Users.ID.Eq(userID)).First()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 检查是否已绑定邀请人
|
||||
if user.InviterID != 0 {
|
||||
return ErrAlreadyBound
|
||||
}
|
||||
|
||||
// 3. 查找邀请人
|
||||
inviter, err := tx.Users.WithContext(ctx).Where(tx.Users.InviteCode.Eq(in.InviteCode)).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrInvalidCode
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 不能邀请自己
|
||||
if inviter.ID == userID {
|
||||
return ErrCannotInviteSelf
|
||||
}
|
||||
|
||||
// 5. 创建邀请记录
|
||||
invite := &model.UserInvites{
|
||||
InviterID: inviter.ID,
|
||||
InviteeID: userID,
|
||||
InviteCode: in.InviteCode,
|
||||
}
|
||||
if err := tx.UserInvites.WithContext(ctx).Create(invite); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 6. 更新用户的邀请人ID
|
||||
if _, err := tx.Users.WithContext(ctx).Where(tx.Users.ID.Eq(userID)).
|
||||
UpdateColumn(tx.Users.InviterID, inviter.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = &BindInviterOutput{
|
||||
InviterID: inviter.ID,
|
||||
InviterNickname: inviter.Nickname,
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@ -80,6 +80,8 @@ type Service interface {
|
||||
SendSmsCode(ctx context.Context, mobile string) error
|
||||
LoginByCode(ctx context.Context, in SmsLoginInput) (*SmsLoginOutput, error)
|
||||
GrantGamePass(ctx context.Context, userID int64, packageID int64, count int32, orderNo string) error
|
||||
// 邀请人绑定
|
||||
BindInviter(ctx context.Context, userID int64, in BindInviterInput) (*BindInviterOutput, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user