package admin import ( "encoding/json" "fmt" "net/http" "sort" "strconv" "strings" "time" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation" "bindbox-game/internal/repository/mysql/model" financesvc "bindbox-game/internal/service/finance" "gorm.io/gorm" ) func computeActivityProfit(spending, cost int64) (int64, float64) { profit := spending - cost if spending <= 0 { return profit, 0 } return profit, float64(profit) / float64(spending) } 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"` // 奖品标价总和 (分) SpendingPaidCoupon int64 `json:"spending_paid_coupon"` SpendingGamePass int64 `json:"spending_game_pass"` PrizeCostBase int64 `json:"prize_cost_base"` PrizeCostMultiplier int64 `json:"prize_cost_multiplier"` PrizeCostFinal int64 `json:"prize_cost_final"` 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%' OR (orders.actual_amount = 0 AND COALESCE(orders.remark, '') LIKE '%use_game_pass%') ) 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%' OR (orders.actual_amount = 0 AND COALESCE(orders.remark, '') LIKE '%use_game_pass%') ) 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. 按活动汇总收入(现金/优惠券/次卡) type activityRevenueStat struct { ActivityID int64 SourceType int32 OrderNo string OrderAmount int64 DiscountAmount int64 OrderRemark string DrawCount int64 ActivityPrice int64 } var revenueStats []activityRevenueStat db.Table(model.TableNameOrders). Select(` activity_issues.activity_id, orders.source_type, orders.order_no, COALESCE(orders.actual_amount, 0) as order_amount, COALESCE(orders.discount_amount, 0) as discount_amount, COALESCE(orders.remark, '') as order_remark, COUNT(activity_draw_logs.id) as draw_count, COALESCE(MAX(activities.price_draw), 0) as activity_price `). Joins("JOIN activity_draw_logs ON activity_draw_logs.order_id = orders.id"). Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id"). Joins("JOIN activities ON activities.id = activity_issues.activity_id"). Where("orders.status = ?", 2). Where("activity_issues.activity_id IN ?", activityIDs). Group("orders.id, activity_issues.activity_id, orders.source_type, orders.order_no, orders.actual_amount, orders.discount_amount, orders.remark"). Scan(&revenueStats) for _, stat := range revenueStats { item, ok := activityMap[stat.ActivityID] if !ok { continue } if financesvc.IsGamePassOrder(stat.SourceType, stat.OrderNo, stat.OrderAmount, stat.OrderRemark) { item.TotalGamePassValue += stat.DrawCount * stat.ActivityPrice continue } item.TotalRevenue += stat.OrderAmount item.TotalDiscount += stat.DiscountAmount } // 4. 按活动汇总产出成本,统一使用 products.cost_price type activityCostStat struct { ActivityID int64 TotalCost int64 } var costStats []activityCostStat db.Table(model.TableNameActivityDrawLogs). Select(` activity_issues.activity_id, SUM(COALESCE(products.cost_price, 0) * ( COALESCE(NULLIF(activity_reward_settings.drop_quantity, 0), 1) + CASE WHEN user_item_cards.used_draw_log_id = activity_draw_logs.id AND system_item_cards.effect_type = 1 AND system_item_cards.reward_multiplier_x1000 >= 2000 THEN 1 ELSE 0 END )) as total_cost `). Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_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_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"). Where("activity_issues.activity_id IN ?", activityIDs). Where("orders.status = ?", 2). Group("activity_issues.activity_id"). Scan(&costStats) for _, stat := range costStats { if item, ok := activityMap[stat.ActivityID]; ok { item.TotalCost = stat.TotalCost item.PrizeCostBase = stat.TotalCost item.PrizeCostMultiplier = 1000 item.PrizeCostFinal = stat.TotalCost } } // 5. 计算盈亏和比率 finalList := make([]activityProfitLossItem, 0, len(activities)) for _, a := range activities { item := activityMap[a.ID] item.SpendingPaidCoupon = item.TotalRevenue + item.TotalDiscount item.SpendingGamePass = item.TotalGamePassValue spending := item.TotalRevenue + item.TotalDiscount + item.TotalGamePassValue item.Profit, item.ProfitRate = computeActivityProfit(spending, item.TotalCost) 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"` PlayerKeyword string `form:"player_keyword"` PrizeKeyword string `form:"prize_keyword"` } 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 } req.PlayerKeyword = strings.TrimSpace(req.PlayerKeyword) req.PrizeKeyword = strings.TrimSpace(req.PrizeKeyword) 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"). 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"). Where("activity_issues.activity_id = ?", activityID) countQuery = applyActivityLogFilters(countQuery, req) countQuery.Count(&total) var logs []struct { ID int64 UserID int64 Nickname string Avatar string ProductID int64 ProductName string ImagesJSON string ProductPrice int64 ProductCost int64 OrderAmount int64 DiscountAmount int64 PointsAmount int64 // 积分抵扣金额 OrderStatus int32 // 订单状态 SourceType int32 CouponID int64 CouponName string ItemCardID int64 ItemCardName string EffectType int32 Multiplier int32 DropQuantity int64 OrderRemark string // BUG修复:增加remark字段用于解析次数卡使用信息 OrderNo string // 订单号 DrawCount int64 // 该订单的总抽奖次数(用于金额分摊) UsedDrawLogID int64 // 道具卡实际使用的日志ID CreatedAt time.Time ActivityPrice int64 } 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(products.cost_price, 0) as product_cost, 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(NULLIF(activity_reward_settings.drop_quantity, 0), 1) as drop_quantity, 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, COALESCE(activities.price_draw, 0) as activity_price `). Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id"). Joins("JOIN activities ON activities.id = activity_issues.activity_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) logsQuery = applyActivityLogFilters(logsQuery, req) 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] } quantity := l.DropQuantity if quantity <= 0 { quantity = 1 } // Determine PayType and UsedCard + PaymentDetails payType := "现金支付" usedCard := "" paymentDetails := PaymentDetails{} // 金额将在 drawCount 计算后设置 isGamePassOrder := financesvc.IsGamePassOrder(l.SourceType, l.OrderNo, l.OrderAmount, l.OrderRemark) // 检查是否使用了优惠券 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 >= 2000 { quantity++ } } // 检查是否使用了次数卡(统一口径:source_type/order_no/remark 三条件) if isGamePassOrder { 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 // actual_amount 分摊(现金) perDrawDiscountAmount := l.DiscountAmount / drawCount // 展示用 perDrawPointsAmount := l.PointsAmount / drawCount // 展示用 perDrawGamePassAmount := int64(0) // 次卡单口径:仅记次卡价值,不再叠加 discount,避免”次卡+现金”双计 if isGamePassOrder { perDrawOrderAmount = 0 perDrawDiscountAmount = 0 perDrawPointsAmount = 0 if l.ActivityPrice > 0 { perDrawGamePassAmount = l.ActivityPrice } } // 设置支付详情中的分摊金额 paymentDetails.CouponDiscount = perDrawDiscountAmount paymentDetails.PointsDiscount = perDrawPointsAmount prizeCost := l.ProductCost * quantity profitSpending := perDrawOrderAmount + perDrawDiscountAmount + perDrawGamePassAmount 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.ProductCost, ProductQuantity: quantity, OrderAmount: perDrawOrderAmount + perDrawGamePassAmount, OrderNo: l.OrderNo, DiscountAmount: perDrawDiscountAmount, PayType: payType, UsedCard: usedCard, OrderStatus: l.OrderStatus, Profit: profitSpending - prizeCost, CreatedAt: l.CreatedAt, PaymentDetails: paymentDetails, } } ctx.Payload(&activityLogsResponse{ Page: req.Page, PageSize: req.PageSize, Total: total, List: list, }) } } func applyActivityLogFilters(q *gorm.DB, req *activityLogsRequest) *gorm.DB { if req == nil { return q } if req.UserID > 0 { q = q.Where("activity_draw_logs.user_id = ?", req.UserID) } if kw := req.PlayerKeyword; kw != "" { like := "%" + kw + "%" var args []interface{} condition := "(users.nickname LIKE ? OR users.mobile LIKE ? OR users.invite_code LIKE ?" args = append(args, like, like, like) if playerID, err := strconv.ParseInt(kw, 10, 64); err == nil { condition += " OR users.id = ?" args = append(args, playerID) } condition += ")" q = q.Where(condition, args...) } if kw := req.PrizeKeyword; kw != "" { like := "%" + kw + "%" args := []interface{}{like, like} condition := "(products.name LIKE ? OR CAST(products.id AS CHAR) LIKE ?" if prizeID, err := strconv.ParseInt(kw, 10, 64); err == nil { condition += " OR products.id = ?" args = append(args, prizeID) } condition += ")" q = q.Where(condition, args...) } return q } 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}) } }