fix: 资产钱包自动创建机制 — 修复C端购买时钱包不存在报错
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s

- client_order: 新增 getOrCreateWallet 兜底,钱包不存在时自动创建
- device_import: 设备导入事务内同步创建设备钱包
- iot_card_import: IoT卡批量导入后批量创建卡钱包
- queue/handler: 传递 AssetWalletStore 给两个导入 handler
- migration 000098: 为存量IoT卡和设备补建资产钱包
This commit is contained in:
2026-03-30 11:37:41 +08:00
parent 40809d11c5
commit f339fb1987
36 changed files with 1811 additions and 26 deletions

View File

@@ -333,12 +333,9 @@ func (s *Service) createForceRechargeOrder(
return nil, err
}
wallet, err := s.walletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID)
wallet, err := s.getOrCreateWallet(ctx, resourceType, resourceID, resolveSellerShopID(validationResult))
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeWalletNotFound, "钱包不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败")
return nil, err
}
linkedPackageIDs, err := sonic.Marshal(extractPackageIDs(validationResult.Packages))
@@ -558,6 +555,46 @@ func buildLinkedPackageInfo(result *purchase_validation.PurchaseValidationResult
}
}
// getOrCreateWallet 获取或自动创建资产钱包
// 兜底机制:当钱包不存在时自动创建,避免因钱包未提前创建导致业务中断
func (s *Service) getOrCreateWallet(ctx context.Context, resourceType string, resourceID uint, shopIDTag uint) (*model.AssetWallet, error) {
wallet, err := s.walletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID)
if err == nil {
return wallet, nil
}
if err != gorm.ErrRecordNotFound {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败")
}
// 钱包不存在,自动创建空钱包
s.logger.Info("资产钱包不存在,自动创建",
zap.String("resource_type", resourceType),
zap.Uint("resource_id", resourceID),
zap.Uint("shop_id_tag", shopIDTag),
)
newWallet := &model.AssetWallet{
ResourceType: resourceType,
ResourceID: resourceID,
Balance: 0,
FrozenBalance: 0,
Currency: "CNY",
Status: constants.AssetWalletStatusNormal,
Version: 0,
ShopIDTag: shopIDTag,
}
if createErr := s.walletStore.Create(ctx, newWallet); createErr != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, createErr, "自动创建资产钱包失败")
}
// 重新查询获取完整数据(含数据库生成的字段)
wallet, err = s.walletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败")
}
return wallet, nil
}
func resolveWalletResource(result *purchase_validation.PurchaseValidationResult) (string, uint, error) {
if result.Card != nil {
return constants.AssetWalletResourceTypeIotCard, result.Card.ID, nil

View File

@@ -34,6 +34,7 @@ type DeviceImportHandler struct {
deviceStore *postgres.DeviceStore
deviceSimBindingStore *postgres.DeviceSimBindingStore
iotCardStore *postgres.IotCardStore
assetWalletStore *postgres.AssetWalletStore
storageService *storage.Service
logger *zap.Logger
}
@@ -45,6 +46,7 @@ func NewDeviceImportHandler(
deviceStore *postgres.DeviceStore,
deviceSimBindingStore *postgres.DeviceSimBindingStore,
iotCardStore *postgres.IotCardStore,
assetWalletStore *postgres.AssetWalletStore,
storageSvc *storage.Service,
logger *zap.Logger,
) *DeviceImportHandler {
@@ -55,6 +57,7 @@ func NewDeviceImportHandler(
deviceStore: deviceStore,
deviceSimBindingStore: deviceSimBindingStore,
iotCardStore: iotCardStore,
assetWalletStore: assetWalletStore,
storageService: storageSvc,
logger: logger,
}
@@ -294,6 +297,21 @@ func (h *DeviceImportHandler) processBatch(ctx context.Context, task *model.Devi
}
}
// 创建设备资产钱包(设备存在即应有钱包,避免购买时才发现缺失)
txWalletStore := postgres.NewAssetWalletStore(tx, nil)
deviceWallet := &model.AssetWallet{
ResourceType: constants.AssetWalletResourceTypeDevice,
ResourceID: device.ID,
Balance: 0,
FrozenBalance: 0,
Currency: "CNY",
Status: constants.AssetWalletStatusNormal,
Version: 0,
}
if err := txWalletStore.Create(ctx, deviceWallet); err != nil {
return err
}
return nil
})

View File

@@ -41,13 +41,14 @@ type PollingCallback interface {
}
type IotCardImportHandler struct {
db *gorm.DB
redis *redis.Client
importTaskStore *postgres.IotCardImportTaskStore
iotCardStore *postgres.IotCardStore
storageService *storage.Service
pollingCallback PollingCallback
logger *zap.Logger
db *gorm.DB
redis *redis.Client
importTaskStore *postgres.IotCardImportTaskStore
iotCardStore *postgres.IotCardStore
assetWalletStore *postgres.AssetWalletStore
storageService *storage.Service
pollingCallback PollingCallback
logger *zap.Logger
}
func NewIotCardImportHandler(
@@ -55,18 +56,20 @@ func NewIotCardImportHandler(
redis *redis.Client,
importTaskStore *postgres.IotCardImportTaskStore,
iotCardStore *postgres.IotCardStore,
assetWalletStore *postgres.AssetWalletStore,
storageSvc *storage.Service,
pollingCallback PollingCallback,
logger *zap.Logger,
) *IotCardImportHandler {
return &IotCardImportHandler{
db: db,
redis: redis,
importTaskStore: importTaskStore,
iotCardStore: iotCardStore,
storageService: storageSvc,
pollingCallback: pollingCallback,
logger: logger,
db: db,
redis: redis,
importTaskStore: importTaskStore,
iotCardStore: iotCardStore,
assetWalletStore: assetWalletStore,
storageService: storageSvc,
pollingCallback: pollingCallback,
logger: logger,
}
}
@@ -369,7 +372,44 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot
result.successCount += len(finalCards)
// 为导入的卡批量创建资产钱包(钱包创建失败不影响卡导入结果,后续访问时 getOrCreateWallet 兜底)
h.batchCreateWallets(ctx, iotCards)
if h.pollingCallback != nil {
h.pollingCallback.OnBatchCardsCreated(ctx, iotCards)
}
}
// batchCreateWallets 批量为 IoT 卡创建资产钱包
func (h *IotCardImportHandler) batchCreateWallets(ctx context.Context, cards []*model.IotCard) {
if h.assetWalletStore == nil {
return
}
wallets := make([]*model.AssetWallet, 0, len(cards))
for _, card := range cards {
if card.ID == 0 {
continue
}
wallets = append(wallets, &model.AssetWallet{
ResourceType: constants.AssetWalletResourceTypeIotCard,
ResourceID: card.ID,
Balance: 0,
FrozenBalance: 0,
Currency: "CNY",
Status: constants.AssetWalletStatusNormal,
Version: 0,
})
}
if len(wallets) == 0 {
return
}
if err := h.db.WithContext(ctx).Create(&wallets).Error; err != nil {
h.logger.Warn("批量创建 IoT 卡资产钱包失败,后续访问时将自动创建",
zap.Int("count", len(wallets)),
zap.Error(err),
)
}
}