本次提交修复了管理端复制活动接口在复制奖励配置时写入零时间导致的 MySQL datetime 报错,并补齐了活动奖励复制过程遗漏的快照与业务字段,避免新活动复制后出现配置丢失或插入失败。 - 复制逻辑:在 CopyActivity 中完整复制 price_snapshot_cents、cost_snapshot_cents、price_snapshot_at、min_score、drop_quantity 等奖励字段 - 异常兜底:当历史奖励快照时间为空或为零值时,复制时自动回填为当前时间;当价格/成本快照缺失且存在商品 ID 时,回退读取当前商品价格与成本价 - 兼容历史数据:确保旧奖励配置即使未完整回填快照字段,也能被安全复制,不再因 0000-00-00 时间值触发插入失败 - 测试覆盖:补充活动复制场景测试,验证快照字段原样复制、缺失快照自动补齐,以及 min_score/drop_quantity 等字段不会在复制时丢失
124 lines
3.4 KiB
Go
Executable File
124 lines
3.4 KiB
Go
Executable File
package activity
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"bindbox-game/internal/repository/mysql/dao"
|
||
"bindbox-game/internal/repository/mysql/model"
|
||
)
|
||
|
||
// CopyActivity 复制活动及其期次与奖励
|
||
// 参数: activityID 源活动ID
|
||
// 返回: 新活动ID与错误
|
||
func (s *service) CopyActivity(ctx context.Context, activityID int64) (int64, error) {
|
||
var newActivityID int64
|
||
err := s.writeDB.Transaction(func(tx *dao.Query) error {
|
||
src, err := s.readDB.Activities.WithContext(ctx).Where(s.readDB.Activities.ID.Eq(activityID)).First()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
newAct := &model.Activities{
|
||
Name: src.Name,
|
||
Banner: src.Banner,
|
||
ActivityCategoryID: src.ActivityCategoryID,
|
||
Status: 1,
|
||
PriceDraw: src.PriceDraw,
|
||
IsBoss: src.IsBoss,
|
||
EndTime: time.Now().AddDate(1, 0, 0), // 默认1年后结束,确保活动列表可见
|
||
}
|
||
if err := tx.Activities.WithContext(ctx).Omit(
|
||
tx.Activities.StartTime,
|
||
tx.Activities.ScheduledTime,
|
||
tx.Activities.LastSettledAt,
|
||
).Create(newAct); err != nil {
|
||
return err
|
||
}
|
||
newActivityID = newAct.ID
|
||
|
||
issues, err := s.readDB.ActivityIssues.WithContext(ctx).Where(s.readDB.ActivityIssues.ActivityID.Eq(activityID)).Order(s.readDB.ActivityIssues.Sort.Desc()).Find()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
idMap := make(map[int64]int64, len(issues))
|
||
for _, srcIssue := range issues {
|
||
ni := &model.ActivityIssues{
|
||
ActivityID: newActivityID,
|
||
IssueNumber: srcIssue.IssueNumber,
|
||
Status: 3,
|
||
Sort: srcIssue.Sort,
|
||
}
|
||
if err := tx.ActivityIssues.WithContext(ctx).Create(ni); err != nil {
|
||
return err
|
||
}
|
||
idMap[srcIssue.ID] = ni.ID
|
||
}
|
||
|
||
if len(idMap) == 0 {
|
||
return nil
|
||
}
|
||
|
||
oldIDs := make([]int64, 0, len(idMap))
|
||
for k := range idMap {
|
||
oldIDs = append(oldIDs, k)
|
||
}
|
||
|
||
rewards, err := s.readDB.ActivityRewardSettings.WithContext(ctx).Where(s.readDB.ActivityRewardSettings.IssueID.In(oldIDs...)).Order(s.readDB.ActivityRewardSettings.Sort.Desc()).Find()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for _, r := range rewards {
|
||
priceSnapshot := r.PriceSnapshotCents
|
||
costSnapshot := r.CostSnapshotCents
|
||
snapshotAt := r.PriceSnapshotAt
|
||
if r.ProductID > 0 && (priceSnapshot == 0 || costSnapshot == 0) {
|
||
product, err := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(r.ProductID)).First()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if priceSnapshot == 0 {
|
||
priceSnapshot = product.Price
|
||
}
|
||
if costSnapshot == 0 {
|
||
costSnapshot = product.CostPrice
|
||
}
|
||
}
|
||
if snapshotAt.IsZero() {
|
||
snapshotAt = time.Now()
|
||
}
|
||
dropQuantity := r.DropQuantity
|
||
if dropQuantity < 1 {
|
||
dropQuantity = 1
|
||
}
|
||
|
||
nr := &model.ActivityRewardSettings{
|
||
IssueID: idMap[r.IssueID],
|
||
ProductID: r.ProductID,
|
||
PriceSnapshotCents: priceSnapshot,
|
||
PriceSnapshotAt: snapshotAt,
|
||
Weight: r.Weight,
|
||
Quantity: r.Quantity,
|
||
OriginalQty: r.OriginalQty,
|
||
Level: r.Level,
|
||
Sort: r.Sort,
|
||
IsBoss: r.IsBoss,
|
||
MinScore: r.MinScore,
|
||
DropQuantity: dropQuantity,
|
||
CostSnapshotCents: costSnapshot,
|
||
}
|
||
if err := tx.ActivityRewardSettings.WithContext(ctx).Create(nr); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return newActivityID, nil
|
||
}
|