689 lines
25 KiB
Go
Executable File
689 lines
25 KiB
Go
Executable File
package admin
|
||
|
||
import (
|
||
"bindbox-game/internal/code"
|
||
"bindbox-game/internal/pkg/core"
|
||
"bindbox-game/internal/pkg/validation"
|
||
"bindbox-game/internal/repository/mysql/model"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
type activityProfitLossRequest struct {
|
||
Page int `form:"page"`
|
||
PageSize int `form:"page_size"`
|
||
Name string `form:"name"`
|
||
Status int32 `form:"status"` // 1进行中 2下线
|
||
SortBy string `form:"sort_by"` // profit, profit_asc, profit_rate, draw_count
|
||
}
|
||
|
||
type activityProfitLossItem struct {
|
||
ActivityID int64 `json:"activity_id"`
|
||
ActivityName string `json:"activity_name"`
|
||
Status int32 `json:"status"`
|
||
DrawCount int64 `json:"draw_count"`
|
||
GamePassCount int64 `json:"game_pass_count"` // 次卡抽奖次数
|
||
PaymentCount int64 `json:"payment_count"` // 现金/优惠券抽奖次数
|
||
RefundCount int64 `json:"refund_count"` // 退款/取消抽奖次数
|
||
PlayerCount int64 `json:"player_count"`
|
||
TotalRevenue int64 `json:"total_revenue"` // 实际支付金额 (分)
|
||
TotalDiscount int64 `json:"total_discount"` // 优惠券抵扣金额 (分)
|
||
TotalGamePassValue int64 `json:"total_game_pass_value"` // 次卡价值 (分)
|
||
TotalCost int64 `json:"total_cost"` // 奖品标价总和 (分)
|
||
Profit int64 `json:"profit"` // (Revenue + Discount + GamePassValue) - Cost
|
||
ProfitRate float64 `json:"profit_rate"` // Profit / (Revenue + Discount + GamePassValue)
|
||
}
|
||
|
||
type activityProfitLossResponse struct {
|
||
Page int `json:"page"`
|
||
PageSize int `json:"page_size"`
|
||
Total int64 `json:"total"`
|
||
List []activityProfitLossItem `json:"list"`
|
||
}
|
||
|
||
func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
req := new(activityProfitLossRequest)
|
||
if err := ctx.ShouldBindForm(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
if req.Page <= 0 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize <= 0 {
|
||
req.PageSize = 20
|
||
}
|
||
|
||
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
|
||
|
||
// 1. 获取活动列表基础信息
|
||
// 1. 获取活动列表基础信息
|
||
var activities []model.Activities
|
||
// 仅查询有完整配置(Issue->RewardSettings)且未删除的活动
|
||
// 使用 Raw SQL 避免 GORM 自动注入 ambiguous 的 deleted_at
|
||
rawSubQuery := fmt.Sprintf(`
|
||
SELECT activity_issues.activity_id
|
||
FROM %s AS activity_issues
|
||
JOIN %s AS activity_reward_settings ON activity_reward_settings.issue_id = activity_issues.id
|
||
WHERE activity_issues.deleted_at IS NULL
|
||
AND activity_reward_settings.deleted_at IS NULL
|
||
`, model.TableNameActivityIssues, model.TableNameActivityRewardSettings)
|
||
|
||
query := db.Table(model.TableNameActivities).
|
||
Where("activities.deleted_at IS NULL").
|
||
Where(fmt.Sprintf("activities.id IN (%s)", rawSubQuery))
|
||
|
||
if req.Name != "" {
|
||
query = query.Where("activities.name LIKE ?", "%"+req.Name+"%")
|
||
}
|
||
if req.Status > 0 {
|
||
query = query.Where("activities.status = ?", req.Status)
|
||
}
|
||
|
||
var total int64
|
||
query.Count(&total)
|
||
|
||
// 如果有排序需求,先获取所有活动计算盈亏后排序,再分页
|
||
// 如果没有排序需求,直接数据库分页
|
||
needCustomSort := req.SortBy != ""
|
||
var limitQuery = query
|
||
if !needCustomSort {
|
||
limitQuery = query.Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize)
|
||
}
|
||
|
||
if err := limitQuery.Order("id DESC").Find(&activities).Error; err != nil {
|
||
h.logger.Error(fmt.Sprintf("GetActivityProfitLoss activities error: %v", err))
|
||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 21021, err.Error()))
|
||
return
|
||
}
|
||
|
||
if len(activities) == 0 {
|
||
ctx.Payload(&activityProfitLossResponse{
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
Total: total,
|
||
List: []activityProfitLossItem{},
|
||
})
|
||
return
|
||
}
|
||
|
||
activityIDs := make([]int64, len(activities))
|
||
activityMap := make(map[int64]*activityProfitLossItem)
|
||
for i, a := range activities {
|
||
activityIDs[i] = a.ID
|
||
activityMap[a.ID] = &activityProfitLossItem{
|
||
ActivityID: a.ID,
|
||
ActivityName: a.Name,
|
||
Status: a.Status,
|
||
}
|
||
}
|
||
|
||
// 2. 统计抽奖次数和人数 (通过 activity_draw_logs 关联 activity_issues 和 orders)
|
||
type drawStat struct {
|
||
ActivityID int64
|
||
TotalCount int64
|
||
GamePassCount int64
|
||
PaymentCount int64
|
||
RefundCount int64
|
||
PlayerCount int64
|
||
}
|
||
var drawStats []drawStat
|
||
db.Table(model.TableNameActivityDrawLogs).
|
||
Select(`
|
||
activity_issues.activity_id,
|
||
COUNT(activity_draw_logs.id) as total_count,
|
||
SUM(CASE WHEN orders.status = 2 AND (orders.source_type = 4 OR orders.order_no LIKE 'GP%') THEN 1 ELSE 0 END) as game_pass_count,
|
||
SUM(CASE WHEN orders.status = 2 AND NOT (orders.source_type = 4 OR orders.order_no LIKE 'GP%') THEN 1 ELSE 0 END) as payment_count,
|
||
SUM(CASE WHEN orders.status IN (3, 4) THEN 1 ELSE 0 END) as refund_count,
|
||
COUNT(DISTINCT CASE WHEN orders.status = 2 THEN activity_draw_logs.user_id END) as player_count
|
||
`).
|
||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||
Where("activity_issues.activity_id IN ?", activityIDs).
|
||
Group("activity_issues.activity_id").
|
||
Scan(&drawStats)
|
||
|
||
for _, s := range drawStats {
|
||
if item, ok := activityMap[s.ActivityID]; ok {
|
||
item.DrawCount = s.GamePassCount + s.PaymentCount // 仅统计有效抽奖(次卡+支付)
|
||
item.GamePassCount = s.GamePassCount
|
||
item.PaymentCount = s.PaymentCount
|
||
item.RefundCount = s.RefundCount
|
||
item.PlayerCount = s.PlayerCount
|
||
}
|
||
}
|
||
|
||
// 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
|
||
// 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
|
||
// BUG修复:排除已退款订单(status=4)。
|
||
// 注意: MySQL SUM()运算涉及除法时会返回Decimal类型,需要Scan到float64
|
||
type revenueStat struct {
|
||
ActivityID int64
|
||
TotalRevenue float64
|
||
TotalDiscount float64
|
||
}
|
||
var revenueStats []revenueStat
|
||
|
||
// 修正: 按抽奖次数比例分摊订单金额 (解决多活动订单归因问题)
|
||
// 逻辑: 活动分摊收入 = 订单实际金额 * (该活动在该订单中的抽奖次数 / 该订单总抽奖次数)
|
||
var err error
|
||
err = db.Table(model.TableNameOrders).
|
||
Select(`
|
||
order_activity_draws.activity_id,
|
||
SUM(1.0 * orders.actual_amount * order_activity_draws.draw_count / order_total_draws.total_count) as total_revenue,
|
||
SUM(CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' THEN 0 ELSE 1.0 * orders.discount_amount * order_activity_draws.draw_count / order_total_draws.total_count END) as total_discount
|
||
`).
|
||
// Subquery 1: Calculate draw counts per order per activity (and link to issue->activity)
|
||
Joins(`JOIN (
|
||
SELECT activity_draw_logs.order_id, activity_issues.activity_id, COUNT(*) as draw_count
|
||
FROM activity_draw_logs
|
||
JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id
|
||
GROUP BY activity_draw_logs.order_id, activity_issues.activity_id
|
||
) as order_activity_draws ON order_activity_draws.order_id = orders.id`).
|
||
// Subquery 2: Calculate total draw counts per order
|
||
Joins(`JOIN (
|
||
SELECT order_id, COUNT(*) as total_count
|
||
FROM activity_draw_logs
|
||
GROUP BY order_id
|
||
) as order_total_draws ON order_total_draws.order_id = orders.id`).
|
||
Where("orders.status = ?", 2). // 已支付(排除待支付、取消、退款状态)
|
||
Where("order_activity_draws.activity_id IN ?", activityIDs).
|
||
Group("order_activity_draws.activity_id").
|
||
Scan(&revenueStats).Error
|
||
|
||
if err != nil {
|
||
h.logger.Error(fmt.Sprintf("GetActivityProfitLoss revenue stats error: %v", err))
|
||
}
|
||
|
||
for _, s := range revenueStats {
|
||
if item, ok := activityMap[s.ActivityID]; ok {
|
||
item.TotalRevenue = int64(s.TotalRevenue)
|
||
item.TotalDiscount = int64(s.TotalDiscount)
|
||
}
|
||
}
|
||
|
||
// 4. 统计成本 (通过 user_inventory 关联 products 和 orders)
|
||
// 修正:增加关联 orders 表,过滤掉已退款/取消的订单 (status!=2)
|
||
type costStat struct {
|
||
ActivityID int64
|
||
TotalCost int64
|
||
}
|
||
var costStats []costStat
|
||
db.Table(model.TableNameUserInventory).
|
||
Select("user_inventory.activity_id, SUM(user_inventory.value_cents) as total_cost").
|
||
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
||
Where("user_inventory.activity_id IN ?", activityIDs).
|
||
Where("orders.status = ?", 2). // 仅统计已支付订单产生的成本
|
||
Group("user_inventory.activity_id").
|
||
Scan(&costStats)
|
||
|
||
for _, s := range costStats {
|
||
if item, ok := activityMap[s.ActivityID]; ok {
|
||
item.TotalCost = s.TotalCost
|
||
}
|
||
}
|
||
|
||
// 5. 统计次卡价值 (0元订单按活动单价计算)
|
||
// 先获取各活动的单价
|
||
activityPriceMap := make(map[int64]int64)
|
||
for _, a := range activities {
|
||
activityPriceMap[a.ID] = a.PriceDraw
|
||
}
|
||
|
||
// 统计每个活动的0元订单对应的抽奖次数 (次卡支付)
|
||
// BUG修复:之前统计的是订单数量,但一个订单可能包含多次抽奖
|
||
// 正确做法是统计抽奖次数,再乘以活动单价
|
||
type gamePassStat struct {
|
||
ActivityID int64
|
||
GamePassDraws int64 // 抽奖次数,非订单数
|
||
}
|
||
var gamePassStats []gamePassStat
|
||
db.Table(model.TableNameActivityDrawLogs).
|
||
Select("activity_issues.activity_id, COUNT(activity_draw_logs.id) as game_pass_draws").
|
||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||
Joins("JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||
Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
|
||
Where("orders.actual_amount = 0"). // 0元订单
|
||
Where("orders.source_type = 4 OR orders.order_no LIKE 'GP%'"). // 次数卡 (Lottery SourceType=4 OR Matching Game GP prefix)
|
||
Where("activity_issues.activity_id IN ?", activityIDs).
|
||
Group("activity_issues.activity_id").
|
||
Scan(&gamePassStats)
|
||
|
||
for _, s := range gamePassStats {
|
||
if item, ok := activityMap[s.ActivityID]; ok {
|
||
// 次卡价值 = 次卡抽奖次数 * 活动单价
|
||
item.TotalGamePassValue = s.GamePassDraws * activityPriceMap[s.ActivityID]
|
||
}
|
||
}
|
||
|
||
// 6. 计算盈亏和比率
|
||
// 公式: 盈亏 = (支付金额 + 优惠券抵扣 + 次卡价值) - 产品成本
|
||
finalList := make([]activityProfitLossItem, 0, len(activities))
|
||
for _, a := range activities {
|
||
item := activityMap[a.ID]
|
||
totalIncome := item.TotalRevenue + item.TotalDiscount + item.TotalGamePassValue
|
||
item.Profit = totalIncome - item.TotalCost
|
||
if totalIncome > 0 {
|
||
item.ProfitRate = float64(item.Profit) / float64(totalIncome)
|
||
}
|
||
finalList = append(finalList, *item)
|
||
}
|
||
|
||
// 按请求的字段排序
|
||
if needCustomSort {
|
||
sort.Slice(finalList, func(i, j int) bool {
|
||
switch req.SortBy {
|
||
case "profit":
|
||
return finalList[i].Profit > finalList[j].Profit
|
||
case "profit_asc":
|
||
return finalList[i].Profit < finalList[j].Profit
|
||
case "profit_rate":
|
||
return finalList[i].ProfitRate > finalList[j].ProfitRate
|
||
case "draw_count":
|
||
return finalList[i].DrawCount > finalList[j].DrawCount
|
||
default:
|
||
return false // 保持原有顺序 (id DESC)
|
||
}
|
||
})
|
||
|
||
// 排序后再分页
|
||
start := (req.Page - 1) * req.PageSize
|
||
end := start + req.PageSize
|
||
if start > len(finalList) {
|
||
start = len(finalList)
|
||
}
|
||
if end > len(finalList) {
|
||
end = len(finalList)
|
||
}
|
||
finalList = finalList[start:end]
|
||
}
|
||
|
||
ctx.Payload(&activityProfitLossResponse{
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
Total: total,
|
||
List: finalList,
|
||
})
|
||
}
|
||
}
|
||
|
||
type activityLogsRequest struct {
|
||
Page int `form:"page"`
|
||
PageSize int `form:"page_size"`
|
||
UserID int64 `form:"user_id"`
|
||
}
|
||
|
||
type activityLogItem struct {
|
||
ID int64 `json:"id"`
|
||
UserID int64 `json:"user_id"`
|
||
Nickname string `json:"nickname"`
|
||
Avatar string `json:"avatar"`
|
||
ProductID int64 `json:"product_id"`
|
||
ProductName string `json:"product_name"`
|
||
ProductImage string `json:"product_image"`
|
||
ProductPrice int64 `json:"product_price"`
|
||
ProductQuantity int64 `json:"product_quantity"` // 奖品数量
|
||
OrderAmount int64 `json:"order_amount"`
|
||
OrderNo string `json:"order_no"` // 订单号
|
||
DiscountAmount int64 `json:"discount_amount"` // 优惠金额(分)
|
||
PayType string `json:"pay_type"` // 支付方式/类型 (现金/道具卡/次数卡)
|
||
UsedCard string `json:"used_card"` // 使用的卡券名称(兼容旧字段)
|
||
OrderStatus int32 `json:"order_status"` // 订单状态: 1待支付 2已支付 3已取消 4已退款
|
||
Profit int64 `json:"profit"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
// 新增:详细支付信息
|
||
PaymentDetails PaymentDetails `json:"payment_details"`
|
||
}
|
||
|
||
// PaymentDetails 支付详细信息
|
||
type PaymentDetails struct {
|
||
CouponUsed bool `json:"coupon_used"` // 是否使用优惠券
|
||
CouponName string `json:"coupon_name"` // 优惠券名称
|
||
CouponDiscount int64 `json:"coupon_discount"` // 优惠券抵扣金额(分)
|
||
ItemCardUsed bool `json:"item_card_used"` // 是否使用道具卡
|
||
ItemCardName string `json:"item_card_name"` // 道具卡名称
|
||
GamePassUsed bool `json:"game_pass_used"` // 是否使用次数卡
|
||
GamePassInfo string `json:"game_pass_info"` // 次数卡使用信息
|
||
PointsUsed bool `json:"points_used"` // 是否使用积分
|
||
PointsDiscount int64 `json:"points_discount"` // 积分抵扣金额(分)
|
||
}
|
||
|
||
type activityLogsResponse struct {
|
||
Page int `json:"page"`
|
||
PageSize int `json:"page_size"`
|
||
Total int64 `json:"total"`
|
||
List []activityLogItem `json:"list"`
|
||
}
|
||
|
||
func (h *handler) DashboardActivityLogs() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
activityID, _ := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
|
||
if activityID <= 0 {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "Invalid activity ID"))
|
||
return
|
||
}
|
||
|
||
req := new(activityLogsRequest)
|
||
if err := ctx.ShouldBindForm(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
if req.Page <= 0 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize <= 0 {
|
||
req.PageSize = 20
|
||
}
|
||
|
||
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
|
||
|
||
var total int64
|
||
countQuery := db.Table(model.TableNameActivityDrawLogs).
|
||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||
Where("activity_issues.activity_id = ?", activityID)
|
||
if req.UserID > 0 {
|
||
countQuery = countQuery.Where("activity_draw_logs.user_id = ?", req.UserID)
|
||
}
|
||
countQuery.Count(&total)
|
||
|
||
var logs []struct {
|
||
ID int64
|
||
UserID int64
|
||
Nickname string
|
||
Avatar string
|
||
ProductID int64
|
||
ProductName string
|
||
ImagesJSON string
|
||
ProductPrice int64
|
||
OrderAmount int64
|
||
DiscountAmount int64
|
||
PointsAmount int64 // 积分抵扣金额
|
||
OrderStatus int32 // 订单状态
|
||
SourceType int32
|
||
CouponID int64
|
||
CouponName string
|
||
ItemCardID int64
|
||
ItemCardName string
|
||
EffectType int32
|
||
Multiplier int32
|
||
OrderRemark string // BUG修复:增加remark字段用于解析次数卡使用信息
|
||
OrderNo string // 订单号
|
||
DrawCount int64 // 该订单的总抽奖次数(用于金额分摊)
|
||
UsedDrawLogID int64 // 道具卡实际使用的日志ID
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
logsQuery := db.Table(model.TableNameActivityDrawLogs).
|
||
Select(`
|
||
activity_draw_logs.id,
|
||
activity_draw_logs.user_id,
|
||
COALESCE(users.nickname, '') as nickname,
|
||
COALESCE(users.avatar, '') as avatar,
|
||
activity_reward_settings.product_id,
|
||
COALESCE(products.name, '') as product_name,
|
||
COALESCE(products.images_json, '[]') as images_json,
|
||
COALESCE(activity_reward_settings.price_snapshot_cents, products.price, 0) as product_price,
|
||
COALESCE(orders.actual_amount, 0) as order_amount,
|
||
COALESCE(orders.discount_amount, 0) as discount_amount,
|
||
COALESCE(orders.points_amount, 0) as points_amount,
|
||
COALESCE(orders.status, 0) as order_status,
|
||
orders.source_type,
|
||
COALESCE(orders.coupon_id, 0) as coupon_id,
|
||
COALESCE(system_coupons.name, '') as coupon_name,
|
||
COALESCE(orders.item_card_id, 0) as item_card_id,
|
||
COALESCE(system_item_cards.name, '') as item_card_name,
|
||
COALESCE(system_item_cards.effect_type, 0) as effect_type,
|
||
COALESCE(system_item_cards.reward_multiplier_x1000, 0) as multiplier,
|
||
COALESCE(orders.remark, '') as order_remark,
|
||
COALESCE(orders.order_no, '') as order_no,
|
||
COALESCE(order_draw_counts.draw_count, 1) as draw_count,
|
||
COALESCE(user_item_cards.used_draw_log_id, 0) as used_draw_log_id,
|
||
activity_draw_logs.created_at
|
||
`).
|
||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||
Joins("LEFT JOIN users ON users.id = activity_draw_logs.user_id").
|
||
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
|
||
Joins("LEFT JOIN products ON products.id = activity_reward_settings.product_id").
|
||
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||
Joins("LEFT JOIN user_coupons ON user_coupons.id = orders.coupon_id").
|
||
Joins("LEFT JOIN system_coupons ON system_coupons.id = user_coupons.coupon_id").
|
||
Joins("LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id").
|
||
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id").
|
||
Joins("LEFT JOIN (SELECT order_id, COUNT(*) as draw_count FROM activity_draw_logs GROUP BY order_id) as order_draw_counts ON order_draw_counts.order_id = activity_draw_logs.order_id").
|
||
Where("activity_issues.activity_id = ?", activityID)
|
||
if req.UserID > 0 {
|
||
logsQuery = logsQuery.Where("activity_draw_logs.user_id = ?", req.UserID)
|
||
}
|
||
err := logsQuery.
|
||
Order("activity_draw_logs.id DESC").
|
||
Offset((req.Page - 1) * req.PageSize).
|
||
Limit(req.PageSize).
|
||
Scan(&logs).Error
|
||
|
||
if err != nil {
|
||
h.logger.Error(fmt.Sprintf("GetActivityLogs error: %v", err))
|
||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 21022, err.Error()))
|
||
return
|
||
}
|
||
|
||
list := make([]activityLogItem, len(logs))
|
||
for i, l := range logs {
|
||
var images []string
|
||
_ = json.Unmarshal([]byte(l.ImagesJSON), &images)
|
||
productImage := ""
|
||
if len(images) > 0 {
|
||
productImage = images[0]
|
||
}
|
||
|
||
// Default quantity is 1
|
||
quantity := int64(1)
|
||
|
||
// Determine PayType and UsedCard + PaymentDetails
|
||
payType := "现金支付"
|
||
usedCard := ""
|
||
paymentDetails := PaymentDetails{} // 金额将在 drawCount 计算后设置
|
||
|
||
// 检查是否使用了优惠券
|
||
if l.CouponID > 0 || l.CouponName != "" {
|
||
paymentDetails.CouponUsed = true
|
||
paymentDetails.CouponName = l.CouponName
|
||
if paymentDetails.CouponName == "" {
|
||
paymentDetails.CouponName = "优惠券"
|
||
}
|
||
usedCard = paymentDetails.CouponName
|
||
payType = "优惠券"
|
||
}
|
||
|
||
// 检查是否使用了道具卡
|
||
// BUG FIX: 仅当该条日志的 ID 等于 item_card 记录的 used_draw_log_id 时,才显示道具卡信息
|
||
// 防止一个订单下的所有抽奖记录都显示 "双倍快乐水"
|
||
isCardValidForThisLog := (l.UsedDrawLogID == 0) || (l.UsedDrawLogID == l.ID)
|
||
|
||
if (l.ItemCardID > 0 || l.ItemCardName != "") && isCardValidForThisLog {
|
||
paymentDetails.ItemCardUsed = true
|
||
paymentDetails.ItemCardName = l.ItemCardName
|
||
if paymentDetails.ItemCardName == "" {
|
||
paymentDetails.ItemCardName = "道具卡"
|
||
}
|
||
if usedCard != "" {
|
||
usedCard = usedCard + " + " + paymentDetails.ItemCardName
|
||
} else {
|
||
usedCard = paymentDetails.ItemCardName
|
||
}
|
||
payType = "道具卡"
|
||
|
||
// 计算双倍/多倍卡数量
|
||
if l.EffectType == 1 && l.Multiplier > 1000 {
|
||
quantity = quantity * int64(l.Multiplier) / 1000
|
||
}
|
||
}
|
||
|
||
// 检查是否使用了次数卡 (source_type=4 或 remark包含use_game_pass)
|
||
if l.SourceType == 4 || strings.Contains(l.OrderRemark, "use_game_pass") {
|
||
paymentDetails.GamePassUsed = true
|
||
// 解析 gp_use:ID:Count 格式获取次数卡使用信息
|
||
gamePassInfo := "次数卡"
|
||
if strings.Contains(l.OrderRemark, "gp_use:") {
|
||
// 从remark中提取次数卡信息,格式: use_game_pass;gp_use:ID:Count;gp_use:ID:Count
|
||
parts := strings.Split(l.OrderRemark, ";")
|
||
var gpParts []string
|
||
for _, p := range parts {
|
||
if strings.HasPrefix(p, "gp_use:") {
|
||
gpParts = append(gpParts, p)
|
||
}
|
||
}
|
||
if len(gpParts) > 0 {
|
||
gamePassInfo = fmt.Sprintf("使用%d种次数卡", len(gpParts))
|
||
}
|
||
}
|
||
paymentDetails.GamePassInfo = gamePassInfo
|
||
if usedCard != "" {
|
||
usedCard = usedCard + " + " + gamePassInfo
|
||
} else {
|
||
usedCard = gamePassInfo
|
||
}
|
||
payType = "次数卡"
|
||
}
|
||
|
||
// 检查是否使用了积分
|
||
if l.PointsAmount > 0 {
|
||
paymentDetails.PointsUsed = true
|
||
}
|
||
|
||
// 如果同时使用了多种方式,标记为组合支付
|
||
usedCount := 0
|
||
if paymentDetails.CouponUsed {
|
||
usedCount++
|
||
}
|
||
if paymentDetails.ItemCardUsed {
|
||
usedCount++
|
||
}
|
||
if paymentDetails.GamePassUsed {
|
||
usedCount++
|
||
}
|
||
if usedCount > 1 {
|
||
payType = "组合支付"
|
||
} else if usedCount == 0 && l.OrderAmount > 0 {
|
||
payType = "现金支付"
|
||
} else if usedCount == 0 && l.OrderAmount == 0 {
|
||
// 0元支付默认视为次数卡使用(实际业务中几乎不存在真正免费的情况)
|
||
payType = "次数卡"
|
||
paymentDetails.GamePassUsed = true
|
||
if paymentDetails.GamePassInfo == "" {
|
||
paymentDetails.GamePassInfo = "次数卡"
|
||
}
|
||
}
|
||
|
||
// 计算单次抽奖的分摊金额(一个订单可能包含多次抽奖)
|
||
drawCount := l.DrawCount
|
||
if drawCount <= 0 {
|
||
drawCount = 1
|
||
}
|
||
perDrawOrderAmount := l.OrderAmount / drawCount
|
||
perDrawDiscountAmount := l.DiscountAmount / drawCount
|
||
perDrawPointsAmount := l.PointsAmount / drawCount
|
||
|
||
// 设置支付详情中的分摊金额
|
||
paymentDetails.CouponDiscount = perDrawDiscountAmount
|
||
paymentDetails.PointsDiscount = perDrawPointsAmount
|
||
|
||
list[i] = activityLogItem{
|
||
ID: l.ID,
|
||
UserID: l.UserID,
|
||
Nickname: l.Nickname,
|
||
Avatar: l.Avatar,
|
||
ProductID: l.ProductID,
|
||
ProductName: l.ProductName,
|
||
ProductImage: productImage,
|
||
ProductPrice: l.ProductPrice,
|
||
ProductQuantity: quantity,
|
||
OrderAmount: perDrawOrderAmount, // 单次抽奖分摊的支付金额
|
||
OrderNo: l.OrderNo, // 订单号
|
||
DiscountAmount: perDrawDiscountAmount, // 单次抽奖分摊的优惠金额
|
||
PayType: payType,
|
||
UsedCard: usedCard,
|
||
OrderStatus: l.OrderStatus,
|
||
Profit: perDrawOrderAmount + perDrawDiscountAmount - l.ProductPrice*quantity, // 单次盈亏 = 分摊收入 - 成本*数量
|
||
CreatedAt: l.CreatedAt,
|
||
PaymentDetails: paymentDetails,
|
||
}
|
||
}
|
||
|
||
ctx.Payload(&activityLogsResponse{
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
Total: total,
|
||
List: list,
|
||
})
|
||
}
|
||
}
|
||
|
||
type ensureActivityProfitLossMenuResponse struct {
|
||
Ensured bool `json:"ensured"`
|
||
Parent int64 `json:"parent_id"`
|
||
MenuID int64 `json:"menu_id"`
|
||
}
|
||
|
||
// EnsureActivityProfitLossMenu 确保运营分析下存在"活动盈亏”菜单
|
||
func (h *handler) EnsureActivityProfitLossMenu() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
// 1. 查找是否存在"控制台”或者"运营中心”类的父菜单
|
||
// 很多系统会将概览放在 Dashboard 下。根据 titles_seed.go,运营是 Operations。
|
||
parent, _ := h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Name.Eq("Operations")).First()
|
||
var parentID int64
|
||
if parent == nil {
|
||
// 如果没有 Operations,尝试查找 Dashboard
|
||
parent, _ = h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Name.Eq("Dashboard")).First()
|
||
}
|
||
|
||
if parent != nil {
|
||
parentID = parent.ID
|
||
}
|
||
|
||
// 2. 查找活动盈亏菜单
|
||
// 路径指向控制台并带上查参数
|
||
menuPath := "/dashboard/console?tab=activity-profit"
|
||
exists, _ := h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Path.Eq(menuPath)).First()
|
||
if exists != nil {
|
||
ctx.Payload(&ensureActivityProfitLossMenuResponse{Ensured: true, Parent: parentID, MenuID: exists.ID})
|
||
return
|
||
}
|
||
|
||
// 3. 创建菜单
|
||
newMenu := &model.Menus{
|
||
ParentID: parentID,
|
||
Path: menuPath,
|
||
Name: "活动盈亏",
|
||
Component: "/dashboard/console/index",
|
||
Icon: "ri:pie-chart-2-fill",
|
||
Sort: 60, // 排序在称号之后
|
||
Status: true,
|
||
KeepAlive: true,
|
||
IsHide: false,
|
||
IsHideTab: false,
|
||
CreatedUser: "system",
|
||
UpdatedUser: "system",
|
||
}
|
||
|
||
if err := h.writeDB.Menus.WithContext(ctx.RequestContext()).Create(newMenu); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 21023, "创建菜单失败: "+err.Error()))
|
||
return
|
||
}
|
||
|
||
// 读取新创建的 ID
|
||
created, _ := h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Path.Eq(menuPath)).First()
|
||
menuID := int64(0)
|
||
if created != nil {
|
||
menuID = created.ID
|
||
}
|
||
|
||
ctx.Payload(&ensureActivityProfitLossMenuResponse{Ensured: true, Parent: parentID, MenuID: menuID})
|
||
}
|
||
}
|