feat(prize): freeze value snapshots across grant redeem refund and reports
This commit is contained in:
parent
70e45b09ab
commit
8b7af03400
@ -216,8 +216,7 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
var costStats []costStat
|
var costStats []costStat
|
||||||
db.Table(model.TableNameUserInventory).
|
db.Table(model.TableNameUserInventory).
|
||||||
Select("user_inventory.activity_id, SUM(products.price) as total_cost").
|
Select("user_inventory.activity_id, SUM(user_inventory.value_cents) as total_cost").
|
||||||
Joins("JOIN products ON products.id = user_inventory.product_id").
|
|
||||||
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
||||||
Where("user_inventory.activity_id IN ?", activityIDs).
|
Where("user_inventory.activity_id IN ?", activityIDs).
|
||||||
Where("orders.status = ?", 2). // 仅统计已支付订单产生的成本
|
Where("orders.status = ?", 2). // 仅统计已支付订单产生的成本
|
||||||
@ -429,7 +428,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
|
|||||||
activity_reward_settings.product_id,
|
activity_reward_settings.product_id,
|
||||||
COALESCE(products.name, '') as product_name,
|
COALESCE(products.name, '') as product_name,
|
||||||
COALESCE(products.images_json, '[]') as images_json,
|
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.actual_amount, 0) as order_amount,
|
||||||
COALESCE(orders.discount_amount, 0) as discount_amount,
|
COALESCE(orders.discount_amount, 0) as discount_amount,
|
||||||
COALESCE(orders.points_amount, 0) as points_amount,
|
COALESCE(orders.points_amount, 0) as points_amount,
|
||||||
|
|||||||
@ -216,7 +216,6 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
|||||||
|
|
||||||
// Join with Products, Activities, and Orders (for livestream detection)
|
// Join with Products, Activities, and Orders (for livestream detection)
|
||||||
query := db.Table(model.TableNameUserInventory).
|
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 activities ON activities.id = user_inventory.activity_id").
|
||||||
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
||||||
Where("user_inventory.user_id IN ?", userIDs)
|
Where("user_inventory.user_id IN ?", userIDs)
|
||||||
@ -231,10 +230,10 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
|||||||
|
|
||||||
err := query.Select(`
|
err := query.Select(`
|
||||||
user_inventory.user_id,
|
user_inventory.user_id,
|
||||||
SUM(products.price) as total_value,
|
SUM(user_inventory.value_cents) 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 = 1 THEN user_inventory.value_cents 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 = 2 THEN user_inventory.value_cents ELSE 0 END) as infinite_prize,
|
||||||
SUM(CASE WHEN activities.activity_category_id = 3 THEN products.price ELSE 0 END) as matching_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").
|
Group("user_inventory.user_id").
|
||||||
Scan(&invStats).Error
|
Scan(&invStats).Error
|
||||||
|
|||||||
@ -120,7 +120,6 @@ func (h *handler) GetUserSpendingDashboard() core.HandlerFunc {
|
|||||||
var prizeStats []prizeStat
|
var prizeStats []prizeStat
|
||||||
|
|
||||||
prizeQuery := db.Table(model.TableNameUserInventory).
|
prizeQuery := db.Table(model.TableNameUserInventory).
|
||||||
Joins("JOIN products ON products.id = user_inventory.product_id").
|
|
||||||
Where("user_inventory.user_id = ?", userID).
|
Where("user_inventory.user_id = ?", userID).
|
||||||
Where("user_inventory.status IN ?", []int{1, 3}).
|
Where("user_inventory.status IN ?", []int{1, 3}).
|
||||||
Where("user_inventory.remark NOT LIKE ?", "%void%")
|
Where("user_inventory.remark NOT LIKE ?", "%void%")
|
||||||
@ -131,7 +130,7 @@ func (h *handler) GetUserSpendingDashboard() core.HandlerFunc {
|
|||||||
|
|
||||||
prizeQuery.Select(`
|
prizeQuery.Select(`
|
||||||
COALESCE(user_inventory.activity_id, 0) as activity_id,
|
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)").
|
Group("COALESCE(user_inventory.activity_id, 0)").
|
||||||
Scan(&prizeStats)
|
Scan(&prizeStats)
|
||||||
|
|||||||
@ -177,6 +177,14 @@ func (h *handler) CreateRefund() core.HandlerFunc {
|
|||||||
|
|
||||||
// 全额退款:回收中奖资产与奖品库存(包含已兑换积分的资产)
|
// 全额退款:回收中奖资产与奖品库存(包含已兑换积分的资产)
|
||||||
svc := usersvc.New(h.logger, h.repo)
|
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 清理格位
|
// 直接使用已初始化的 activity service 清理格位
|
||||||
_ = h.activity.ClearIchibanPositionsByOrderID(ctx.RequestContext(), order.ID)
|
_ = h.activity.ClearIchibanPositionsByOrderID(ctx.RequestContext(), order.ID)
|
||||||
|
|
||||||
@ -191,18 +199,25 @@ func (h *handler) CreateRefund() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
} else if inv.Status == 3 {
|
} else if inv.Status == 3 {
|
||||||
// 状态3(已兑换):扣除积分并作废
|
// 状态3(已兑换):扣除积分并作废
|
||||||
|
deductPoints := int64(0)
|
||||||
matches := rePoints.FindStringSubmatch(inv.Remark)
|
matches := rePoints.FindStringSubmatch(inv.Remark)
|
||||||
if len(matches) > 1 {
|
if len(matches) > 1 {
|
||||||
p, _ := strconv.ParseInt(matches[1], 10, 64)
|
p, _ := strconv.ParseInt(matches[1], 10, 64)
|
||||||
if p > 0 {
|
if p > 0 {
|
||||||
// 扣除积分(记录流水)- 使用柔性扣减
|
deductPoints = p
|
||||||
_, 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 deductPoints <= 0 && inv.ValueCents > 0 {
|
||||||
}
|
deductPoints = inv.ValueCents * rate
|
||||||
if consumed < p {
|
}
|
||||||
pointsShortage = true
|
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 (作废)
|
// 更新状态为2 (作废)
|
||||||
|
|||||||
@ -12,18 +12,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type rewardItem struct {
|
type rewardItem struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
ProductID int64 `json:"product_id"`
|
ProductID int64 `json:"product_id"`
|
||||||
Weight float64 `json:"weight" binding:"required"`
|
Weight float64 `json:"weight" binding:"required"`
|
||||||
Quantity int64 `json:"quantity" binding:"required"`
|
Quantity int64 `json:"quantity" binding:"required"`
|
||||||
OriginalQty int64 `json:"original_qty" binding:"required"`
|
OriginalQty int64 `json:"original_qty" binding:"required"`
|
||||||
Level int32 `json:"level" binding:"required"`
|
Level int32 `json:"level" binding:"required"`
|
||||||
Sort int32 `json:"sort"`
|
Sort int32 `json:"sort"`
|
||||||
IsBoss int32 `json:"is_boss"`
|
IsBoss int32 `json:"is_boss"`
|
||||||
MinScore int64 `json:"min_score"`
|
MinScore int64 `json:"min_score"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
ProductImageUrl string `json:"product_image_url"`
|
ProductImageUrl string `json:"product_image_url"`
|
||||||
ProductPrice float64 `json:"product_price"`
|
ProductPrice float64 `json:"product_price"` // 兼容:返回配置快照价
|
||||||
|
ProductPriceSnapshot float64 `json:"product_price_snapshot"`
|
||||||
|
ProductPriceCurrent float64 `json:"product_price_current"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type createRewardsRequest struct {
|
type createRewardsRequest struct {
|
||||||
@ -151,9 +153,11 @@ func (h *handler) ListIssueRewards() core.HandlerFunc {
|
|||||||
if p, ok := pm[v.ProductID]; ok {
|
if p, ok := pm[v.ProductID]; ok {
|
||||||
it.ProductName = p.Name
|
it.ProductName = p.Name
|
||||||
it.ProductImageUrl = p.ImagesJSON
|
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
|
res.List[i] = it
|
||||||
}
|
}
|
||||||
ctx.Payload(res)
|
ctx.Payload(res)
|
||||||
|
|||||||
@ -344,7 +344,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
var invRes []invResult
|
var invRes []invResult
|
||||||
h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
|
h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
|
||||||
LeftJoin(h.readDB.Products, h.readDB.Products.ID.EqCol(h.readDB.UserInventory.ProductID)).
|
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.UserID.In(userIDs...)).
|
||||||
Where(h.readDB.UserInventory.Status.Eq(1)). // 1=持有
|
Where(h.readDB.UserInventory.Status.Eq(1)). // 1=持有
|
||||||
Group(h.readDB.UserInventory.UserID).
|
Group(h.readDB.UserInventory.UserID).
|
||||||
@ -564,9 +564,8 @@ func (h *handler) ListUserInvites() core.HandlerFunc {
|
|||||||
`, userID).Scan(&summaryConsume).Error
|
`, userID).Scan(&summaryConsume).Error
|
||||||
// 资产价值汇总(不包含已兑换的商品)
|
// 资产价值汇总(不包含已兑换的商品)
|
||||||
_ = h.repo.GetDbR().Raw(`
|
_ = h.repo.GetDbR().Raw(`
|
||||||
SELECT COALESCE(SUM(p.price), 0)
|
SELECT COALESCE(SUM(ui.value_cents), 0)
|
||||||
FROM user_inventory ui
|
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 = ?)
|
WHERE ui.user_id IN (SELECT invitee_id FROM user_invites WHERE inviter_id = ?)
|
||||||
AND ui.status != 2
|
AND ui.status != 2
|
||||||
`, userID).Scan(&summaryAsset).Error
|
`, userID).Scan(&summaryAsset).Error
|
||||||
@ -763,7 +762,7 @@ func (h *handler) ListUserInventory() core.HandlerFunc {
|
|||||||
sql := `
|
sql := `
|
||||||
SELECT ui.id, ui.user_id, ui.product_id, ui.order_id, ui.activity_id, ui.reward_id,
|
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,
|
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
|
FROM user_inventory ui
|
||||||
LEFT JOIN products p ON p.id = ui.product_id
|
LEFT JOIN products p ON p.id = ui.product_id
|
||||||
WHERE ui.user_id = ?
|
WHERE ui.user_id = ?
|
||||||
@ -2158,9 +2157,9 @@ func (h *handler) AdminSearchUsers() core.HandlerFunc {
|
|||||||
|
|
||||||
// 按手机号或昵称模糊匹配
|
// 按手机号或昵称模糊匹配
|
||||||
rows, _ := q.Where(
|
rows, _ := q.Where(
|
||||||
h.readDB.Users.Mobile.Like("%"+req.Keyword+"%"),
|
h.readDB.Users.Mobile.Like("%" + req.Keyword + "%"),
|
||||||
).Or(
|
).Or(
|
||||||
h.readDB.Users.Nickname.Like("%"+req.Keyword+"%"),
|
h.readDB.Users.Nickname.Like("%" + req.Keyword + "%"),
|
||||||
).Limit(10).Find()
|
).Limit(10).Find()
|
||||||
|
|
||||||
items := make([]userItem, 0, len(rows))
|
items := make([]userItem, 0, len(rows))
|
||||||
|
|||||||
111
internal/service/activity/reward_snapshot_test.go
Normal file
111
internal/service/activity/reward_snapshot_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package activity
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
@ -12,17 +13,40 @@ import (
|
|||||||
// 返回: 错误信息
|
// 返回: 错误信息
|
||||||
func (s *service) CreateIssueRewards(ctx context.Context, issueID int64, rewards []CreateRewardInput) error {
|
func (s *service) CreateIssueRewards(ctx context.Context, issueID int64, rewards []CreateRewardInput) error {
|
||||||
return s.writeDB.Transaction(func(tx *dao.Query) 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 {
|
for _, r := range rewards {
|
||||||
item := &model.ActivityRewardSettings{
|
item := &model.ActivityRewardSettings{
|
||||||
IssueID: issueID,
|
IssueID: issueID,
|
||||||
ProductID: r.ProductID,
|
ProductID: r.ProductID,
|
||||||
Weight: r.Weight,
|
PriceSnapshotCents: productPriceMap[r.ProductID],
|
||||||
Quantity: r.Quantity,
|
PriceSnapshotAt: time.Now(),
|
||||||
OriginalQty: r.OriginalQty,
|
Weight: r.Weight,
|
||||||
Level: r.Level,
|
Quantity: r.Quantity,
|
||||||
Sort: r.Sort,
|
OriginalQty: r.OriginalQty,
|
||||||
IsBoss: r.IsBoss,
|
Level: r.Level,
|
||||||
MinScore: r.MinScore,
|
Sort: r.Sort,
|
||||||
|
IsBoss: r.IsBoss,
|
||||||
|
MinScore: r.MinScore,
|
||||||
}
|
}
|
||||||
if err := tx.ActivityRewardSettings.WithContext(ctx).Create(item); err != nil {
|
if err := tx.ActivityRewardSettings.WithContext(ctx).Create(item); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -17,6 +17,16 @@ func (s *service) ModifyIssueReward(ctx context.Context, rewardID int64, in Modi
|
|||||||
}
|
}
|
||||||
if in.ProductID != nil {
|
if in.ProductID != nil {
|
||||||
item.ProductID = *in.ProductID
|
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 {
|
if in.Weight != nil {
|
||||||
item.Weight = int32(*in.Weight)
|
item.Weight = int32(*in.Weight)
|
||||||
|
|||||||
@ -319,6 +319,15 @@ func (s *service) reclaimLivestreamAssets(ctx context.Context, log *model.Livest
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 回收资产
|
// 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 {
|
for _, inv := range inventories {
|
||||||
if inv.Status == 1 {
|
if inv.Status == 1 {
|
||||||
// 状态1(持有):作废
|
// 状态1(持有):作废
|
||||||
@ -332,25 +341,28 @@ func (s *service) reclaimLivestreamAssets(ctx context.Context, log *model.Livest
|
|||||||
)
|
)
|
||||||
} else if inv.Status == 3 {
|
} else if inv.Status == 3 {
|
||||||
// 状态3(已兑换/发货):扣除积分
|
// 状态3(已兑换/发货):扣除积分
|
||||||
// 查找商品价格作为积分扣除依据
|
pointsToDeduct := inv.ValueCents * rate
|
||||||
var product model.Products
|
if pointsToDeduct <= 0 {
|
||||||
if err := s.repo.GetDbR().Where("id = ?", inv.ProductID).First(&product).Error; err == nil {
|
// 兼容历史数据,兜底回退商品价格
|
||||||
pointsToDeduct := product.Price / 100 // 分转换为积分(假设 1积分=1分钱)
|
var product model.Products
|
||||||
if pointsToDeduct > 0 {
|
if err := s.repo.GetDbR().Where("id = ?", inv.ProductID).First(&product).Error; err == nil {
|
||||||
_, consumed, err := s.userSvc.ConsumePointsForRefund(ctx, inv.UserID, pointsToDeduct, "user_inventory", fmt.Sprintf("%d", inv.ID), "直播退款回收已兑换资产")
|
pointsToDeduct = product.Price * rate
|
||||||
if err != nil {
|
}
|
||||||
s.logger.Error("[资产回收] 扣除积分失败", zap.Error(err), zap.Int64("user_id", inv.UserID))
|
}
|
||||||
}
|
if pointsToDeduct > 0 {
|
||||||
if consumed < pointsToDeduct {
|
_, consumed, err := s.userSvc.ConsumePointsForRefund(ctx, inv.UserID, pointsToDeduct, "user_inventory", fmt.Sprintf("%d", inv.ID), "直播退款回收已兑换资产")
|
||||||
// 积分不足,标记用户
|
if err != nil {
|
||||||
s.logger.Warn("[资产回收] 用户积分不足",
|
s.logger.Error("[资产回收] 扣除积分失败", zap.Error(err), zap.Int64("user_id", inv.UserID))
|
||||||
zap.Int64("user_id", inv.UserID),
|
}
|
||||||
zap.Int64("needed", pointsToDeduct),
|
if consumed < pointsToDeduct {
|
||||||
zap.Int64("consumed", consumed),
|
// 积分不足,标记用户
|
||||||
)
|
s.logger.Warn("[资产回收] 用户积分不足",
|
||||||
// 可选:加入黑名单
|
zap.Int64("user_id", inv.UserID),
|
||||||
// db.Exec("UPDATE users SET status = 3 WHERE id = ?", inv.UserID)
|
zap.Int64("needed", pointsToDeduct),
|
||||||
}
|
zap.Int64("consumed", consumed),
|
||||||
|
)
|
||||||
|
// 可选:加入黑名单
|
||||||
|
// db.Exec("UPDATE users SET status = 3 WHERE id = ?", inv.UserID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 作废记录
|
// 作废记录
|
||||||
|
|||||||
@ -500,9 +500,20 @@ func (s *service) RedeemInventoryToPoints(ctx context.Context, userID int64, inv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
p, err := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(inv.ProductID)).First()
|
valueCents := inv.ValueCents
|
||||||
if err != nil {
|
valueSource := inv.ValueSource
|
||||||
return 0, err
|
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()
|
cfg, _ := s.readDB.SystemConfigs.WithContext(ctx).Where(s.readDB.SystemConfigs.ConfigKey.Eq("points_exchange_per_cent")).First()
|
||||||
rate := int64(1)
|
rate := int64(1)
|
||||||
@ -513,7 +524,7 @@ func (s *service) RedeemInventoryToPoints(ctx context.Context, userID int64, inv
|
|||||||
rate = r
|
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 {
|
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
|
return 0, err
|
||||||
}
|
}
|
||||||
@ -569,39 +580,63 @@ func (s *service) RedeemInventoriesToPoints(ctx context.Context, userID int64, i
|
|||||||
return 0, fmt.Errorf("no_valid_inventory")
|
return 0, fmt.Errorf("no_valid_inventory")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建inventory映射和收集productID
|
// 4. 按资产快照计算总积分,缺失快照时回退商品价格并回写
|
||||||
invMap := make(map[int64]*model.UserInventory, len(invList))
|
|
||||||
productIDs := make([]int64, 0, len(invList))
|
productIDs := make([]int64, 0, len(invList))
|
||||||
productIDSet := make(map[int64]struct{})
|
productIDSet := make(map[int64]struct{})
|
||||||
for _, inv := range invList {
|
for _, inv := range invList {
|
||||||
invMap[inv.ID] = inv
|
if inv.ValueCents <= 0 {
|
||||||
if _, ok := productIDSet[inv.ProductID]; !ok {
|
if _, ok := productIDSet[inv.ProductID]; !ok {
|
||||||
productIDSet[inv.ProductID] = struct{}{}
|
productIDSet[inv.ProductID] = struct{}{}
|
||||||
productIDs = append(productIDs, inv.ProductID)
|
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次)
|
// 5. 计算总积分和准备批量更新
|
||||||
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. 计算总积分和准备批量更新数据
|
|
||||||
var totalPoints int64
|
var totalPoints int64
|
||||||
validIDs := make([]int64, 0, len(invList))
|
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 {
|
for _, inv := range invList {
|
||||||
p := productMap[inv.ProductID]
|
valueCents := inv.ValueCents
|
||||||
if p == nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
points := p.Price * rate
|
points := valueCents * rate
|
||||||
totalPoints += points
|
totalPoints += points
|
||||||
validIDs = append(validIDs, inv.ID)
|
validIDs = append(validIDs, inv.ID)
|
||||||
}
|
}
|
||||||
@ -639,6 +674,14 @@ func (s *service) RedeemInventoriesToPoints(ctx context.Context, userID int64, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 批量更新inventory状态(一次UPDATE替代N次)
|
// 批量更新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(
|
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",
|
"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,
|
validIDs, userID,
|
||||||
|
|||||||
@ -91,10 +91,12 @@ func (s *service) ListInventoryWithProduct(ctx context.Context, userID int64, pa
|
|||||||
p := products[r.ProductID]
|
p := products[r.ProductID]
|
||||||
name := ""
|
name := ""
|
||||||
images := ""
|
images := ""
|
||||||
var price int64
|
price := r.ValueCents
|
||||||
if p != nil {
|
if p != nil {
|
||||||
name = p.Name
|
name = p.Name
|
||||||
images = p.ImagesJSON
|
images = p.ImagesJSON
|
||||||
|
}
|
||||||
|
if price <= 0 && p != nil {
|
||||||
price = p.Price
|
price = p.Price
|
||||||
}
|
}
|
||||||
sh := shipMap[r.ID]
|
sh := shipMap[r.ID]
|
||||||
@ -177,10 +179,12 @@ func (s *service) ListInventoryWithProductActive(ctx context.Context, userID int
|
|||||||
p := products[r.ProductID]
|
p := products[r.ProductID]
|
||||||
name := ""
|
name := ""
|
||||||
images := ""
|
images := ""
|
||||||
var price int64
|
price := r.ValueCents
|
||||||
if p != nil {
|
if p != nil {
|
||||||
name = p.Name
|
name = p.Name
|
||||||
images = p.ImagesJSON
|
images = p.ImagesJSON
|
||||||
|
}
|
||||||
|
if price <= 0 && p != nil {
|
||||||
price = p.Price
|
price = p.Price
|
||||||
}
|
}
|
||||||
sh := shipMap[r.ID]
|
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)
|
// 1. 获取聚合后的商品ID列表 (GROUP BY product_id, status)
|
||||||
var groupResults []struct {
|
var groupResults []struct {
|
||||||
ProductID int64 `gorm:"column:product_id"`
|
ProductID int64 `gorm:"column:product_id"`
|
||||||
Status int32 `gorm:"column:status"`
|
Status int32 `gorm:"column:status"`
|
||||||
Count int64 `gorm:"column:count"`
|
Count int64 `gorm:"column:count"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
ValueCents int64 `gorm:"column:value_cents"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
q := s.readDB.UserInventory.WithContext(ctx).ReadDB().
|
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.ProductID,
|
||||||
s.readDB.UserInventory.Status,
|
s.readDB.UserInventory.Status,
|
||||||
s.readDB.UserInventory.ID.Count().As("count"),
|
s.readDB.UserInventory.ID.Count().As("count"),
|
||||||
|
s.readDB.UserInventory.ValueCents.Max().As("value_cents"),
|
||||||
s.readDB.UserInventory.UpdatedAt.Max().As("updated_at"),
|
s.readDB.UserInventory.UpdatedAt.Max().As("updated_at"),
|
||||||
).
|
).
|
||||||
Where(s.readDB.UserInventory.UserID.Eq(userID))
|
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()
|
p, _ := s.readDB.Products.WithContext(ctx).ReadDB().Where(s.readDB.Products.ID.Eq(g.ProductID)).First()
|
||||||
name := "未知商品"
|
name := "未知商品"
|
||||||
images := ""
|
images := ""
|
||||||
var price int64
|
price := g.ValueCents
|
||||||
if p != nil {
|
if p != nil {
|
||||||
name = p.Name
|
name = p.Name
|
||||||
images = p.ImagesJSON
|
images = p.ImagesJSON
|
||||||
|
}
|
||||||
|
if price <= 0 && p != nil {
|
||||||
price = p.Price
|
price = p.Price
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,11 +47,13 @@ func (s *service) GrantReward(ctx context.Context, userID int64, req GrantReward
|
|||||||
// 执行事务
|
// 执行事务
|
||||||
err := s.writeDB.Transaction(func(tx *dao.Query) error {
|
err := s.writeDB.Transaction(func(tx *dao.Query) error {
|
||||||
logger.Info("开始事务处理")
|
logger.Info("开始事务处理")
|
||||||
|
var rewardSetting *model.ActivityRewardSettings
|
||||||
|
var err error
|
||||||
|
|
||||||
// 1. 检查奖励配置库存(如果提供了reward_id)
|
// 1. 检查奖励配置库存(如果提供了reward_id)
|
||||||
if req.RewardID != nil {
|
if req.RewardID != nil {
|
||||||
logger.Info("检查奖励配置", zap.Int64("reward_id", *req.RewardID))
|
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),
|
tx.ActivityRewardSettings.ID.Eq(*req.RewardID),
|
||||||
).First()
|
).First()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -109,7 +111,7 @@ func (s *service) GrantReward(ctx context.Context, userID int64, req GrantReward
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("创建订单", zap.Any("order", order))
|
logger.Info("创建订单", zap.Any("order", order))
|
||||||
err := tx.Orders.WithContext(ctx).Create(order)
|
err = tx.Orders.WithContext(ctx).Create(order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("创建订单失败", zap.Error(err))
|
logger.Error("创建订单失败", zap.Error(err))
|
||||||
return fmt.Errorf("创建订单失败: %w", err)
|
return fmt.Errorf("创建订单失败: %w", err)
|
||||||
@ -163,7 +165,25 @@ func (s *service) GrantReward(ctx context.Context, userID int64, req GrantReward
|
|||||||
inventory := &model.UserInventory{
|
inventory := &model.UserInventory{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ProductID: req.ProductID,
|
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 {
|
ActivityID: func() int64 {
|
||||||
if req.ActivityID != nil {
|
if req.ActivityID != nil {
|
||||||
return *req.ActivityID
|
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 {
|
err := s.writeDB.Transaction(func(tx *dao.Query) error {
|
||||||
logger.Info("开始事务处理")
|
logger.Info("开始事务处理")
|
||||||
|
var rewardSetting *model.ActivityRewardSettings
|
||||||
|
|
||||||
// 1. 验证订单存在且属于该用户
|
// 1. 验证订单存在且属于该用户
|
||||||
order, err := tx.Orders.WithContext(ctx).Where(
|
order, err := tx.Orders.WithContext(ctx).Where(
|
||||||
@ -322,6 +343,13 @@ func (s *service) GrantRewardToOrder(ctx context.Context, userID int64, req Gran
|
|||||||
logger.Error("奖励库存不足或不存在")
|
logger.Error("奖励库存不足或不存在")
|
||||||
return fmt.Errorf("奖励库存不足或不存在")
|
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("奖励库存扣减成功(乐观锁)")
|
logger.Info("奖励库存扣减成功(乐观锁)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,7 +383,25 @@ func (s *service) GrantRewardToOrder(ctx context.Context, userID int64, req Gran
|
|||||||
inventory := &model.UserInventory{
|
inventory := &model.UserInventory{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ProductID: req.ProductID,
|
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 {
|
ActivityID: func() int64 {
|
||||||
if req.ActivityID != nil {
|
if req.ActivityID != nil {
|
||||||
return *req.ActivityID
|
return *req.ActivityID
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
@ -60,6 +61,26 @@ func (s *service) BatchGrantRewardsToOrder(ctx context.Context, userID int64, or
|
|||||||
for _, p := range products {
|
for _, p := range products {
|
||||||
productMap[p.ID] = p
|
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. 批量创建订单项和库存记录
|
// 3. 批量创建订单项和库存记录
|
||||||
var orderItems []*model.OrderItems
|
var orderItems []*model.OrderItems
|
||||||
@ -83,8 +104,32 @@ func (s *service) BatchGrantRewardsToOrder(ctx context.Context, userID int64, or
|
|||||||
})
|
})
|
||||||
|
|
||||||
inventories = append(inventories, &model.UserInventory{
|
inventories = append(inventories, &model.UserInventory{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ProductID: item.ProductID,
|
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,
|
OrderID: orderID,
|
||||||
ActivityID: item.ActivityID,
|
ActivityID: item.ActivityID,
|
||||||
RewardID: func() int64 {
|
RewardID: func() int64 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user