Compare commits

..

1 Commits
main ... zuncle

Author SHA1 Message Date
bd91c0fad1 fix(transfer): 修复赠送资产并发漏洞及转赠积分薅取问题
- SubmitAddressShare 事务内 SELECT FOR UPDATE 锁定资产行,防止并发重复提交
- 检查 UPDATE RowsAffected,静默失败时回滚事务
- 防重检查从 readDB 移入事务内写库,消除主从延迟竞态
- RedeemInventoryToPoints/RedeemInventoriesToPoints 添加转赠来源校验,
  禁止通过转赠获得的资产兑换积分
2026-03-11 14:14:34 +08:00

View File

@ -569,6 +569,15 @@ func (s *service) RedeemInventoryToPoints(ctx context.Context, userID int64, inv
return 0, err
}
// 校验转赠来源:通过转赠获得的资产不允许兑换积分(防薅积分漏洞)
transferCnt, _ := s.readDB.UserInventoryTransfers.WithContext(ctx).Where(
s.readDB.UserInventoryTransfers.InventoryID.Eq(inventoryID),
s.readDB.UserInventoryTransfers.ToUserID.Eq(userID),
).Count()
if transferCnt > 0 {
return 0, fmt.Errorf("transfer_inventory_cannot_redeem")
}
valueCents := inv.ValueCents
valueSource := inv.ValueSource
valueSnapshotAt := inv.ValueSnapshotAt
@ -649,6 +658,30 @@ func (s *service) RedeemInventoriesToPoints(ctx context.Context, userID int64, i
return 0, fmt.Errorf("no_valid_inventory")
}
// 3.5 排除通过转赠获得的资产(防薅积分漏洞)
invIDs := make([]int64, 0, len(invList))
for _, inv := range invList {
invIDs = append(invIDs, inv.ID)
}
transferredInvs, _ := s.readDB.UserInventoryTransfers.WithContext(ctx).
Where(s.readDB.UserInventoryTransfers.InventoryID.In(invIDs...)).
Where(s.readDB.UserInventoryTransfers.ToUserID.Eq(userID)).
Find()
transferredSet := make(map[int64]struct{}, len(transferredInvs))
for _, t := range transferredInvs {
transferredSet[t.InventoryID] = struct{}{}
}
filteredInvList := make([]*model.UserInventory, 0, len(invList))
for _, inv := range invList {
if _, isTransferred := transferredSet[inv.ID]; !isTransferred {
filteredInvList = append(filteredInvList, inv)
}
}
if len(filteredInvList) == 0 {
return 0, fmt.Errorf("transfer_inventory_cannot_redeem")
}
invList = filteredInvList
// 4. 按资产快照计算总积分,缺失快照时回退商品价格并回写
productIDs := make([]int64, 0, len(invList))
productIDSet := make(map[int64]struct{})