feat: 钱包系统分离 - 代理钱包与卡钱包完全隔离
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m17s

## 变更概述
将统一钱包系统拆分为代理钱包和卡钱包两个独立系统,实现数据表和代码层面的完全隔离。

## 数据库变更
- 新增 6 张表:tb_agent_wallet、tb_agent_wallet_transaction、tb_agent_recharge_record、tb_card_wallet、tb_card_wallet_transaction、tb_card_recharge_record
- 删除 3 张旧表:tb_wallet、tb_wallet_transaction、tb_recharge_record
- 代理钱包:按 (shop_id, wallet_type) 唯一标识,支持主钱包和分佣钱包
- 卡钱包:按 (resource_type, resource_id) 唯一标识,支持物联网卡和设备

## 代码变更
- Model 层:新增 AgentWallet、AgentWalletTransaction、AgentRechargeRecord、CardWallet、CardWalletTransaction、CardRechargeRecord 模型
- Store 层:新增 6 个独立 Store,支持事务、乐观锁、Redis 缓存
- Service 层:重构 commission_calculation、commission_withdrawal、order、recharge 等 8 个服务
- Bootstrap 层:更新 Store 和 Service 依赖注入
- 常量层:按钱包类型重新组织常量和 Redis Key 生成函数

## 技术特性
- 乐观锁:使用 version 字段防止并发冲突
- 多租户:支持 shop_id_tag 和 enterprise_id_tag 过滤
- 事务管理:所有余额变动使用事务保证 ACID
- 缓存策略:Cache-Aside 模式,余额变动后删除缓存

## 业务影响
- 代理钱包和卡钱包业务完全隔离,互不影响
- 为独立监控、优化、扩展打下基础
- 提升代理钱包的稳定性和独立性

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 09:51:00 +08:00
parent f32d32cd36
commit 18daeae65a
66 changed files with 4420 additions and 1090 deletions

View File

