收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
494
internal/service/exchange/audit.go
Normal file
494
internal/service/exchange/audit.go
Normal file
@@ -0,0 +1,494 @@
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
type cardExchangeAuditBefore struct {
|
||||
Wallets map[uint]model.AssetWallet
|
||||
DeviceBindings []*model.PersonalCustomerDevice
|
||||
ICCIDBindings []*model.PersonalCustomerICCID
|
||||
}
|
||||
|
||||
// SetAccessAudit 注入卡与设备换货完整用例的统一审计 Writer。
|
||||
func (s *Service) SetAccessAudit(writer *audit.Writer) {
|
||||
s.auditWriter = writer
|
||||
}
|
||||
|
||||
func (s *Service) appendCardExchangeAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
actionCode, summary, result string,
|
||||
order *model.ExchangeOrder,
|
||||
oldCard, newCard *model.IotCard,
|
||||
orderBefore, orderAfter map[string]any,
|
||||
oldCardBefore, oldCardAfter map[string]any,
|
||||
newCardBefore, newCardAfter map[string]any,
|
||||
extra []audit.ResourceInput,
|
||||
businessErr error,
|
||||
) error {
|
||||
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeIotCard {
|
||||
return nil
|
||||
}
|
||||
if s.auditWriter == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "卡换货统一审计接缝未配置")
|
||||
}
|
||||
internalOnly := actionCode == constants.AuditActionCardExchangeRenewed
|
||||
resources := []audit.ResourceInput{cardExchangeOrderAuditResource(order, summary, internalOnly, orderBefore, orderAfter)}
|
||||
if oldCard != nil {
|
||||
resources = append(resources, cardExchangeCardAuditResource(oldCard, constants.AuditResourceRoleCardExchangeOldCard, summary, internalOnly, oldCardBefore, oldCardAfter))
|
||||
}
|
||||
if newCard != nil {
|
||||
resources = append(resources, cardExchangeCardAuditResource(newCard, constants.AuditResourceRoleCardExchangeNewCard, summary, internalOnly, newCardBefore, newCardAfter))
|
||||
}
|
||||
shopResource, err := loadCardExchangeShopAuditResource(ctx, tx, order.ShopID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if shopResource != nil {
|
||||
resources = append(resources, *shopResource)
|
||||
}
|
||||
resources = append(resources, extra...)
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
scopeType, scopeID := constants.AuditScopePlatform, ""
|
||||
if actionCode == constants.AuditActionCardExchangeShippingInfoSubmitted {
|
||||
scopeType = constants.AuditScopePersonalCustomer
|
||||
scopeID = auditcontext.From(ctx).ActorID
|
||||
}
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: scopeType, ScopeID: scopeID,
|
||||
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
Metadata: map[string]any{"flow_type": effectiveExchangeFlowType(order.FlowType), "migrate_data": order.MigrateData},
|
||||
Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func cardExchangeOrderAuditResource(order *model.ExchangeOrder, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput {
|
||||
key := order.ExchangeNo
|
||||
if key == "" {
|
||||
key = "iot_card:" + strconv.FormatUint(uint64(order.OldAssetID), 10) + ":exchange"
|
||||
}
|
||||
var id *string
|
||||
if order.ID > 0 {
|
||||
value := strconv.FormatUint(uint64(order.ID), 10)
|
||||
id = &value
|
||||
}
|
||||
resource := audit.ResourceInput{
|
||||
Type: constants.AuditResourceExchangeOrder, ID: id, Key: key, DisplayName: key,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleCardExchangeOrder,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": order.ID, "exchange_no": order.ExchangeNo, "flow_type": effectiveExchangeFlowType(order.FlowType),
|
||||
"old_asset_type": order.OldAssetType, "old_asset_id": order.OldAssetID, "old_asset_identifier": order.OldAssetIdentifier,
|
||||
"new_asset_type": order.NewAssetType, "new_asset_id": order.NewAssetID, "new_asset_identifier": order.NewAssetIdentifier,
|
||||
"shop_id": order.ShopID, "status": order.Status,
|
||||
},
|
||||
BeforeData: beforeData, AfterData: afterData,
|
||||
}
|
||||
if internalOnly {
|
||||
resource.SubjectVisibility = constants.AuditSubjectInternalOnly
|
||||
} else {
|
||||
resource.SubjectVisibility = constants.AuditSubjectResult
|
||||
resource.SubjectSummary = summary
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
func cardExchangeCardAuditResource(card *model.IotCard, role, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput {
|
||||
id := strconv.FormatUint(uint64(card.ID), 10)
|
||||
relation := constants.AuditResourceRelationReference
|
||||
if len(beforeData) > 0 || len(afterData) > 0 {
|
||||
relation = constants.AuditResourceRelationAffected
|
||||
}
|
||||
resource := audit.ResourceInput{
|
||||
Type: constants.AuditResourceIotCard, ID: &id, Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: relation, Role: role, IdentitySnapshot: audit.IotCardIdentitySnapshot(card),
|
||||
BeforeData: beforeData, AfterData: afterData,
|
||||
}
|
||||
if internalOnly {
|
||||
resource.SubjectVisibility = constants.AuditSubjectInternalOnly
|
||||
} else {
|
||||
resource.SubjectVisibility = constants.AuditSubjectResult
|
||||
resource.SubjectSummary = summary
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
func loadCardExchangeShopAuditResource(ctx context.Context, tx *gorm.DB, shopID *uint) (*audit.ResourceInput, error) {
|
||||
if shopID == nil || *shopID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var shop model.Shop
|
||||
if err := tx.WithContext(ctx).Unscoped().Where("id = ?", *shopID).First(&shop).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货所属店铺失败")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(shop.ID), 10)
|
||||
return &audit.ResourceInput{
|
||||
Type: constants.AuditResourceShop, ID: &id, Key: id, DisplayName: shop.ShopName,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleCardExchangeShop,
|
||||
IdentitySnapshot: map[string]any{"id": shop.ID, "shop_code": shop.ShopCode, "shop_name": shop.ShopName, "parent_id": shop.ParentID, "level": shop.Level},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) captureCardExchangeAuditBefore(ctx context.Context, tx *gorm.DB, oldCard, newCard *model.IotCard) (*cardExchangeAuditBefore, error) {
|
||||
state := &cardExchangeAuditBefore{Wallets: make(map[uint]model.AssetWallet)}
|
||||
cardIDs := make([]uint, 0, 2)
|
||||
if oldCard != nil {
|
||||
cardIDs = append(cardIDs, oldCard.ID)
|
||||
}
|
||||
if newCard != nil {
|
||||
cardIDs = append(cardIDs, newCard.ID)
|
||||
}
|
||||
if len(cardIDs) > 0 {
|
||||
var wallets []model.AssetWallet
|
||||
if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id IN ?", constants.ExchangeAssetTypeIotCard, cardIDs).Find(&wallets).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货钱包审计快照失败")
|
||||
}
|
||||
for _, wallet := range wallets {
|
||||
state.Wallets[wallet.ResourceID] = wallet
|
||||
}
|
||||
}
|
||||
devices, iccids, err := loadCardExchangeBindings(ctx, tx, oldCard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.DeviceBindings, state.ICCIDBindings = devices, iccids
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func loadCardExchangeBindings(ctx context.Context, tx *gorm.DB, card *model.IotCard) ([]*model.PersonalCustomerDevice, []*model.PersonalCustomerICCID, error) {
|
||||
if card == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
if card.VirtualNo != "" {
|
||||
var rows []*model.PersonalCustomerDevice
|
||||
if err := tx.WithContext(ctx).Where("virtual_no = ? AND status = ?", card.VirtualNo, constants.StatusEnabled).Find(&rows).Error; err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货客户绑定失败")
|
||||
}
|
||||
return rows, nil, nil
|
||||
}
|
||||
var rows []*model.PersonalCustomerICCID
|
||||
if err := tx.WithContext(ctx).Where("iccid = ? AND status = ?", card.ICCID, constants.StatusEnabled).Find(&rows).Error; err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货 ICCID 绑定失败")
|
||||
}
|
||||
return nil, rows, nil
|
||||
}
|
||||
|
||||
func (s *Service) buildCardExchangeCompletionResources(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
order *model.ExchangeOrder,
|
||||
oldCard, newCard *model.IotCard,
|
||||
before *cardExchangeAuditBefore,
|
||||
migration *exchangeMigrationResult,
|
||||
) ([]audit.ResourceInput, error) {
|
||||
resources := cardExchangeOldBindingResources(before)
|
||||
devices, iccids, err := loadCardExchangeBindings(ctx, tx, newCard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, cardExchangeNewBindingResources(devices, iccids)...)
|
||||
beforeWallets := map[uint]model.AssetWallet(nil)
|
||||
if before != nil {
|
||||
beforeWallets = before.Wallets
|
||||
}
|
||||
walletResources, err := loadCardExchangeWalletResources(ctx, tx, oldCard.ID, newCard.ID, beforeWallets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, walletResources...)
|
||||
if migration == nil {
|
||||
return resources, nil
|
||||
}
|
||||
transactionResources, err := loadCardExchangeTransactionResources(ctx, tx, order.ExchangeNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, transactionResources...)
|
||||
usageResources, err := loadCardExchangePackageUsageResources(ctx, tx, migration.PackageUsageIDs, oldCard.ID, newCard.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(resources, usageResources...), nil
|
||||
}
|
||||
|
||||
func cardExchangeOldBindingResources(before *cardExchangeAuditBefore) []audit.ResourceInput {
|
||||
if before == nil {
|
||||
return nil
|
||||
}
|
||||
resources := make([]audit.ResourceInput, 0, len(before.DeviceBindings)+len(before.ICCIDBindings))
|
||||
for _, row := range before.DeviceBindings {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
id := strconv.FormatUint(uint64(row.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourcePersonalCustomerDevice, ID: &id, Key: id, DisplayName: row.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePersonalCustomerOldAssetBinding,
|
||||
IdentitySnapshot: map[string]any{"id": row.ID, "customer_id": row.CustomerID, "virtual_no": row.VirtualNo, "bind_at": row.BindAt, "last_used_at": row.LastUsedAt, "status": row.Status},
|
||||
BeforeData: map[string]any{"virtual_no": row.VirtualNo, "status": row.Status}, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
for _, row := range before.ICCIDBindings {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
id := strconv.FormatUint(uint64(row.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourcePersonalCustomerICCID, ID: &id, Key: id, DisplayName: row.ICCID,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePersonalCustomerOldAssetBinding,
|
||||
IdentitySnapshot: map[string]any{"id": row.ID, "customer_id": row.CustomerID, "iccid": row.ICCID, "iccid_19": row.ICCID19, "bind_at": row.BindAt, "last_used_at": row.LastUsedAt, "status": row.Status},
|
||||
BeforeData: map[string]any{"iccid": row.ICCID, "status": row.Status}, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
func cardExchangeNewBindingResources(devices []*model.PersonalCustomerDevice, iccids []*model.PersonalCustomerICCID) []audit.ResourceInput {
|
||||
resources := make([]audit.ResourceInput, 0, len(devices)+len(iccids))
|
||||
for _, row := range devices {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
id := strconv.FormatUint(uint64(row.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourcePersonalCustomerDevice, ID: &id, Key: id, DisplayName: row.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePersonalCustomerNewAssetBinding,
|
||||
IdentitySnapshot: map[string]any{"id": row.ID, "customer_id": row.CustomerID, "virtual_no": row.VirtualNo, "bind_at": row.BindAt, "last_used_at": row.LastUsedAt, "status": row.Status},
|
||||
AfterData: map[string]any{"virtual_no": row.VirtualNo, "status": row.Status}, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
for _, row := range iccids {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
id := strconv.FormatUint(uint64(row.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourcePersonalCustomerICCID, ID: &id, Key: id, DisplayName: row.ICCID,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePersonalCustomerNewAssetBinding,
|
||||
IdentitySnapshot: map[string]any{"id": row.ID, "customer_id": row.CustomerID, "iccid": row.ICCID, "iccid_19": row.ICCID19, "bind_at": row.BindAt, "last_used_at": row.LastUsedAt, "status": row.Status},
|
||||
AfterData: map[string]any{"iccid": row.ICCID, "status": row.Status}, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
func loadCardExchangeWalletResources(ctx context.Context, tx *gorm.DB, oldCardID, newCardID uint, before map[uint]model.AssetWallet) ([]audit.ResourceInput, error) {
|
||||
return loadExchangeWalletResources(ctx, tx, constants.ExchangeAssetTypeIotCard, oldCardID, newCardID, before,
|
||||
constants.AuditResourceRoleCardExchangeOldWallet, constants.AuditResourceRoleCardExchangeNewWallet, "卡")
|
||||
}
|
||||
|
||||
func loadExchangeWalletResources(ctx context.Context, tx *gorm.DB, assetType string, oldAssetID, newAssetID uint, before map[uint]model.AssetWallet, oldRole, newRole, assetName string) ([]audit.ResourceInput, error) {
|
||||
var wallets []model.AssetWallet
|
||||
if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id IN ?", assetType, []uint{oldAssetID, newAssetID}).Find(&wallets).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货迁移后钱包失败")
|
||||
}
|
||||
resources := make([]audit.ResourceInput, 0, len(wallets))
|
||||
for _, wallet := range wallets {
|
||||
id := strconv.FormatUint(uint64(wallet.ID), 10)
|
||||
role := newRole
|
||||
if wallet.ResourceID == oldAssetID {
|
||||
role = oldRole
|
||||
}
|
||||
relation := constants.AuditResourceRelationAffected
|
||||
beforeData, afterData := map[string]any{"exists": false}, cardExchangeWalletData(wallet)
|
||||
if previous, ok := before[wallet.ResourceID]; ok {
|
||||
if !cardExchangeWalletChanged(previous, wallet) {
|
||||
relation, beforeData, afterData = constants.AuditResourceRelationReference, nil, nil
|
||||
} else {
|
||||
beforeData = cardExchangeWalletData(previous)
|
||||
}
|
||||
}
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAssetWallet, ID: &id, Key: id, DisplayName: id,
|
||||
Relation: relation, Role: role,
|
||||
IdentitySnapshot: map[string]any{"id": wallet.ID, "resource_type": wallet.ResourceType, "resource_id": wallet.ResourceID, "currency": wallet.Currency, "shop_id_tag": wallet.ShopIDTag, "enterprise_id_tag": wallet.EnterpriseIDTag},
|
||||
BeforeData: beforeData, AfterData: afterData, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func cardExchangeWalletData(wallet model.AssetWallet) map[string]any {
|
||||
return map[string]any{"balance": wallet.Balance, "frozen_balance": wallet.FrozenBalance, "status": wallet.Status, "version": wallet.Version, "shop_id_tag": wallet.ShopIDTag, "enterprise_id_tag": wallet.EnterpriseIDTag}
|
||||
}
|
||||
|
||||
func cardExchangeWalletChanged(before, after model.AssetWallet) bool {
|
||||
return before.Balance != after.Balance || before.FrozenBalance != after.FrozenBalance || before.Status != after.Status ||
|
||||
before.Version != after.Version || before.ShopIDTag != after.ShopIDTag || !sameOptionalUint(before.EnterpriseIDTag, after.EnterpriseIDTag)
|
||||
}
|
||||
|
||||
func sameOptionalUint(left, right *uint) bool {
|
||||
return left == nil && right == nil || left != nil && right != nil && *left == *right
|
||||
}
|
||||
|
||||
func loadCardExchangeRenewWalletResource(ctx context.Context, tx *gorm.DB, cardID uint, before map[uint]model.AssetWallet) (*audit.ResourceInput, error) {
|
||||
return loadExchangeRenewWalletResource(ctx, tx, constants.ExchangeAssetTypeIotCard, cardID, before, constants.AuditResourceRoleCardExchangeOldWallet, "卡")
|
||||
}
|
||||
|
||||
func loadExchangeRenewWalletResource(ctx context.Context, tx *gorm.DB, assetType string, assetID uint, before map[uint]model.AssetWallet, role, assetName string) (*audit.ResourceInput, error) {
|
||||
var wallet model.AssetWallet
|
||||
if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id = ?", assetType, assetID).First(&wallet).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询旧"+assetName+"转新钱包失败")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(wallet.ID), 10)
|
||||
beforeData := map[string]any{"exists": false}
|
||||
if previous, ok := before[assetID]; ok {
|
||||
beforeData = cardExchangeWalletData(previous)
|
||||
}
|
||||
return &audit.ResourceInput{
|
||||
Type: constants.AuditResourceAssetWallet, ID: &id, Key: id, DisplayName: id,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: role,
|
||||
IdentitySnapshot: map[string]any{"id": wallet.ID, "resource_type": wallet.ResourceType, "resource_id": wallet.ResourceID, "currency": wallet.Currency, "shop_id_tag": wallet.ShopIDTag, "enterprise_id_tag": wallet.EnterpriseIDTag},
|
||||
BeforeData: beforeData, AfterData: cardExchangeWalletData(wallet), SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadCardExchangeTransactionResources(ctx context.Context, tx *gorm.DB, exchangeNo string) ([]audit.ResourceInput, error) {
|
||||
return loadExchangeTransactionResources(ctx, tx, exchangeNo, constants.AuditResourceRoleCardExchangeWalletTransaction, "卡")
|
||||
}
|
||||
|
||||
func loadExchangeTransactionResources(ctx context.Context, tx *gorm.DB, exchangeNo, role, assetName string) ([]audit.ResourceInput, error) {
|
||||
var rows []model.AssetWalletTransaction
|
||||
if err := tx.WithContext(ctx).Where("transaction_type = ? AND reference_type = ? AND reference_no = ?", constants.AssetTransactionTypeExchange, constants.ReferenceTypeExchange, exchangeNo).Find(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货钱包流水失败")
|
||||
}
|
||||
resources := make([]audit.ResourceInput, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
id := strconv.FormatUint(uint64(row.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAssetWalletTransaction, ID: &id, Key: id, DisplayName: exchangeNo,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: role,
|
||||
IdentitySnapshot: map[string]any{"id": row.ID, "asset_wallet_id": row.AssetWalletID, "resource_type": row.ResourceType, "resource_id": row.ResourceID, "transaction_type": row.TransactionType, "reference_type": row.ReferenceType, "reference_no": row.ReferenceNo, "status": row.Status},
|
||||
AfterData: map[string]any{"amount": row.Amount, "balance_before": row.BalanceBefore, "balance_after": row.BalanceAfter}, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func loadCardExchangePackageUsageResources(ctx context.Context, tx *gorm.DB, ids []uint, oldCardID, newCardID uint) ([]audit.ResourceInput, error) {
|
||||
return loadExchangePackageUsageResources(ctx, tx, ids, "iot_card_id", oldCardID, newCardID, constants.AuditResourceRoleCardExchangePackageUsage, "卡")
|
||||
}
|
||||
|
||||
func loadExchangePackageUsageResources(ctx context.Context, tx *gorm.DB, ids []uint, assetIDField string, oldAssetID, newAssetID uint, role, assetName string) ([]audit.ResourceInput, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var rows []model.PackageUsage
|
||||
if err := tx.WithContext(ctx).Where("id IN ?", ids).Order("id ASC").Find(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货套餐权益失败")
|
||||
}
|
||||
orderIDs := make(map[uint]struct{}, len(rows))
|
||||
packageIDs := make(map[uint]struct{}, len(rows))
|
||||
resources := make([]audit.ResourceInput, 0, len(rows)*3)
|
||||
for _, row := range rows {
|
||||
resource := audit.PackageUsageResource(&row, constants.AuditResourceRelationAffected, role,
|
||||
map[string]any{assetIDField: oldAssetID}, map[string]any{assetIDField: newAssetID})
|
||||
resource.SubjectVisibility = constants.AuditSubjectInternalOnly
|
||||
resources = append(resources, resource)
|
||||
orderIDs[row.OrderID] = struct{}{}
|
||||
packageIDs[row.PackageID] = struct{}{}
|
||||
}
|
||||
var orders []model.Order
|
||||
if err := tx.WithContext(ctx).Where("id IN ?", exchangeUintKeys(orderIDs)).Order("id ASC").Find(&orders).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货套餐权益关联订单失败")
|
||||
}
|
||||
for i := range orders {
|
||||
resources = append(resources, audit.OrderResource(&orders[i], constants.AuditResourceRelationReference, constants.AuditResourceRolePackageUsageOrder))
|
||||
}
|
||||
var packages []model.Package
|
||||
if err := tx.WithContext(ctx).Where("id IN ?", exchangeUintKeys(packageIDs)).Order("id ASC").Find(&packages).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货套餐权益关联套餐失败")
|
||||
}
|
||||
for i := range packages {
|
||||
resources = append(resources, audit.PackageResource(&packages[i], constants.AuditResourceRelationReference, constants.AuditResourceRolePackageUsagePackage, nil, nil))
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func exchangeUintKeys(values map[uint]struct{}) []uint {
|
||||
result := make([]uint, 0, len(values))
|
||||
for value := range values {
|
||||
if value > 0 {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) recordCardExchangeFailure(ctx context.Context, actionCode, summary, result string, order *model.ExchangeOrder, oldCard, newCard *model.IotCard, businessErr error) {
|
||||
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeIotCard {
|
||||
return
|
||||
}
|
||||
if s.db == nil || s.auditWriter == nil {
|
||||
recordCardExchangeAuditSecondaryFailure(ctx, actionCode, order.ExchangeNo, businessErr, errors.New(errors.CodeInvalidStatus, "卡换货统一审计接缝未配置"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendCardExchangeAudit(ctx, tx, actionCode, summary, result, order, oldCard, newCard,
|
||||
nil, nil, nil, nil, nil, nil, nil, businessErr)
|
||||
}); err != nil {
|
||||
recordCardExchangeAuditSecondaryFailure(ctx, actionCode, order.ExchangeNo, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func recordCardExchangeAuditSecondaryFailure(ctx context.Context, actionCode, exchangeNo string, businessErr, auditErr error) {
|
||||
errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
linkage := auditcontext.From(ctx)
|
||||
auditfailure.RecordSecondaryWriteFailure(actionCode, exchangeNo, linkage.RequestID, linkage.CorrelationID, errorCode, auditErr)
|
||||
}
|
||||
|
||||
func (s *Service) recordCardExchangeOrderFailure(ctx context.Context, actionCode, summary string, order *model.ExchangeOrder, businessErr error) {
|
||||
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeIotCard {
|
||||
return
|
||||
}
|
||||
oldCard, newCard := s.loadCardExchangeAuditCards(ctx, order)
|
||||
s.recordCardExchangeFailure(ctx, actionCode, summary, cardExchangeFailureResult(businessErr), order, oldCard, newCard, businessErr)
|
||||
}
|
||||
|
||||
func (s *Service) loadCardExchangeAuditCards(ctx context.Context, order *model.ExchangeOrder) (*model.IotCard, *model.IotCard) {
|
||||
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeIotCard {
|
||||
return nil, nil
|
||||
}
|
||||
var oldCard *model.IotCard
|
||||
if s.iotCardStore != nil {
|
||||
oldCard, _ = s.iotCardStore.GetByID(ctx, order.OldAssetID)
|
||||
}
|
||||
if oldCard == nil {
|
||||
oldCard = &model.IotCard{Model: gorm.Model{ID: order.OldAssetID}, ICCID: order.OldAssetIdentifier, ShopID: order.ShopID}
|
||||
}
|
||||
var newCard *model.IotCard
|
||||
if order.NewAssetID != nil && *order.NewAssetID > 0 {
|
||||
if s.iotCardStore != nil {
|
||||
newCard, _ = s.iotCardStore.GetByID(ctx, *order.NewAssetID)
|
||||
}
|
||||
if newCard == nil {
|
||||
newCard = &model.IotCard{Model: gorm.Model{ID: *order.NewAssetID}, ICCID: order.NewAssetIdentifier, ShopID: order.ShopID}
|
||||
}
|
||||
}
|
||||
return oldCard, newCard
|
||||
}
|
||||
|
||||
func cardExchangeFailureResult(err error) string {
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
if !ok {
|
||||
return constants.AuditResultFailed
|
||||
}
|
||||
switch appErr.Code {
|
||||
case errors.CodeDatabaseError, errors.CodeInternalError, errors.CodeExchangeMigrationFailed:
|
||||
return constants.AuditResultFailed
|
||||
default:
|
||||
return constants.AuditResultDenied
|
||||
}
|
||||
}
|
||||
393
internal/service/exchange/device_audit.go
Normal file
393
internal/service/exchange/device_audit.go
Normal file
@@ -0,0 +1,393 @@
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
type deviceExchangeAuditBefore struct {
|
||||
Wallets map[uint]model.AssetWallet
|
||||
CustomerBindings []*model.PersonalCustomerDevice
|
||||
}
|
||||
|
||||
func (s *Service) appendExchangeAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
cardActionCode, summary, result string,
|
||||
order *model.ExchangeOrder,
|
||||
oldAsset, newAsset *resolvedExchangeAsset,
|
||||
orderBefore, orderAfter map[string]any,
|
||||
oldAssetBefore, oldAssetAfter map[string]any,
|
||||
newAssetBefore, newAssetAfter map[string]any,
|
||||
extra []audit.ResourceInput,
|
||||
businessErr error,
|
||||
) error {
|
||||
if order != nil && order.OldAssetType == constants.ExchangeAssetTypeDevice {
|
||||
return s.appendDeviceExchangeAudit(ctx, tx, deviceExchangeActionCode(cardActionCode), strings.ReplaceAll(summary, "卡", "设备"), result,
|
||||
order, resolvedDevice(oldAsset), resolvedDevice(newAsset), orderBefore, orderAfter,
|
||||
oldAssetBefore, oldAssetAfter, newAssetBefore, newAssetAfter, extra, businessErr)
|
||||
}
|
||||
return s.appendCardExchangeAudit(ctx, tx, cardActionCode, summary, result, order, resolvedCard(oldAsset), resolvedCard(newAsset),
|
||||
orderBefore, orderAfter, oldAssetBefore, oldAssetAfter, newAssetBefore, newAssetAfter, extra, businessErr)
|
||||
}
|
||||
|
||||
func resolvedCard(asset *resolvedExchangeAsset) *model.IotCard {
|
||||
if asset == nil {
|
||||
return nil
|
||||
}
|
||||
return asset.Card
|
||||
}
|
||||
|
||||
func resolvedDevice(asset *resolvedExchangeAsset) *model.Device {
|
||||
if asset == nil {
|
||||
return nil
|
||||
}
|
||||
return asset.Device
|
||||
}
|
||||
|
||||
func deviceExchangeActionCode(cardActionCode string) string {
|
||||
switch cardActionCode {
|
||||
case constants.AuditActionCardExchangeCreated:
|
||||
return constants.AuditActionDeviceExchangeCreated
|
||||
case constants.AuditActionCardExchangeShippingInfoSubmitted:
|
||||
return constants.AuditActionDeviceExchangeShippingInfoSubmitted
|
||||
case constants.AuditActionCardExchangeShipped:
|
||||
return constants.AuditActionDeviceExchangeShipped
|
||||
case constants.AuditActionCardExchangeCompleted:
|
||||
return constants.AuditActionDeviceExchangeCompleted
|
||||
case constants.AuditActionCardExchangeCancelled:
|
||||
return constants.AuditActionDeviceExchangeCancelled
|
||||
case constants.AuditActionCardExchangeRenewed:
|
||||
return constants.AuditActionDeviceExchangeRenewed
|
||||
default:
|
||||
return cardActionCode
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) appendDeviceExchangeAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
actionCode, summary, result string,
|
||||
order *model.ExchangeOrder,
|
||||
oldDevice, newDevice *model.Device,
|
||||
orderBefore, orderAfter map[string]any,
|
||||
oldDeviceBefore, oldDeviceAfter map[string]any,
|
||||
newDeviceBefore, newDeviceAfter map[string]any,
|
||||
extra []audit.ResourceInput,
|
||||
businessErr error,
|
||||
) error {
|
||||
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeDevice {
|
||||
return nil
|
||||
}
|
||||
if s.auditWriter == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "设备换货统一审计接缝未配置")
|
||||
}
|
||||
internalOnly := actionCode == constants.AuditActionDeviceExchangeRenewed
|
||||
resources := []audit.ResourceInput{deviceExchangeOrderAuditResource(order, summary, internalOnly, orderBefore, orderAfter)}
|
||||
if oldDevice != nil {
|
||||
resources = append(resources, deviceExchangeDeviceAuditResource(oldDevice, constants.AuditResourceRoleDeviceExchangeOldDevice, summary, internalOnly, oldDeviceBefore, oldDeviceAfter))
|
||||
}
|
||||
if newDevice != nil {
|
||||
resources = append(resources, deviceExchangeDeviceAuditResource(newDevice, constants.AuditResourceRoleDeviceExchangeNewDevice, summary, internalOnly, newDeviceBefore, newDeviceAfter))
|
||||
}
|
||||
shopResource, err := loadDeviceExchangeShopAuditResource(ctx, tx, order.ShopID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if shopResource != nil {
|
||||
resources = append(resources, *shopResource)
|
||||
}
|
||||
resources = append(resources, extra...)
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
scopeType, scopeID := constants.AuditScopePlatform, ""
|
||||
if actionCode == constants.AuditActionDeviceExchangeShippingInfoSubmitted {
|
||||
scopeType = constants.AuditScopePersonalCustomer
|
||||
scopeID = auditcontext.From(ctx).ActorID
|
||||
}
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: scopeType, ScopeID: scopeID,
|
||||
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
Metadata: map[string]any{"flow_type": effectiveExchangeFlowType(order.FlowType), "migrate_data": order.MigrateData},
|
||||
Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func deviceExchangeOrderAuditResource(order *model.ExchangeOrder, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput {
|
||||
resource := cardExchangeOrderAuditResource(order, summary, internalOnly, beforeData, afterData)
|
||||
resource.Role = constants.AuditResourceRoleDeviceExchangeOrder
|
||||
return resource
|
||||
}
|
||||
|
||||
func deviceExchangeDeviceAuditResource(device *model.Device, role, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput {
|
||||
id := strconv.FormatUint(uint64(device.ID), 10)
|
||||
relation := constants.AuditResourceRelationReference
|
||||
if len(beforeData) > 0 || len(afterData) > 0 {
|
||||
relation = constants.AuditResourceRelationAffected
|
||||
}
|
||||
resource := audit.ResourceInput{
|
||||
Type: constants.AuditResourceDevice, ID: &id, Key: audit.DeviceResourceKey(device), DisplayName: preferredDeviceIdentifier(device),
|
||||
Relation: relation, Role: role, IdentitySnapshot: audit.DeviceIdentitySnapshot(device),
|
||||
BeforeData: beforeData, AfterData: afterData,
|
||||
}
|
||||
if internalOnly {
|
||||
resource.SubjectVisibility = constants.AuditSubjectInternalOnly
|
||||
} else {
|
||||
resource.SubjectVisibility = constants.AuditSubjectResult
|
||||
resource.SubjectSummary = summary
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
func loadDeviceExchangeShopAuditResource(ctx context.Context, tx *gorm.DB, shopID *uint) (*audit.ResourceInput, error) {
|
||||
resource, err := loadCardExchangeShopAuditResource(ctx, tx, shopID)
|
||||
if resource != nil {
|
||||
resource.Role = constants.AuditResourceRoleDeviceExchangeShop
|
||||
}
|
||||
return resource, err
|
||||
}
|
||||
|
||||
func (s *Service) captureDeviceExchangeAuditBefore(ctx context.Context, tx *gorm.DB, oldDevice, newDevice *model.Device) (*deviceExchangeAuditBefore, error) {
|
||||
state := &deviceExchangeAuditBefore{Wallets: make(map[uint]model.AssetWallet)}
|
||||
deviceIDs := make([]uint, 0, 2)
|
||||
if oldDevice != nil {
|
||||
deviceIDs = append(deviceIDs, oldDevice.ID)
|
||||
}
|
||||
if newDevice != nil {
|
||||
deviceIDs = append(deviceIDs, newDevice.ID)
|
||||
}
|
||||
if len(deviceIDs) > 0 {
|
||||
var wallets []model.AssetWallet
|
||||
if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id IN ?", constants.ExchangeAssetTypeDevice, deviceIDs).Find(&wallets).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货钱包审计快照失败")
|
||||
}
|
||||
for _, wallet := range wallets {
|
||||
state.Wallets[wallet.ResourceID] = wallet
|
||||
}
|
||||
}
|
||||
rows, err := loadDeviceExchangeCustomerBindings(ctx, tx, oldDevice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.CustomerBindings = rows
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func loadDeviceExchangeCustomerBindings(ctx context.Context, tx *gorm.DB, device *model.Device) ([]*model.PersonalCustomerDevice, error) {
|
||||
if device == nil {
|
||||
return nil, nil
|
||||
}
|
||||
key := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: device, VirtualNo: device.VirtualNo})
|
||||
if key == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var rows []*model.PersonalCustomerDevice
|
||||
if err := tx.WithContext(ctx).Where("virtual_no = ? AND status = ?", key, constants.StatusEnabled).Find(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货客户绑定失败")
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (s *Service) buildDeviceExchangeCompletionResources(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
order *model.ExchangeOrder,
|
||||
oldDevice, newDevice *model.Device,
|
||||
before *deviceExchangeAuditBefore,
|
||||
migration *exchangeMigrationResult,
|
||||
) ([]audit.ResourceInput, error) {
|
||||
resources := deviceExchangeOldCustomerBindingResources(before)
|
||||
newBindings, err := loadDeviceExchangeCustomerBindings(ctx, tx, newDevice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, deviceExchangeNewCustomerBindingResources(newBindings)...)
|
||||
simResources, err := loadDeviceExchangeSIMResources(ctx, tx, oldDevice, newDevice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, simResources...)
|
||||
beforeWallets := map[uint]model.AssetWallet(nil)
|
||||
if before != nil {
|
||||
beforeWallets = before.Wallets
|
||||
}
|
||||
walletResources, err := loadDeviceExchangeWalletResources(ctx, tx, oldDevice.ID, newDevice.ID, beforeWallets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, walletResources...)
|
||||
if migration == nil {
|
||||
return resources, nil
|
||||
}
|
||||
transactions, err := loadDeviceExchangeTransactionResources(ctx, tx, order.ExchangeNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, transactions...)
|
||||
usages, err := loadDeviceExchangePackageUsageResources(ctx, tx, migration.PackageUsageIDs, oldDevice.ID, newDevice.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(resources, usages...), nil
|
||||
}
|
||||
|
||||
func deviceExchangeOldCustomerBindingResources(before *deviceExchangeAuditBefore) []audit.ResourceInput {
|
||||
if before == nil {
|
||||
return nil
|
||||
}
|
||||
return deviceExchangeCustomerBindingResources(before.CustomerBindings, constants.AuditResourceRoleDeviceExchangeOldCustomerBinding, true)
|
||||
}
|
||||
|
||||
func deviceExchangeNewCustomerBindingResources(rows []*model.PersonalCustomerDevice) []audit.ResourceInput {
|
||||
return deviceExchangeCustomerBindingResources(rows, constants.AuditResourceRoleDeviceExchangeNewCustomerBinding, false)
|
||||
}
|
||||
|
||||
func deviceExchangeCustomerBindingResources(rows []*model.PersonalCustomerDevice, role string, before bool) []audit.ResourceInput {
|
||||
resources := make([]audit.ResourceInput, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
id := strconv.FormatUint(uint64(row.ID), 10)
|
||||
resource := audit.ResourceInput{
|
||||
Type: constants.AuditResourcePersonalCustomerDevice, ID: &id, Key: id, DisplayName: row.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: role,
|
||||
IdentitySnapshot: map[string]any{"id": row.ID, "customer_id": row.CustomerID, "virtual_no": row.VirtualNo, "bind_at": row.BindAt, "last_used_at": row.LastUsedAt, "status": row.Status},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}
|
||||
if before {
|
||||
resource.BeforeData = map[string]any{"virtual_no": row.VirtualNo, "status": row.Status}
|
||||
} else {
|
||||
resource.AfterData = map[string]any{"virtual_no": row.VirtualNo, "status": row.Status}
|
||||
}
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
func loadDeviceExchangeSIMResources(ctx context.Context, tx *gorm.DB, oldDevice, newDevice *model.Device) ([]audit.ResourceInput, error) {
|
||||
resources := make([]audit.ResourceInput, 0)
|
||||
for _, item := range []struct {
|
||||
device *model.Device
|
||||
cardRole string
|
||||
bindingRole string
|
||||
}{
|
||||
{oldDevice, constants.AuditResourceRoleDeviceExchangeOldBoundCard, constants.AuditResourceRoleDeviceExchangeOldSIMBinding},
|
||||
{newDevice, constants.AuditResourceRoleDeviceExchangeNewBoundCard, constants.AuditResourceRoleDeviceExchangeNewSIMBinding},
|
||||
} {
|
||||
if item.device == nil {
|
||||
continue
|
||||
}
|
||||
var bindings []*model.DeviceSimBinding
|
||||
if err := tx.WithContext(ctx).Where("device_id = ? AND bind_status = ?", item.device.ID, constants.BindStatusBound).
|
||||
Order("slot_position ASC, id ASC").Find(&bindings).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货卡槽绑定失败")
|
||||
}
|
||||
cardIDs := make([]uint, 0, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
cardIDs = append(cardIDs, binding.IotCardID)
|
||||
}
|
||||
cards := make(map[uint]*model.IotCard, len(cardIDs))
|
||||
if len(cardIDs) > 0 {
|
||||
var rows []*model.IotCard
|
||||
if err := tx.WithContext(ctx).Where("id IN ?", cardIDs).Find(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货绑定卡失败")
|
||||
}
|
||||
for _, card := range rows {
|
||||
cards[card.ID] = card
|
||||
}
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
card := cards[binding.IotCardID]
|
||||
if card == nil {
|
||||
return nil, errors.New(errors.CodeAssetNotFound, "设备换货绑定卡不存在")
|
||||
}
|
||||
cardID := strconv.FormatUint(uint64(card.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceIotCard, ID: &cardID, Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationReference, Role: item.cardRole,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(card), SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
bindingID := strconv.FormatUint(uint64(binding.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID, Key: bindingID, DisplayName: preferredDeviceIdentifier(item.device),
|
||||
Relation: constants.AuditResourceRelationReference, Role: item.bindingRole,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": binding.ID, "device_id": binding.DeviceID, "device_virtual_no": item.device.VirtualNo,
|
||||
"slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID,
|
||||
"iccid": card.ICCID, "virtual_no": card.VirtualNo, "is_current": binding.IsCurrent,
|
||||
}, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func loadDeviceExchangeWalletResources(ctx context.Context, tx *gorm.DB, oldDeviceID, newDeviceID uint, before map[uint]model.AssetWallet) ([]audit.ResourceInput, error) {
|
||||
return loadExchangeWalletResources(ctx, tx, constants.ExchangeAssetTypeDevice, oldDeviceID, newDeviceID, before,
|
||||
constants.AuditResourceRoleDeviceExchangeOldWallet, constants.AuditResourceRoleDeviceExchangeNewWallet, "设备")
|
||||
}
|
||||
|
||||
func loadDeviceExchangeRenewWalletResource(ctx context.Context, tx *gorm.DB, deviceID uint, before map[uint]model.AssetWallet) (*audit.ResourceInput, error) {
|
||||
return loadExchangeRenewWalletResource(ctx, tx, constants.ExchangeAssetTypeDevice, deviceID, before, constants.AuditResourceRoleDeviceExchangeOldWallet, "设备")
|
||||
}
|
||||
|
||||
func loadDeviceExchangeTransactionResources(ctx context.Context, tx *gorm.DB, exchangeNo string) ([]audit.ResourceInput, error) {
|
||||
return loadExchangeTransactionResources(ctx, tx, exchangeNo, constants.AuditResourceRoleDeviceExchangeWalletTransaction, "设备")
|
||||
}
|
||||
|
||||
func loadDeviceExchangePackageUsageResources(ctx context.Context, tx *gorm.DB, ids []uint, oldDeviceID, newDeviceID uint) ([]audit.ResourceInput, error) {
|
||||
return loadExchangePackageUsageResources(ctx, tx, ids, "device_id", oldDeviceID, newDeviceID, constants.AuditResourceRoleDeviceExchangePackageUsage, "设备")
|
||||
}
|
||||
|
||||
func (s *Service) recordExchangeOrderFailure(ctx context.Context, cardActionCode, summary string, order *model.ExchangeOrder, businessErr error) {
|
||||
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeDevice {
|
||||
s.recordCardExchangeOrderFailure(ctx, cardActionCode, summary, order, businessErr)
|
||||
return
|
||||
}
|
||||
oldDevice, newDevice := s.loadDeviceExchangeAuditDevices(ctx, order)
|
||||
if s.db == nil || s.auditWriter == nil {
|
||||
recordCardExchangeAuditSecondaryFailure(ctx, deviceExchangeActionCode(cardActionCode), order.ExchangeNo, businessErr,
|
||||
errors.New(errors.CodeInvalidStatus, "设备换货统一审计接缝未配置"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendDeviceExchangeAudit(ctx, tx, deviceExchangeActionCode(cardActionCode), strings.ReplaceAll(summary, "卡", "设备"),
|
||||
cardExchangeFailureResult(businessErr), order, oldDevice, newDevice,
|
||||
nil, nil, nil, nil, nil, nil, nil, businessErr)
|
||||
}); err != nil {
|
||||
recordCardExchangeAuditSecondaryFailure(ctx, deviceExchangeActionCode(cardActionCode), order.ExchangeNo, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) loadDeviceExchangeAuditDevices(ctx context.Context, order *model.ExchangeOrder) (*model.Device, *model.Device) {
|
||||
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeDevice {
|
||||
return nil, nil
|
||||
}
|
||||
var oldDevice *model.Device
|
||||
if s.deviceStore != nil {
|
||||
oldDevice, _ = s.deviceStore.GetByID(ctx, order.OldAssetID)
|
||||
}
|
||||
if oldDevice == nil {
|
||||
oldDevice = &model.Device{Model: gorm.Model{ID: order.OldAssetID}, ShopID: order.ShopID}
|
||||
}
|
||||
var newDevice *model.Device
|
||||
if order.NewAssetID != nil && *order.NewAssetID > 0 {
|
||||
if s.deviceStore != nil {
|
||||
newDevice, _ = s.deviceStore.GetByID(ctx, *order.NewAssetID)
|
||||
}
|
||||
if newDevice == nil {
|
||||
newDevice = &model.Device{Model: gorm.Model{ID: *order.NewAssetID}, ShopID: order.ShopID}
|
||||
}
|
||||
}
|
||||
return oldDevice, newDevice
|
||||
}
|
||||
@@ -12,21 +12,27 @@ import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (s *Service) executeMigrationWithTx(ctx context.Context, tx *gorm.DB, order *model.ExchangeOrder, oldAsset, newAsset *resolvedExchangeAsset) (int64, error) {
|
||||
type exchangeMigrationResult struct {
|
||||
Balance int64
|
||||
PackageUsageIDs []uint
|
||||
}
|
||||
|
||||
func (s *Service) executeMigrationWithTx(ctx context.Context, tx *gorm.DB, order *model.ExchangeOrder, oldAsset, newAsset *resolvedExchangeAsset) (*exchangeMigrationResult, error) {
|
||||
migrationBalance, err := s.transferWalletBalanceWithTx(ctx, tx, order, oldAsset, newAsset)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errors.CodeExchangeMigrationFailed, err, "执行钱包迁移失败")
|
||||
return nil, errors.Wrap(errors.CodeExchangeMigrationFailed, err, "执行钱包迁移失败")
|
||||
}
|
||||
if err = s.migratePackageUsageWithTx(ctx, tx, oldAsset, newAsset); err != nil {
|
||||
return 0, errors.Wrap(errors.CodeExchangeMigrationFailed, err, "迁移套餐使用记录失败")
|
||||
usageIDs, err := s.migratePackageUsageWithTx(ctx, tx, oldAsset, newAsset)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeExchangeMigrationFailed, err, "迁移套餐使用记录失败")
|
||||
}
|
||||
if err = s.copyAccumulatedFieldsWithTx(tx, oldAsset, newAsset); err != nil {
|
||||
return 0, errors.Wrap(errors.CodeExchangeMigrationFailed, err, "复制累计充值字段失败")
|
||||
return nil, errors.Wrap(errors.CodeExchangeMigrationFailed, err, "复制累计充值字段失败")
|
||||
}
|
||||
if err = s.copyResourceTagsWithTx(ctx, tx, oldAsset, newAsset); err != nil {
|
||||
return 0, errors.Wrap(errors.CodeExchangeMigrationFailed, err, "复制资产标签失败")
|
||||
return nil, errors.Wrap(errors.CodeExchangeMigrationFailed, err, "复制资产标签失败")
|
||||
}
|
||||
return migrationBalance, nil
|
||||
return &exchangeMigrationResult{Balance: migrationBalance, PackageUsageIDs: usageIDs}, nil
|
||||
}
|
||||
|
||||
func (s *Service) transferWalletBalanceWithTx(ctx context.Context, tx *gorm.DB, order *model.ExchangeOrder, oldAsset, newAsset *resolvedExchangeAsset) (int64, error) {
|
||||
@@ -95,7 +101,7 @@ func (s *Service) transferWalletBalanceWithTx(ctx context.Context, tx *gorm.DB,
|
||||
return migrationBalance, nil
|
||||
}
|
||||
|
||||
func (s *Service) migratePackageUsageWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||||
func (s *Service) migratePackageUsageWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) ([]uint, error) {
|
||||
query := tx.WithContext(ctx).Model(&model.PackageUsage{}).Where("status IN ?", []int{constants.PackageUsageStatusPending, constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted})
|
||||
if oldAsset.AssetType == constants.ExchangeAssetTypeIotCard {
|
||||
query = query.Where("iot_card_id = ?", oldAsset.AssetID)
|
||||
@@ -105,11 +111,11 @@ func (s *Service) migratePackageUsageWithTx(ctx context.Context, tx *gorm.DB, ol
|
||||
|
||||
var usageIDs []uint
|
||||
if err := query.Pluck("id", &usageIDs).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
}
|
||||
|
||||
if len(usageIDs) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
updates := map[string]any{"updated_at": time.Now()}
|
||||
@@ -120,14 +126,14 @@ func (s *Service) migratePackageUsageWithTx(ctx context.Context, tx *gorm.DB, ol
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).Model(&model.PackageUsage{}).Where("id IN ?", usageIDs).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "迁移套餐使用记录失败")
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "迁移套餐使用记录失败")
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).Model(&model.PackageUsageDailyRecord{}).Where("package_usage_id IN ?", usageIDs).Update("updated_at", gorm.Expr("updated_at")).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "迁移套餐日记录失败")
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "迁移套餐日记录失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
return usageIDs, nil
|
||||
}
|
||||
|
||||
func (s *Service) copyAccumulatedFieldsWithTx(tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
exchangeapp "github.com/break/junhong_cmp_fiber/internal/application/exchange"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
customerBindingSvc "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||
@@ -31,6 +32,7 @@ type Service struct {
|
||||
resourceTagStore *postgres.ResourceTagStore
|
||||
customerBinding *customerBindingSvc.Service
|
||||
shippingCreatedNotifier *exchangeapp.ShippingCreatedNotifier
|
||||
auditWriter *audit.Writer
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -81,31 +83,14 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateExchangeRequest) (*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isExchangeableAssetStatus(asset.AssetStatus) {
|
||||
return nil, oldAssetStatusError(asset.AssetStatus)
|
||||
migrateData := false
|
||||
if req.MigrateData != nil {
|
||||
migrateData = *req.MigrateData
|
||||
}
|
||||
hasUnfinishedRefund, err := s.refundStore.HasUnfinishedByAsset(ctx, asset.AssetType, asset.AssetID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产退款申请失败")
|
||||
}
|
||||
if hasUnfinishedRefund {
|
||||
return nil, errors.New(errors.CodeExchangeActiveRefund)
|
||||
}
|
||||
|
||||
if _, err = s.exchangeStore.FindActiveByOldAsset(ctx, asset.AssetType, asset.AssetID); err == nil {
|
||||
return nil, errors.New(errors.CodeExchangeInProgress)
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询进行中换货单失败")
|
||||
}
|
||||
|
||||
if flowType == constants.ExchangeFlowTypeDirect {
|
||||
return s.createDirectExchange(ctx, req, asset)
|
||||
}
|
||||
|
||||
creator := middleware.GetUserIDFromContext(ctx)
|
||||
order := &model.ExchangeOrder{
|
||||
ExchangeNo: model.GenerateExchangeNo(),
|
||||
FlowType: constants.ExchangeFlowTypeShipping,
|
||||
FlowType: flowType,
|
||||
OldAssetType: asset.AssetType,
|
||||
OldAssetID: asset.AssetID,
|
||||
OldAssetIdentifier: asset.Identifier,
|
||||
@@ -114,15 +99,57 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateExchangeRequest) (*
|
||||
Status: constants.ExchangeStatusPendingInfo,
|
||||
MigrationCompleted: false,
|
||||
MigrationBalance: 0,
|
||||
MigrateData: false,
|
||||
MigrateData: flowType == constants.ExchangeFlowTypeDirect && migrateData,
|
||||
BaseModel: model.BaseModel{Creator: creator, Updater: creator},
|
||||
}
|
||||
if asset.ShopID != nil {
|
||||
order.ShopID = asset.ShopID
|
||||
}
|
||||
if !isExchangeableAssetStatus(asset.AssetStatus) {
|
||||
err = oldAssetStatusError(asset.AssetStatus)
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单被拒绝", order, err)
|
||||
return nil, err
|
||||
}
|
||||
hasUnfinishedRefund, err := s.refundStore.HasUnfinishedByAsset(ctx, asset.AssetType, asset.AssetID)
|
||||
if err != nil {
|
||||
err = errors.Wrap(errors.CodeDatabaseError, err, "查询资产退款申请失败")
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单失败", order, err)
|
||||
return nil, err
|
||||
}
|
||||
if hasUnfinishedRefund {
|
||||
err = errors.New(errors.CodeExchangeActiveRefund)
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单被拒绝", order, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = s.exchangeStore.FindActiveByOldAsset(ctx, asset.AssetType, asset.AssetID); err == nil {
|
||||
err = errors.New(errors.CodeExchangeInProgress)
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单被拒绝", order, err)
|
||||
return nil, err
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
err = errors.Wrap(errors.CodeDatabaseError, err, "查询进行中换货单失败")
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单失败", order, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if flowType == constants.ExchangeFlowTypeDirect {
|
||||
orderID, directErr := s.createDirectExchange(ctx, req, asset, order)
|
||||
if directErr != nil {
|
||||
order.ID = 0
|
||||
order.Status = constants.ExchangeStatusPendingInfo
|
||||
order.MigrationCompleted = false
|
||||
order.MigrationBalance = 0
|
||||
order.CompletedAt = nil
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡直接换货失败", order, directErr)
|
||||
return nil, directErr
|
||||
}
|
||||
return s.Get(ctx, orderID)
|
||||
}
|
||||
|
||||
if s.shippingCreatedNotifier == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "物流换货通知服务未配置")
|
||||
err = errors.New(errors.CodeInternalError, "物流换货通知服务未配置")
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单失败", order, err)
|
||||
return nil, err
|
||||
}
|
||||
requestID := ""
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
@@ -145,9 +172,14 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateExchangeRequest) (*
|
||||
return notifyErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeCreated, "已创建卡换货单", constants.AuditResultSuccess,
|
||||
order, asset, nil,
|
||||
map[string]any{"exists": false}, map[string]any{"status": constants.ExchangeStatusPendingInfo},
|
||||
nil, nil, nil, nil, nil, nil)
|
||||
})
|
||||
if err != nil {
|
||||
order.ID = 0
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单失败", order, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -178,13 +210,18 @@ func (s *Service) Ship(ctx context.Context, id uint, req *dto.ExchangeShipReques
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||||
}
|
||||
if order.Status != constants.ExchangeStatusPendingShip {
|
||||
return nil, errors.New(errors.CodeExchangeStatusInvalid)
|
||||
err = errors.New(errors.CodeExchangeStatusInvalid)
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShipped, "卡换货发货被拒绝", order, err)
|
||||
return nil, err
|
||||
}
|
||||
if !isShippingExchangeFlow(order.FlowType) {
|
||||
return nil, errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持发货")
|
||||
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持发货")
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShipped, "卡换货发货被拒绝", order, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = s.shipWithTx(ctx, order, req); err != nil {
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShipped, "卡换货发货失败", order, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -200,13 +237,17 @@ func (s *Service) Complete(ctx context.Context, id uint) error {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||||
}
|
||||
if order.Status != constants.ExchangeStatusShipped {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||||
err = errors.New(errors.CodeExchangeStatusInvalid)
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货被拒绝", order, err)
|
||||
return err
|
||||
}
|
||||
if !isShippingExchangeFlow(order.FlowType) {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持确认完成")
|
||||
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持确认完成")
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货被拒绝", order, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
lockedOrder, lockErr := s.lockExchangeOrderByID(ctx, tx, id)
|
||||
if lockErr != nil {
|
||||
return lockErr
|
||||
@@ -219,6 +260,10 @@ func (s *Service) Complete(ctx context.Context, id uint) error {
|
||||
}
|
||||
return s.completeExchangeWithTx(ctx, tx, lockedOrder, constants.ExchangeStatusShipped)
|
||||
})
|
||||
if err != nil {
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货失败", order, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) Cancel(ctx context.Context, id uint, req *dto.ExchangeCancelRequest) error {
|
||||
@@ -230,10 +275,14 @@ func (s *Service) Cancel(ctx context.Context, id uint, req *dto.ExchangeCancelRe
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||||
}
|
||||
if order.Status != constants.ExchangeStatusPendingInfo && order.Status != constants.ExchangeStatusPendingShip {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||||
err = errors.New(errors.CodeExchangeStatusInvalid)
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货被拒绝", order, err)
|
||||
return err
|
||||
}
|
||||
if !isShippingExchangeFlow(order.FlowType) {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持取消")
|
||||
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持取消")
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货被拒绝", order, err)
|
||||
return err
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
@@ -243,13 +292,36 @@ func (s *Service) Cancel(ctx context.Context, id uint, req *dto.ExchangeCancelRe
|
||||
if req != nil {
|
||||
updates["remark"] = req.Remark
|
||||
}
|
||||
if err = s.exchangeStore.UpdateStatus(ctx, id, order.Status, constants.ExchangeStatusCancelled, updates); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
oldAsset, resolveErr := s.resolveAssetByID(ctx, order.OldAssetType, order.OldAssetID)
|
||||
if resolveErr != nil {
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货失败", order, resolveErr)
|
||||
return resolveErr
|
||||
}
|
||||
fromStatus := order.Status
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
values := make(map[string]any, len(updates)+1)
|
||||
for key, value := range updates {
|
||||
values[key] = value
|
||||
}
|
||||
values["status"] = constants.ExchangeStatusCancelled
|
||||
result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).Where("id = ? AND status = ?", id, fromStatus).Updates(values)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "取消换货失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "取消换货失败")
|
||||
order.Status = constants.ExchangeStatusCancelled
|
||||
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeCancelled, "已取消卡换货单", constants.AuditResultSuccess,
|
||||
order, oldAsset, nil,
|
||||
map[string]any{"status": fromStatus}, map[string]any{"status": constants.ExchangeStatusCancelled},
|
||||
nil, nil, nil, nil, nil, nil)
|
||||
})
|
||||
if err != nil {
|
||||
order.Status = fromStatus
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货失败", order, err)
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) Renew(ctx context.Context, id uint) error {
|
||||
@@ -261,52 +333,84 @@ func (s *Service) Renew(ctx context.Context, id uint) error {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||||
}
|
||||
if order.Status != constants.ExchangeStatusCompleted {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||||
err = errors.New(errors.CodeExchangeStatusInvalid)
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeRenewed, "换出旧卡转新被拒绝", order, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if order.OldAssetType == constants.ExchangeAssetTypeIotCard {
|
||||
var card model.IotCard
|
||||
if err = tx.Where("id = ?", order.OldAssetID).First(&card).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
if queryErr := tx.WithContext(ctx).Where("id = ?", order.OldAssetID).First(&card).Error; queryErr != nil {
|
||||
if queryErr == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询旧卡失败")
|
||||
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询旧卡失败")
|
||||
}
|
||||
if card.AssetStatus != constants.AssetStatusExchanged {
|
||||
return errors.New(errors.CodeExchangeAssetNotExchanged)
|
||||
}
|
||||
var newCard *model.IotCard
|
||||
if order.NewAssetID != nil && *order.NewAssetID > 0 {
|
||||
var value model.IotCard
|
||||
if queryErr := tx.WithContext(ctx).Where("id = ?", *order.NewAssetID).First(&value).Error; queryErr != nil {
|
||||
if queryErr == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询换货新卡失败")
|
||||
}
|
||||
newCard = &value
|
||||
}
|
||||
auditBefore, auditErr := s.captureCardExchangeAuditBefore(ctx, tx, &card, newCard)
|
||||
if auditErr != nil {
|
||||
return auditErr
|
||||
}
|
||||
cardBefore := map[string]any{"generation": card.Generation, "asset_status": card.AssetStatus}
|
||||
|
||||
if err = tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(map[string]any{
|
||||
if updateErr := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(map[string]any{
|
||||
"generation": card.Generation + 1,
|
||||
"asset_status": constants.AssetStatusInStock,
|
||||
"accumulated_recharge_by_series": "{}",
|
||||
"first_recharge_triggered_by_series": "{}",
|
||||
"updater": middleware.GetUserIDFromContext(ctx),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "重置旧卡转新状态失败")
|
||||
}).Error; updateErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, updateErr, "重置旧卡转新状态失败")
|
||||
}
|
||||
|
||||
cardKey := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, Card: &card, VirtualNo: card.VirtualNo})
|
||||
if cardKey != "" {
|
||||
if err = tx.Where("virtual_no = ?", cardKey).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "清理个人客户绑定失败")
|
||||
if unbindErr := s.customerBinding.UnbindByVirtualNo(ctx, tx, constants.ExchangeAssetTypeIotCard, card.ID, cardKey); unbindErr != nil {
|
||||
return unbindErr
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeIotCard, card.ID).Delete(&model.AssetWallet{}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "清理旧钱包失败")
|
||||
if deleteErr := tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeIotCard, card.ID).Delete(&model.AssetWallet{}).Error; deleteErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, deleteErr, "清理旧钱包失败")
|
||||
}
|
||||
|
||||
shopTag := uint(0)
|
||||
if card.ShopID != nil {
|
||||
shopTag = *card.ShopID
|
||||
}
|
||||
if err = tx.Create(&model.AssetWallet{ResourceType: constants.ExchangeAssetTypeIotCard, ResourceID: card.ID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: constants.AssetWalletStatusNormal, Version: 0, ShopIDTag: shopTag}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建新钱包失败")
|
||||
if createErr := tx.Create(&model.AssetWallet{ResourceType: constants.ExchangeAssetTypeIotCard, ResourceID: card.ID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: constants.AssetWalletStatusNormal, Version: 0, ShopIDTag: shopTag}).Error; createErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, createErr, "创建新钱包失败")
|
||||
}
|
||||
return nil
|
||||
var renewedCard model.IotCard
|
||||
if queryErr := tx.WithContext(ctx).Where("id = ?", card.ID).First(&renewedCard).Error; queryErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询旧卡转新结果失败")
|
||||
}
|
||||
walletResource, resourceErr := loadCardExchangeRenewWalletResource(ctx, tx, card.ID, auditBefore.Wallets)
|
||||
if resourceErr != nil {
|
||||
return resourceErr
|
||||
}
|
||||
extra := cardExchangeOldBindingResources(auditBefore)
|
||||
extra = append(extra, *walletResource)
|
||||
return s.appendCardExchangeAudit(ctx, tx, constants.AuditActionCardExchangeRenewed, "换出旧卡已转为新卡状态", constants.AuditResultSuccess,
|
||||
order, &renewedCard, newCard,
|
||||
nil, nil,
|
||||
cardBefore, map[string]any{"generation": renewedCard.Generation, "asset_status": renewedCard.AssetStatus},
|
||||
nil, nil, extra, nil)
|
||||
}
|
||||
|
||||
var device model.Device
|
||||
@@ -319,6 +423,22 @@ func (s *Service) Renew(ctx context.Context, id uint) error {
|
||||
if device.AssetStatus != constants.AssetStatusExchanged {
|
||||
return errors.New(errors.CodeExchangeAssetNotExchanged)
|
||||
}
|
||||
var newDevice *model.Device
|
||||
if order.NewAssetID != nil && *order.NewAssetID > 0 {
|
||||
var value model.Device
|
||||
if queryErr := tx.WithContext(ctx).Where("id = ?", *order.NewAssetID).First(&value).Error; queryErr != nil {
|
||||
if queryErr == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询换货新设备失败")
|
||||
}
|
||||
newDevice = &value
|
||||
}
|
||||
deviceAuditBefore, auditErr := s.captureDeviceExchangeAuditBefore(ctx, tx, &device, newDevice)
|
||||
if auditErr != nil {
|
||||
return auditErr
|
||||
}
|
||||
deviceBefore := map[string]any{"generation": device.Generation, "asset_status": device.AssetStatus}
|
||||
|
||||
if err = tx.Model(&model.Device{}).Where("id = ?", device.ID).Updates(map[string]any{
|
||||
"generation": device.Generation + 1,
|
||||
@@ -333,8 +453,8 @@ func (s *Service) Renew(ctx context.Context, id uint) error {
|
||||
|
||||
deviceKey := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &device, VirtualNo: device.VirtualNo})
|
||||
if deviceKey != "" {
|
||||
if err = tx.Where("virtual_no = ?", deviceKey).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "清理个人客户绑定失败")
|
||||
if err = s.customerBinding.UnbindByVirtualNo(ctx, tx, constants.ExchangeAssetTypeDevice, device.ID, deviceKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,8 +469,31 @@ func (s *Service) Renew(ctx context.Context, id uint) error {
|
||||
if err = tx.Create(&model.AssetWallet{ResourceType: constants.ExchangeAssetTypeDevice, ResourceID: device.ID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: constants.AssetWalletStatusNormal, Version: 0, ShopIDTag: shopTag}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建新钱包失败")
|
||||
}
|
||||
return nil
|
||||
var renewedDevice model.Device
|
||||
if queryErr := tx.WithContext(ctx).Where("id = ?", device.ID).First(&renewedDevice).Error; queryErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询旧设备转新结果失败")
|
||||
}
|
||||
walletResource, resourceErr := loadDeviceExchangeRenewWalletResource(ctx, tx, device.ID, deviceAuditBefore.Wallets)
|
||||
if resourceErr != nil {
|
||||
return resourceErr
|
||||
}
|
||||
extra := deviceExchangeOldCustomerBindingResources(deviceAuditBefore)
|
||||
simResources, resourceErr := loadDeviceExchangeSIMResources(ctx, tx, &renewedDevice, newDevice)
|
||||
if resourceErr != nil {
|
||||
return resourceErr
|
||||
}
|
||||
extra = append(extra, simResources...)
|
||||
extra = append(extra, *walletResource)
|
||||
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeRenewed, "换出旧卡已转为新卡状态", constants.AuditResultSuccess,
|
||||
order, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &renewedDevice}, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: newDevice},
|
||||
nil, nil,
|
||||
deviceBefore, map[string]any{"generation": renewedDevice.Generation, "asset_status": renewedDevice.AssetStatus},
|
||||
nil, nil, extra, nil)
|
||||
})
|
||||
if err != nil {
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeRenewed, "换出旧卡转新失败", order, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) GetPending(ctx context.Context, identifier string) (*dto.ClientExchangePendingResponse, error) {
|
||||
@@ -392,17 +535,24 @@ func (s *Service) SubmitShippingInfo(ctx context.Context, id uint, req *dto.Clie
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||||
}
|
||||
if !isShippingExchangeFlow(order.FlowType) {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持填写收货信息")
|
||||
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持填写收货信息")
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息被拒绝", order, err)
|
||||
return err
|
||||
}
|
||||
if order.Status != constants.ExchangeStatusPendingInfo {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||||
err = errors.New(errors.CodeExchangeStatusInvalid)
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息被拒绝", order, err)
|
||||
return err
|
||||
}
|
||||
oldAsset, err := s.resolveAssetByID(ctx, order.OldAssetType, order.OldAssetID)
|
||||
if err != nil {
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息失败", order, err)
|
||||
return err
|
||||
}
|
||||
if !s.customerOwnsAsset(ctx, oldAsset) {
|
||||
return errors.New(errors.CodeExchangeOrderNotFound)
|
||||
err = errors.New(errors.CodeExchangeOrderNotFound)
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息被拒绝", order, err)
|
||||
return err
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
@@ -411,13 +561,32 @@ func (s *Service) SubmitShippingInfo(ctx context.Context, id uint, req *dto.Clie
|
||||
"recipient_address": req.RecipientAddress,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if err := s.exchangeStore.UpdateStatus(ctx, id, constants.ExchangeStatusPendingInfo, constants.ExchangeStatusPendingShip, updates); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
values := make(map[string]any, len(updates)+1)
|
||||
for key, value := range updates {
|
||||
values[key] = value
|
||||
}
|
||||
values["status"] = constants.ExchangeStatusPendingShip
|
||||
result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
|
||||
Where("id = ? AND status = ?", id, constants.ExchangeStatusPendingInfo).
|
||||
Updates(values)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "提交收货信息失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "提交收货信息失败")
|
||||
order.Status = constants.ExchangeStatusPendingShip
|
||||
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeShippingInfoSubmitted, "已提交卡换货收货信息", constants.AuditResultSuccess,
|
||||
order, oldAsset, nil,
|
||||
map[string]any{"status": constants.ExchangeStatusPendingInfo}, map[string]any{"status": constants.ExchangeStatusPendingShip},
|
||||
nil, nil, nil, nil, nil, nil)
|
||||
})
|
||||
if err != nil {
|
||||
order.Status = constants.ExchangeStatusPendingInfo
|
||||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息失败", order, err)
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
type resolvedExchangeAsset struct {
|
||||
@@ -497,12 +666,8 @@ func isShippingExchangeFlow(flowType string) bool {
|
||||
return effectiveExchangeFlowType(flowType) == constants.ExchangeFlowTypeShipping
|
||||
}
|
||||
|
||||
func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExchangeRequest, oldAsset *resolvedExchangeAsset) (*dto.ExchangeOrderResponse, error) {
|
||||
func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExchangeRequest, oldAsset *resolvedExchangeAsset, order *model.ExchangeOrder) (uint, error) {
|
||||
var orderID uint
|
||||
migrateData := false
|
||||
if req.MigrateData != nil {
|
||||
migrateData = *req.MigrateData
|
||||
}
|
||||
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
lockedOldAsset, err := s.resolveAssetByIDWithTx(ctx, tx, oldAsset.AssetType, oldAsset.AssetID)
|
||||
@@ -524,27 +689,13 @@ func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExcha
|
||||
return errors.New(errors.CodeExchangeAssetTypeMismatch)
|
||||
}
|
||||
|
||||
creator := middleware.GetUserIDFromContext(ctx)
|
||||
order := &model.ExchangeOrder{
|
||||
ExchangeNo: model.GenerateExchangeNo(),
|
||||
FlowType: constants.ExchangeFlowTypeDirect,
|
||||
OldAssetType: lockedOldAsset.AssetType,
|
||||
OldAssetID: lockedOldAsset.AssetID,
|
||||
OldAssetIdentifier: lockedOldAsset.Identifier,
|
||||
NewAssetType: newAsset.AssetType,
|
||||
NewAssetID: &newAsset.AssetID,
|
||||
NewAssetIdentifier: newAsset.Identifier,
|
||||
ExchangeReason: req.ExchangeReason,
|
||||
Remark: req.Remark,
|
||||
Status: constants.ExchangeStatusPendingInfo,
|
||||
MigrationCompleted: false,
|
||||
MigrationBalance: 0,
|
||||
MigrateData: migrateData,
|
||||
BaseModel: model.BaseModel{Creator: creator, Updater: creator},
|
||||
}
|
||||
if lockedOldAsset.ShopID != nil {
|
||||
order.ShopID = lockedOldAsset.ShopID
|
||||
}
|
||||
order.OldAssetType = lockedOldAsset.AssetType
|
||||
order.OldAssetID = lockedOldAsset.AssetID
|
||||
order.OldAssetIdentifier = lockedOldAsset.Identifier
|
||||
order.NewAssetType = newAsset.AssetType
|
||||
order.NewAssetID = &newAsset.AssetID
|
||||
order.NewAssetIdentifier = newAsset.Identifier
|
||||
order.ShopID = cloneShopID(lockedOldAsset.ShopID)
|
||||
if err = tx.WithContext(ctx).Create(order).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建直接换货单失败")
|
||||
}
|
||||
@@ -555,9 +706,9 @@ func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExcha
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
return s.Get(ctx, orderID)
|
||||
return orderID, nil
|
||||
}
|
||||
|
||||
func (s *Service) shipWithTx(ctx context.Context, order *model.ExchangeOrder, req *dto.ExchangeShipRequest) error {
|
||||
@@ -609,7 +760,16 @@ func (s *Service) shipWithTx(ctx context.Context, order *model.ExchangeOrder, re
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||||
}
|
||||
return nil
|
||||
lockedOrder.NewAssetType = newAsset.AssetType
|
||||
lockedOrder.NewAssetID = &newAsset.AssetID
|
||||
lockedOrder.NewAssetIdentifier = newAsset.Identifier
|
||||
lockedOrder.MigrateData = req.MigrateData
|
||||
lockedOrder.ShippedAt = &now
|
||||
lockedOrder.Status = constants.ExchangeStatusShipped
|
||||
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeShipped, "卡换货单已发货", constants.AuditResultSuccess,
|
||||
lockedOrder, oldAsset, newAsset,
|
||||
map[string]any{"status": constants.ExchangeStatusPendingShip}, map[string]any{"status": constants.ExchangeStatusShipped},
|
||||
nil, nil, nil, nil, nil, nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -629,6 +789,19 @@ 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
|
||||
}
|
||||
var auditBefore *cardExchangeAuditBefore
|
||||
var deviceAuditBefore *deviceExchangeAuditBefore
|
||||
if order.OldAssetType == constants.ExchangeAssetTypeIotCard {
|
||||
auditBefore, err = s.captureCardExchangeAuditBefore(ctx, tx, oldAsset.Card, newAsset.Card)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
deviceAuditBefore, err = s.captureDeviceExchangeAuditBefore(ctx, tx, oldAsset.Device, newAsset.Device)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = s.syncNewAssetOwnershipWithTx(ctx, tx, oldAsset, newAsset); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -639,9 +812,9 @@ func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order
|
||||
return err
|
||||
}
|
||||
|
||||
var migrationBalance int64
|
||||
var migration *exchangeMigrationResult
|
||||
if order.MigrateData {
|
||||
migrationBalance, err = s.executeMigrationWithTx(ctx, tx, order, oldAsset, newAsset)
|
||||
migration, err = s.executeMigrationWithTx(ctx, tx, order, oldAsset, newAsset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -656,7 +829,7 @@ func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order
|
||||
}
|
||||
if order.MigrateData {
|
||||
updates["migration_completed"] = true
|
||||
updates["migration_balance"] = migrationBalance
|
||||
updates["migration_balance"] = migration.Balance
|
||||
}
|
||||
result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
|
||||
Where("id = ? AND status = ?", order.ID, fromStatus).
|
||||
@@ -667,7 +840,49 @@ func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||||
}
|
||||
return nil
|
||||
orderBefore := map[string]any{"status": fromStatus, "migration_completed": order.MigrationCompleted, "migration_balance": order.MigrationBalance}
|
||||
order.Status = constants.ExchangeStatusCompleted
|
||||
order.CompletedAt = &now
|
||||
if migration != nil {
|
||||
order.MigrationCompleted = true
|
||||
order.MigrationBalance = migration.Balance
|
||||
}
|
||||
if order.OldAssetType == constants.ExchangeAssetTypeDevice {
|
||||
var oldDeviceAfter, newDeviceAfter model.Device
|
||||
if err = tx.WithContext(ctx).Where("id = ?", oldAsset.AssetID).First(&oldDeviceAfter).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货旧设备结果失败")
|
||||
}
|
||||
if err = tx.WithContext(ctx).Where("id = ?", newAsset.AssetID).First(&newDeviceAfter).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货新设备结果失败")
|
||||
}
|
||||
extra, resourceErr := s.buildDeviceExchangeCompletionResources(ctx, tx, order, &oldDeviceAfter, &newDeviceAfter, deviceAuditBefore, migration)
|
||||
if resourceErr != nil {
|
||||
return resourceErr
|
||||
}
|
||||
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeCompleted, "卡换货已完成", constants.AuditResultSuccess,
|
||||
order, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &oldDeviceAfter}, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &newDeviceAfter},
|
||||
orderBefore, map[string]any{"status": constants.ExchangeStatusCompleted, "migration_completed": order.MigrationCompleted, "migration_balance": order.MigrationBalance},
|
||||
map[string]any{"asset_status": oldAsset.Device.AssetStatus, "shop_id": oldAsset.Device.ShopID}, map[string]any{"asset_status": oldDeviceAfter.AssetStatus, "shop_id": oldDeviceAfter.ShopID},
|
||||
map[string]any{"asset_status": newAsset.Device.AssetStatus, "shop_id": newAsset.Device.ShopID}, map[string]any{"asset_status": newDeviceAfter.AssetStatus, "shop_id": newDeviceAfter.ShopID},
|
||||
extra, nil)
|
||||
}
|
||||
var oldCardAfter, newCardAfter model.IotCard
|
||||
if err = tx.WithContext(ctx).Where("id = ?", oldAsset.AssetID).First(&oldCardAfter).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货旧卡结果失败")
|
||||
}
|
||||
if err = tx.WithContext(ctx).Where("id = ?", newAsset.AssetID).First(&newCardAfter).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货新卡结果失败")
|
||||
}
|
||||
extra, err := s.buildCardExchangeCompletionResources(ctx, tx, order, &oldCardAfter, &newCardAfter, auditBefore, migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeCompleted, "卡换货已完成", constants.AuditResultSuccess,
|
||||
order, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, Card: &oldCardAfter}, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, Card: &newCardAfter},
|
||||
orderBefore, map[string]any{"status": constants.ExchangeStatusCompleted, "migration_completed": order.MigrationCompleted, "migration_balance": order.MigrationBalance},
|
||||
map[string]any{"asset_status": oldAsset.Card.AssetStatus, "shop_id": oldAsset.Card.ShopID}, map[string]any{"asset_status": oldCardAfter.AssetStatus, "shop_id": oldCardAfter.ShopID},
|
||||
map[string]any{"asset_status": newAsset.Card.AssetStatus, "shop_id": newAsset.Card.ShopID}, map[string]any{"asset_status": newCardAfter.AssetStatus, "shop_id": newCardAfter.ShopID},
|
||||
extra, nil)
|
||||
}
|
||||
|
||||
func (s *Service) lockExchangeOrderByID(ctx context.Context, tx *gorm.DB, id uint) (*model.ExchangeOrder, error) {
|
||||
|
||||
Reference in New Issue
Block a user