允许平台库存资产直接完成换货归属继承
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 11m27s

换货完成事务允许平台库存新资产换入,并同步店铺归属、分销状态及钱包租户标签;其他店铺资产继续拒绝。

Constraint: 七月迭代要求归属继承不受 migrate_data 控制

Rejected: 先将新资产人工分配到旧资产店铺 | 会保留冲突校验并增加多余操作

Confidence: high

Scope-risk: narrow

Directive: 后续换货资料迁移不得覆盖归属继承规则

Tested: gofmt;git diff --check

Not-tested: 按用户要求未运行自动化测试
This commit is contained in:
2026-07-24 11:31:45 +08:00
parent 304e42c43e
commit 5d6e23f1a5

View File

@@ -583,6 +583,9 @@ func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order
if err = s.validateExchangeAssetsWithTx(ctx, tx, order.ID, oldAsset, newAsset); err != nil {
return err
}
if err = s.syncNewAssetOwnershipWithTx(ctx, tx, oldAsset, newAsset); err != nil {
return err
}
if err = s.switchCustomerBindingWithTx(ctx, tx, oldAsset, newAsset); err != nil {
return err
}
@@ -755,7 +758,9 @@ func (s *Service) validateExchangeAssetsWithTx(ctx context.Context, tx *gorm.DB,
if newAsset.AssetStatus != constants.AssetStatusInStock {
return errors.New(errors.CodeExchangeNewAssetNotInStock)
}
if !sameShopID(oldAsset.ShopID, newAsset.ShopID) {
// 平台库存资产没有店铺归属,换货完成时会继承旧资产店铺;
// 已归属其他店铺的资产仍必须拒绝,避免跨店铺覆盖资产归属。
if newAsset.ShopID != nil && !sameShopID(oldAsset.ShopID, newAsset.ShopID) {
return errors.New(errors.CodeForbidden, "新旧资产归属不一致")
}
if err := s.ensureNewAssetBindingAvailableWithTx(ctx, tx, oldAsset, newAsset); err != nil {
@@ -793,6 +798,59 @@ func (s *Service) switchCustomerBindingWithTx(ctx context.Context, tx *gorm.DB,
return s.customerBinding.Migrate(ctx, tx, oldAsset.AssetType, oldAsset.AssetID, newAsset.AssetType, newAsset.AssetID)
}
// syncNewAssetOwnershipWithTx 将新资产归属同步为旧资产当前归属。
// 归属继承不受 migrate_data 控制,避免平台库存资产换入店铺后仍处于平台租户范围。
func (s *Service) syncNewAssetOwnershipWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
ownershipStatus := constants.IotCardStatusInStock
if oldAsset.ShopID != nil {
ownershipStatus = constants.IotCardStatusDistributed
}
modelValue := any(&model.IotCard{})
if newAsset.AssetType == constants.ExchangeAssetTypeDevice {
modelValue = &model.Device{}
ownershipStatus = constants.DeviceStatusInStock
if oldAsset.ShopID != nil {
ownershipStatus = constants.DeviceStatusDistributed
}
}
result := tx.WithContext(ctx).Model(modelValue).
Where("id = ? AND asset_status = ?", newAsset.AssetID, constants.AssetStatusInStock).
Updates(map[string]any{
"shop_id": oldAsset.ShopID,
"status": ownershipStatus,
"updated_at": time.Now(),
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "同步新资产店铺归属失败")
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeExchangeNewAssetNotInStock)
}
shopIDTag := uint(0)
if oldAsset.ShopID != nil {
shopIDTag = *oldAsset.ShopID
}
if err := tx.WithContext(ctx).Model(&model.AssetWallet{}).
Where("resource_type = ? AND resource_id = ?", newAsset.AssetType, newAsset.AssetID).
Updates(map[string]any{"shop_id_tag": shopIDTag, "updated_at": time.Now()}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "同步新资产钱包店铺标签失败")
}
newAsset.ShopID = cloneShopID(oldAsset.ShopID)
return nil
}
func cloneShopID(shopID *uint) *uint {
if shopID == nil {
return nil
}
value := *shopID
return &value
}
func (s *Service) updateAssetStatusesForCompletion(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
now := time.Now()
if oldAsset.AssetType == constants.ExchangeAssetTypeIotCard {