@@ -98,8 +98,8 @@ func initServices(s *stores, deps *Dependencies) *services {
PersonalCustomer: personalCustomerSvc.NewService(s.PersonalCustomer, s.PersonalCustomerPhone, deps.VerificationService, deps.JWTManager, deps.WechatOfficialAccount, deps.Logger),
Shop: shopSvc.New(s.Shop, s.Account, s.ShopRole, s.Role),
Auth: authSvc.New(s.Account, s.AccountRole, s.RolePermission, s.Permission, deps.TokenManager, deps.Logger),
ShopCommission: shopCommissionSvc.New(s.Shop, s.Account, s.Wallet, s.CommissionWithdrawalRequest, s.CommissionRecord),
CommissionWithdrawal: commissionWithdrawalSvc.New(deps.DB, s.Shop, s.Account, s.Wallet, s.WalletTransaction, s.CommissionWithdrawalRequest),
ShopCommission: shopCommissionSvc.New(s.Shop, s.Account, s.AgentWallet, s.CommissionWithdrawalRequest, s.CommissionRecord),
CommissionWithdrawal: commissionWithdrawalSvc.New(deps.DB, s.Shop, s.Account, s.AgentWallet, s.AgentWalletTransaction, s.CommissionWithdrawalRequest),
CommissionWithdrawalSetting: commissionWithdrawalSettingSvc.New(deps.DB, s.Account, s.CommissionWithdrawalSetting),
CommissionCalculation: commissionCalculationSvc.New(
deps.DB,
@@ -110,8 +110,8 @@ func initServices(s *stores, deps *Dependencies) *services {
s.PackageSeries,
s.IotCard,
s.Device,
s.Wallet,
s.WalletTransaction,
s.AgentWallet,
s.AgentWalletTransaction,
s.Order,
s.OrderItem,
s.Package,
@@ -123,7 +123,7 @@ func initServices(s *stores, deps *Dependencies) *services {
EnterpriseCard: enterpriseCardSvc.New(deps.DB, s.Enterprise, s.EnterpriseCardAuthorization),
EnterpriseDevice: enterpriseDeviceSvc.New(deps.DB, s.Enterprise, s.Device, s.DeviceSimBinding, s.EnterpriseDeviceAuthorization, s.EnterpriseCardAuthorization, deps.Logger),
Authorization: enterpriseCardSvc.NewAuthorizationService(s.Enterprise, s.IotCard, s.EnterpriseCardAuthorization, deps.Logger),
MyCommission: myCommissionSvc.New(deps.DB, s.Shop, s.Wallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.WalletTransaction),
MyCommission: myCommissionSvc.New(deps.DB, s.Shop, s.AgentWallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.AgentWalletTransaction),
IotCard: iotCard,
IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient),
Device: deviceSvc.New(deps.DB, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.PackageSeries),
@@ -140,8 +140,8 @@ func initServices(s *stores, deps *Dependencies) *services {
ShopPackageBatchPricing: shopPackageBatchPricingSvc.New(deps.DB, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop),
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
PurchaseValidation: purchaseValidation,
Order: orderSvc.New(deps.DB, deps.Redis, s.Order, s.OrderItem, s.Wallet, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, s.PackageUsage, s.Package, deps.WechatPayment, deps.QueueClient, deps.Logger),
Recharge: rechargeSvc.New(deps.DB, s.Recharge, s.Wallet, s.WalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, deps.Logger),
Order: orderSvc.New(deps.DB, deps.Redis, s.Order, s.OrderItem, s.AgentWallet, s.CardWallet, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, s.PackageUsage, s.Package, deps.WechatPayment, deps.QueueClient, deps.Logger),
Recharge: rechargeSvc.New(deps.DB, s.CardRecharge, s.CardWallet, s.CardWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, deps.Logger),
PollingConfig: pollingSvc.NewConfigService(s.PollingConfig),
PollingConcurrency: pollingSvc.NewConcurrencyService(s.PollingConcurrencyConfig, deps.Redis),
PollingMonitoring: pollingSvc.NewMonitoringService(deps.Redis),

View File

@@ -15,10 +15,8 @@ type stores struct {
RolePermission *postgres.RolePermissionStore
PersonalCustomer *postgres.PersonalCustomerStore
PersonalCustomerPhone *postgres.PersonalCustomerPhoneStore
Wallet *postgres.WalletStore
CommissionWithdrawalRequest *postgres.CommissionWithdrawalRequestStore
CommissionRecord *postgres.CommissionRecordStore
WalletTransaction *postgres.WalletTransactionStore
CommissionWithdrawalSetting *postgres.CommissionWithdrawalSettingStore
Enterprise *postgres.EnterpriseStore
EnterpriseCardAuthorization *postgres.EnterpriseCardAuthorizationStore
@@ -40,7 +38,6 @@ type stores struct {
ShopSeriesCommissionStats *postgres.ShopSeriesCommissionStatsStore
Order *postgres.OrderStore
OrderItem *postgres.OrderItemStore
Recharge *postgres.RechargeStore
PollingConfig *postgres.PollingConfigStore
PollingConcurrencyConfig *postgres.PollingConcurrencyConfigStore
PollingAlertRule *postgres.PollingAlertRuleStore
@@ -48,6 +45,14 @@ type stores struct {
DataCleanupConfig *postgres.DataCleanupConfigStore
DataCleanupLog *postgres.DataCleanupLogStore
PollingManualTriggerLog *postgres.PollingManualTriggerLogStore
// 代理钱包系统
AgentWallet *postgres.AgentWalletStore
AgentWalletTransaction *postgres.AgentWalletTransactionStore
AgentRecharge *postgres.AgentRechargeStore
// 卡钱包系统
CardWallet *postgres.CardWalletStore
CardWalletTransaction *postgres.CardWalletTransactionStore
CardRecharge *postgres.CardRechargeStore
}
func initStores(deps *Dependencies) *stores {
@@ -62,10 +67,8 @@ func initStores(deps *Dependencies) *stores {
RolePermission: postgres.NewRolePermissionStore(deps.DB, deps.Redis),
PersonalCustomer: postgres.NewPersonalCustomerStore(deps.DB, deps.Redis),
PersonalCustomerPhone: postgres.NewPersonalCustomerPhoneStore(deps.DB),
Wallet: postgres.NewWalletStore(deps.DB, deps.Redis),
CommissionWithdrawalRequest: postgres.NewCommissionWithdrawalRequestStore(deps.DB, deps.Redis),
CommissionRecord: postgres.NewCommissionRecordStore(deps.DB, deps.Redis),
WalletTransaction: postgres.NewWalletTransactionStore(deps.DB, deps.Redis),
CommissionWithdrawalSetting: postgres.NewCommissionWithdrawalSettingStore(deps.DB, deps.Redis),
Enterprise: postgres.NewEnterpriseStore(deps.DB, deps.Redis),
EnterpriseCardAuthorization: postgres.NewEnterpriseCardAuthorizationStore(deps.DB, deps.Redis),
@@ -87,7 +90,6 @@ func initStores(deps *Dependencies) *stores {
ShopSeriesCommissionStats: postgres.NewShopSeriesCommissionStatsStore(deps.DB),
Order: postgres.NewOrderStore(deps.DB, deps.Redis),
OrderItem: postgres.NewOrderItemStore(deps.DB, deps.Redis),
Recharge: postgres.NewRechargeStore(deps.DB, deps.Redis),
PollingConfig: postgres.NewPollingConfigStore(deps.DB),
PollingConcurrencyConfig: postgres.NewPollingConcurrencyConfigStore(deps.DB),
PollingAlertRule: postgres.NewPollingAlertRuleStore(deps.DB),
@@ -95,5 +97,13 @@ func initStores(deps *Dependencies) *stores {
DataCleanupConfig: postgres.NewDataCleanupConfigStore(deps.DB),
DataCleanupLog: postgres.NewDataCleanupLogStore(deps.DB),
PollingManualTriggerLog: postgres.NewPollingManualTriggerLogStore(deps.DB),
// 代理钱包系统
AgentWallet: postgres.NewAgentWalletStore(deps.DB, deps.Redis),
AgentWalletTransaction: postgres.NewAgentWalletTransactionStore(deps.DB, deps.Redis),
AgentRecharge: postgres.NewAgentRechargeStore(deps.DB, deps.Redis),
// 卡钱包系统
CardWallet: postgres.NewCardWalletStore(deps.DB, deps.Redis),
CardWalletTransaction: postgres.NewCardWalletTransactionStore(deps.DB, deps.Redis),
CardRecharge: postgres.NewCardRechargeStore(deps.DB, deps.Redis),
}
}

View File

@@ -30,8 +30,8 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q
stores.PackageSeries,
stores.IotCard,
stores.Device,
stores.Wallet,
stores.WalletTransaction,
stores.AgentWallet,
stores.AgentWalletTransaction,
stores.Order,
stores.OrderItem,
stores.Package,

View File

@@ -17,8 +17,6 @@ type workerStores struct {
Shop *postgres.ShopStore
ShopSeriesAllocation *postgres.ShopSeriesAllocationStore
PackageSeries *postgres.PackageSeriesStore
Wallet *postgres.WalletStore
WalletTransaction *postgres.WalletTransactionStore
Order *postgres.OrderStore
OrderItem *postgres.OrderItemStore
Package *postgres.PackageStore
@@ -28,6 +26,8 @@ type workerStores struct {
PollingAlertHistory *postgres.PollingAlertHistoryStore
DataCleanupConfig *postgres.DataCleanupConfigStore
DataCleanupLog *postgres.DataCleanupLogStore
AgentWallet *postgres.AgentWalletStore
AgentWalletTransaction *postgres.AgentWalletTransactionStore
}
func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
@@ -43,8 +43,6 @@ func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
Shop: postgres.NewShopStore(deps.DB, deps.Redis),
ShopSeriesAllocation: postgres.NewShopSeriesAllocationStore(deps.DB),
PackageSeries: postgres.NewPackageSeriesStore(deps.DB),
Wallet: postgres.NewWalletStore(deps.DB, deps.Redis),
WalletTransaction: postgres.NewWalletTransactionStore(deps.DB, deps.Redis),
Order: postgres.NewOrderStore(deps.DB, deps.Redis),
OrderItem: postgres.NewOrderItemStore(deps.DB, deps.Redis),
Package: postgres.NewPackageStore(deps.DB),
@@ -54,6 +52,8 @@ func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
PollingAlertHistory: postgres.NewPollingAlertHistoryStore(deps.DB),
DataCleanupConfig: postgres.NewDataCleanupConfigStore(deps.DB),
DataCleanupLog: postgres.NewDataCleanupLogStore(deps.DB),
AgentWallet: postgres.NewAgentWalletStore(deps.DB, deps.Redis),
AgentWalletTransaction: postgres.NewAgentWalletTransactionStore(deps.DB, deps.Redis),
}
return &queue.WorkerStores{
@@ -68,8 +68,6 @@ func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
Shop: stores.Shop,
ShopSeriesAllocation: stores.ShopSeriesAllocation,
PackageSeries: stores.PackageSeries,
Wallet: stores.Wallet,
WalletTransaction: stores.WalletTransaction,
Order: stores.Order,
OrderItem: stores.OrderItem,
Package: stores.Package,
@@ -79,5 +77,7 @@ func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
PollingAlertHistory: stores.PollingAlertHistory,
DataCleanupConfig: stores.DataCleanupConfig,
DataCleanupLog: stores.DataCleanupLog,
AgentWallet: stores.AgentWallet,
AgentWalletTransaction: stores.AgentWalletTransaction,
}
}

View File

@@ -3,11 +3,11 @@ package admin
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/polling"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/response"
)

View File

@@ -0,0 +1,91 @@
package model
import (
"time"
"gorm.io/gorm"
)
// AgentWallet 代理钱包模型
// 管理店铺级别的主钱包和分佣钱包
type AgentWallet struct {
ID uint `gorm:"column:id;primaryKey" json:"id"`
ShopID uint `gorm:"column:shop_id;not null;index;comment:店铺ID" json:"shop_id"`
WalletType string `gorm:"column:wallet_type;type:varchar(20);not null;comment:钱包类型(main-主钱包 | commission-分佣钱包)" json:"wallet_type"`
Balance int64 `gorm:"column:balance;type:bigint;not null;default:0;comment:余额(单位:分)" json:"balance"`
FrozenBalance int64 `gorm:"column:frozen_balance;type:bigint;not null;default:0;comment:冻结余额(单位:分)" json:"frozen_balance"`
Currency string `gorm:"column:currency;type:varchar(10);not null;default:'CNY';comment:币种" json:"currency"`
Status int `gorm:"column:status;type:int;not null;default:1;comment:钱包状态(1-正常 2-冻结 3-关闭)" json:"status"`
Version int `gorm:"column:version;type:int;not null;default:0;comment:版本号(乐观锁)" json:"version"`
ShopIDTag uint `gorm:"column:shop_id_tag;not null;index;comment:店铺ID标签(多租户过滤)" json:"shop_id_tag"`
EnterpriseIDTag *uint `gorm:"column:enterprise_id_tag;index;comment:企业ID标签(多租户过滤)" json:"enterprise_id_tag,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
}
// TableName 指定表名
func (AgentWallet) TableName() string {
return "tb_agent_wallet"
}
// GetAvailableBalance 获取可用余额 = balance - frozen_balance
func (w *AgentWallet) GetAvailableBalance() int64 {
return w.Balance - w.FrozenBalance
}
// AgentWalletTransaction 代理钱包交易记录模型
// 记录所有代理钱包余额变动
type AgentWalletTransaction struct {
ID uint `gorm:"column:id;primaryKey" json:"id"`
AgentWalletID uint `gorm:"column:agent_wallet_id;not null;index;comment:代理钱包ID" json:"agent_wallet_id"`
ShopID uint `gorm:"column:shop_id;not null;index;comment:店铺ID(冗余字段,便于查询)" json:"shop_id"`
UserID uint `gorm:"column:user_id;not null;comment:操作人用户ID" json:"user_id"`
TransactionType string `gorm:"column:transaction_type;type:varchar(20);not null;comment:交易类型(recharge-充值 | deduct-扣款 | refund-退款 | commission-分佣 | withdrawal-提现)" json:"transaction_type"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:变动金额(单位:分,正数为增加,负数为减少)" json:"amount"`
BalanceBefore int64 `gorm:"column:balance_before;type:bigint;not null;comment:变动前余额(单位:分)" json:"balance_before"`
BalanceAfter int64 `gorm:"column:balance_after;type:bigint;not null;comment:变动后余额(单位:分)" json:"balance_after"`
Status int `gorm:"column:status;type:int;not null;default:1;comment:交易状态(1-成功 2-失败 3-处理中)" json:"status"`
ReferenceType *string `gorm:"column:reference_type;type:varchar(50);comment:关联业务类型(order | commission | withdrawal | topup)" json:"reference_type,omitempty"`
ReferenceID *uint `gorm:"column:reference_id;comment:关联业务ID" json:"reference_id,omitempty"`
Remark *string `gorm:"column:remark;type:text;comment:备注" json:"remark,omitempty"`
Metadata *string `gorm:"column:metadata;type:jsonb;comment:扩展信息(如手续费、支付方式等)" json:"metadata,omitempty"`
Creator uint `gorm:"column:creator;not null;comment:创建人ID" json:"creator"`
ShopIDTag uint `gorm:"column:shop_id_tag;not null;index;comment:店铺ID标签(多租户过滤)" json:"shop_id_tag"`
EnterpriseIDTag *uint `gorm:"column:enterprise_id_tag;index;comment:企业ID标签(多租户过滤)" json:"enterprise_id_tag,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
}
// TableName 指定表名
func (AgentWalletTransaction) TableName() string {
return "tb_agent_wallet_transaction"
}
// AgentRechargeRecord 代理充值记录模型
// 记录所有代理充值操作
type AgentRechargeRecord struct {
ID uint `gorm:"column:id;primaryKey" json:"id"`
UserID uint `gorm:"column:user_id;not null;index;comment:操作人用户ID" json:"user_id"`
AgentWalletID uint `gorm:"column:agent_wallet_id;not null;comment:代理钱包ID" json:"agent_wallet_id"`
ShopID uint `gorm:"column:shop_id;not null;index;comment:店铺ID(冗余字段,便于查询)" json:"shop_id"`
RechargeNo string `gorm:"column:recharge_no;type:varchar(50);not null;uniqueIndex;comment:充值订单号(格式:ARCH+时间戳+随机数)" json:"recharge_no"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:充值金额(单位:分,最小10000分=100元)" json:"amount"`
PaymentMethod string `gorm:"column:payment_method;type:varchar(20);not null;comment:支付方式(alipay-支付宝 | wechat-微信 | bank-银行转账 | offline-线下)" json:"payment_method"`
PaymentChannel *string `gorm:"column:payment_channel;type:varchar(50);comment:支付渠道" json:"payment_channel,omitempty"`
PaymentTransactionID *string `gorm:"column:payment_transaction_id;type:varchar(100);comment:第三方支付交易号" json:"payment_transaction_id,omitempty"`
Status int `gorm:"column:status;type:int;not null;default:1;comment:充值状态(1-待支付 2-已支付 3-已完成 4-已关闭 5-已退款)" json:"status"`
PaidAt *time.Time `gorm:"column:paid_at;comment:支付时间" json:"paid_at,omitempty"`
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at,omitempty"`
ShopIDTag uint `gorm:"column:shop_id_tag;not null;index;comment:店铺ID标签(多租户过滤)" json:"shop_id_tag"`
EnterpriseIDTag *uint `gorm:"column:enterprise_id_tag;index;comment:企业ID标签(多租户过滤)" json:"enterprise_id_tag,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
}
// TableName 指定表名
func (AgentRechargeRecord) TableName() string {
return "tb_agent_recharge_record"
}

View File

@@ -0,0 +1,93 @@
package model
import (
"time"
"gorm.io/gorm"
)
// CardWallet 卡钱包模型
// 管理物联网卡和设备级别的钱包
type CardWallet struct {
ID uint `gorm:"column:id;primaryKey" json:"id"`
ResourceType string `gorm:"column:resource_type;type:varchar(20);not null;index;comment:资源类型(iot_card-物联网卡 | device-设备)" json:"resource_type"`
ResourceID uint `gorm:"column:resource_id;not null;index;comment:资源ID(关联tb_iot_card.id或tb_device.id)" json:"resource_id"`
Balance int64 `gorm:"column:balance;type:bigint;not null;default:0;comment:余额(单位:分)" json:"balance"`
FrozenBalance int64 `gorm:"column:frozen_balance;type:bigint;not null;default:0;comment:冻结余额(单位:分)" json:"frozen_balance"`
Currency string `gorm:"column:currency;type:varchar(10);not null;default:'CNY';comment:币种" json:"currency"`
Status int `gorm:"column:status;type:int;not null;default:1;comment:钱包状态(1-正常 2-冻结 3-关闭)" json:"status"`
Version int `gorm:"column:version;type:int;not null;default:0;comment:版本号(乐观锁)" json:"version"`
ShopIDTag uint `gorm:"column:shop_id_tag;not null;index;comment:店铺ID标签(多租户过滤)" json:"shop_id_tag"`
EnterpriseIDTag *uint `gorm:"column:enterprise_id_tag;index;comment:企业ID标签(多租户过滤)" json:"enterprise_id_tag,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
}
// TableName 指定表名
func (CardWallet) TableName() string {
return "tb_card_wallet"
}
// GetAvailableBalance 获取可用余额 = balance - frozen_balance
func (w *CardWallet) GetAvailableBalance() int64 {
return w.Balance - w.FrozenBalance
}
// CardWalletTransaction 卡钱包交易记录模型
// 记录所有卡钱包余额变动
type CardWalletTransaction struct {
ID uint `gorm:"column:id;primaryKey" json:"id"`
CardWalletID uint `gorm:"column:card_wallet_id;not null;index;comment:卡钱包ID" json:"card_wallet_id"`
ResourceType string `gorm:"column:resource_type;type:varchar(20);not null;index;comment:资源类型(冗余字段,便于查询)" json:"resource_type"`
ResourceID uint `gorm:"column:resource_id;not null;index;comment:资源ID(冗余字段,便于查询)" json:"resource_id"`
UserID uint `gorm:"column:user_id;not null;comment:操作人用户ID" json:"user_id"`
TransactionType string `gorm:"column:transaction_type;type:varchar(20);not null;comment:交易类型(recharge-充值 | deduct-扣款 | refund-退款)" json:"transaction_type"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:变动金额(单位:分,正数为增加,负数为减少)" json:"amount"`
BalanceBefore int64 `gorm:"column:balance_before;type:bigint;not null;comment:变动前余额(单位:分)" json:"balance_before"`
BalanceAfter int64 `gorm:"column:balance_after;type:bigint;not null;comment:变动后余额(单位:分)" json:"balance_after"`
Status int `gorm:"column:status;type:int;not null;default:1;comment:交易状态(1-成功 2-失败 3-处理中)" json:"status"`
ReferenceType *string `gorm:"column:reference_type;type:varchar(50);comment:关联业务类型(order | topup)" json:"reference_type,omitempty"`
ReferenceID *uint `gorm:"column:reference_id;comment:关联业务ID" json:"reference_id,omitempty"`
Remark *string `gorm:"column:remark;type:text;comment:备注" json:"remark,omitempty"`
Metadata *string `gorm:"column:metadata;type:jsonb;comment:扩展信息(如套餐信息、支付方式等)" json:"metadata,omitempty"`
Creator uint `gorm:"column:creator;not null;comment:创建人ID" json:"creator"`
ShopIDTag uint `gorm:"column:shop_id_tag;not null;index;comment:店铺ID标签(多租户过滤)" json:"shop_id_tag"`
EnterpriseIDTag *uint `gorm:"column:enterprise_id_tag;index;comment:企业ID标签(多租户过滤)" json:"enterprise_id_tag,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
}
// TableName 指定表名
func (CardWalletTransaction) TableName() string {
return "tb_card_wallet_transaction"
}
// CardRechargeRecord 卡充值记录模型
// 记录所有卡充值操作
type CardRechargeRecord struct {
ID uint `gorm:"column:id;primaryKey" json:"id"`
UserID uint `gorm:"column:user_id;not null;index;comment:操作人用户ID" json:"user_id"`
CardWalletID uint `gorm:"column:card_wallet_id;not null;comment:卡钱包ID" json:"card_wallet_id"`
ResourceType string `gorm:"column:resource_type;type:varchar(20);not null;index;comment:资源类型(冗余字段)" json:"resource_type"`
ResourceID uint `gorm:"column:resource_id;not null;index;comment:资源ID(冗余字段)" json:"resource_id"`
RechargeNo string `gorm:"column:recharge_no;type:varchar(50);not null;uniqueIndex;comment:充值订单号(格式:CRCH+时间戳+随机数)" json:"recharge_no"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:充值金额(单位:分,最小100分=1元)" json:"amount"`
PaymentMethod string `gorm:"column:payment_method;type:varchar(20);not null;comment:支付方式(alipay-支付宝 | wechat-微信)" json:"payment_method"`
PaymentChannel *string `gorm:"column:payment_channel;type:varchar(50);comment:支付渠道" json:"payment_channel,omitempty"`
PaymentTransactionID *string `gorm:"column:payment_transaction_id;type:varchar(100);comment:第三方支付交易号" json:"payment_transaction_id,omitempty"`
Status int `gorm:"column:status;type:int;not null;default:1;comment:充值状态(1-待支付 2-已支付 3-已完成 4-已关闭 5-已退款)" json:"status"`
PaidAt *time.Time `gorm:"column:paid_at;comment:支付时间" json:"paid_at,omitempty"`
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at,omitempty"`
ShopIDTag uint `gorm:"column:shop_id_tag;not null;index;comment:店铺ID标签(多租户过滤)" json:"shop_id_tag"`
EnterpriseIDTag *uint `gorm:"column:enterprise_id_tag;index;comment:企业ID标签(多租户过滤)" json:"enterprise_id_tag,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
}
// TableName 指定表名
func (CardRechargeRecord) TableName() string {
return "tb_card_recharge_record"
}

View File

@@ -20,24 +20,24 @@ type ListStandaloneIotCardRequest struct {
}
type StandaloneIotCardResponse struct {
ID uint `json:"id" description:"卡ID"`
ICCID string `json:"iccid" description:"ICCID"`
CardCategory string `json:"card_category" description:"卡业务类型 (normal:普通卡, industry:行业卡)"`
CarrierID uint `json:"carrier_id" description:"运营商ID"`
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
IMSI string `json:"imsi,omitempty" description:"IMSI"`
MSISDN string `json:"msisdn,omitempty" description:"卡接入号"`
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
Supplier string `json:"supplier,omitempty" description:"供应商"`
CostPrice int64 `json:"cost_price" description:"成本价(分)"`
DistributePrice int64 `json:"distribute_price" description:"分销价(分)"`
Status int `json:"status" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
ShopID *uint `json:"shop_id,omitempty" description:"店铺ID"`
ShopName string `json:"shop_name,omitempty" description:"店铺名称"`
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
ActivationStatus int `json:"activation_status" description:"激活状态 (0:未激活, 1:已激活)"`
RealNameStatus int `json:"real_name_status" description:"实名状态 (0:未实名, 1:已实名)"`
ID uint `json:"id" description:"卡ID"`
ICCID string `json:"iccid" description:"ICCID"`
CardCategory string `json:"card_category" description:"卡业务类型 (normal:普通卡, industry:行业卡)"`
CarrierID uint `json:"carrier_id" description:"运营商ID"`
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
IMSI string `json:"imsi,omitempty" description:"IMSI"`
MSISDN string `json:"msisdn,omitempty" description:"卡接入号"`
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
Supplier string `json:"supplier,omitempty" description:"供应商"`
CostPrice int64 `json:"cost_price" description:"成本价(分)"`
DistributePrice int64 `json:"distribute_price" description:"分销价(分)"`
Status int `json:"status" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
ShopID *uint `json:"shop_id,omitempty" description:"店铺ID"`
ShopName string `json:"shop_name,omitempty" description:"店铺名称"`
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
ActivationStatus int `json:"activation_status" description:"激活状态 (0:未激活, 1:已激活)"`
RealNameStatus int `json:"real_name_status" description:"实名状态 (0:未实名, 1:已实名)"`
NetworkStatus int `json:"network_status" description:"网络状态 (0:停机, 1:开机)"`
DataUsageMB int64 `json:"data_usage_mb" description:"累计流量使用(MB)"`
CurrentMonthUsageMB float64 `json:"current_month_usage_mb" description:"本月已用流量(MB)"`
@@ -47,11 +47,11 @@ type StandaloneIotCardResponse struct {
LastRealNameCheckAt *time.Time `json:"last_real_name_check_at,omitempty" description:"最后实名检查时间"`
EnablePolling bool `json:"enable_polling" description:"是否参与轮询"`
SeriesID *uint `json:"series_id,omitempty" description:"套餐系列ID"`
SeriesName string `json:"series_name,omitempty" description:"套餐系列名称"`
FirstCommissionPaid bool `json:"first_commission_paid" description:"一次性佣金是否已发放"`
AccumulatedRecharge int64 `json:"accumulated_recharge" description:"累计充值金额(分)"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
SeriesName string `json:"series_name,omitempty" description:"套餐系列名称"`
FirstCommissionPaid bool `json:"first_commission_paid" description:"一次性佣金是否已发放"`
AccumulatedRecharge int64 `json:"accumulated_recharge" description:"累计充值金额(分)"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
}
type ListStandaloneIotCardResponse struct {

View File

@@ -26,15 +26,15 @@ type UpdateDataCleanupConfigParams struct {
// DataCleanupConfigResp 数据清理配置响应
type DataCleanupConfigResp struct {
ID uint `json:"id" description:"配置ID"`
TargetTable string `json:"table_name" description:"表名"`
RetentionDays int `json:"retention_days" description:"保留天数"`
BatchSize int `json:"batch_size" description:"每批删除条数"`
Enabled int `json:"enabled" description:"是否启用0-禁用1-启用"`
Description string `json:"description" description:"配置说明"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
UpdatedBy *uint `json:"updated_by,omitempty" description:"更新人ID"`
ID uint `json:"id" description:"配置ID"`
TargetTable string `json:"table_name" description:"表名"`
RetentionDays int `json:"retention_days" description:"保留天数"`
BatchSize int `json:"batch_size" description:"每批删除条数"`
Enabled int `json:"enabled" description:"是否启用0-禁用1-启用"`
Description string `json:"description" description:"配置说明"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
UpdatedBy *uint `json:"updated_by,omitempty" description:"更新人ID"`
}
// DataCleanupConfigListResp 数据清理配置列表响应

View File

@@ -57,27 +57,27 @@ func (Package) TableName() string {
// 跟踪单卡套餐和设备级套餐的流量使用
type PackageUsage struct {
gorm.Model
BaseModel `gorm:"embedded"`
OrderID uint `gorm:"column:order_id;index;not null;comment:订单ID" json:"order_id"`
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
UsageType string `gorm:"column:usage_type;type:varchar(20);not null;comment:使用类型 single_card-单卡套餐 device-设备级套餐" json:"usage_type"`
IotCardID uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡套餐时有值)" json:"iot_card_id"`
DeviceID uint `gorm:"column:device_id;index;comment:设备ID(设备级套餐时有值)" json:"device_id"`
DataLimitMB int64 `gorm:"column:data_limit_mb;type:bigint;not null;comment:流量限额(MB)" json:"data_limit_mb"`
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:已使用流量(MB)" json:"data_usage_mb"`
RealDataUsageMB int64 `gorm:"column:real_data_usage_mb;type:bigint;default:0;comment:真流量使用(MB)" json:"real_data_usage_mb"`
VirtualDataUsageMB int64 `gorm:"column:virtual_data_usage_mb;type:bigint;default:0;comment:虚流量使用(MB)" json:"virtual_data_usage_mb"`
ActivatedAt time.Time `gorm:"column:activated_at;not null;comment:套餐生效时间" json:"activated_at"`
ExpiresAt time.Time `gorm:"column:expires_at;not null;comment:套餐过期时间" json:"expires_at"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 0-待生效 1-生效中 2-已用完 3-已过期 4-已失效" json:"status"`
LastPackageCheckAt *time.Time `gorm:"column:last_package_check_at;comment:最后一次套餐流量检查时间" json:"last_package_check_at"`
Priority int `gorm:"column:priority;type:int;default:1;index:idx_package_usage_priority;comment:优先级(主套餐和加油包按此字段排队,数字越小优先级越高)" json:"priority"`
MasterUsageID *uint `gorm:"column:master_usage_id;type:bigint;index:idx_package_usage_master_usage_id;comment:主套餐使用记录ID(加油包关联主套餐,主套餐此字段为NULL)" json:"master_usage_id"`
HasIndependentExpiry bool `gorm:"column:has_independent_expiry;type:boolean;default:false;comment:加油包是否有独立有效期(true-有独立到期时间 false-跟随主套餐)" json:"has_independent_expiry"`
PendingRealnameActivation bool `gorm:"column:pending_realname_activation;type:boolean;default:false;comment:是否等待实名激活(true-待实名后激活 false-已激活或不需实名)" json:"pending_realname_activation"`
DataResetCycle string `gorm:"column:data_reset_cycle;type:varchar(20);comment:流量重置周期(从Package复制,用于历史记录)" json:"data_reset_cycle"`
LastResetAt *time.Time `gorm:"column:last_reset_at;comment:最后一次流量重置时间" json:"last_reset_at"`
NextResetAt *time.Time `gorm:"column:next_reset_at;index:idx_package_usage_next_reset_at;comment:下次流量重置时间(用于定时任务查询)" json:"next_reset_at"`
BaseModel `gorm:"embedded"`
OrderID uint `gorm:"column:order_id;index;not null;comment:订单ID" json:"order_id"`
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
UsageType string `gorm:"column:usage_type;type:varchar(20);not null;comment:使用类型 single_card-单卡套餐 device-设备级套餐" json:"usage_type"`
IotCardID uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡套餐时有值)" json:"iot_card_id"`
DeviceID uint `gorm:"column:device_id;index;comment:设备ID(设备级套餐时有值)" json:"device_id"`
DataLimitMB int64 `gorm:"column:data_limit_mb;type:bigint;not null;comment:流量限额(MB)" json:"data_limit_mb"`
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:已使用流量(MB)" json:"data_usage_mb"`
RealDataUsageMB int64 `gorm:"column:real_data_usage_mb;type:bigint;default:0;comment:真流量使用(MB)" json:"real_data_usage_mb"`
VirtualDataUsageMB int64 `gorm:"column:virtual_data_usage_mb;type:bigint;default:0;comment:虚流量使用(MB)" json:"virtual_data_usage_mb"`
ActivatedAt time.Time `gorm:"column:activated_at;not null;comment:套餐生效时间" json:"activated_at"`
ExpiresAt time.Time `gorm:"column:expires_at;not null;comment:套餐过期时间" json:"expires_at"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 0-待生效 1-生效中 2-已用完 3-已过期 4-已失效" json:"status"`
LastPackageCheckAt *time.Time `gorm:"column:last_package_check_at;comment:最后一次套餐流量检查时间" json:"last_package_check_at"`
Priority int `gorm:"column:priority;type:int;default:1;index:idx_package_usage_priority;comment:优先级(主套餐和加油包按此字段排队,数字越小优先级越高)" json:"priority"`
MasterUsageID *uint `gorm:"column:master_usage_id;type:bigint;index:idx_package_usage_master_usage_id;comment:主套餐使用记录ID(加油包关联主套餐,主套餐此字段为NULL)" json:"master_usage_id"`
HasIndependentExpiry bool `gorm:"column:has_independent_expiry;type:boolean;default:false;comment:加油包是否有独立有效期(true-有独立到期时间 false-跟随主套餐)" json:"has_independent_expiry"`
PendingRealnameActivation bool `gorm:"column:pending_realname_activation;type:boolean;default:false;comment:是否等待实名激活(true-待实名后激活 false-已激活或不需实名)" json:"pending_realname_activation"`
DataResetCycle string `gorm:"column:data_reset_cycle;type:varchar(20);comment:流量重置周期(从Package复制,用于历史记录)" json:"data_reset_cycle"`
LastResetAt *time.Time `gorm:"column:last_reset_at;comment:最后一次流量重置时间" json:"last_reset_at"`
NextResetAt *time.Time `gorm:"column:next_reset_at;index:idx_package_usage_next_reset_at;comment:下次流量重置时间(用于定时任务查询)" json:"next_reset_at"`
}
// TableName 指定表名
@@ -88,13 +88,13 @@ func (PackageUsage) TableName() string {
// PackageUsageDailyRecord 套餐流量日记录模型
// 记录每个套餐每天的流量使用情况,用于流量详单查询
type PackageUsageDailyRecord struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
PackageUsageID uint `gorm:"column:package_usage_id;not null;uniqueIndex:idx_package_usage_daily_record_unique;index:idx_package_usage_daily_record_date;comment:套餐使用记录ID" json:"package_usage_id"`
Date time.Time `gorm:"column:date;type:date;not null;uniqueIndex:idx_package_usage_daily_record_unique;comment:日期" json:"date"`
DailyUsageMB int `gorm:"column:daily_usage_mb;type:int;default:0;comment:当日流量使用量(MB)" json:"daily_usage_mb"`
CumulativeUsageMB int64 `gorm:"column:cumulative_usage_mb;type:bigint;default:0;comment:截止当日的累计流量(MB)" json:"cumulative_usage_mb"`
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"`
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
PackageUsageID uint `gorm:"column:package_usage_id;not null;uniqueIndex:idx_package_usage_daily_record_unique;index:idx_package_usage_daily_record_date;comment:套餐使用记录ID" json:"package_usage_id"`
Date time.Time `gorm:"column:date;type:date;not null;uniqueIndex:idx_package_usage_daily_record_unique;comment:日期" json:"date"`
DailyUsageMB int `gorm:"column:daily_usage_mb;type:int;default:0;comment:当日流量使用量(MB)" json:"daily_usage_mb"`
CumulativeUsageMB int64 `gorm:"column:cumulative_usage_mb;type:bigint;default:0;comment:截止当日的累计流量(MB)" json:"cumulative_usage_mb"`
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"`
}
// TableName 指定表名

View File

@@ -6,21 +6,21 @@ import (
// PollingConfig 轮询配置表
type PollingConfig struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
ConfigName string `gorm:"column:config_name;type:varchar(100);not null;comment:配置名称" json:"config_name"`
CardCondition string `gorm:"column:card_condition;type:varchar(50);comment:卡状态条件not_real_name/real_name/activated/suspended" json:"card_condition"`
CardCategory string `gorm:"column:card_category;type:varchar(50);comment:卡业务类型normal/industry" json:"card_category"`
CarrierID *uint `gorm:"column:carrier_id;comment:运营商ID可选精确匹配" json:"carrier_id"`
Priority int `gorm:"column:priority;not null;default:100;comment:优先级(数字越小优先级越高)" json:"priority"`
RealnameCheckInterval *int `gorm:"column:realname_check_interval;comment:实名检查间隔NULL表示不检查" json:"realname_check_interval"`
CarddataCheckInterval *int `gorm:"column:carddata_check_interval;comment:流量检查间隔NULL表示不检查" json:"carddata_check_interval"`
PackageCheckInterval *int `gorm:"column:package_check_interval;comment:套餐检查间隔NULL表示不检查" json:"package_check_interval"`
Status int16 `gorm:"column:status;type:smallint;not null;default:1;comment:状态0-禁用1-启用" json:"status"`
Description string `gorm:"column:description;type:text;comment:配置说明" json:"description"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"`
CreatedBy *uint `gorm:"column:created_by;comment:创建人ID" json:"created_by"`
UpdatedBy *uint `gorm:"column:updated_by;comment:更新人ID" json:"updated_by"`
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
ConfigName string `gorm:"column:config_name;type:varchar(100);not null;comment:配置名称" json:"config_name"`
CardCondition string `gorm:"column:card_condition;type:varchar(50);comment:卡状态条件not_real_name/real_name/activated/suspended" json:"card_condition"`
CardCategory string `gorm:"column:card_category;type:varchar(50);comment:卡业务类型normal/industry" json:"card_category"`
CarrierID *uint `gorm:"column:carrier_id;comment:运营商ID可选精确匹配" json:"carrier_id"`
Priority int `gorm:"column:priority;not null;default:100;comment:优先级(数字越小优先级越高)" json:"priority"`
RealnameCheckInterval *int `gorm:"column:realname_check_interval;comment:实名检查间隔NULL表示不检查" json:"realname_check_interval"`
CarddataCheckInterval *int `gorm:"column:carddata_check_interval;comment:流量检查间隔NULL表示不检查" json:"carddata_check_interval"`
PackageCheckInterval *int `gorm:"column:package_check_interval;comment:套餐检查间隔NULL表示不检查" json:"package_check_interval"`
Status int16 `gorm:"column:status;type:smallint;not null;default:1;comment:状态0-禁用1-启用" json:"status"`
Description string `gorm:"column:description;type:text;comment:配置说明" json:"description"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"`
CreatedBy *uint `gorm:"column:created_by;comment:创建人ID" json:"created_by"`
UpdatedBy *uint `gorm:"column:updated_by;comment:更新人ID" json:"updated_by"`
}
// TableName 指定表名

View File

@@ -1,99 +0,0 @@
package model
import (
"database/sql/driver"
"encoding/json"
"time"
"gorm.io/gorm"
)
// Wallet 钱包模型
// 个人客户和代理商的资金账户,支持充值、消费、提现等操作
// 使用乐观锁version字段防止并发余额冲突
// 钱包归属资源(卡/设备/店铺),支持资源转手场景
type Wallet struct {
gorm.Model
BaseModel `gorm:"embedded"`
ResourceType string `gorm:"column:resource_type;type:varchar(20);not null;uniqueIndex:idx_wallet_resource_type_currency,priority:1;index:idx_wallet_resource,priority:1;comment:资源类型 iot_card-物联网卡 device-设备 shop-店铺" json:"resource_type"`
ResourceID uint `gorm:"column:resource_id;not null;uniqueIndex:idx_wallet_resource_type_currency,priority:2;index:idx_wallet_resource,priority:2;comment:资源ID" json:"resource_id"`
WalletType string `gorm:"column:wallet_type;type:varchar(20);not null;uniqueIndex:idx_wallet_resource_type_currency,priority:3;comment:钱包类型 main-主钱包 commission-分佣钱包" json:"wallet_type"`
Balance int64 `gorm:"column:balance;type:bigint;not null;default:0;comment:余额(分)" json:"balance"`
FrozenBalance int64 `gorm:"column:frozen_balance;type:bigint;not null;default:0;comment:冻结余额(分)" json:"frozen_balance"`
Currency string `gorm:"column:currency;type:varchar(10);not null;default:'CNY';uniqueIndex:idx_wallet_resource_type_currency,priority:4;comment:币种" json:"currency"`
Status int `gorm:"column:status;type:int;not null;default:1;index:idx_wallet_status;comment:钱包状态 1-正常 2-冻结 3-关闭" json:"status"`
Version int `gorm:"column:version;type:int;not null;default:0;comment:版本号(乐观锁)" json:"version"`
}
// TableName 指定表名
func (Wallet) TableName() string {
return "tb_wallet"
}
// WalletMetadata 钱包交易扩展信息
// 用于存储交易相关的额外数据JSONB格式
type WalletMetadata map[string]interface{}
// Value 实现 driver.Valuer 接口
func (m WalletMetadata) Value() (driver.Value, error) {
return json.Marshal(m)
}
// Scan 实现 sql.Scanner 接口
func (m *WalletMetadata) Scan(value interface{}) error {
if value == nil {
*m = make(WalletMetadata)
return nil
}
bytes, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(bytes, m)
}
// WalletTransaction 钱包交易记录模型
// 记录所有钱包余额变动,包含变动前后余额用于对账
// 支持关联业务对象(订单、分佣、提现等)
type WalletTransaction struct {
gorm.Model
WalletID uint `gorm:"column:wallet_id;not null;index:idx_wallet_tx_wallet;comment:钱包ID" json:"wallet_id"`
UserID uint `gorm:"column:user_id;not null;index:idx_wallet_tx_user;comment:用户ID" json:"user_id"`
TransactionType string `gorm:"column:transaction_type;type:varchar(20);not null;comment:交易类型 recharge-充值 deduct-扣款 refund-退款 commission-分佣 withdrawal-提现" json:"transaction_type"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:变动金额(分)正数为增加 负数为减少" json:"amount"`
BalanceBefore int64 `gorm:"column:balance_before;type:bigint;not null;comment:变动前余额(分)" json:"balance_before"`
BalanceAfter int64 `gorm:"column:balance_after;type:bigint;not null;comment:变动后余额(分)" json:"balance_after"`
Status int `gorm:"column:status;type:int;not null;default:1;comment:交易状态 1-成功 2-失败 3-处理中" json:"status"`
ReferenceType *string `gorm:"column:reference_type;type:varchar(50);index:idx_wallet_tx_ref,priority:1;comment:关联业务类型 order/commission/withdrawal/topup" json:"reference_type,omitempty"`
ReferenceID *uint `gorm:"column:reference_id;type:bigint;index:idx_wallet_tx_ref,priority:2;comment:关联业务ID" json:"reference_id,omitempty"`
Remark *string `gorm:"column:remark;type:text;comment:备注" json:"remark,omitempty"`
Metadata WalletMetadata `gorm:"column:metadata;type:jsonb;comment:扩展信息" json:"metadata,omitempty"`
Creator uint `gorm:"column:creator;comment:创建人ID" json:"creator"`
}
// TableName 指定表名
func (WalletTransaction) TableName() string {
return "tb_wallet_transaction"
}
// RechargeRecord 充值记录模型
// 用户和代理的钱包充值订单,记录支付流程和状态
type RechargeRecord struct {
gorm.Model
BaseModel `gorm:"embedded"`
UserID uint `gorm:"column:user_id;not null;index:idx_recharge_user;comment:用户ID" json:"user_id"`
WalletID uint `gorm:"column:wallet_id;not null;comment:钱包ID" json:"wallet_id"`
RechargeNo string `gorm:"column:recharge_no;type:varchar(50);not null;uniqueIndex:idx_recharge_no;comment:充值订单号" json:"recharge_no"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:充值金额(分)" json:"amount"`
PaymentMethod string `gorm:"column:payment_method;type:varchar(20);not null;comment:支付方式 alipay-支付宝 wechat-微信 bank-银行转账 offline-线下" json:"payment_method"`
PaymentChannel *string `gorm:"column:payment_channel;type:varchar(50);comment:支付渠道" json:"payment_channel,omitempty"`
PaymentTransactionID *string `gorm:"column:payment_transaction_id;type:varchar(100);comment:第三方支付交易号" json:"payment_transaction_id,omitempty"`
Status int `gorm:"column:status;type:int;not null;default:1;index:idx_recharge_status;comment:充值状态 1-待支付 2-已支付 3-已完成 4-已关闭 5-已退款" json:"status"`
PaidAt *time.Time `gorm:"column:paid_at;comment:支付时间" json:"paid_at,omitempty"`
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at,omitempty"`
}
// TableName 指定表名
func (RechargeRecord) TableName() string {
return "tb_recharge_record"
}

View File

@@ -19,18 +19,18 @@ import (
// PackageActivationHandler 套餐激活检查处理器
// 任务 19: 处理主套餐过期、加油包级联失效、待生效主套餐激活
type PackageActivationHandler struct {
db *gorm.DB
redis *redis.Client
queueClient *asynq.Client
packageUsageStore *postgres.PackageUsageStore
activationService *packagepkg.ActivationService
logger *zap.Logger
db *gorm.DB
redis *redis.Client
queueClient *asynq.Client
packageUsageStore *postgres.PackageUsageStore
activationService *packagepkg.ActivationService
logger *zap.Logger
}
// PackageActivationPayload 套餐激活任务载荷
type PackageActivationPayload struct {
PackageUsageID uint `json:"package_usage_id"`
CarrierType string `json:"carrier_type"` // "iot_card" 或 "device"
CarrierType string `json:"carrier_type"` // "iot_card" 或 "device"
CarrierID uint `json:"carrier_id"`
ActivationType string `json:"activation_type"` // "queue" 或 "realname"
Timestamp int64 `json:"timestamp"`
@@ -102,7 +102,7 @@ func (h *PackageActivationHandler) findExpiredMainPackages(ctx context.Context)
Where("status = ?", constants.PackageUsageStatusActive).
Where("expires_at <= ?", now).
Where("master_usage_id IS NULL"). // 主套餐没有 master_usage_id
Limit(1000). // 每次最多处理 1000 个,避免长事务
Limit(1000). // 每次最多处理 1000 个,避免长事务
Find(&packages).Error
return packages, err
@@ -353,9 +353,9 @@ func (h *PackageActivationHandler) HandlePackageFirstActivation(ctx context.Cont
// ActivationService 未注入,直接更新状态(备用逻辑)
now := time.Now()
if err := h.db.Model(&pkg).Updates(map[string]any{
"status": constants.PackageUsageStatusActive,
"activated_at": now,
"pending_realname_activation": false,
"status": constants.PackageUsageStatusActive,
"activated_at": now,
"pending_realname_activation": false,
}).Error; err != nil {
return err
}

View File

@@ -20,13 +20,13 @@ import (
// Scheduler 轮询调度器
// 负责管理 IoT 卡的定期检查任务(实名、流量、套餐)
type Scheduler struct {
db *gorm.DB
redis *redis.Client
queueClient *asynq.Client
logger *zap.Logger
configStore *postgres.PollingConfigStore
iotCardStore *postgres.IotCardStore
concurrencyStore *postgres.PollingConcurrencyConfigStore
db *gorm.DB
redis *redis.Client
queueClient *asynq.Client
logger *zap.Logger
configStore *postgres.PollingConfigStore
iotCardStore *postgres.IotCardStore
concurrencyStore *postgres.PollingConcurrencyConfigStore
// 任务 19: 套餐激活检查处理器
packageActivationHandler *PackageActivationHandler
@@ -50,12 +50,12 @@ type Scheduler struct {
// InitProgress 初始化进度
type InitProgress struct {
mu sync.RWMutex
TotalCards int64 `json:"total_cards"` // 总卡数
LoadedCards int64 `json:"loaded_cards"` // 已加载卡数
StartTime time.Time `json:"start_time"` // 开始时间
TotalCards int64 `json:"total_cards"` // 总卡数
LoadedCards int64 `json:"loaded_cards"` // 已加载卡数
StartTime time.Time `json:"start_time"` // 开始时间
LastBatchTime time.Time `json:"last_batch_time"` // 最后一批处理时间
Status string `json:"status"` // 状态: pending, running, completed, failed
ErrorMessage string `json:"error_message"` // 错误信息
Status string `json:"status"` // 状态: pending, running, completed, failed
ErrorMessage string `json:"error_message"` // 错误信息
}
// SchedulerConfig 调度器配置
@@ -74,13 +74,13 @@ type SchedulerConfig struct {
// 单 Worker 设计吞吐50000 张/秒,支持多 Worker 水平扩展
func DefaultSchedulerConfig() *SchedulerConfig {
return &SchedulerConfig{
ScheduleInterval: 1 * time.Second, // 1秒调度一次提高响应速度
InitBatchSize: 100000, // 10万张/批初始化
ScheduleInterval: 1 * time.Second, // 1秒调度一次提高响应速度
InitBatchSize: 100000, // 10万张/批初始化
InitBatchSleepDuration: 500 * time.Millisecond, // 500ms 间隔,加快初始化
ConfigCacheTTL: 5 * time.Minute,
CardCacheTTL: 7 * 24 * time.Hour,
ScheduleBatchSize: 50000, // 每次取 5 万张,每秒可调度 5 万张
MaxManualBatchSize: 1000, // 手动触发每次处理 1000 张
ScheduleBatchSize: 50000, // 每次取 5 万张,每秒可调度 5 万张
MaxManualBatchSize: 1000, // 手动触发每次处理 1000 张
}
}
@@ -383,8 +383,8 @@ func (s *Scheduler) enqueueTask(ctx context.Context, taskType, cardID string, is
}
task := asynq.NewTask(taskType, mustMarshal(payload),
asynq.MaxRetry(0), // 不重试,失败后重新入队
asynq.Timeout(30*time.Second), // 30秒超时
asynq.MaxRetry(0), // 不重试,失败后重新入队
asynq.Timeout(30*time.Second), // 30秒超时
asynq.Queue(constants.QueueDefault),
)
@@ -681,13 +681,13 @@ func (s *Scheduler) cacheCardInfo(ctx context.Context, card *model.IotCard, cach
config := DefaultSchedulerConfig()
data := map[string]interface{}{
"id": card.ID,
"iccid": card.ICCID,
"card_category": card.CardCategory,
"real_name_status": card.RealNameStatus,
"network_status": card.NetworkStatus,
"carrier_id": card.CarrierID,
"cached_at": cachedAt.Unix(),
"id": card.ID,
"iccid": card.ICCID,
"card_category": card.CardCategory,
"real_name_status": card.RealNameStatus,
"network_status": card.NetworkStatus,
"carrier_id": card.CarrierID,
"cached_at": cachedAt.Unix(),
}
pipe := s.redis.Pipeline()

View File

@@ -14,22 +14,22 @@ import (
)
type Service struct {
db *gorm.DB
commissionRecordStore *postgres.CommissionRecordStore
shopStore *postgres.ShopStore
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
packageSeriesStore *postgres.PackageSeriesStore
iotCardStore *postgres.IotCardStore
deviceStore *postgres.DeviceStore
walletStore *postgres.WalletStore
walletTransactionStore *postgres.WalletTransactionStore
orderStore *postgres.OrderStore
orderItemStore *postgres.OrderItemStore
packageStore *postgres.PackageStore
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore
commissionStatsService *commission_stats.Service
logger *zap.Logger
db *gorm.DB
commissionRecordStore *postgres.CommissionRecordStore
shopStore *postgres.ShopStore
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
packageSeriesStore *postgres.PackageSeriesStore
iotCardStore *postgres.IotCardStore
deviceStore *postgres.DeviceStore
agentWalletStore *postgres.AgentWalletStore
agentWalletTransactionStore *postgres.AgentWalletTransactionStore
orderStore *postgres.OrderStore
orderItemStore *postgres.OrderItemStore
packageStore *postgres.PackageStore
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore
commissionStatsService *commission_stats.Service
logger *zap.Logger
}
func New(
@@ -41,8 +41,8 @@ func New(
packageSeriesStore *postgres.PackageSeriesStore,
iotCardStore *postgres.IotCardStore,
deviceStore *postgres.DeviceStore,
walletStore *postgres.WalletStore,
walletTransactionStore *postgres.WalletTransactionStore,
agentWalletStore *postgres.AgentWalletStore,
agentWalletTransactionStore *postgres.AgentWalletTransactionStore,
orderStore *postgres.OrderStore,
orderItemStore *postgres.OrderItemStore,
packageStore *postgres.PackageStore,
@@ -51,22 +51,22 @@ func New(
logger *zap.Logger,
) *Service {
return &Service{
db: db,
commissionRecordStore: commissionRecordStore,
shopStore: shopStore,
shopPackageAllocationStore: shopPackageAllocationStore,
shopSeriesAllocationStore: shopSeriesAllocationStore,
packageSeriesStore: packageSeriesStore,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
walletStore: walletStore,
walletTransactionStore: walletTransactionStore,
orderStore: orderStore,
orderItemStore: orderItemStore,
packageStore: packageStore,
commissionStatsStore: commissionStatsStore,
commissionStatsService: commissionStatsService,
logger: logger,
db: db,
commissionRecordStore: commissionRecordStore,
shopStore: shopStore,
shopPackageAllocationStore: shopPackageAllocationStore,
shopSeriesAllocationStore: shopSeriesAllocationStore,
packageSeriesStore: packageSeriesStore,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
agentWalletStore: agentWalletStore,
agentWalletTransactionStore: agentWalletTransactionStore,
orderStore: orderStore,
orderItemStore: orderItemStore,
packageStore: packageStore,
commissionStatsStore: commissionStatsStore,
commissionStatsService: commissionStatsService,
logger: logger,
}
}
@@ -562,25 +562,20 @@ func (s *Service) matchOneTimeCommissionTier(ctx context.Context, shopID uint, s
}
func (s *Service) creditCommissionInTx(ctx context.Context, tx *gorm.DB, record *model.CommissionRecord) error {
var wallet model.Wallet
if err := tx.Where("resource_type = ? AND resource_id = ? AND wallet_type = ?", "shop", record.ShopID, "commission").First(&wallet).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "获取店铺钱包失败")
// 获取店铺的分佣钱包
var wallet model.AgentWallet
if err := tx.Where("shop_id = ? AND wallet_type = ?", record.ShopID, "commission").First(&wallet).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "获取店铺分佣钱包失败")
}
balanceBefore := wallet.Balance
result := tx.Model(&model.Wallet{}).
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance + ?", record.Amount),
"version": gorm.Expr("version + 1"),
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新钱包余额失败")
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeInternalError, "钱包版本冲突,请重试")
// 使用 AgentWalletStore 的方法增加余额(带事务)
if err := s.agentWalletStore.AddBalanceWithTx(ctx, tx, wallet.ID, record.Amount); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新钱包余额失败")
}
// 更新佣金记录状态
now := time.Now()
if err := tx.Model(record).Updates(map[string]any{
"balance_after": balanceBefore + record.Amount,
@@ -590,9 +585,11 @@ func (s *Service) creditCommissionInTx(ctx context.Context, tx *gorm.DB, record
return errors.Wrap(errors.CodeDatabaseError, err, "更新佣金记录失败")
}
// 创建代理钱包交易记录
remark := "佣金入账"
transaction := &model.WalletTransaction{
WalletID: wallet.ID,
transaction := &model.AgentWalletTransaction{
AgentWalletID: wallet.ID,
ShopID: record.ShopID,
UserID: record.Creator,
TransactionType: "commission",
Amount: record.Amount,
@@ -603,8 +600,9 @@ func (s *Service) creditCommissionInTx(ctx context.Context, tx *gorm.DB, record
ReferenceID: &record.ID,
Remark: &remark,
Creator: record.Creator,
ShopIDTag: record.ShopID,
}
if err := tx.Create(transaction).Error; err != nil {
if err := s.agentWalletTransactionStore.CreateWithTx(ctx, tx, transaction); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败")
}

View File

@@ -19,8 +19,8 @@ type Service struct {
db *gorm.DB
shopStore *postgres.ShopStore
accountStore *postgres.AccountStore
walletStore *postgres.WalletStore
walletTransactionStore *postgres.WalletTransactionStore
agentWalletStore *postgres.AgentWalletStore
agentWalletTransactionStore *postgres.AgentWalletTransactionStore
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore
}
@@ -28,16 +28,16 @@ func New(
db *gorm.DB,
shopStore *postgres.ShopStore,
accountStore *postgres.AccountStore,
walletStore *postgres.WalletStore,
walletTransactionStore *postgres.WalletTransactionStore,
agentWalletStore *postgres.AgentWalletStore,
agentWalletTransactionStore *postgres.AgentWalletTransactionStore,
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore,
) *Service {
return &Service{
db: db,
shopStore: shopStore,
accountStore: accountStore,
walletStore: walletStore,
walletTransactionStore: walletTransactionStore,
agentWalletStore: agentWalletStore,
agentWalletTransactionStore: agentWalletTransactionStore,
commissionWithdrawalReqStore: commissionWithdrawalReqStore,
}
}
@@ -157,7 +157,8 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveWithdraw
return nil, errors.New(errors.CodeInvalidStatus, "申请状态不允许此操作")
}
wallet, err := s.walletStore.GetShopCommissionWallet(ctx, withdrawal.ShopID)
// 获取店铺分佣钱包
wallet, err := s.agentWalletStore.GetCommissionWallet(ctx, withdrawal.ShopID)
if err != nil {
return nil, errors.New(errors.CodeNotFound, "店铺佣金钱包不存在")
}
@@ -173,14 +174,17 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveWithdraw
now := time.Now()
err = s.db.Transaction(func(tx *gorm.DB) error {
if err := s.walletStore.DeductFrozenBalanceWithTx(ctx, tx, wallet.ID, amount); err != nil {
// 从冻结余额扣款
if err := s.agentWalletStore.DeductFrozenBalanceWithTx(ctx, tx, wallet.ID, amount); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "扣除冻结余额失败")
}
// 创建代理钱包交易记录
refType := "withdrawal"
refID := withdrawal.ID
transaction := &model.WalletTransaction{
WalletID: wallet.ID,
transaction := &model.AgentWalletTransaction{
AgentWalletID: wallet.ID,
ShopID: withdrawal.ShopID,
UserID: currentUserID,
TransactionType: "withdrawal",
Amount: -amount,
@@ -190,8 +194,9 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveWithdraw
ReferenceType: &refType,
ReferenceID: &refID,
Creator: currentUserID,
ShopIDTag: withdrawal.ShopID,
}
if err := s.walletTransactionStore.CreateWithTx(ctx, tx, transaction); err != nil {
if err := s.agentWalletTransactionStore.CreateWithTx(ctx, tx, transaction); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "创建交易流水失败")
}
@@ -265,21 +270,22 @@ func (s *Service) Reject(ctx context.Context, id uint, req *dto.RejectWithdrawal
return nil, errors.New(errors.CodeInvalidStatus, "申请状态不允许此操作")
}
wallet, err := s.walletStore.GetShopCommissionWallet(ctx, withdrawal.ShopID)
wallet, err := s.agentWalletStore.GetCommissionWallet(ctx, withdrawal.ShopID)
if err != nil {
return nil, errors.New(errors.CodeNotFound, "店铺佣金钱包不存在")
}
now := time.Now()
err = s.db.Transaction(func(tx *gorm.DB) error {
if err := s.walletStore.UnfreezeBalanceWithTx(ctx, tx, wallet.ID, withdrawal.Amount); err != nil {
if err := s.agentWalletStore.UnfreezeBalanceWithTx(ctx, tx, wallet.ID, withdrawal.Amount); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "解冻余额失败")
}
refType := "withdrawal"
refID := withdrawal.ID
transaction := &model.WalletTransaction{
WalletID: wallet.ID,
transaction := &model.AgentWalletTransaction{
AgentWalletID: wallet.ID,
ShopID: withdrawal.ShopID,
UserID: currentUserID,
TransactionType: "refund",
Amount: withdrawal.Amount,
@@ -289,8 +295,9 @@ func (s *Service) Reject(ctx context.Context, id uint, req *dto.RejectWithdrawal
ReferenceType: &refType,
ReferenceID: &refID,
Creator: currentUserID,
ShopIDTag: withdrawal.ShopID,
}
if err := s.walletTransactionStore.CreateWithTx(ctx, tx, transaction); err != nil {
if err := s.agentWalletTransactionStore.CreateWithTx(ctx, tx, transaction); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "创建交易流水失败")
}

View File

@@ -42,7 +42,7 @@ func NewStopResumeService(
iotCardStore: iotCardStore,
gatewayClient: gatewayClient,
logger: logger,
maxRetries: 3, // 默认最多重试 3 次
maxRetries: 3, // 默认最多重试 3 次
retryInterval: 2 * time.Second, // 默认重试间隔 2 秒
}
}

View File

@@ -19,30 +19,30 @@ import (
type Service struct {
db *gorm.DB
shopStore *postgres.ShopStore
walletStore *postgres.WalletStore
agentWalletStore *postgres.AgentWalletStore
commissionWithdrawalRequestStore *postgres.CommissionWithdrawalRequestStore
commissionWithdrawalSettingStore *postgres.CommissionWithdrawalSettingStore
commissionRecordStore *postgres.CommissionRecordStore
walletTransactionStore *postgres.WalletTransactionStore
agentWalletTransactionStore *postgres.AgentWalletTransactionStore
}
func New(
db *gorm.DB,
shopStore *postgres.ShopStore,
walletStore *postgres.WalletStore,
agentWalletStore *postgres.AgentWalletStore,
commissionWithdrawalRequestStore *postgres.CommissionWithdrawalRequestStore,
commissionWithdrawalSettingStore *postgres.CommissionWithdrawalSettingStore,
commissionRecordStore *postgres.CommissionRecordStore,
walletTransactionStore *postgres.WalletTransactionStore,
agentWalletTransactionStore *postgres.AgentWalletTransactionStore,
) *Service {
return &Service{
db: db,
shopStore: shopStore,
walletStore: walletStore,
agentWalletStore: agentWalletStore,
commissionWithdrawalRequestStore: commissionWithdrawalRequestStore,
commissionWithdrawalSettingStore: commissionWithdrawalSettingStore,
commissionRecordStore: commissionRecordStore,
walletTransactionStore: walletTransactionStore,
agentWalletTransactionStore: agentWalletTransactionStore,
}
}
@@ -63,8 +63,8 @@ func (s *Service) GetCommissionSummary(ctx context.Context) (*dto.MyCommissionSu
return nil, errors.New(errors.CodeShopNotFound, "店铺不存在")
}
// 使用 GetShopCommissionWallet 获取店铺佣金钱包
wallet, err := s.walletStore.GetShopCommissionWallet(ctx, shopID)
// 使用 GetCommissionWallet 获取店铺佣金钱包
wallet, err := s.agentWalletStore.GetCommissionWallet(ctx, shopID)
if err != nil {
// 钱包不存在时返回空数据
return &dto.MyCommissionSummaryResp{
@@ -120,7 +120,7 @@ func (s *Service) CreateWithdrawalRequest(ctx context.Context, req *dto.CreateMy
}
// 获取钱包
wallet, err := s.walletStore.GetShopCommissionWallet(ctx, shopID)
wallet, err := s.agentWalletStore.GetCommissionWallet(ctx, shopID)
if err != nil {
return nil, errors.New(errors.CodeInsufficientBalance, "钱包不存在")
}
@@ -160,7 +160,7 @@ func (s *Service) CreateWithdrawalRequest(ctx context.Context, req *dto.CreateMy
err = s.db.Transaction(func(tx *gorm.DB) error {
// 冻结余额
if err := tx.WithContext(ctx).Model(&model.Wallet{}).
if err := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND balance >= ?", wallet.ID, req.Amount).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", req.Amount),
@@ -192,8 +192,9 @@ func (s *Service) CreateWithdrawalRequest(ctx context.Context, req *dto.CreateMy
// 创建钱包流水记录
remark := fmt.Sprintf("提现冻结,单号:%s", withdrawalNo)
refType := constants.ReferenceTypeWithdrawal
transaction := &model.WalletTransaction{
WalletID: wallet.ID,
transaction := &model.AgentWalletTransaction{
AgentWalletID: wallet.ID,
ShopID: shopID,
UserID: currentUserID,
TransactionType: constants.TransactionTypeWithdrawal,
Amount: -req.Amount, // 冻结为负数
@@ -204,6 +205,7 @@ func (s *Service) CreateWithdrawalRequest(ctx context.Context, req *dto.CreateMy
ReferenceID: &withdrawalRequest.ID,
Remark: &remark,
Creator: currentUserID,
ShopIDTag: shopID,
}
if err := tx.WithContext(ctx).Create(transaction).Error; err != nil {

View File

@@ -30,7 +30,8 @@ type Service struct {
redis *redis.Client
orderStore *postgres.OrderStore
orderItemStore *postgres.OrderItemStore
walletStore *postgres.WalletStore
agentWalletStore *postgres.AgentWalletStore
cardWalletStore *postgres.CardWalletStore
purchaseValidationService *purchase_validation.Service
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
@@ -49,7 +50,8 @@ func New(
redisClient *redis.Client,
orderStore *postgres.OrderStore,
orderItemStore *postgres.OrderItemStore,
walletStore *postgres.WalletStore,
agentWalletStore *postgres.AgentWalletStore,
cardWalletStore *postgres.CardWalletStore,
purchaseValidationService *purchase_validation.Service,
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
@@ -67,7 +69,8 @@ func New(
redis: redisClient,
orderStore: orderStore,
orderItemStore: orderItemStore,
walletStore: walletStore,
agentWalletStore: agentWalletStore,
cardWalletStore: cardWalletStore,
purchaseValidationService: purchaseValidationService,
shopPackageAllocationStore: shopPackageAllocationStore,
shopSeriesAllocationStore: shopSeriesAllocationStore,
@@ -430,64 +433,128 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
return errors.New(errors.CodeInvalidParam, "不支持的买家类型")
}
wallet, err := s.walletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID, "main")
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
}
return err
}
if wallet.Balance < order.TotalAmount {
return errors.New(errors.CodeInsufficientBalance, "余额不足")
}
// 根据资源类型选择对应的钱包系统
now := time.Now()
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
Updates(map[string]any{
"payment_status": model.PaymentStatusPaid,
"payment_method": model.PaymentMethodWallet,
"paid_at": now,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
if resourceType == "shop" {
// 代理钱包系统(店铺)
wallet, err := s.agentWalletStore.GetMainWallet(ctx, resourceID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
}
return err
}
if result.RowsAffected == 0 {
var currentOrder model.Order
if err := tx.First(&currentOrder, orderID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败")
if wallet.Balance < order.TotalAmount {
return errors.New(errors.CodeInsufficientBalance, "余额不足")
}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
Updates(map[string]any{
"payment_status": model.PaymentStatusPaid,
"payment_method": model.PaymentMethodWallet,
"paid_at": now,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
}
switch currentOrder.PaymentStatus {
case model.PaymentStatusPaid:
return nil
case model.PaymentStatusCancelled:
return errors.New(errors.CodeInvalidStatus, "订单已取消,无法支付")
case model.PaymentStatusRefunded:
return errors.New(errors.CodeInvalidStatus, "订单已退款,无法支付")
default:
return errors.New(errors.CodeInvalidStatus, "订单状态异常")
if result.RowsAffected == 0 {
var currentOrder model.Order
if err := tx.First(&currentOrder, orderID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败")
}
switch currentOrder.PaymentStatus {
case model.PaymentStatusPaid:
return nil
case model.PaymentStatusCancelled:
return errors.New(errors.CodeInvalidStatus, "订单已取消,无法支付")
case model.PaymentStatusRefunded:
return errors.New(errors.CodeInvalidStatus, "订单已退款,无法支付")
default:
return errors.New(errors.CodeInvalidStatus, "订单状态异常")
}
}
walletResult := tx.Model(&model.AgentWallet{}).
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance - ?", order.TotalAmount),
"version": gorm.Expr("version + 1"),
})
if walletResult.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, walletResult.Error, "扣减钱包余额失败")
}
if walletResult.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
}
return s.activatePackage(ctx, tx, order)
})
} else {
// 卡钱包系统iot_card 或 device
wallet, err := s.cardWalletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
}
return err
}
walletResult := tx.Model(&model.Wallet{}).
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance - ?", order.TotalAmount),
"version": gorm.Expr("version + 1"),
})
if walletResult.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, walletResult.Error, "扣减钱包余额失败")
}
if walletResult.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
if wallet.Balance < order.TotalAmount {
return errors.New(errors.CodeInsufficientBalance, "余额不足")
}
return s.activatePackage(ctx, tx, order)
})
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
Updates(map[string]any{
"payment_status": model.PaymentStatusPaid,
"payment_method": model.PaymentMethodWallet,
"paid_at": now,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
}
if result.RowsAffected == 0 {
var currentOrder model.Order
if err := tx.First(&currentOrder, orderID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败")
}
switch currentOrder.PaymentStatus {
case model.PaymentStatusPaid:
return nil
case model.PaymentStatusCancelled:
return errors.New(errors.CodeInvalidStatus, "订单已取消,无法支付")
case model.PaymentStatusRefunded:
return errors.New(errors.CodeInvalidStatus, "订单已退款,无法支付")
default:
return errors.New(errors.CodeInvalidStatus, "订单状态异常")
}
}
walletResult := tx.Model(&model.CardWallet{}).
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance - ?", order.TotalAmount),
"version": gorm.Expr("version + 1"),
})
if walletResult.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, walletResult.Error, "扣减钱包余额失败")
}
if walletResult.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
}
return s.activatePackage(ctx, tx, order)
})
}
if err != nil {
return err

View File

@@ -71,10 +71,10 @@ func (s *ResetService) resetDailyUsageWithDB(ctx context.Context, db *gorm.DB) e
// 批量更新
updates := map[string]interface{}{
"data_usage_mb": 0,
"last_reset_at": now,
"next_reset_at": nextReset,
"status": constants.PackageUsageStatusActive, // 重置后恢复为生效中
"data_usage_mb": 0,
"last_reset_at": now,
"next_reset_at": nextReset,
"status": constants.PackageUsageStatusActive, // 重置后恢复为生效中
}
if err := tx.Model(&model.PackageUsage{}).
@@ -221,4 +221,3 @@ func (s *ResetService) resetYearlyUsageWithDB(ctx context.Context, db *gorm.DB)
return nil
})
}

View File

@@ -152,13 +152,13 @@ func (s *CleanupService) Preview(ctx context.Context) ([]*CleanupPreview, error)
// CleanupProgress 清理进度
type CleanupProgress struct {
IsRunning bool `json:"is_running"`
CurrentTable string `json:"current_table,omitempty"`
TotalTables int `json:"total_tables"`
ProcessedTables int `json:"processed_tables"`
TotalDeleted int64 `json:"total_deleted"`
StartedAt *time.Time `json:"started_at,omitempty"`
LastLog *model.DataCleanupLog `json:"last_log,omitempty"`
IsRunning bool `json:"is_running"`
CurrentTable string `json:"current_table,omitempty"`
TotalTables int `json:"total_tables"`
ProcessedTables int `json:"processed_tables"`
TotalDeleted int64 `json:"total_deleted"`
StartedAt *time.Time `json:"started_at,omitempty"`
LastLog *model.DataCleanupLog `json:"last_log,omitempty"`
}
// GetProgress 获取清理进度

View File

@@ -28,11 +28,11 @@ func NewConcurrencyService(store *postgres.PollingConcurrencyConfigStore, redis
// ConcurrencyStatus 并发状态
type ConcurrencyStatus struct {
TaskType string `json:"task_type"`
TaskTypeName string `json:"task_type_name"`
MaxConcurrency int `json:"max_concurrency"`
Current int64 `json:"current"`
Available int64 `json:"available"`
TaskType string `json:"task_type"`
TaskTypeName string `json:"task_type_name"`
MaxConcurrency int `json:"max_concurrency"`
Current int64 `json:"current"`
Available int64 `json:"available"`
Utilization float64 `json:"utilization"`
}

View File

@@ -207,13 +207,13 @@ func (s *ManualTriggerService) processBatchTrigger(ctx context.Context, logID ui
// ConditionFilter 条件筛选参数
type ConditionFilter struct {
CardStatus string `json:"card_status,omitempty"` // 卡状态
CarrierCode string `json:"carrier_code,omitempty"` // 运营商代码
CardType string `json:"card_type,omitempty"` // 卡类型
ShopID *uint `json:"shop_id,omitempty"` // 店铺ID
PackageIDs []uint `json:"package_ids,omitempty"` // 套餐ID列表
EnablePolling *bool `json:"enable_polling,omitempty"` // 是否启用轮询
Limit int `json:"limit,omitempty"` // 限制数量
CardStatus string `json:"card_status,omitempty"` // 卡状态
CarrierCode string `json:"carrier_code,omitempty"` // 运营商代码
CardType string `json:"card_type,omitempty"` // 卡类型
ShopID *uint `json:"shop_id,omitempty"` // 店铺ID
PackageIDs []uint `json:"package_ids,omitempty"` // 套餐ID列表
EnablePolling *bool `json:"enable_polling,omitempty"` // 是否启用轮询
Limit int `json:"limit,omitempty"` // 限制数量
}
// TriggerByCondition 条件筛选触发

View File

@@ -29,26 +29,26 @@ type ForceRechargeRequirement struct {
}
// Service 充值服务
// 负责充值订单创建、预检、支付回调处理等业务逻辑
// 负责卡钱包IoT卡/设备)的充值订单创建、预检、支付回调处理等业务逻辑
type Service struct {
db *gorm.DB
rechargeStore *postgres.RechargeStore
walletStore *postgres.WalletStore
walletTransactionStore *postgres.WalletTransactionStore
iotCardStore *postgres.IotCardStore
deviceStore *postgres.DeviceStore
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
packageSeriesStore *postgres.PackageSeriesStore
commissionRecordStore *postgres.CommissionRecordStore
logger *zap.Logger
db *gorm.DB
cardRechargeStore *postgres.CardRechargeStore
cardWalletStore *postgres.CardWalletStore
cardWalletTransactionStore *postgres.CardWalletTransactionStore
iotCardStore *postgres.IotCardStore
deviceStore *postgres.DeviceStore
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
packageSeriesStore *postgres.PackageSeriesStore
commissionRecordStore *postgres.CommissionRecordStore
logger *zap.Logger
}
// New 创建充值服务实例
func New(
db *gorm.DB,
rechargeStore *postgres.RechargeStore,
walletStore *postgres.WalletStore,
walletTransactionStore *postgres.WalletTransactionStore,
cardRechargeStore *postgres.CardRechargeStore,
cardWalletStore *postgres.CardWalletStore,
cardWalletTransactionStore *postgres.CardWalletTransactionStore,
iotCardStore *postgres.IotCardStore,
deviceStore *postgres.DeviceStore,
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
@@ -57,16 +57,16 @@ func New(
logger *zap.Logger,
) *Service {
return &Service{
db: db,
rechargeStore: rechargeStore,
walletStore: walletStore,
walletTransactionStore: walletTransactionStore,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
shopSeriesAllocationStore: shopSeriesAllocationStore,
packageSeriesStore: packageSeriesStore,
commissionRecordStore: commissionRecordStore,
logger: logger,
db: db,
cardRechargeStore: cardRechargeStore,
cardWalletStore: cardWalletStore,
cardWalletTransactionStore: cardWalletTransactionStore,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
shopSeriesAllocationStore: shopSeriesAllocationStore,
packageSeriesStore: packageSeriesStore,
commissionRecordStore: commissionRecordStore,
logger: logger,
}
}
@@ -81,14 +81,14 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRechargeRequest, us
return nil, errors.New(errors.CodeRechargeAmountInvalid, "充值金额不能超过100000元")
}
// 2. 获取资源(卡或设备)
var wallet *model.Wallet
// 2. 获取资源(卡或设备)钱包
var wallet *model.CardWallet
var err error
if req.ResourceType == "iot_card" {
wallet, err = s.walletStore.GetByResourceTypeAndID(ctx, "iot_card", req.ResourceID, "main")
wallet, err = s.cardWalletStore.GetByResourceTypeAndID(ctx, "iot_card", req.ResourceID)
} else if req.ResourceType == "device" {
wallet, err = s.walletStore.GetByResourceTypeAndID(ctx, "device", req.ResourceID, "main")
wallet, err = s.cardWalletStore.GetByResourceTypeAndID(ctx, "device", req.ResourceID)
} else {
return nil, errors.New(errors.CodeInvalidParam, "无效的资源类型")
}
@@ -115,20 +115,20 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRechargeRequest, us
rechargeNo := s.generateRechargeNo()
// 5. 创建充值订单
recharge := &model.RechargeRecord{
BaseModel: model.BaseModel{
Creator: userID,
Updater: userID,
},
UserID: userID,
WalletID: wallet.ID,
RechargeNo: rechargeNo,
Amount: req.Amount,
PaymentMethod: req.PaymentMethod,
Status: constants.RechargeStatusPending,
recharge := &model.CardRechargeRecord{
UserID: userID,
CardWalletID: wallet.ID,
ResourceType: req.ResourceType,
ResourceID: req.ResourceID,
RechargeNo: rechargeNo,
Amount: req.Amount,
PaymentMethod: req.PaymentMethod,
Status: constants.RechargeStatusPending,
ShopIDTag: wallet.ShopIDTag,
EnterpriseIDTag: wallet.EnterpriseIDTag,
}
if err := s.rechargeStore.Create(ctx, recharge); err != nil {
if err := s.cardRechargeStore.Create(ctx, recharge); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
}
@@ -173,7 +173,7 @@ func (s *Service) GetRechargeCheck(ctx context.Context, resourceType string, res
// GetByID 根据ID查询充值订单详情
// 支持数据权限过滤
func (s *Service) GetByID(ctx context.Context, id uint, userID uint) (*dto.RechargeResponse, error) {
recharge, err := s.rechargeStore.GetByID(ctx, id)
recharge, err := s.cardRechargeStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeRechargeNotFound, "充值订单不存在")
@@ -201,7 +201,7 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID
pageSize = constants.DefaultPageSize
}
params := &postgres.ListRechargeParams{
params := &postgres.ListCardRechargeParams{
Page: page,
PageSize: pageSize,
UserID: &userID, // 数据权限:只能查看自己的
@@ -211,7 +211,8 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID
params.Status = req.Status
}
if req.WalletID != nil {
params.WalletID = req.WalletID
walletID := *req.WalletID
params.CardWalletID = &walletID
}
if req.StartTime != nil {
params.StartTime = req.StartTime
@@ -220,7 +221,7 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID
params.EndTime = req.EndTime
}
recharges, total, err := s.rechargeStore.List(ctx, params)
recharges, total, err := s.cardRechargeStore.List(ctx, params)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单列表失败")
}
@@ -248,13 +249,13 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID
// 支持幂等性检查、事务处理、更新余额、触发佣金
func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error {
// 1. 查询充值订单
recharge, err := s.rechargeStore.GetByRechargeNo(ctx, rechargeNo)
recharge, err := s.cardRechargeStore.GetByRechargeNo(ctx, rechargeNo)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeRechargeNotFound, "充值订单不存在")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败")
}
if recharge == nil {
return errors.New(errors.CodeRechargeNotFound, "充值订单不存在")
}
// 2. 幂等性检查:已支付则直接返回成功
if recharge.Status == constants.RechargeStatusPaid || recharge.Status == constants.RechargeStatusCompleted {
@@ -271,21 +272,21 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
}
// 4. 获取钱包信息
wallet, err := s.walletStore.GetByID(ctx, recharge.WalletID)
wallet, err := s.cardWalletStore.GetByID(ctx, recharge.CardWalletID)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
}
// 5. 获取钱包对应的资源类型和ID
resourceType := wallet.ResourceType
resourceID := wallet.ResourceID
// 5. 获取钱包对应的资源类型和ID(从充值记录中直接获取)
resourceType := recharge.ResourceType
resourceID := recharge.ResourceID
// 6. 事务处理:更新订单状态、增加余额、更新累计充值、触发佣金
now := time.Now()
err = s.db.Transaction(func(tx *gorm.DB) error {
// 6.1 更新充值订单状态(带状态检查,实现乐观锁)
oldStatus := constants.RechargeStatusPending
if err := s.rechargeStore.UpdateStatus(ctx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil {
if err := s.cardRechargeStore.UpdateStatusWithOptimisticLock(ctx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil {
if err == gorm.ErrRecordNotFound {
// 状态已变更,幂等处理
return nil
@@ -294,13 +295,13 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
}
// 6.2 更新支付信息
if err := s.rechargeStore.UpdatePaymentInfo(ctx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil {
if err := s.cardRechargeStore.UpdatePaymentInfo(ctx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新支付信息失败")
}
// 6.3 增加钱包余额(使用乐观锁)
balanceBefore := wallet.Balance
result := tx.Model(&model.Wallet{}).
result := tx.Model(&model.CardWallet{}).
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance + ?", recharge.Amount),
@@ -316,8 +317,10 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
// 6.4 创建钱包交易记录
remark := "钱包充值"
refType := "recharge"
transaction := &model.WalletTransaction{
WalletID: wallet.ID,
transaction := &model.CardWalletTransaction{
CardWalletID: wallet.ID,
ResourceType: resourceType,
ResourceID: resourceID,
UserID: recharge.UserID,
TransactionType: "recharge",
Amount: recharge.Amount,
@@ -327,7 +330,8 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
ReferenceType: &refType,
ReferenceID: &recharge.ID,
Remark: &remark,
Creator: recharge.UserID,
ShopIDTag: wallet.ShopIDTag,
EnterpriseIDTag: wallet.EnterpriseIDTag,
}
if err := tx.Create(transaction).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败")
@@ -344,7 +348,7 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
}
// 6.7 更新充值订单状态为已完成
if err := tx.Model(&model.RechargeRecord{}).
if err := tx.Model(&model.CardRechargeRecord{}).
Where("id = ?", recharge.ID).
Update("status", constants.RechargeStatusCompleted).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单完成状态失败")
@@ -590,8 +594,8 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
return nil
}
var commissionWallet model.Wallet
if err := tx.Where("resource_type = ? AND resource_id = ? AND wallet_type = ?", "shop", *shopID, "commission").
var commissionWallet model.AgentWallet
if err := tx.Where("shop_id = ? AND wallet_type = ?", *shopID, constants.AgentWalletTypeCommission).
First(&commissionWallet).Error; err != nil {
if err == gorm.ErrRecordNotFound {
s.logger.Warn("店铺佣金钱包不存在,跳过佣金发放",
@@ -629,7 +633,7 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
// 11. 佣金入账到店铺佣金钱包
balanceBefore := commissionWallet.Balance
result := tx.Model(&model.Wallet{}).
result := tx.Model(&model.AgentWallet{}).
Where("id = ? AND version = ?", commissionWallet.ID, commissionWallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance + ?", commissionAmount),
@@ -654,8 +658,9 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
// 13. 创建佣金钱包交易记录
remark := "一次性佣金入账(充值触发)"
refType := "commission"
commissionTransaction := &model.WalletTransaction{
WalletID: commissionWallet.ID,
commissionTransaction := &model.AgentWalletTransaction{
AgentWalletID: commissionWallet.ID,
ShopID: *shopID,
UserID: userID,
TransactionType: "commission",
Amount: commissionAmount,
@@ -665,7 +670,7 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
ReferenceType: &refType,
ReferenceID: &commissionRecord.ID,
Remark: &remark,
Creator: userID,
ShopIDTag: *shopID,
}
if err := tx.Create(commissionTransaction).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金钱包交易记录失败")
@@ -724,7 +729,7 @@ func (s *Service) generateRechargeNo() string {
}
// buildRechargeResponse 构建充值订单响应
func (s *Service) buildRechargeResponse(recharge *model.RechargeRecord) *dto.RechargeResponse {
func (s *Service) buildRechargeResponse(recharge *model.CardRechargeRecord) *dto.RechargeResponse {
statusText := ""
switch recharge.Status {
case constants.RechargeStatusPending:
@@ -743,7 +748,7 @@ func (s *Service) buildRechargeResponse(recharge *model.RechargeRecord) *dto.Rec
ID: recharge.ID,
RechargeNo: recharge.RechargeNo,
UserID: recharge.UserID,
WalletID: recharge.WalletID,
WalletID: recharge.CardWalletID,
Amount: recharge.Amount,
PaymentMethod: recharge.PaymentMethod,
PaymentChannel: recharge.PaymentChannel,

View File

@@ -16,7 +16,7 @@ import (
type Service struct {
shopStore *postgres.ShopStore
accountStore *postgres.AccountStore
walletStore *postgres.WalletStore
agentWalletStore *postgres.AgentWalletStore
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore
commissionRecordStore *postgres.CommissionRecordStore
}
@@ -24,14 +24,14 @@ type Service struct {
func New(
shopStore *postgres.ShopStore,
accountStore *postgres.AccountStore,
walletStore *postgres.WalletStore,
agentWalletStore *postgres.AgentWalletStore,
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore,
commissionRecordStore *postgres.CommissionRecordStore,
) *Service {
return &Service{
shopStore: shopStore,
accountStore: accountStore,
walletStore: walletStore,
agentWalletStore: agentWalletStore,
commissionWithdrawalReqStore: commissionWithdrawalReqStore,
commissionRecordStore: commissionRecordStore,
}
@@ -74,7 +74,7 @@ func (s *Service) ListShopCommissionSummary(ctx context.Context, req *dto.ShopCo
shopIDs = append(shopIDs, shop.ID)
}
walletSummaries, err := s.walletStore.GetShopCommissionSummaryBatch(ctx, shopIDs)
walletSummaries, err := s.agentWalletStore.GetShopCommissionSummaryBatch(ctx, shopIDs)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺钱包汇总失败")
}
@@ -123,7 +123,7 @@ func (s *Service) ListShopCommissionSummary(ctx context.Context, req *dto.ShopCo
}, nil
}
func (s *Service) buildCommissionSummaryItem(shop *model.Shop, walletSummary *postgres.ShopCommissionSummary, withdrawnAmount, withdrawingAmount int64, account *model.Account) dto.ShopCommissionSummaryItem {
func (s *Service) buildCommissionSummaryItem(shop *model.Shop, walletSummary *model.AgentWallet, withdrawnAmount, withdrawingAmount int64, account *model.Account) dto.ShopCommissionSummaryItem {
var balance, frozenBalance int64
if walletSummary != nil {
balance = walletSummary.Balance

View File

@@ -0,0 +1,125 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// AgentRechargeStore 代理充值记录数据访问层
type AgentRechargeStore struct {
db *gorm.DB
redis *redis.Client
}
// NewAgentRechargeStore 创建代理充值记录 Store
func NewAgentRechargeStore(db *gorm.DB, redis *redis.Client) *AgentRechargeStore {
return &AgentRechargeStore{
db: db,
redis: redis,
}
}
// Create 创建充值记录
func (s *AgentRechargeStore) Create(ctx context.Context, record *model.AgentRechargeRecord) error {
return s.db.WithContext(ctx).Create(record).Error
}
// CreateWithTx 创建充值记录(带事务)
func (s *AgentRechargeStore) CreateWithTx(ctx context.Context, tx *gorm.DB, record *model.AgentRechargeRecord) error {
return tx.WithContext(ctx).Create(record).Error
}
// GetByRechargeNo 根据充值订单号查询
func (s *AgentRechargeStore) GetByRechargeNo(ctx context.Context, rechargeNo string) (*model.AgentRechargeRecord, error) {
var record model.AgentRechargeRecord
err := s.db.WithContext(ctx).
Where("recharge_no = ?", rechargeNo).
First(&record).Error
if err != nil {
return nil, err
}
return &record, nil
}
// GetByID 根据 ID 查询
func (s *AgentRechargeStore) GetByID(ctx context.Context, id uint) (*model.AgentRechargeRecord, error) {
var record model.AgentRechargeRecord
if err := s.db.WithContext(ctx).First(&record, id).Error; err != nil {
return nil, err
}
return &record, nil
}
// UpdateStatus 更新充值状态
func (s *AgentRechargeStore) UpdateStatus(ctx context.Context, id uint, status int) error {
return s.db.WithContext(ctx).
Model(&model.AgentRechargeRecord{}).
Where("id = ?", id).
Update("status", status).Error
}
// UpdateStatusWithTx 更新充值状态(带事务)
func (s *AgentRechargeStore) UpdateStatusWithTx(ctx context.Context, tx *gorm.DB, id uint, status int) error {
return tx.WithContext(ctx).
Model(&model.AgentRechargeRecord{}).
Where("id = ?", id).
Update("status", status).Error
}
// Update 更新充值记录
func (s *AgentRechargeStore) Update(ctx context.Context, record *model.AgentRechargeRecord) error {
return s.db.WithContext(ctx).Save(record).Error
}
// UpdateWithTx 更新充值记录(带事务)
func (s *AgentRechargeStore) UpdateWithTx(ctx context.Context, tx *gorm.DB, record *model.AgentRechargeRecord) error {
return tx.WithContext(ctx).Save(record).Error
}
// ListByShopID 按店铺查询充值记录(支持分页)
func (s *AgentRechargeStore) ListByShopID(ctx context.Context, shopID uint, offset, limit int) ([]*model.AgentRechargeRecord, error) {
var records []*model.AgentRechargeRecord
err := s.db.WithContext(ctx).
Where("shop_id = ?", shopID).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
// ListByUserID 按用户查询充值记录(支持分页)
func (s *AgentRechargeStore) ListByUserID(ctx context.Context, userID uint, offset, limit int) ([]*model.AgentRechargeRecord, error) {
var records []*model.AgentRechargeRecord
err := s.db.WithContext(ctx).
Where("user_id = ?", userID).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
// ListByStatus 按状态查询充值记录(支持分页)
func (s *AgentRechargeStore) ListByStatus(ctx context.Context, status int, offset, limit int) ([]*model.AgentRechargeRecord, error) {
var records []*model.AgentRechargeRecord
err := s.db.WithContext(ctx).
Where("status = ?", status).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}

View File

@@ -0,0 +1,238 @@
package postgres
import (
"context"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// AgentWalletStore 代理钱包数据访问层
type AgentWalletStore struct {
db *gorm.DB
redis *redis.Client
}
// NewAgentWalletStore 创建代理钱包 Store
func NewAgentWalletStore(db *gorm.DB, redis *redis.Client) *AgentWalletStore {
return &AgentWalletStore{
db: db,
redis: redis,
}
}
// GetCommissionWallet 获取店铺的分佣钱包
func (s *AgentWalletStore) GetCommissionWallet(ctx context.Context, shopID uint) (*model.AgentWallet, error) {
return s.GetByShopIDAndType(ctx, shopID, constants.AgentWalletTypeCommission)
}
// GetMainWallet 获取店铺的主钱包
func (s *AgentWalletStore) GetMainWallet(ctx context.Context, shopID uint) (*model.AgentWallet, error) {
return s.GetByShopIDAndType(ctx, shopID, constants.AgentWalletTypeMain)
}
// GetByShopIDAndType 根据店铺 ID 和钱包类型查询钱包
func (s *AgentWalletStore) GetByShopIDAndType(ctx context.Context, shopID uint, walletType string) (*model.AgentWallet, error) {
// 尝试从缓存获取
cacheKey := constants.RedisAgentWalletBalanceKey(shopID, walletType)
// 注意:这里简化处理,实际项目中可以缓存完整的钱包信息
var wallet model.AgentWallet
err := s.db.WithContext(ctx).
Where("shop_id = ? AND wallet_type = ?", shopID, walletType).
First(&wallet).Error
if err != nil {
return nil, err
}
// 更新缓存(可选)
// 这里简化处理,不缓存完整对象
_ = cacheKey
return &wallet, nil
}
// GetByID 根据钱包 ID 查询
func (s *AgentWalletStore) GetByID(ctx context.Context, id uint) (*model.AgentWallet, error) {
var wallet model.AgentWallet
if err := s.db.WithContext(ctx).First(&wallet, id).Error; err != nil {
return nil, err
}
return &wallet, nil
}
// Create 创建代理钱包
func (s *AgentWalletStore) Create(ctx context.Context, wallet *model.AgentWallet) error {
return s.db.WithContext(ctx).Create(wallet).Error
}
// CreateWithTx 创建代理钱包(带事务)
func (s *AgentWalletStore) CreateWithTx(ctx context.Context, tx *gorm.DB, wallet *model.AgentWallet) error {
return tx.WithContext(ctx).Create(wallet).Error
}
// DeductFrozenBalanceWithTx 从冻结余额扣款(带事务)
// 用于提现完成后,从冻结余额中扣除金额
func (s *AgentWalletStore) DeductFrozenBalanceWithTx(ctx context.Context, tx *gorm.DB, walletID uint, amount int64) error {
// 扣除冻结余额和总余额
result := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND frozen_balance >= ?", walletID, amount).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", amount),
"frozen_balance": gorm.Expr("frozen_balance - ?", amount),
"updated_at": time.Now(),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound // 冻结余额不足
}
// 删除缓存
s.clearWalletCache(ctx, walletID)
return nil
}
// UnfreezeBalanceWithTx 解冻余额到可用余额(带事务)
// 用于提现取消,将冻结余额转回可用余额
func (s *AgentWalletStore) UnfreezeBalanceWithTx(ctx context.Context, tx *gorm.DB, walletID uint, amount int64) error {
// 减少冻结余额(总余额不变)
result := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND frozen_balance >= ?", walletID, amount).
Updates(map[string]interface{}{
"frozen_balance": gorm.Expr("frozen_balance - ?", amount),
"updated_at": time.Now(),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound // 冻结余额不足
}
// 删除缓存
s.clearWalletCache(ctx, walletID)
return nil
}
// FreezeBalanceWithTx 冻结余额(带事务,使用乐观锁)
// 用于提现申请,将可用余额转为冻结状态
func (s *AgentWalletStore) FreezeBalanceWithTx(ctx context.Context, tx *gorm.DB, walletID uint, amount int64, version int) error {
// 增加冻结余额(总余额不变),使用乐观锁
result := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND balance - frozen_balance >= ? AND version = ?", walletID, amount, version).
Updates(map[string]interface{}{
"frozen_balance": gorm.Expr("frozen_balance + ?", amount),
"version": gorm.Expr("version + 1"),
"updated_at": time.Now(),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound // 可用余额不足或版本冲突
}
// 删除缓存
s.clearWalletCache(ctx, walletID)
return nil
}
// AddBalanceWithTx 增加余额(带事务)
// 用于充值、退款等增加余额的操作
func (s *AgentWalletStore) AddBalanceWithTx(ctx context.Context, tx *gorm.DB, walletID uint, amount int64) error {
result := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ?", walletID).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance + ?", amount),
"updated_at": time.Now(),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
// 删除缓存
s.clearWalletCache(ctx, walletID)
return nil
}
// DeductBalanceWithTx 扣除余额(带事务,使用乐观锁)
// 用于扣款操作,检查可用余额是否充足
func (s *AgentWalletStore) DeductBalanceWithTx(ctx context.Context, tx *gorm.DB, walletID uint, amount int64, version int) error {
// 使用乐观锁,检查可用余额是否充足
result := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND balance - frozen_balance >= ? AND version = ?", walletID, amount, version).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", amount),
"version": gorm.Expr("version + 1"),
"updated_at": time.Now(),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound // 余额不足或版本冲突
}
// 删除缓存
s.clearWalletCache(ctx, walletID)
return nil
}
// GetShopCommissionSummaryBatch 批量获取店铺佣金钱包汇总
// 返回 map[shopID]*AgentWallet
func (s *AgentWalletStore) GetShopCommissionSummaryBatch(ctx context.Context, shopIDs []uint) (map[uint]*model.AgentWallet, error) {
if len(shopIDs) == 0 {
return make(map[uint]*model.AgentWallet), nil
}
var wallets []model.AgentWallet
err := s.db.WithContext(ctx).
Where("shop_id IN ? AND wallet_type = ?", shopIDs, constants.AgentWalletTypeCommission).
Find(&wallets).Error
if err != nil {
return nil, err
}
// 转换为 map
result := make(map[uint]*model.AgentWallet, len(wallets))
for i := range wallets {
result[wallets[i].ShopID] = &wallets[i]
}
return result, nil
}
// clearWalletCache 清除钱包缓存
func (s *AgentWalletStore) clearWalletCache(ctx context.Context, walletID uint) {
// 查询钱包信息以获取 shop_id 和 wallet_type
var wallet model.AgentWallet
if err := s.db.WithContext(ctx).Select("shop_id, wallet_type").First(&wallet, walletID).Error; err != nil {
return
}
cacheKey := constants.RedisAgentWalletBalanceKey(wallet.ShopID, wallet.WalletType)
_ = s.redis.Del(ctx, cacheKey).Err()
}

View File

@@ -0,0 +1,80 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// AgentWalletTransactionStore 代理钱包交易记录数据访问层
type AgentWalletTransactionStore struct {
db *gorm.DB
redis *redis.Client
}
// NewAgentWalletTransactionStore 创建代理钱包交易记录 Store
func NewAgentWalletTransactionStore(db *gorm.DB, redis *redis.Client) *AgentWalletTransactionStore {
return &AgentWalletTransactionStore{
db: db,
redis: redis,
}
}
// CreateWithTx 创建代理钱包交易记录(带事务)
func (s *AgentWalletTransactionStore) CreateWithTx(ctx context.Context, tx *gorm.DB, transaction *model.AgentWalletTransaction) error {
return tx.WithContext(ctx).Create(transaction).Error
}
// ListByShopID 按店铺查询交易记录(支持分页)
func (s *AgentWalletTransactionStore) ListByShopID(ctx context.Context, shopID uint, offset, limit int) ([]*model.AgentWalletTransaction, error) {
var transactions []*model.AgentWalletTransaction
err := s.db.WithContext(ctx).
Where("shop_id = ?", shopID).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&transactions).Error
if err != nil {
return nil, err
}
return transactions, nil
}
// CountByShopID 统计店铺的交易记录数量
func (s *AgentWalletTransactionStore) CountByShopID(ctx context.Context, shopID uint) (int64, error) {
var count int64
err := s.db.WithContext(ctx).
Model(&model.AgentWalletTransaction{}).
Where("shop_id = ?", shopID).
Count(&count).Error
return count, err
}
// ListByWalletID 按钱包查询交易记录(支持分页)
func (s *AgentWalletTransactionStore) ListByWalletID(ctx context.Context, walletID uint, offset, limit int) ([]*model.AgentWalletTransaction, error) {
var transactions []*model.AgentWalletTransaction
err := s.db.WithContext(ctx).
Where("agent_wallet_id = ?", walletID).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&transactions).Error
if err != nil {
return nil, err
}
return transactions, nil
}
// GetByReference 根据关联业务查询交易记录
func (s *AgentWalletTransactionStore) GetByReference(ctx context.Context, referenceType string, referenceID uint) (*model.AgentWalletTransaction, error) {
var transaction model.AgentWalletTransaction
err := s.db.WithContext(ctx).
Where("reference_type = ? AND reference_id = ?", referenceType, referenceID).
First(&transaction).Error
if err != nil {
return nil, err
}
return &transaction, nil
}

View File

@@ -0,0 +1,240 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// CardRechargeStore 卡充值记录数据访问层
type CardRechargeStore struct {
db *gorm.DB
redis *redis.Client
}
// NewCardRechargeStore 创建卡充值记录 Store
func NewCardRechargeStore(db *gorm.DB, redis *redis.Client) *CardRechargeStore {
return &CardRechargeStore{
db: db,
redis: redis,
}
}
// Create 创建充值记录
func (s *CardRechargeStore) Create(ctx context.Context, record *model.CardRechargeRecord) error {
return s.db.WithContext(ctx).Create(record).Error
}
// CreateWithTx 创建充值记录(带事务)
func (s *CardRechargeStore) CreateWithTx(ctx context.Context, tx *gorm.DB, record *model.CardRechargeRecord) error {
return tx.WithContext(ctx).Create(record).Error
}
// GetByRechargeNo 根据充值订单号查询
func (s *CardRechargeStore) GetByRechargeNo(ctx context.Context, rechargeNo string) (*model.CardRechargeRecord, error) {
var record model.CardRechargeRecord
err := s.db.WithContext(ctx).
Where("recharge_no = ?", rechargeNo).
First(&record).Error
if err != nil {
return nil, err
}
return &record, nil
}
// GetByID 根据 ID 查询
func (s *CardRechargeStore) GetByID(ctx context.Context, id uint) (*model.CardRechargeRecord, error) {
var record model.CardRechargeRecord
if err := s.db.WithContext(ctx).First(&record, id).Error; err != nil {
return nil, err
}
return &record, nil
}
// UpdateStatus 更新充值状态
func (s *CardRechargeStore) UpdateStatus(ctx context.Context, id uint, status int) error {
return s.db.WithContext(ctx).
Model(&model.CardRechargeRecord{}).
Where("id = ?", id).
Update("status", status).Error
}
// UpdateStatusWithTx 更新充值状态(带事务)
func (s *CardRechargeStore) UpdateStatusWithTx(ctx context.Context, tx *gorm.DB, id uint, status int) error {
return tx.WithContext(ctx).
Model(&model.CardRechargeRecord{}).
Where("id = ?", id).
Update("status", status).Error
}
// Update 更新充值记录
func (s *CardRechargeStore) Update(ctx context.Context, record *model.CardRechargeRecord) error {
return s.db.WithContext(ctx).Save(record).Error
}
// UpdateWithTx 更新充值记录(带事务)
func (s *CardRechargeStore) UpdateWithTx(ctx context.Context, tx *gorm.DB, record *model.CardRechargeRecord) error {
return tx.WithContext(ctx).Save(record).Error
}
// ListByResourceID 按资源查询充值记录(支持分页)
func (s *CardRechargeStore) ListByResourceID(ctx context.Context, resourceType string, resourceID uint, offset, limit int) ([]*model.CardRechargeRecord, error) {
var records []*model.CardRechargeRecord
err := s.db.WithContext(ctx).
Where("resource_type = ? AND resource_id = ?", resourceType, resourceID).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
// ListByUserID 按用户查询充值记录(支持分页)
func (s *CardRechargeStore) ListByUserID(ctx context.Context, userID uint, offset, limit int) ([]*model.CardRechargeRecord, error) {
var records []*model.CardRechargeRecord
err := s.db.WithContext(ctx).
Where("user_id = ?", userID).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
// ListByStatus 按状态查询充值记录(支持分页)
func (s *CardRechargeStore) ListByStatus(ctx context.Context, status int, offset, limit int) ([]*model.CardRechargeRecord, error) {
var records []*model.CardRechargeRecord
err := s.db.WithContext(ctx).
Where("status = ?", status).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
// ListRechargeParams 充值记录列表查询参数
type ListCardRechargeParams struct {
Page int
PageSize int
UserID *uint
CardWalletID *uint
ResourceType *string
ResourceID *uint
Status *int
StartTime interface{}
EndTime interface{}
}
// List 查询充值记录列表(支持分页和筛选)
func (s *CardRechargeStore) List(ctx context.Context, params *ListCardRechargeParams) ([]*model.CardRechargeRecord, int64, error) {
var records []*model.CardRechargeRecord
var total int64
query := s.db.WithContext(ctx).Model(&model.CardRechargeRecord{})
if params.UserID != nil {
query = query.Where("user_id = ?", *params.UserID)
}
if params.CardWalletID != nil {
query = query.Where("card_wallet_id = ?", *params.CardWalletID)
}
if params.ResourceType != nil {
query = query.Where("resource_type = ?", *params.ResourceType)
}
if params.ResourceID != nil {
query = query.Where("resource_id = ?", *params.ResourceID)
}
if params.Status != nil {
query = query.Where("status = ?", *params.Status)
}
if params.StartTime != nil {
query = query.Where("created_at >= ?", params.StartTime)
}
if params.EndTime != nil {
query = query.Where("created_at <= ?", params.EndTime)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
page := params.Page
if page < 1 {
page = 1
}
pageSize := params.PageSize
if pageSize < 1 {
pageSize = 20
}
offset := (page - 1) * pageSize
if err := query.Order("id DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
return nil, 0, err
}
return records, total, nil
}
// UpdatePaymentInfo 更新支付信息
func (s *CardRechargeStore) UpdatePaymentInfo(ctx context.Context, id uint, paymentMethod *string, paymentTransactionID *string) error {
updates := map[string]interface{}{}
if paymentMethod != nil {
updates["payment_method"] = paymentMethod
}
if paymentTransactionID != nil {
updates["payment_transaction_id"] = paymentTransactionID
}
if len(updates) == 0 {
return nil
}
result := s.db.WithContext(ctx).Model(&model.CardRechargeRecord{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// UpdateStatusWithOptimisticLock 更新充值状态(支持乐观锁)
func (s *CardRechargeStore) UpdateStatusWithOptimisticLock(ctx context.Context, id uint, oldStatus *int, newStatus int, paidAt interface{}, completedAt interface{}) error {
updates := map[string]interface{}{
"status": newStatus,
}
if paidAt != nil {
updates["paid_at"] = paidAt
}
if completedAt != nil {
updates["completed_at"] = completedAt
}
query := s.db.WithContext(ctx).Model(&model.CardRechargeRecord{}).Where("id = ?", id)
if oldStatus != nil {
query = query.Where("status = ?", *oldStatus)
}
result := query.Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}

View File

@@ -0,0 +1,116 @@
package postgres
import (
"context"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// CardWalletStore 卡钱包数据访问层
type CardWalletStore struct {
db *gorm.DB
redis *redis.Client
}
// NewCardWalletStore 创建卡钱包 Store
func NewCardWalletStore(db *gorm.DB, redis *redis.Client) *CardWalletStore {
return &CardWalletStore{
db: db,
redis: redis,
}
}
// GetByResourceTypeAndID 根据资源类型和 ID 查询钱包
func (s *CardWalletStore) GetByResourceTypeAndID(ctx context.Context, resourceType string, resourceID uint) (*model.CardWallet, error) {
var wallet model.CardWallet
err := s.db.WithContext(ctx).
Where("resource_type = ? AND resource_id = ?", resourceType, resourceID).
First(&wallet).Error
if err != nil {
return nil, err
}
return &wallet, nil
}
// GetByID 根据钱包 ID 查询
func (s *CardWalletStore) GetByID(ctx context.Context, id uint) (*model.CardWallet, error) {
var wallet model.CardWallet
if err := s.db.WithContext(ctx).First(&wallet, id).Error; err != nil {
return nil, err
}
return &wallet, nil
}
// Create 创建卡钱包
func (s *CardWalletStore) Create(ctx context.Context, wallet *model.CardWallet) error {
return s.db.WithContext(ctx).Create(wallet).Error
}
// CreateWithTx 创建卡钱包(带事务)
func (s *CardWalletStore) CreateWithTx(ctx context.Context, tx *gorm.DB, wallet *model.CardWallet) error {
return tx.WithContext(ctx).Create(wallet).Error
}
// DeductBalanceWithTx 扣款(带事务,使用乐观锁)
func (s *CardWalletStore) DeductBalanceWithTx(ctx context.Context, tx *gorm.DB, walletID uint, amount int64, version int) error {
// 使用乐观锁,检查可用余额是否充足
result := tx.WithContext(ctx).Model(&model.CardWallet{}).
Where("id = ? AND balance - frozen_balance >= ? AND version = ?", walletID, amount, version).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", amount),
"version": gorm.Expr("version + 1"),
"updated_at": time.Now(),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound // 余额不足或版本冲突
}
// 删除缓存
s.clearWalletCache(ctx, walletID)
return nil
}
// AddBalanceWithTx 增加余额(带事务)
func (s *CardWalletStore) AddBalanceWithTx(ctx context.Context, tx *gorm.DB, walletID uint, amount int64) error {
result := tx.WithContext(ctx).Model(&model.CardWallet{}).
Where("id = ?", walletID).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance + ?", amount),
"updated_at": time.Now(),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
// 删除缓存
s.clearWalletCache(ctx, walletID)
return nil
}
// clearWalletCache 清除钱包缓存
func (s *CardWalletStore) clearWalletCache(ctx context.Context, walletID uint) {
// 查询钱包信息以获取 resource_type 和 resource_id
var wallet model.CardWallet
if err := s.db.WithContext(ctx).Select("resource_type, resource_id").First(&wallet, walletID).Error; err != nil {
return
}
cacheKey := constants.RedisCardWalletBalanceKey(wallet.ResourceType, wallet.ResourceID)
_ = s.redis.Del(ctx, cacheKey).Err()
}

View File

@@ -0,0 +1,80 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// CardWalletTransactionStore 卡钱包交易记录数据访问层
type CardWalletTransactionStore struct {
db *gorm.DB
redis *redis.Client
}
// NewCardWalletTransactionStore 创建卡钱包交易记录 Store
func NewCardWalletTransactionStore(db *gorm.DB, redis *redis.Client) *CardWalletTransactionStore {
return &CardWalletTransactionStore{
db: db,
redis: redis,
}
}
// CreateWithTx 创建卡钱包交易记录(带事务)
func (s *CardWalletTransactionStore) CreateWithTx(ctx context.Context, tx *gorm.DB, transaction *model.CardWalletTransaction) error {
return tx.WithContext(ctx).Create(transaction).Error
}
// ListByResourceID 按资源查询交易记录(支持分页)
func (s *CardWalletTransactionStore) ListByResourceID(ctx context.Context, resourceType string, resourceID uint, offset, limit int) ([]*model.CardWalletTransaction, error) {
var transactions []*model.CardWalletTransaction
err := s.db.WithContext(ctx).
Where("resource_type = ? AND resource_id = ?", resourceType, resourceID).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&transactions).Error
if err != nil {
return nil, err
}
return transactions, nil
}
// CountByResourceID 统计资源的交易记录数量
func (s *CardWalletTransactionStore) CountByResourceID(ctx context.Context, resourceType string, resourceID uint) (int64, error) {
var count int64
err := s.db.WithContext(ctx).
Model(&model.CardWalletTransaction{}).
Where("resource_type = ? AND resource_id = ?", resourceType, resourceID).
Count(&count).Error
return count, err
}
// ListByWalletID 按钱包查询交易记录(支持分页)
func (s *CardWalletTransactionStore) ListByWalletID(ctx context.Context, walletID uint, offset, limit int) ([]*model.CardWalletTransaction, error) {
var transactions []*model.CardWalletTransaction
err := s.db.WithContext(ctx).
Where("card_wallet_id = ?", walletID).
Order("created_at DESC").
Offset(offset).
Limit(limit).
Find(&transactions).Error
if err != nil {
return nil, err
}
return transactions, nil
}
// GetByReference 根据关联业务查询交易记录
func (s *CardWalletTransactionStore) GetByReference(ctx context.Context, referenceType string, referenceID uint) (*model.CardWalletTransaction, error) {
var transaction model.CardWalletTransaction
err := s.db.WithContext(ctx).
Where("reference_type = ? AND reference_id = ?", referenceType, referenceID).
First(&transaction).Error
if err != nil {
return nil, err
}
return &transaction, nil
}

View File

@@ -1,166 +0,0 @@
package postgres
import (
"context"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type RechargeStore struct {
db *gorm.DB
redis *redis.Client
}
// NewRechargeStore 创建充值订单 Store 实例
func NewRechargeStore(db *gorm.DB, redis *redis.Client) *RechargeStore {
return &RechargeStore{
db: db,
redis: redis,
}
}
// Create 创建充值订单
func (s *RechargeStore) Create(ctx context.Context, recharge *model.RechargeRecord) error {
return s.db.WithContext(ctx).Create(recharge).Error
}
// GetByRechargeNo 根据充值订单号查询充值订单
// 不存在时返回 nil, nil
func (s *RechargeStore) GetByRechargeNo(ctx context.Context, rechargeNo string) (*model.RechargeRecord, error) {
var recharge model.RechargeRecord
err := s.db.WithContext(ctx).Where("recharge_no = ?", rechargeNo).First(&recharge).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &recharge, nil
}
// GetByID 根据 ID 查询充值订单
func (s *RechargeStore) GetByID(ctx context.Context, id uint) (*model.RechargeRecord, error) {
var recharge model.RechargeRecord
if err := s.db.WithContext(ctx).First(&recharge, id).Error; err != nil {
return nil, err
}
return &recharge, nil
}
// ListRechargeParams 充值订单列表查询参数
type ListRechargeParams struct {
Page int // 页码(从 1 开始)
PageSize int // 每页数量
UserID *uint // 用户 ID 筛选
WalletID *uint // 钱包 ID 筛选
Status *int // 状态筛选
StartTime *time.Time // 开始时间
EndTime *time.Time // 结束时间
}
// List 查询充值订单列表(支持分页和筛选)
func (s *RechargeStore) List(ctx context.Context, params *ListRechargeParams) ([]*model.RechargeRecord, int64, error) {
var recharges []*model.RechargeRecord
var total int64
query := s.db.WithContext(ctx).Model(&model.RechargeRecord{})
// 应用筛选条件
if params.UserID != nil {
query = query.Where("user_id = ?", *params.UserID)
}
if params.WalletID != nil {
query = query.Where("wallet_id = ?", *params.WalletID)
}
if params.Status != nil {
query = query.Where("status = ?", *params.Status)
}
if params.StartTime != nil {
query = query.Where("created_at >= ?", *params.StartTime)
}
if params.EndTime != nil {
query = query.Where("created_at <= ?", *params.EndTime)
}
// 统计总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
page := params.Page
if page < 1 {
page = 1
}
pageSize := params.PageSize
if pageSize < 1 {
pageSize = 20
}
offset := (page - 1) * pageSize
if err := query.Order("id DESC").Offset(offset).Limit(pageSize).Find(&recharges).Error; err != nil {
return nil, 0, err
}
return recharges, total, nil
}
// UpdateStatus 更新充值订单状态(支持乐观锁检查)
// oldStatus: 原状态(用于乐观锁检查,传 nil 则跳过检查)
// newStatus: 新状态
// paidAt: 支付时间(状态变为已支付时传入)
// completedAt: 完成时间(状态变为已完成时传入)
func (s *RechargeStore) UpdateStatus(ctx context.Context, id uint, oldStatus *int, newStatus int, paidAt *time.Time, completedAt *time.Time) error {
updates := map[string]interface{}{
"status": newStatus,
}
if paidAt != nil {
updates["paid_at"] = paidAt
}
if completedAt != nil {
updates["completed_at"] = completedAt
}
query := s.db.WithContext(ctx).Model(&model.RechargeRecord{}).Where("id = ?", id)
// 乐观锁检查
if oldStatus != nil {
query = query.Where("status = ?", *oldStatus)
}
result := query.Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// UpdatePaymentInfo 更新支付信息
func (s *RechargeStore) UpdatePaymentInfo(ctx context.Context, id uint, paymentChannel *string, paymentTransactionID *string) error {
updates := map[string]interface{}{}
if paymentChannel != nil {
updates["payment_channel"] = paymentChannel
}
if paymentTransactionID != nil {
updates["payment_transaction_id"] = paymentTransactionID
}
if len(updates) == 0 {
return nil
}
result := s.db.WithContext(ctx).Model(&model.RechargeRecord{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}

View File

@@ -1,108 +0,0 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type WalletStore struct {
db *gorm.DB
redis *redis.Client
}
func NewWalletStore(db *gorm.DB, redis *redis.Client) *WalletStore {
return &WalletStore{
db: db,
redis: redis,
}
}
func (s *WalletStore) GetByResourceTypeAndID(ctx context.Context, resourceType string, resourceID uint, walletType string) (*model.Wallet, error) {
var wallet model.Wallet
err := s.db.WithContext(ctx).
Where("resource_type = ? AND resource_id = ? AND wallet_type = ?", resourceType, resourceID, walletType).
First(&wallet).Error
if err != nil {
return nil, err
}
return &wallet, nil
}
func (s *WalletStore) GetShopCommissionWallet(ctx context.Context, shopID uint) (*model.Wallet, error) {
return s.GetByResourceTypeAndID(ctx, "shop", shopID, "commission")
}
type ShopCommissionSummary struct {
ShopID uint
Balance int64
FrozenBalance int64
}
func (s *WalletStore) GetShopCommissionSummaryBatch(ctx context.Context, shopIDs []uint) (map[uint]*ShopCommissionSummary, error) {
if len(shopIDs) == 0 {
return make(map[uint]*ShopCommissionSummary), nil
}
var wallets []model.Wallet
err := s.db.WithContext(ctx).
Where("resource_type = ? AND resource_id IN ? AND wallet_type = ?", "shop", shopIDs, "commission").
Find(&wallets).Error
if err != nil {
return nil, err
}
result := make(map[uint]*ShopCommissionSummary)
for _, w := range wallets {
result[w.ResourceID] = &ShopCommissionSummary{
ShopID: w.ResourceID,
Balance: w.Balance,
FrozenBalance: w.FrozenBalance,
}
}
return result, nil
}
func (s *WalletStore) GetByID(ctx context.Context, id uint) (*model.Wallet, error) {
var wallet model.Wallet
if err := s.db.WithContext(ctx).First(&wallet, id).Error; err != nil {
return nil, err
}
return &wallet, nil
}
func (s *WalletStore) DeductFrozenBalanceWithTx(ctx context.Context, tx *gorm.DB, walletID uint, amount int64) error {
result := tx.WithContext(ctx).
Model(&model.Wallet{}).
Where("id = ? AND frozen_balance >= ?", walletID, amount).
Updates(map[string]interface{}{
"frozen_balance": gorm.Expr("frozen_balance - ?", amount),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (s *WalletStore) UnfreezeBalanceWithTx(ctx context.Context, tx *gorm.DB, walletID uint, amount int64) error {
result := tx.WithContext(ctx).
Model(&model.Wallet{}).
Where("id = ? AND frozen_balance >= ?", walletID, amount).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance + ?", amount),
"frozen_balance": gorm.Expr("frozen_balance - ?", amount),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}

View File

@@ -1,37 +0,0 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type WalletTransactionStore struct {
db *gorm.DB
redis *redis.Client
}
func NewWalletTransactionStore(db *gorm.DB, redis *redis.Client) *WalletTransactionStore {
return &WalletTransactionStore{
db: db,
redis: redis,
}
}
func (s *WalletTransactionStore) CreateWithTx(ctx context.Context, tx *gorm.DB, transaction *model.WalletTransaction) error {
return tx.WithContext(ctx).Create(transaction).Error
}
func (s *WalletTransactionStore) Create(ctx context.Context, transaction *model.WalletTransaction) error {
return s.db.WithContext(ctx).Create(transaction).Error
}
func (s *WalletTransactionStore) GetByID(ctx context.Context, id uint) (*model.WalletTransaction, error) {
var tx model.WalletTransaction
if err := s.db.WithContext(ctx).First(&tx, id).Error; err != nil {
return nil, err
}
return &tx, nil
}

View File

@@ -28,16 +28,16 @@ type PollingTaskPayload struct {
// PollingHandler 轮询任务处理器
type PollingHandler struct {
db *gorm.DB
redis *redis.Client
gatewayClient *gateway.Client
iotCardStore *postgres.IotCardStore
concurrencyStore *postgres.PollingConcurrencyConfigStore
deviceSimBindingStore *postgres.DeviceSimBindingStore
dataUsageRecordStore *postgres.DataUsageRecordStore
packageUsageStore *postgres.PackageUsageStore
usageService *packagepkg.UsageService
logger *zap.Logger
db *gorm.DB
redis *redis.Client
gatewayClient *gateway.Client
iotCardStore *postgres.IotCardStore
concurrencyStore *postgres.PollingConcurrencyConfigStore
deviceSimBindingStore *postgres.DeviceSimBindingStore
dataUsageRecordStore *postgres.DataUsageRecordStore
packageUsageStore *postgres.PackageUsageStore
usageService *packagepkg.UsageService
logger *zap.Logger
}
// NewPollingHandler 创建轮询任务处理器
@@ -925,14 +925,14 @@ func (h *PollingHandler) getCardWithCache(ctx context.Context, cardID uint) (*mo
go func() {
cacheCtx := context.Background()
cacheData := map[string]any{
"id": card.ID,
"iccid": card.ICCID,
"card_category": card.CardCategory,
"real_name_status": card.RealNameStatus,
"network_status": card.NetworkStatus,
"carrier_id": card.CarrierID,
"id": card.ID,
"iccid": card.ICCID,
"card_category": card.CardCategory,
"real_name_status": card.RealNameStatus,
"network_status": card.NetworkStatus,
"carrier_id": card.CarrierID,
"current_month_usage_mb": card.CurrentMonthUsageMB,
"cached_at": time.Now().Unix(),
"cached_at": time.Now().Unix(),
}
if card.SeriesID != nil {
cacheData["series_id"] = *card.SeriesID