diff --git a/internal/api/admin/dashboard_activity.go b/internal/api/admin/dashboard_activity.go index 832210f..d155c5c 100755 --- a/internal/api/admin/dashboard_activity.go +++ b/internal/api/admin/dashboard_activity.go @@ -216,8 +216,7 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc { } var costStats []costStat db.Table(model.TableNameUserInventory). - Select("user_inventory.activity_id, SUM(products.price) as total_cost"). - Joins("JOIN products ON products.id = user_inventory.product_id"). + 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). // 仅统计已支付订单产生的成本 @@ -429,7 +428,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { activity_reward_settings.product_id, COALESCE(products.name, '') as product_name, COALESCE(products.images_json, '[]') as images_json, - COALESCE(products.price, 0) as product_price, + 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, diff --git a/internal/api/admin/dashboard_spending.go b/internal/api/admin/dashboard_spending.go index 28b587e..9ccfe9b 100755 --- a/internal/api/admin/dashboard_spending.go +++ b/internal/api/admin/dashboard_spending.go @@ -216,7 +216,6 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc { // Join with Products, Activities, and Orders (for livestream detection) query := db.Table(model.TableNameUserInventory). - Joins("JOIN products ON products.id = user_inventory.product_id"). Joins("LEFT JOIN activities ON activities.id = user_inventory.activity_id"). Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id"). Where("user_inventory.user_id IN ?", userIDs) @@ -231,10 +230,10 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc { err := query.Select(` user_inventory.user_id, - SUM(products.price) as total_value, - SUM(CASE WHEN activities.activity_category_id = 1 THEN products.price ELSE 0 END) as ichiban_prize, - SUM(CASE WHEN activities.activity_category_id = 2 THEN products.price ELSE 0 END) as infinite_prize, - SUM(CASE WHEN activities.activity_category_id = 3 THEN products.price ELSE 0 END) as matching_prize + SUM(user_inventory.value_cents) as total_value, + SUM(CASE WHEN activities.activity_category_id = 1 THEN user_inventory.value_cents ELSE 0 END) as ichiban_prize, + SUM(CASE WHEN activities.activity_category_id = 2 THEN user_inventory.value_cents ELSE 0 END) as infinite_prize, + SUM(CASE WHEN activities.activity_category_id = 3 THEN user_inventory.value_cents ELSE 0 END) as matching_prize `). Group("user_inventory.user_id"). Scan(&invStats).Error diff --git a/internal/api/admin/dashboard_user_spending.go b/internal/api/admin/dashboard_user_spending.go index 97a0cbf..0ec1a05 100755 --- a/internal/api/admin/dashboard_user_spending.go +++ b/internal/api/admin/dashboard_user_spending.go @@ -120,7 +120,6 @@ func (h *handler) GetUserSpendingDashboard() core.HandlerFunc { var prizeStats []prizeStat prizeQuery := db.Table(model.TableNameUserInventory). - Joins("JOIN products ON products.id = user_inventory.product_id"). Where("user_inventory.user_id = ?", userID). Where("user_inventory.status IN ?", []int{1, 3}). Where("user_inventory.remark NOT LIKE ?", "%void%") @@ -131,7 +130,7 @@ func (h *handler) GetUserSpendingDashboard() core.HandlerFunc { prizeQuery.Select(` COALESCE(user_inventory.activity_id, 0) as activity_id, - SUM(products.price) as prize_value + SUM(user_inventory.value_cents) as prize_value `). Group("COALESCE(user_inventory.activity_id, 0)"). Scan(&prizeStats) diff --git a/internal/api/admin/pay_refund_admin.go b/internal/api/admin/pay_refund_admin.go index 04d722f..364754a 100755 --- a/internal/api/admin/pay_refund_admin.go +++ b/internal/api/admin/pay_refund_admin.go @@ -177,6 +177,14 @@ func (h *handler) CreateRefund() core.HandlerFunc { // 全额退款:回收中奖资产与奖品库存(包含已兑换积分的资产) svc := usersvc.New(h.logger, h.repo) + rate := int64(1) + if cfgRate, _ := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq("points_exchange_per_cent")).First(); cfgRate != nil { + var rv int64 + _, _ = fmt.Sscanf(cfgRate.ConfigValue, "%d", &rv) + if rv > 0 { + rate = rv + } + } // 直接使用已初始化的 activity service 清理格位 _ = h.activity.ClearIchibanPositionsByOrderID(ctx.RequestContext(), order.ID) @@ -191,18 +199,25 @@ func (h *handler) CreateRefund() core.HandlerFunc { } } else if inv.Status == 3 { // 状态3(已兑换):扣除积分并作废 + deductPoints := int64(0) matches := rePoints.FindStringSubmatch(inv.Remark) if len(matches) > 1 { p, _ := strconv.ParseInt(matches[1], 10, 64) if p > 0 { - // 扣除积分(记录流水)- 使用柔性扣减 - _, consumed, err := svc.ConsumePointsForRefund(ctx.RequestContext(), order.UserID, p, "user_inventory", strconv.FormatInt(inv.ID, 10), "订单退款回收兑换积分") - if err != nil { - h.logger.Error(fmt.Sprintf("refund reclaim points failed: order=%s user=%d points=%d err=%v", order.OrderNo, order.UserID, p, err)) - } - if consumed < p { - pointsShortage = true - } + deductPoints = p + } + } + if deductPoints <= 0 && inv.ValueCents > 0 { + deductPoints = inv.ValueCents * rate + } + if deductPoints > 0 { + // 扣除积分(记录流水)- 使用柔性扣减 + _, consumed, err := svc.ConsumePointsForRefund(ctx.RequestContext(), order.UserID, deductPoints, "user_inventory", strconv.FormatInt(inv.ID, 10), "订单退款回收兑换积分") + if err != nil { + h.logger.Error(fmt.Sprintf("refund reclaim points failed: order=%s user=%d points=%d err=%v", order.OrderNo, order.UserID, deductPoints, err)) + } + if consumed < deductPoints { + pointsShortage = true } } // 更新状态为2 (作废) diff --git a/internal/api/admin/rewards_admin.go b/internal/api/admin/rewards_admin.go index 6563873..7a18360 100755 --- a/internal/api/admin/rewards_admin.go +++ b/internal/api/admin/rewards_admin.go @@ -12,18 +12,20 @@ import ( ) type rewardItem struct { - ID int64 `json:"id"` - ProductID int64 `json:"product_id"` - Weight float64 `json:"weight" binding:"required"` - Quantity int64 `json:"quantity" binding:"required"` - OriginalQty int64 `json:"original_qty" binding:"required"` - Level int32 `json:"level" binding:"required"` - Sort int32 `json:"sort"` - IsBoss int32 `json:"is_boss"` - MinScore int64 `json:"min_score"` - ProductName string `json:"product_name"` - ProductImageUrl string `json:"product_image_url"` - ProductPrice float64 `json:"product_price"` + ID int64 `json:"id"` + ProductID int64 `json:"product_id"` + Weight float64 `json:"weight" binding:"required"` + Quantity int64 `json:"quantity" binding:"required"` + OriginalQty int64 `json:"original_qty" binding:"required"` + Level int32 `json:"level" binding:"required"` + Sort int32 `json:"sort"` + IsBoss int32 `json:"is_boss"` + MinScore int64 `json:"min_score"` + ProductName string `json:"product_name"` + ProductImageUrl string `json:"product_image_url"` + ProductPrice float64 `json:"product_price"` // 兼容:返回配置快照价 + ProductPriceSnapshot float64 `json:"product_price_snapshot"` + ProductPriceCurrent float64 `json:"product_price_current"` } type createRewardsRequest struct { @@ -151,9 +153,11 @@ func (h *handler) ListIssueRewards() core.HandlerFunc { if p, ok := pm[v.ProductID]; ok { it.ProductName = p.Name it.ProductImageUrl = p.ImagesJSON - it.ProductPrice = float64(p.Price) / 100 + it.ProductPriceCurrent = float64(p.Price) / 100 } } + it.ProductPriceSnapshot = float64(v.PriceSnapshotCents) / 100 + it.ProductPrice = it.ProductPriceSnapshot res.List[i] = it } ctx.Payload(res) diff --git a/internal/api/admin/users_admin.go b/internal/api/admin/users_admin.go index c09e867..aaaaa94 100755 --- a/internal/api/admin/users_admin.go +++ b/internal/api/admin/users_admin.go @@ -344,7 +344,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc { var invRes []invResult 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")). + Select(h.readDB.UserInventory.UserID, h.readDB.UserInventory.ValueCents.Sum().As("value")). Where(h.readDB.UserInventory.UserID.In(userIDs...)). Where(h.readDB.UserInventory.Status.Eq(1)). // 1=持有 Group(h.readDB.UserInventory.UserID). @@ -564,9 +564,8 @@ func (h *handler) ListUserInvites() core.HandlerFunc { `, userID).Scan(&summaryConsume).Error // 资产价值汇总(不包含已兑换的商品) _ = h.repo.GetDbR().Raw(` - SELECT COALESCE(SUM(p.price), 0) + SELECT COALESCE(SUM(ui.value_cents), 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 @@ -763,7 +762,7 @@ func (h *handler) ListUserInventory() core.HandlerFunc { sql := ` SELECT ui.id, ui.user_id, ui.product_id, ui.order_id, ui.activity_id, ui.reward_id, ui.status, ui.remark, ui.created_at, ui.updated_at, - p.name as product_name, p.images_json as product_images, p.price as product_price + p.name as product_name, p.images_json as product_images, COALESCE(ui.value_cents, p.price, 0) as product_price FROM user_inventory ui LEFT JOIN products p ON p.id = ui.product_id WHERE ui.user_id = ? @@ -2158,9 +2157,9 @@ func (h *handler) AdminSearchUsers() core.HandlerFunc { // 按手机号或昵称模糊匹配 rows, _ := q.Where( - h.readDB.Users.Mobile.Like("%"+req.Keyword+"%"), + h.readDB.Users.Mobile.Like("%" + req.Keyword + "%"), ).Or( - h.readDB.Users.Nickname.Like("%"+req.Keyword+"%"), + h.readDB.Users.Nickname.Like("%" + req.Keyword + "%"), ).Limit(10).Find() items := make([]userItem, 0, len(rows)) diff --git a/internal/service/activity/reward_snapshot_test.go b/internal/service/activity/reward_snapshot_test.go new file mode 100644 index 0000000..c4c3b71 --- /dev/null +++ b/internal/service/activity/reward_snapshot_test.go @@ -0,0 +1,111 @@ +package activity + +import ( + "context" + "testing" + + "bindbox-game/internal/repository/mysql/dao" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func newRewardSnapshotTestService(t *testing.T) (*service, *dao.Query, *gorm.DB) { + t.Helper() + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite failed: %v", err) + } + if err := db.Exec(`CREATE TABLE products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + price INTEGER NOT NULL, + stock INTEGER NOT NULL, + images_json TEXT, + updated_at DATETIME, + deleted_at DATETIME + );`).Error; err != nil { + t.Fatalf("create products failed: %v", err) + } + if err := db.Exec(`CREATE TABLE activity_reward_settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATETIME, + updated_at DATETIME, + issue_id INTEGER NOT NULL, + product_id INTEGER, + price_snapshot_cents INTEGER NOT NULL DEFAULT 0, + price_snapshot_at DATETIME, + weight INTEGER NOT NULL, + quantity INTEGER NOT NULL, + original_qty INTEGER NOT NULL, + level INTEGER NOT NULL, + sort INTEGER, + is_boss INTEGER, + min_score INTEGER NOT NULL DEFAULT 0, + deleted_at DATETIME + );`).Error; err != nil { + t.Fatalf("create activity_reward_settings failed: %v", err) + } + + q := dao.Use(db) + svc := &service{readDB: q, writeDB: q} + return svc, q, db +} + +func TestCreateIssueRewards_SnapshotFromProductPrice(t *testing.T) { + svc, q, db := newRewardSnapshotTestService(t) + ctx := context.Background() + + if err := db.Exec("INSERT INTO products (id, name, price, stock, images_json) VALUES (101, 'A', 1000, 10, '[]')").Error; err != nil { + t.Fatalf("insert product failed: %v", err) + } + + err := svc.CreateIssueRewards(ctx, 88, []CreateRewardInput{ + { + ProductID: 101, + Weight: 1, + Quantity: 2, + OriginalQty: 2, + Level: 1, + Sort: 1, + IsBoss: 0, + MinScore: 0, + }, + }) + if err != nil { + t.Fatalf("CreateIssueRewards failed: %v", err) + } + + row, err := q.ActivityRewardSettings.WithContext(ctx).Where(q.ActivityRewardSettings.IssueID.Eq(88)).First() + if err != nil { + t.Fatalf("query reward failed: %v", err) + } + if row.PriceSnapshotCents != 1000 { + t.Fatalf("expected snapshot=1000, got=%d", row.PriceSnapshotCents) + } +} + +func TestModifyIssueReward_ProductChanged_RecomputeSnapshot(t *testing.T) { + svc, q, db := newRewardSnapshotTestService(t) + ctx := context.Background() + + _ = db.Exec("INSERT INTO products (id, name, price, stock, images_json) VALUES (101, 'A', 1000, 10, '[]')").Error + _ = db.Exec("INSERT INTO products (id, name, price, stock, images_json) VALUES (102, 'B', 2300, 10, '[]')").Error + _ = db.Exec("INSERT INTO activity_reward_settings (id, issue_id, product_id, price_snapshot_cents, weight, quantity, original_qty, level, sort, is_boss, min_score) VALUES (1, 9, 101, 1000, 1, 1, 1, 1, 1, 0, 0)").Error + + newProductID := int64(102) + if err := svc.ModifyIssueReward(ctx, 1, ModifyRewardInput{ProductID: &newProductID}); err != nil { + t.Fatalf("ModifyIssueReward failed: %v", err) + } + + row, err := q.ActivityRewardSettings.WithContext(ctx).Where(q.ActivityRewardSettings.ID.Eq(1)).First() + if err != nil { + t.Fatalf("query reward failed: %v", err) + } + if row.ProductID != 102 { + t.Fatalf("expected product_id=102, got=%d", row.ProductID) + } + if row.PriceSnapshotCents != 2300 { + t.Fatalf("expected snapshot=2300, got=%d", row.PriceSnapshotCents) + } +} diff --git a/internal/service/activity/rewards_create.go b/internal/service/activity/rewards_create.go index 4d38840..bd30f79 100755 --- a/internal/service/activity/rewards_create.go +++ b/internal/service/activity/rewards_create.go @@ -2,6 +2,7 @@ package activity import ( "context" + "time" "bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/model" @@ -12,17 +13,40 @@ import ( // 返回: 错误信息 func (s *service) CreateIssueRewards(ctx context.Context, issueID int64, rewards []CreateRewardInput) error { return s.writeDB.Transaction(func(tx *dao.Query) error { + productIDs := make(map[int64]struct{}) + for _, r := range rewards { + if r.ProductID > 0 { + productIDs[r.ProductID] = struct{}{} + } + } + productPriceMap := make(map[int64]int64) + if len(productIDs) > 0 { + ids := make([]int64, 0, len(productIDs)) + for id := range productIDs { + ids = append(ids, id) + } + products, err := tx.Products.WithContext(ctx).Where(tx.Products.ID.In(ids...)).Find() + if err != nil { + return err + } + for _, p := range products { + productPriceMap[p.ID] = p.Price + } + } + for _, r := range rewards { item := &model.ActivityRewardSettings{ - IssueID: issueID, - ProductID: r.ProductID, - Weight: r.Weight, - Quantity: r.Quantity, - OriginalQty: r.OriginalQty, - Level: r.Level, - Sort: r.Sort, - IsBoss: r.IsBoss, - MinScore: r.MinScore, + IssueID: issueID, + ProductID: r.ProductID, + PriceSnapshotCents: productPriceMap[r.ProductID], + PriceSnapshotAt: time.Now(), + Weight: r.Weight, + Quantity: r.Quantity, + OriginalQty: r.OriginalQty, + Level: r.Level, + Sort: r.Sort, + IsBoss: r.IsBoss, + MinScore: r.MinScore, } if err := tx.ActivityRewardSettings.WithContext(ctx).Create(item); err != nil { return err diff --git a/internal/service/activity/rewards_modify.go b/internal/service/activity/rewards_modify.go index 7596323..ff3dc85 100755 --- a/internal/service/activity/rewards_modify.go +++ b/internal/service/activity/rewards_modify.go @@ -17,6 +17,16 @@ func (s *service) ModifyIssueReward(ctx context.Context, rewardID int64, in Modi } if in.ProductID != nil { item.ProductID = *in.ProductID + priceSnapshot := int64(0) + if *in.ProductID > 0 { + product, err := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(*in.ProductID)).First() + if err != nil { + return err + } + priceSnapshot = product.Price + } + item.PriceSnapshotCents = priceSnapshot + item.PriceSnapshotAt = time.Now() } if in.Weight != nil { item.Weight = int32(*in.Weight) diff --git a/internal/service/douyin/scheduler.go b/internal/service/douyin/scheduler.go index 3fa2125..8e8ae19 100755 --- a/internal/service/douyin/scheduler.go +++ b/internal/service/douyin/scheduler.go @@ -319,6 +319,15 @@ func (s *service) reclaimLivestreamAssets(ctx context.Context, log *model.Livest } // 2. 回收资产 + rate := int64(1) + var cfg model.SystemConfigs + if err := s.repo.GetDbR().Where("config_key = ?", "points_exchange_per_cent").First(&cfg).Error; err == nil { + var rv int64 + _, _ = fmt.Sscanf(cfg.ConfigValue, "%d", &rv) + if rv > 0 { + rate = rv + } + } for _, inv := range inventories { if inv.Status == 1 { // 状态1(持有):作废 @@ -332,25 +341,28 @@ func (s *service) reclaimLivestreamAssets(ctx context.Context, log *model.Livest ) } else if inv.Status == 3 { // 状态3(已兑换/发货):扣除积分 - // 查找商品价格作为积分扣除依据 - var product model.Products - if err := s.repo.GetDbR().Where("id = ?", inv.ProductID).First(&product).Error; err == nil { - pointsToDeduct := product.Price / 100 // 分转换为积分(假设 1积分=1分钱) - if pointsToDeduct > 0 { - _, consumed, err := s.userSvc.ConsumePointsForRefund(ctx, inv.UserID, pointsToDeduct, "user_inventory", fmt.Sprintf("%d", inv.ID), "直播退款回收已兑换资产") - if err != nil { - s.logger.Error("[资产回收] 扣除积分失败", zap.Error(err), zap.Int64("user_id", inv.UserID)) - } - if consumed < pointsToDeduct { - // 积分不足,标记用户 - s.logger.Warn("[资产回收] 用户积分不足", - zap.Int64("user_id", inv.UserID), - zap.Int64("needed", pointsToDeduct), - zap.Int64("consumed", consumed), - ) - // 可选:加入黑名单 - // db.Exec("UPDATE users SET status = 3 WHERE id = ?", inv.UserID) - } + pointsToDeduct := inv.ValueCents * rate + if pointsToDeduct <= 0 { + // 兼容历史数据,兜底回退商品价格 + var product model.Products + if err := s.repo.GetDbR().Where("id = ?", inv.ProductID).First(&product).Error; err == nil { + pointsToDeduct = product.Price * rate + } + } + if pointsToDeduct > 0 { + _, consumed, err := s.userSvc.ConsumePointsForRefund(ctx, inv.UserID, pointsToDeduct, "user_inventory", fmt.Sprintf("%d", inv.ID), "直播退款回收已兑换资产") + if err != nil { + s.logger.Error("[资产回收] 扣除积分失败", zap.Error(err), zap.Int64("user_id", inv.UserID)) + } + if consumed < pointsToDeduct { + // 积分不足,标记用户 + s.logger.Warn("[资产回收] 用户积分不足", + zap.Int64("user_id", inv.UserID), + zap.Int64("needed", pointsToDeduct), + zap.Int64("consumed", consumed), + ) + // 可选:加入黑名单 + // db.Exec("UPDATE users SET status = 3 WHERE id = ?", inv.UserID) } } // 作废记录 diff --git a/internal/service/user/address_share.go b/internal/service/user/address_share.go index 72634fd..c8c0a83 100755 --- a/internal/service/user/address_share.go +++ b/internal/service/user/address_share.go @@ -500,9 +500,20 @@ func (s *service) RedeemInventoryToPoints(ctx context.Context, userID int64, inv if err != nil { return 0, err } - p, err := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(inv.ProductID)).First() - if err != nil { - return 0, err + valueCents := inv.ValueCents + valueSource := inv.ValueSource + valueSnapshotAt := inv.ValueSnapshotAt + if valueCents <= 0 { + p, err := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(inv.ProductID)).First() + if err != nil { + return 0, err + } + valueCents = p.Price + valueSource = 2 + valueSnapshotAt = time.Now() + if db := s.repo.GetDbW().Exec("UPDATE user_inventory SET value_cents=?, value_source=?, value_snapshot_at=? WHERE id=? AND user_id=?", valueCents, valueSource, valueSnapshotAt, inventoryID, userID); db.Error != nil { + return 0, db.Error + } } cfg, _ := s.readDB.SystemConfigs.WithContext(ctx).Where(s.readDB.SystemConfigs.ConfigKey.Eq("points_exchange_per_cent")).First() rate := int64(1) @@ -513,7 +524,7 @@ func (s *service) RedeemInventoryToPoints(ctx context.Context, userID int64, inv rate = r } } - points := p.Price * rate + points := valueCents * rate if err = s.AddPoints(ctx, userID, points, "redeem_reward", fmt.Sprintf("inventory:%d product:%d", inventoryID, inv.ProductID), nil, nil); err != nil { return 0, err } @@ -569,39 +580,63 @@ func (s *service) RedeemInventoriesToPoints(ctx context.Context, userID int64, i return 0, fmt.Errorf("no_valid_inventory") } - // 构建inventory映射和收集productID - invMap := make(map[int64]*model.UserInventory, len(invList)) + // 4. 按资产快照计算总积分,缺失快照时回退商品价格并回写 productIDs := make([]int64, 0, len(invList)) productIDSet := make(map[int64]struct{}) for _, inv := range invList { - invMap[inv.ID] = inv - if _, ok := productIDSet[inv.ProductID]; !ok { - productIDSet[inv.ProductID] = struct{}{} - productIDs = append(productIDs, inv.ProductID) + if inv.ValueCents <= 0 { + if _, ok := productIDSet[inv.ProductID]; !ok { + productIDSet[inv.ProductID] = struct{}{} + productIDs = append(productIDs, inv.ProductID) + } + } + } + productPriceMap := make(map[int64]int64) + if len(productIDs) > 0 { + products, err := s.readDB.Products.WithContext(ctx). + Where(s.readDB.Products.ID.In(productIDs...)). + Find() + if err != nil { + return 0, err + } + for _, p := range products { + productPriceMap[p.ID] = p.Price } } - // 4. 批量查询所有products(一次查询替代N次) - products, err := s.readDB.Products.WithContext(ctx). - Where(s.readDB.Products.ID.In(productIDs...)). - Find() - if err != nil { - return 0, err - } - productMap := make(map[int64]*model.Products, len(products)) - for _, p := range products { - productMap[p.ID] = p - } - - // 5. 计算总积分和准备批量更新数据 + // 5. 计算总积分和准备批量更新 var totalPoints int64 validIDs := make([]int64, 0, len(invList)) + type valueFix struct { + ID int64 + ValueCents int64 + ValueSource int32 + ValueSnapAt time.Time + } + valueFixes := make([]valueFix, 0) for _, inv := range invList { - p := productMap[inv.ProductID] - if p == nil { + valueCents := inv.ValueCents + valueSource := inv.ValueSource + valueSnapshotAt := inv.ValueSnapshotAt + if valueCents <= 0 { + price, ok := productPriceMap[inv.ProductID] + if !ok { + continue + } + valueCents = price + valueSource = 2 + valueSnapshotAt = time.Now() + valueFixes = append(valueFixes, valueFix{ + ID: inv.ID, + ValueCents: valueCents, + ValueSource: valueSource, + ValueSnapAt: valueSnapshotAt, + }) + } + if valueCents <= 0 { continue } - points := p.Price * rate + points := valueCents * rate totalPoints += points validIDs = append(validIDs, inv.ID) } @@ -639,6 +674,14 @@ func (s *service) RedeemInventoriesToPoints(ctx context.Context, userID int64, i } // 批量更新inventory状态(一次UPDATE替代N次) + for _, fix := range valueFixes { + if err := tx.Exec( + "UPDATE user_inventory SET value_cents=?, value_source=?, value_snapshot_at=? WHERE id=? AND user_id=?", + fix.ValueCents, fix.ValueSource, fix.ValueSnapAt, fix.ID, userID, + ).Error; err != nil { + return err + } + } if err := tx.Exec( "UPDATE user_inventory SET status=3, updated_at=NOW(3), remark=CONCAT(IFNULL(remark,''),'|batch_redeemed') WHERE id IN ? AND user_id=? AND status=1", validIDs, userID, diff --git a/internal/service/user/inventory_list.go b/internal/service/user/inventory_list.go index f650dc5..c0b7200 100755 --- a/internal/service/user/inventory_list.go +++ b/internal/service/user/inventory_list.go @@ -91,10 +91,12 @@ func (s *service) ListInventoryWithProduct(ctx context.Context, userID int64, pa p := products[r.ProductID] name := "" images := "" - var price int64 + price := r.ValueCents if p != nil { name = p.Name images = p.ImagesJSON + } + if price <= 0 && p != nil { price = p.Price } sh := shipMap[r.ID] @@ -177,10 +179,12 @@ func (s *service) ListInventoryWithProductActive(ctx context.Context, userID int p := products[r.ProductID] name := "" images := "" - var price int64 + price := r.ValueCents if p != nil { name = p.Name images = p.ImagesJSON + } + if price <= 0 && p != nil { price = p.Price } sh := shipMap[r.ID] @@ -214,10 +218,11 @@ func (s *service) ListInventoryAggregated(ctx context.Context, userID int64, pag // 1. 获取聚合后的商品ID列表 (GROUP BY product_id, status) var groupResults []struct { - ProductID int64 `gorm:"column:product_id"` - Status int32 `gorm:"column:status"` - Count int64 `gorm:"column:count"` - UpdatedAt time.Time `gorm:"column:updated_at"` + ProductID int64 `gorm:"column:product_id"` + Status int32 `gorm:"column:status"` + Count int64 `gorm:"column:count"` + ValueCents int64 `gorm:"column:value_cents"` + UpdatedAt time.Time `gorm:"column:updated_at"` } q := s.readDB.UserInventory.WithContext(ctx).ReadDB(). @@ -225,6 +230,7 @@ func (s *service) ListInventoryAggregated(ctx context.Context, userID int64, pag s.readDB.UserInventory.ProductID, s.readDB.UserInventory.Status, s.readDB.UserInventory.ID.Count().As("count"), + s.readDB.UserInventory.ValueCents.Max().As("value_cents"), s.readDB.UserInventory.UpdatedAt.Max().As("updated_at"), ). Where(s.readDB.UserInventory.UserID.Eq(userID)) @@ -272,10 +278,12 @@ func (s *service) ListInventoryAggregated(ctx context.Context, userID int64, pag p, _ := s.readDB.Products.WithContext(ctx).ReadDB().Where(s.readDB.Products.ID.Eq(g.ProductID)).First() name := "未知商品" images := "" - var price int64 + price := g.ValueCents if p != nil { name = p.Name images = p.ImagesJSON + } + if price <= 0 && p != nil { price = p.Price } diff --git a/internal/service/user/reward_grant.go b/internal/service/user/reward_grant.go index 4840ee9..cba8ce9 100755 --- a/internal/service/user/reward_grant.go +++ b/internal/service/user/reward_grant.go @@ -47,11 +47,13 @@ func (s *service) GrantReward(ctx context.Context, userID int64, req GrantReward // 执行事务 err := s.writeDB.Transaction(func(tx *dao.Query) error { logger.Info("开始事务处理") + var rewardSetting *model.ActivityRewardSettings + var err error // 1. 检查奖励配置库存(如果提供了reward_id) if req.RewardID != nil { logger.Info("检查奖励配置", zap.Int64("reward_id", *req.RewardID)) - rewardSetting, err := tx.ActivityRewardSettings.WithContext(ctx).Where( + rewardSetting, err = tx.ActivityRewardSettings.WithContext(ctx).Where( tx.ActivityRewardSettings.ID.Eq(*req.RewardID), ).First() if err != nil { @@ -109,7 +111,7 @@ func (s *service) GrantReward(ctx context.Context, userID int64, req GrantReward } logger.Info("创建订单", zap.Any("order", order)) - err := tx.Orders.WithContext(ctx).Create(order) + err = tx.Orders.WithContext(ctx).Create(order) if err != nil { logger.Error("创建订单失败", zap.Error(err)) return fmt.Errorf("创建订单失败: %w", err) @@ -163,7 +165,25 @@ func (s *service) GrantReward(ctx context.Context, userID int64, req GrantReward inventory := &model.UserInventory{ UserID: userID, ProductID: req.ProductID, - OrderID: orderID, + ValueCents: func() int64 { + if rewardSetting != nil && rewardSetting.PriceSnapshotCents > 0 { + return rewardSetting.PriceSnapshotCents + } + return product.Price + }(), + ValueSource: func() int32 { + if rewardSetting != nil && rewardSetting.PriceSnapshotCents > 0 { + return 1 + } + return 2 + }(), + ValueSnapshotAt: func() time.Time { + if rewardSetting != nil && !rewardSetting.PriceSnapshotAt.IsZero() { + return rewardSetting.PriceSnapshotAt + } + return time.Now() + }(), + OrderID: orderID, ActivityID: func() int64 { if req.ActivityID != nil { return *req.ActivityID @@ -288,6 +308,7 @@ func (s *service) GrantRewardToOrder(ctx context.Context, userID int64, req Gran // 执行事务 err := s.writeDB.Transaction(func(tx *dao.Query) error { logger.Info("开始事务处理") + var rewardSetting *model.ActivityRewardSettings // 1. 验证订单存在且属于该用户 order, err := tx.Orders.WithContext(ctx).Where( @@ -322,6 +343,13 @@ func (s *service) GrantRewardToOrder(ctx context.Context, userID int64, req Gran logger.Error("奖励库存不足或不存在") return fmt.Errorf("奖励库存不足或不存在") } + rewardSetting, err = tx.ActivityRewardSettings.WithContext(ctx).Where( + tx.ActivityRewardSettings.ID.Eq(*req.RewardID), + ).First() + if err != nil { + logger.Error("查询奖励配置失败", zap.Error(err)) + return fmt.Errorf("查询奖励配置失败: %w", err) + } logger.Info("奖励库存扣减成功(乐观锁)") } @@ -355,7 +383,25 @@ func (s *service) GrantRewardToOrder(ctx context.Context, userID int64, req Gran inventory := &model.UserInventory{ UserID: userID, ProductID: req.ProductID, - OrderID: req.OrderID, // 关联到原抽奖订单 + ValueCents: func() int64 { + if rewardSetting != nil && rewardSetting.PriceSnapshotCents > 0 { + return rewardSetting.PriceSnapshotCents + } + return product.Price + }(), + ValueSource: func() int32 { + if rewardSetting != nil && rewardSetting.PriceSnapshotCents > 0 { + return 1 + } + return 2 + }(), + ValueSnapshotAt: func() time.Time { + if rewardSetting != nil && !rewardSetting.PriceSnapshotAt.IsZero() { + return rewardSetting.PriceSnapshotAt + } + return time.Now() + }(), + OrderID: req.OrderID, // 关联到原抽奖订单 ActivityID: func() int64 { if req.ActivityID != nil { return *req.ActivityID diff --git a/internal/service/user/reward_grant_batch.go b/internal/service/user/reward_grant_batch.go index 6c354f2..0654f17 100755 --- a/internal/service/user/reward_grant_batch.go +++ b/internal/service/user/reward_grant_batch.go @@ -3,6 +3,7 @@ package user import ( "context" "fmt" + "time" "bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/model" @@ -60,6 +61,26 @@ func (s *service) BatchGrantRewardsToOrder(ctx context.Context, userID int64, or for _, p := range products { productMap[p.ID] = p } + rewardSnapshotMap := make(map[int64]*model.ActivityRewardSettings) + rewardIDSet := make(map[int64]struct{}) + for _, item := range items { + if item.RewardID != nil && *item.RewardID > 0 { + rewardIDSet[*item.RewardID] = struct{}{} + } + } + if len(rewardIDSet) > 0 { + rewardIDs := make([]int64, 0, len(rewardIDSet)) + for id := range rewardIDSet { + rewardIDs = append(rewardIDs, id) + } + rewardRows, err := tx.ActivityRewardSettings.WithContext(ctx).Where(tx.ActivityRewardSettings.ID.In(rewardIDs...)).Find() + if err != nil { + return fmt.Errorf("查询奖励配置失败: %w", err) + } + for _, row := range rewardRows { + rewardSnapshotMap[row.ID] = row + } + } // 3. 批量创建订单项和库存记录 var orderItems []*model.OrderItems @@ -83,8 +104,32 @@ func (s *service) BatchGrantRewardsToOrder(ctx context.Context, userID int64, or }) inventories = append(inventories, &model.UserInventory{ - UserID: userID, - ProductID: item.ProductID, + UserID: userID, + ProductID: item.ProductID, + ValueCents: func() int64 { + if item.RewardID != nil { + if reward, ok := rewardSnapshotMap[*item.RewardID]; ok && reward.PriceSnapshotCents > 0 { + return reward.PriceSnapshotCents + } + } + return product.Price + }(), + ValueSource: func() int32 { + if item.RewardID != nil { + if reward, ok := rewardSnapshotMap[*item.RewardID]; ok && reward.PriceSnapshotCents > 0 { + return 1 + } + } + return 2 + }(), + ValueSnapshotAt: func() time.Time { + if item.RewardID != nil { + if reward, ok := rewardSnapshotMap[*item.RewardID]; ok && !reward.PriceSnapshotAt.IsZero() { + return reward.PriceSnapshotAt + } + } + return time.Now() + }(), OrderID: orderID, ActivityID: item.ActivityID, RewardID: func() int64 {