diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index b707bfb..fe86d19 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -1557,6 +1557,16 @@ components: description: 卡ID minimum: 0 type: integer + carrier_id: + description: 运营商ID + minimum: 0 + type: integer + carrier_name: + description: 运营商名称 + type: string + carrier_type: + description: 运营商类型 + type: string iccid: description: ICCID type: string @@ -1731,6 +1741,9 @@ components: type: object DtoClientCreateOrderResponse: properties: + idempotent: + description: 幂等标志(true 表示返回的是已存在的待支付充值订单) + type: boolean linked_package_info: $ref: '#/components/schemas/DtoLinkedPackageInfo' order: @@ -23368,6 +23381,98 @@ paths: summary: 获取实名认证链接 tags: - 个人客户 - 实名 + /api/c/v1/recharge-orders: + get: + parameters: + - description: 页码 + in: query + name: page + schema: + description: 页码 + type: integer + - description: 每页数量 + in: query + name: page_size + schema: + description: 每页数量 + type: integer + - description: 状态过滤 (0:待支付, 1:已支付, 2:已关闭, 3:已退款) + in: query + name: status + schema: + description: 状态过滤 (0:待支付, 1:已支付, 2:已关闭, 3:已退款) + type: integer + responses: + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 请求参数错误 + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 未认证或认证已过期 + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 无权访问 + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 服务器内部错误 + security: + - BearerAuth: [] + summary: 充值订单列表 + tags: + - 个人客户 - 充值订单 + /api/c/v1/recharge-orders/{id}: + get: + parameters: + - description: ID + in: path + name: id + required: true + schema: + description: ID + minimum: 0 + type: integer + responses: + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 请求参数错误 + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 未认证或认证已过期 + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 无权访问 + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 服务器内部错误 + security: + - BearerAuth: [] + summary: 充值订单详情 + tags: + - 个人客户 - 充值订单 /api/c/v1/wallet/detail: get: parameters: diff --git a/internal/bootstrap/handlers.go b/internal/bootstrap/handlers.go index c132fe3..2fbb316 100644 --- a/internal/bootstrap/handlers.go +++ b/internal/bootstrap/handlers.go @@ -8,6 +8,7 @@ import ( pollingPkg "github.com/break/junhong_cmp_fiber/internal/polling" clientOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/client_order" pollingSvcPkg "github.com/break/junhong_cmp_fiber/internal/service/polling" + rechargeOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge_order" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/go-playground/validator/v10" @@ -22,7 +23,6 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers { iotCardStore := postgres.NewIotCardStore(deps.DB, deps.Redis) deviceStore := postgres.NewDeviceStore(deps.DB, deps.Redis) assetWalletTransactionStore := postgres.NewAssetWalletTransactionStore(deps.DB, deps.Redis) - assetRechargeStore := postgres.NewAssetRechargeStore(deps.DB, deps.Redis) personalCustomerOpenIDStore := postgres.NewPersonalCustomerOpenIDStore(deps.DB) personalCustomerStore := postgres.NewPersonalCustomerStore(deps.DB, deps.Redis) personalCustomerPhoneStore := postgres.NewPersonalCustomerPhoneStore(deps.DB) @@ -31,11 +31,29 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers { shopSeriesAllocationStore := postgres.NewShopSeriesAllocationStore(deps.DB) deviceSimBindingStore := postgres.NewDeviceSimBindingStore(deps.DB, deps.Redis) carrierStore := postgres.NewCarrierStore(deps.DB) + rechargeOrderStore := postgres.NewRechargeOrderStore(deps.DB, deps.Redis) + paymentStore := postgres.NewPaymentStore(deps.DB, deps.Redis) + commissionRecordStore := postgres.NewCommissionRecordStore(deps.DB, deps.Redis) + rechargeOrderService := rechargeOrderSvc.New( + deps.DB, + rechargeOrderStore, + paymentStore, + assetWalletStore, + assetWalletTransactionStore, + iotCardStore, + deviceStore, + packageSeriesStore, + shopSeriesAllocationStore, + commissionRecordStore, + deps.QueueClient, + deps.Logger, + ) clientOrderService := clientOrderSvc.New( svc.Asset, svc.PurchaseValidation, orderStore, - assetRechargeStore, + rechargeOrderStore, + paymentStore, assetWalletStore, personalCustomerDeviceStore, personalCustomerOpenIDStore, @@ -60,11 +78,12 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers { PersonalCustomer: app.NewPersonalCustomerHandler(svc.PersonalCustomer, deps.Logger), ClientAuth: app.NewClientAuthHandler(svc.ClientAuth, deps.Logger), ClientAsset: app.NewClientAssetHandler(svc.Asset, personalCustomerDeviceStore, assetWalletStore, packageStore, shopPackageAllocationStore, iotCardStore, deviceStore, deps.DB, deps.Logger), - ClientWallet: app.NewClientWalletHandler(svc.Asset, personalCustomerDeviceStore, assetWalletStore, assetWalletTransactionStore, assetRechargeStore, svc.Recharge, personalCustomerOpenIDStore, svc.WechatConfig, deps.Redis, deps.Logger, deps.DB, iotCardStore, deviceStore), + ClientWallet: app.NewClientWalletHandler(svc.Asset, personalCustomerDeviceStore, assetWalletStore, assetWalletTransactionStore, rechargeOrderStore, paymentStore, svc.Recharge, personalCustomerOpenIDStore, svc.WechatConfig, deps.Redis, deps.Logger, deps.DB, iotCardStore, deviceStore), ClientOrder: app.NewClientOrderHandler(clientOrderService, deps.Logger), ClientExchange: app.NewClientExchangeHandler(svc.Exchange), ClientRealname: app.NewClientRealnameHandler(svc.Asset, personalCustomerDeviceStore, iotCardStore, deviceSimBindingStore, carrierStore, deps.GatewayClient, deps.Logger, svc.PollingManualTrigger), ClientDevice: app.NewClientDeviceHandler(svc.Asset, personalCustomerDeviceStore, deviceStore, deviceSimBindingStore, iotCardStore, deps.GatewayClient, deps.Logger), + ClientRechargeOrder: app.NewClientRechargeOrderHandler(rechargeOrderStore, paymentStore, deps.Logger), Shop: admin.NewShopHandler(svc.Shop), ShopRole: admin.NewShopRoleHandler(svc.Shop), AdminAuth: admin.NewAuthHandler(svc.Auth, validate), @@ -90,7 +109,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers { ShopSeriesGrant: admin.NewShopSeriesGrantHandler(svc.ShopSeriesGrant), AdminOrder: admin.NewOrderHandler(svc.Order, validate), AdminExchange: admin.NewExchangeHandler(svc.Exchange, validate), - PaymentCallback: callback.NewPaymentHandler(svc.Order, svc.Recharge, svc.AgentRecharge, deps.WechatPayment, svc.WechatConfig, deps.Logger), + PaymentCallback: callback.NewPaymentHandler(svc.Order, svc.Recharge, rechargeOrderService, svc.AgentRecharge, deps.WechatPayment, svc.WechatConfig, paymentStore, deps.Logger), PollingConfig: admin.NewPollingConfigHandler(svc.PollingConfig), PollingConcurrency: admin.NewPollingConcurrencyHandler(svc.PollingConcurrency), PollingMonitoring: admin.NewPollingMonitoringHandler(svc.PollingMonitoring), diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index 4780dd4..7f2ee39 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -124,7 +124,7 @@ func initServices(s *stores, deps *Dependencies) *services { iotCard.SetDataDeductor(usageService) // 创建支付配置服务(Order 和 Recharge 依赖) - wechatConfig := wechatConfigSvc.New(s.WechatConfig, s.Order, s.AssetRecharge, s.AgentRecharge, accountAudit, deps.Redis, deps.Logger) + wechatConfig := wechatConfigSvc.New(s.WechatConfig, s.Order, s.RechargeOrder, s.AgentRecharge, accountAudit, deps.Redis, deps.Logger) // 创建支付配置动态加载器(Order 和 Recharge 依赖) paymentLoader := payment.NewPaymentConfigLoader(s.WechatConfig, deps.Redis, deps.Logger) @@ -203,9 +203,9 @@ func initServices(s *stores, deps *Dependencies) *services { ShopSeriesGrant: shopSeriesGrantSvc.New(deps.DB, s.ShopSeriesAllocation, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package, s.PackageSeries, deps.Logger), CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats), PurchaseValidation: purchaseValidation, - Order: orderSvc.New(deps.DB, deps.Redis, s.Order, s.OrderItem, s.AgentWallet, s.AssetWallet, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, s.PackageUsage, s.Package, wechatConfig, deps.WechatPayment, paymentLoader, deps.QueueClient, deps.Logger, s.AssetIdentifier, s.PersonalCustomer, s.PersonalCustomerPhone), + Order: orderSvc.New(deps.DB, deps.Redis, s.Order, s.OrderItem, s.AgentWallet, s.AssetWallet, s.Payment, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, s.PackageUsage, s.Package, wechatConfig, deps.WechatPayment, paymentLoader, deps.QueueClient, deps.Logger, s.AssetIdentifier, s.PersonalCustomer, s.PersonalCustomerPhone), Exchange: exchangeSvc.New(deps.DB, s.ExchangeOrder, s.IotCard, s.Device, s.AssetWallet, s.AssetWalletTransaction, s.PackageUsage, s.PackageUsageDailyRecord, s.ResourceTag, s.PersonalCustomerDevice, deps.Logger), - Recharge: rechargeSvc.New(deps.DB, s.AssetRecharge, s.AssetWallet, s.AssetWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, wechatConfig, paymentLoader, deps.Logger, deps.QueueClient), + Recharge: rechargeSvc.New(deps.DB, s.AssetWallet, s.AssetWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, wechatConfig, paymentLoader, deps.Logger), PollingConfig: pollingSvc.NewConfigService(s.PollingConfig), PollingConcurrency: pollingSvc.NewConcurrencyService(s.PollingConcurrencyConfig, deps.Redis), PollingMonitoring: pollingSvc.NewMonitoringService(deps.Redis), diff --git a/internal/bootstrap/stores.go b/internal/bootstrap/stores.go index 8f8599c..ef93a9c 100644 --- a/internal/bootstrap/stores.go +++ b/internal/bootstrap/stores.go @@ -56,7 +56,9 @@ type stores struct { // 资产钱包系统 AssetWallet *postgres.AssetWalletStore AssetWalletTransaction *postgres.AssetWalletTransactionStore - AssetRecharge *postgres.AssetRechargeStore + // 充值订单和支付记录 + RechargeOrder *postgres.RechargeOrderStore + Payment *postgres.PaymentStore // 微信参数配置 WechatConfig *postgres.WechatConfigStore // 退款系统 @@ -120,7 +122,8 @@ func initStores(deps *Dependencies) *stores { // 资产钱包系统 AssetWallet: postgres.NewAssetWalletStore(deps.DB, deps.Redis), AssetWalletTransaction: postgres.NewAssetWalletTransactionStore(deps.DB, deps.Redis), - AssetRecharge: postgres.NewAssetRechargeStore(deps.DB, deps.Redis), + RechargeOrder: postgres.NewRechargeOrderStore(deps.DB, deps.Redis), + Payment: postgres.NewPaymentStore(deps.DB, deps.Redis), WechatConfig: postgres.NewWechatConfigStore(deps.DB, deps.Redis), RefundRequest: postgres.NewRefundStore(deps.DB), CardDailyUsage: postgres.NewCardDailyUsageStore(deps.DB), diff --git a/internal/bootstrap/types.go b/internal/bootstrap/types.go index 32d60ef..c66856c 100644 --- a/internal/bootstrap/types.go +++ b/internal/bootstrap/types.go @@ -22,6 +22,7 @@ type Handlers struct { ClientExchange *app.ClientExchangeHandler ClientRealname *app.ClientRealnameHandler ClientDevice *app.ClientDeviceHandler + ClientRechargeOrder *app.ClientRechargeOrderHandler Shop *admin.ShopHandler ShopRole *admin.ShopRoleHandler AdminAuth *admin.AuthHandler diff --git a/internal/bootstrap/worker_services.go b/internal/bootstrap/worker_services.go index 52700a5..8c87235 100644 --- a/internal/bootstrap/worker_services.go +++ b/internal/bootstrap/worker_services.go @@ -88,6 +88,7 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q stores.OrderItem, stores.AgentWallet, stores.AssetWallet, + nil, // paymentStore: 超时取消不需要 nil, // purchaseValidationService: 超时取消不需要 stores.ShopPackageAllocation, stores.ShopSeriesAllocation, diff --git a/internal/handler/app/client_recharge_order.go b/internal/handler/app/client_recharge_order.go new file mode 100644 index 0000000..182d94d --- /dev/null +++ b/internal/handler/app/client_recharge_order.go @@ -0,0 +1,196 @@ +package app + +import ( + "context" + "strconv" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" + + "github.com/break/junhong_cmp_fiber/internal/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/store/postgres" + "github.com/break/junhong_cmp_fiber/pkg/constants" + "github.com/break/junhong_cmp_fiber/pkg/errors" + "github.com/break/junhong_cmp_fiber/pkg/response" +) + +type ClientRechargeOrderHandler struct { + rechargeOrderStore *postgres.RechargeOrderStore + paymentStore *postgres.PaymentStore + logger *zap.Logger +} + +func NewClientRechargeOrderHandler( + rechargeOrderStore *postgres.RechargeOrderStore, + paymentStore *postgres.PaymentStore, + logger *zap.Logger, +) *ClientRechargeOrderHandler { + return &ClientRechargeOrderHandler{ + rechargeOrderStore: rechargeOrderStore, + paymentStore: paymentStore, + logger: logger, + } +} + +func (h *ClientRechargeOrderHandler) ListRechargeOrders(c *fiber.Ctx) error { + var req dto.ClientRechargeOrderListRequest + if err := c.QueryParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam) + } + + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 { + req.PageSize = constants.DefaultPageSize + } + if req.PageSize > constants.MaxPageSize { + req.PageSize = constants.MaxPageSize + } + + customerID, ok := middleware.GetCustomerID(c) + if !ok || customerID == 0 { + return errors.New(errors.CodeUnauthorized) + } + + skipCtx := context.WithValue(c.UserContext(), constants.ContextKeySubordinateShopIDs, []uint{}) + + params := &postgres.ListRechargeOrderParams{ + Page: req.Page, + PageSize: req.PageSize, + UserID: &customerID, + } + if req.Status > 0 { + params.Status = &req.Status + } + + orders, total, err := h.rechargeOrderStore.List(skipCtx, params) + if err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") + } + + items := make([]dto.ClientRechargeOrderListItem, 0, len(orders)) + for _, order := range orders { + if order == nil { + continue + } + items = append(items, dto.ClientRechargeOrderListItem{ + RechargeOrderID: order.ID, + RechargeOrderNo: order.RechargeOrderNo, + Amount: order.Amount, + Status: order.Status, + StatusName: getRechargeOrderStatusName(order.Status), + AutoPurchaseStatus: order.AutoPurchaseStatus, + CreatedAt: order.CreatedAt.Format(time.RFC3339), + }) + } + + return response.SuccessWithPagination(c, items, total, req.Page, req.PageSize) +} + +func (h *ClientRechargeOrderHandler) GetRechargeOrderDetail(c *fiber.Ctx) error { + idStr := c.Params("id") + if idStr == "" { + return errors.New(errors.CodeInvalidParam) + } + + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return errors.New(errors.CodeInvalidParam) + } + + customerID, ok := middleware.GetCustomerID(c) + if !ok || customerID == 0 { + return errors.New(errors.CodeUnauthorized) + } + + skipCtx := context.WithValue(c.UserContext(), constants.ContextKeySubordinateShopIDs, []uint{}) + + order, err := h.rechargeOrderStore.GetByID(skipCtx, uint(id)) + if err != nil { + if strings.Contains(err.Error(), "record not found") { + return errors.New(errors.CodeNotFound, "充值订单不存在") + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") + } + + if order.UserID != customerID { + return errors.New(errors.CodeForbidden, "无权访问该充值订单") + } + + payments, _ := h.paymentStore.ListByOrderID(skipCtx, order.ID, model.PaymentOrderTypeRecharge) + + var paymentDetails []dto.ClientRechargeOrderPaymentDetail + for _, p := range payments { + if p == nil { + continue + } + paymentDetails = append(paymentDetails, dto.ClientRechargeOrderPaymentDetail{ + PaymentID: p.ID, + PaymentNo: p.PaymentNo, + PaymentMethod: p.PaymentMethod, + Amount: p.Amount, + Status: p.Status, + StatusName: getPaymentStatusName(p.Status), + ThirdPartyTradeNo: p.ThirdPartyTradeNo, + PaidAt: formatTimePtr(p.PaidAt), + CreatedAt: p.CreatedAt.Format(time.RFC3339), + }) + } + + resp := &dto.ClientRechargeOrderDetail{ + RechargeOrderID: order.ID, + RechargeOrderNo: order.RechargeOrderNo, + Amount: order.Amount, + Status: order.Status, + StatusName: getRechargeOrderStatusName(order.Status), + AutoPurchaseStatus: order.AutoPurchaseStatus, + LinkedPackageIDs: order.LinkedPackageIDs, + CreatedAt: order.CreatedAt.Format(time.RFC3339), + PaidAt: formatTimePtr(order.PaidAt), + Payments: paymentDetails, + } + + return response.Success(c, resp) +} + +func getRechargeOrderStatusName(status int) string { + switch status { + case model.RechargeOrderStatusPending: + return "待支付" + case model.RechargeOrderStatusPaid: + return "已支付" + case model.RechargeOrderStatusClosed: + return "已关闭" + case model.RechargeOrderStatusRefunded: + return "已退款" + default: + return "未知" + } +} + +func getPaymentStatusName(status int) string { + switch status { + case model.PaymentRecordStatusPending: + return "待支付" + case model.PaymentRecordStatusPaid: + return "已支付" + case model.PaymentRecordStatusFailed: + return "已失败" + case model.PaymentRecordStatusRefunded: + return "已退款" + default: + return "未知" + } +} + +func formatTimePtr(t *time.Time) string { + if t == nil { + return "" + } + return t.Format(time.RFC3339) +} diff --git a/internal/handler/app/client_wallet.go b/internal/handler/app/client_wallet.go index b2e96c6..69e4cc5 100644 --- a/internal/handler/app/client_wallet.go +++ b/internal/handler/app/client_wallet.go @@ -31,7 +31,8 @@ type ClientWalletHandler struct { personalDeviceStore *postgres.PersonalCustomerDeviceStore walletStore *postgres.AssetWalletStore transactionStore *postgres.AssetWalletTransactionStore - rechargeStore *postgres.AssetRechargeStore + rechargeOrderStore *postgres.RechargeOrderStore + paymentStore *postgres.PaymentStore rechargeService *rechargeSvc.Service openIDStore *postgres.PersonalCustomerOpenIDStore wechatConfigService *wechatConfigSvc.Service @@ -48,7 +49,8 @@ func NewClientWalletHandler( personalDeviceStore *postgres.PersonalCustomerDeviceStore, walletStore *postgres.AssetWalletStore, transactionStore *postgres.AssetWalletTransactionStore, - rechargeStore *postgres.AssetRechargeStore, + rechargeOrderStore *postgres.RechargeOrderStore, + paymentStore *postgres.PaymentStore, rechargeService *rechargeSvc.Service, openIDStore *postgres.PersonalCustomerOpenIDStore, wechatConfigService *wechatConfigSvc.Service, @@ -63,7 +65,8 @@ func NewClientWalletHandler( personalDeviceStore: personalDeviceStore, walletStore: walletStore, transactionStore: transactionStore, - rechargeStore: rechargeStore, + rechargeOrderStore: rechargeOrderStore, + paymentStore: paymentStore, rechargeService: rechargeService, openIDStore: openIDStore, wechatConfigService: wechatConfigService, @@ -301,33 +304,45 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error { return err } - // 支付通道确认可用后,再创建充值记录 - recharge := &model.AssetRechargeRecord{ + // 支付通道确认可用后,再创建充值订单和支付记录 + rechargeOrder := &model.RechargeOrder{ + RechargeOrderNo: rechargeNo, UserID: resolved.CustomerID, AssetWalletID: wallet.ID, ResourceType: resolved.ResourceType, ResourceID: resolved.Asset.AssetID, - RechargeNo: rechargeNo, Amount: req.Amount, - PaymentMethod: constants.RechargeMethodWechat, + Status: model.RechargeOrderStatusPending, PaymentConfigID: &config.ID, - Status: constants.RechargeStatusPending, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, OperatorType: constants.OperatorTypePersonalCustomer, Generation: resolved.Generation, } - if err := h.rechargeStore.Create(resolved.SkipPermissionCtx, recharge); err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "创建充值记录失败") + if err := h.rechargeOrderStore.Create(resolved.SkipPermissionCtx, rechargeOrder); err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败") + } + + payment := &model.Payment{ + PaymentNo: rechargeNo, + OrderID: rechargeOrder.ID, + OrderType: model.PaymentOrderTypeRecharge, + PaymentMethod: model.PaymentByWechat, + Amount: req.Amount, + Status: model.PaymentRecordStatusPending, + PaymentConfigID: &config.ID, + } + if err := h.paymentStore.Create(resolved.SkipPermissionCtx, payment); err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败") } payConfig := buildClientRechargePayConfig(appID, payResult) resp := &dto.ClientRechargeResponse{ Recharge: dto.ClientRechargeResult{ - RechargeID: recharge.ID, - RechargeNo: recharge.RechargeNo, - Amount: recharge.Amount, - Status: recharge.Status, + RechargeID: rechargeOrder.ID, + RechargeNo: rechargeOrder.RechargeOrderNo, + Amount: rechargeOrder.Amount, + Status: rechargeOrder.Status, }, PayConfig: payConfig, } @@ -359,7 +374,7 @@ func (h *ClientWalletHandler) GetRechargeList(c *fiber.Ctx) error { } query := h.db.WithContext(resolved.SkipPermissionCtx). - Model(&model.AssetRechargeRecord{}). + Model(&model.RechargeOrder{}). Where("resource_type = ? AND resource_id = ? AND generation = ?", resolved.ResourceType, resolved.Asset.AssetID, resolved.Generation) if req.Status != nil { query = query.Where("status = ?", *req.Status) @@ -367,28 +382,28 @@ func (h *ClientWalletHandler) GetRechargeList(c *fiber.Ctx) error { var total int64 if err := query.Count(&total).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录总数失败") + return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单总数失败") } - var records []*model.AssetRechargeRecord + var orders []*model.RechargeOrder offset := (req.Page - 1) * req.PageSize - if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&records).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录失败") + if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&orders).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") } - items := make([]dto.ClientRechargeListItem, 0, len(records)) - for _, record := range records { - if record == nil { + items := make([]dto.ClientRechargeListItem, 0, len(orders)) + for _, order := range orders { + if order == nil { continue } items = append(items, dto.ClientRechargeListItem{ - RechargeID: record.ID, - RechargeNo: record.RechargeNo, - Amount: record.Amount, - Status: record.Status, - PaymentMethod: record.PaymentMethod, - CreatedAt: record.CreatedAt.Format(time.RFC3339), - AutoPurchaseStatus: record.AutoPurchaseStatus, + RechargeID: order.ID, + RechargeNo: order.RechargeOrderNo, + Amount: order.Amount, + Status: order.Status, + PaymentMethod: "", // 支付方式在 Payment 表中,列表查询不返回 + CreatedAt: order.CreatedAt.Format(time.RFC3339), + AutoPurchaseStatus: order.AutoPurchaseStatus, }) } diff --git a/internal/handler/callback/payment.go b/internal/handler/callback/payment.go index 84896e8..e44c404 100644 --- a/internal/handler/callback/payment.go +++ b/internal/handler/callback/payment.go @@ -13,6 +13,8 @@ import ( "github.com/break/junhong_cmp_fiber/internal/model" orderService "github.com/break/junhong_cmp_fiber/internal/service/order" rechargeService "github.com/break/junhong_cmp_fiber/internal/service/recharge" + rechargeOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge_order" + "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/fuiou" @@ -33,26 +35,32 @@ type WechatConfigServiceInterface interface { type PaymentHandler struct { orderService *orderService.Service rechargeService *rechargeService.Service + rechargeOrderService *rechargeOrderSvc.Service agentRechargeService AgentRechargeServiceInterface wechatPayment wechat.PaymentServiceInterface wechatConfigService WechatConfigServiceInterface + paymentStore *postgres.PaymentStore logger *zap.Logger } func NewPaymentHandler( orderService *orderService.Service, rechargeService *rechargeService.Service, + rechargeOrderService *rechargeOrderSvc.Service, agentRechargeService AgentRechargeServiceInterface, wechatPayment wechat.PaymentServiceInterface, wechatConfigService WechatConfigServiceInterface, + paymentStore *postgres.PaymentStore, logger *zap.Logger, ) *PaymentHandler { return &PaymentHandler{ orderService: orderService, rechargeService: rechargeService, + rechargeOrderService: rechargeOrderService, agentRechargeService: agentRechargeService, wechatPayment: wechatPayment, wechatConfigService: wechatConfigService, + paymentStore: paymentStore, logger: logger, } } @@ -122,7 +130,10 @@ func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, case strings.HasPrefix(outTradeNo, "ORD"): return h.orderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat) case strings.HasPrefix(outTradeNo, constants.AssetRechargeOrderPrefix): - return h.rechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID) + if h.rechargeOrderService != nil { + return h.rechargeOrderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentByWechat, transactionID) + } + return fmt.Errorf("充值订单服务未配置,无法处理订单: %s", outTradeNo) case strings.HasPrefix(outTradeNo, constants.AgentRechargeOrderPrefix): if h.agentRechargeService != nil { return h.agentRechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID) @@ -165,9 +176,10 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error { return err } case strings.HasPrefix(req.OrderNo, constants.AssetRechargeOrderPrefix): - if err := h.rechargeService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, ""); err != nil { - return err + if h.rechargeOrderService != nil { + return h.rechargeOrderService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentByAlipay, "") } + return fmt.Errorf("充值订单服务未配置,无法处理订单: %s", req.OrderNo) case strings.HasPrefix(req.OrderNo, constants.AgentRechargeOrderPrefix): if h.agentRechargeService != nil { if err := h.agentRechargeService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, ""); err != nil { @@ -242,9 +254,13 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error { return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) } case strings.HasPrefix(orderNo, constants.AssetRechargeOrderPrefix): - if err := h.rechargeService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil { - return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) + if h.rechargeOrderService != nil { + if err := h.rechargeOrderService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil { + return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) + } + return c.Send(fuiou.BuildNotifySuccessResponse()) } + return c.Send(fuiou.BuildNotifyFailResponse("充值订单服务未配置")) case strings.HasPrefix(orderNo, constants.AgentRechargeOrderPrefix): if h.agentRechargeService != nil { if err := h.agentRechargeService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil { diff --git a/internal/model/asset_wallet.go b/internal/model/asset_wallet.go index 461d3e3..53a1787 100644 --- a/internal/model/asset_wallet.go +++ b/internal/model/asset_wallet.go @@ -3,7 +3,6 @@ package model import ( "time" - "gorm.io/datatypes" "gorm.io/gorm" ) @@ -64,39 +63,3 @@ type AssetWalletTransaction struct { func (AssetWalletTransaction) TableName() string { return "tb_asset_wallet_transaction" } - -// AssetRechargeRecord 资产充值记录模型 -// 记录所有资产钱包充值操作 -type AssetRechargeRecord struct { - ID uint `gorm:"column:id;primaryKey" json:"id"` - UserID uint `gorm:"column:user_id;not null;index;comment:操作人用户ID" json:"user_id"` - AssetWalletID uint `gorm:"column:asset_wallet_id;not null;comment:资产钱包ID" json:"asset_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"` - PaymentConfigID *uint `gorm:"column:payment_config_id;index;comment:支付配置ID(关联tb_wechat_config.id)" json:"payment_config_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"` - OperatorType string `gorm:"column:operator_type;type:varchar(20);not null;default:'admin_user';comment:操作人类型" json:"operator_type"` - Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"` - LinkedPackageIDs datatypes.JSON `gorm:"column:linked_package_ids;type:jsonb;default:'[]';comment:强充关联套餐ID列表" json:"linked_package_ids,omitempty"` - LinkedOrderType string `gorm:"column:linked_order_type;type:varchar(20);comment:关联订单类型" json:"linked_order_type,omitempty"` - LinkedCarrierType string `gorm:"column:linked_carrier_type;type:varchar(20);comment:关联载体类型" json:"linked_carrier_type,omitempty"` - LinkedCarrierID *uint `gorm:"column:linked_carrier_id;type:bigint;comment:关联载体ID" json:"linked_carrier_id,omitempty"` - AutoPurchaseStatus string `gorm:"column:auto_purchase_status;type:varchar(20);default:'';comment:强充自动代购状态(pending-待处理 success-成功 failed-失败)" json:"auto_purchase_status,omitempty"` - DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"` -} - -// TableName 指定表名 -func (AssetRechargeRecord) TableName() string { - return "tb_asset_recharge_record" -} diff --git a/internal/model/dto/client_order_dto.go b/internal/model/dto/client_order_dto.go index c8e2123..3b8bab3 100644 --- a/internal/model/dto/client_order_dto.go +++ b/internal/model/dto/client_order_dto.go @@ -19,6 +19,7 @@ type ClientCreateOrderResponse struct { Recharge *ClientRechargeInfo `json:"recharge,omitempty" description:"充值订单信息"` PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信支付配置(仅强充场景返回)"` LinkedPackageInfo *LinkedPackageInfo `json:"linked_package_info,omitempty" description:"关联套餐信息"` + Idempotent bool `json:"idempotent,omitempty" description:"幂等标志(true 表示返回的是已存在的待支付充值订单)"` } // ClientOrderInfo D1 套餐订单信息 diff --git a/internal/model/dto/client_recharge_order_dto.go b/internal/model/dto/client_recharge_order_dto.go new file mode 100644 index 0000000..0e45e43 --- /dev/null +++ b/internal/model/dto/client_recharge_order_dto.go @@ -0,0 +1,46 @@ +package dto + +// ClientRechargeOrderListRequest C6 充值订单列表请求 +type ClientRechargeOrderListRequest struct { + Page int `json:"page" query:"page" description:"页码"` + PageSize int `json:"page_size" query:"page_size" description:"每页数量"` + Status int `json:"status" query:"status" description:"状态过滤 (0:待支付, 1:已支付, 2:已关闭, 3:已退款)"` +} + +// ClientRechargeOrderListItem C6 充值订单列表项 +type ClientRechargeOrderListItem struct { + RechargeOrderID uint `json:"recharge_order_id" description:"充值订单ID"` + RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"` + Amount int64 `json:"amount" description:"充值金额(分)"` + Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已关闭, 3:已退款)"` + StatusName string `json:"status_name" description:"状态名称(中文)"` + AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态 (pending/success/failed)"` + CreatedAt string `json:"created_at" description:"创建时间"` +} + +// ClientRechargeOrderDetail C7 充值订单详情 +type ClientRechargeOrderDetail struct { + RechargeOrderID uint `json:"recharge_order_id" description:"充值订单ID"` + RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"` + Amount int64 `json:"amount" description:"充值金额(分)"` + Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已关闭, 3:已退款)"` + StatusName string `json:"status_name" description:"状态名称(中文)"` + AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态"` + LinkedPackageIDs interface{} `json:"linked_package_ids" description:"关联套餐ID列表"` + CreatedAt string `json:"created_at" description:"创建时间"` + PaidAt string `json:"paid_at,omitempty" description:"支付时间"` + Payments []ClientRechargeOrderPaymentDetail `json:"payments,omitempty" description:"支付记录列表"` +} + +// ClientRechargeOrderPaymentDetail 充值订单支付记录详情 +type ClientRechargeOrderPaymentDetail struct { + PaymentID uint `json:"payment_id" description:"支付记录ID"` + PaymentNo string `json:"payment_no" description:"支付单号"` + PaymentMethod string `json:"payment_method" description:"支付方式 (wechat/alipay/wallet)"` + Amount int64 `json:"amount" description:"支付金额(分)"` + Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已失败, 3:已退款)"` + StatusName string `json:"status_name" description:"状态名称(中文)"` + ThirdPartyTradeNo string `json:"third_party_trade_no,omitempty" description:"第三方交易号"` + PaidAt string `json:"paid_at,omitempty" description:"支付时间"` + CreatedAt string `json:"created_at" description:"创建时间"` +} diff --git a/internal/model/payment.go b/internal/model/payment.go new file mode 100644 index 0000000..1fe26bc --- /dev/null +++ b/internal/model/payment.go @@ -0,0 +1,46 @@ +package model + +import ( + "gorm.io/gorm" + "time" +) + +type Payment struct { + ID uint `gorm:"column:id;primaryKey" json:"id"` + PaymentNo string `gorm:"column:payment_no;type:varchar(40);uniqueIndex;not null" json:"payment_no"` + OrderID uint `gorm:"column:order_id;not null" json:"order_id"` + OrderType string `gorm:"column:order_type;type:varchar(30);not null" json:"order_type"` + PaymentMethod string `gorm:"column:payment_method;type:varchar(20);not null" json:"payment_method"` + Amount int64 `gorm:"column:amount;type:bigint;not null" json:"amount"` + Status int `gorm:"column:status;type:smallint;not null;default:0" json:"status"` + ThirdPartyTradeNo string `gorm:"column:third_party_trade_no;type:varchar(100)" json:"third_party_trade_no,omitempty"` + PaymentConfigID *uint `gorm:"column:payment_config_id" json:"payment_config_id,omitempty"` + PaymentVoucherKey string `gorm:"column:payment_voucher_key;type:varchar(500)" json:"payment_voucher_key,omitempty"` + PaidAt *time.Time `gorm:"column:paid_at" json:"paid_at,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"` +} + +func (Payment) TableName() string { + return "tb_payment" +} + +const ( + PaymentRecordStatusPending = 0 // 待支付 + PaymentRecordStatusPaid = 1 // 已支付 + PaymentRecordStatusFailed = 2 // 已失败 + PaymentRecordStatusRefunded = 3 // 已退款 +) + +const ( + PaymentOrderTypePackage = "package_order" + PaymentOrderTypeRecharge = "recharge_order" +) + +const ( + PaymentByWallet = "wallet" + PaymentByWechat = "wechat" + PaymentByAlipay = "alipay" + PaymentByOffline = "offline" +) diff --git a/internal/model/recharge_order.go b/internal/model/recharge_order.go new file mode 100644 index 0000000..7ded3ec --- /dev/null +++ b/internal/model/recharge_order.go @@ -0,0 +1,55 @@ +package model + +import ( + "gorm.io/datatypes" + "gorm.io/gorm" + "time" +) + +// RechargeOrder 充值订单模型 +// 记录用户发起充值请求后生成的订单,支持强充和普通充值两种类型 +type RechargeOrder struct { + ID uint `gorm:"column:id;primaryKey" json:"id"` + RechargeOrderNo string `gorm:"column:recharge_order_no;type:varchar(40);uniqueIndex;not null" json:"recharge_order_no"` + UserID uint `gorm:"column:user_id;not null;index" json:"user_id"` + AssetWalletID uint `gorm:"column:asset_wallet_id;not null" json:"asset_wallet_id"` + ResourceType string `gorm:"column:resource_type;type:varchar(20);not null" json:"resource_type"` + ResourceID uint `gorm:"column:resource_id;not null" json:"resource_id"` + IotCardID *uint `gorm:"column:iot_card_id" json:"iot_card_id,omitempty"` + DeviceID *uint `gorm:"column:device_id" json:"device_id,omitempty"` + Amount int64 `gorm:"column:amount;type:bigint;not null" json:"amount"` + Status int `gorm:"column:status;type:smallint;not null;default:0" json:"status"` + PaidAt *time.Time `gorm:"column:paid_at" json:"paid_at,omitempty"` + ShopIDTag uint `gorm:"column:shop_id_tag;not null;default:0" json:"shop_id_tag"` + EnterpriseIDTag *uint `gorm:"column:enterprise_id_tag" json:"enterprise_id_tag,omitempty"` + OperatorType string `gorm:"column:operator_type;type:varchar(30);not null" json:"operator_type"` + Generation int `gorm:"column:generation;not null;default:1" json:"generation"` + LinkedPackageIDs datatypes.JSON `gorm:"column:linked_package_ids;type:jsonb" json:"linked_package_ids,omitempty"` + LinkedOrderType string `gorm:"column:linked_order_type;type:varchar(20)" json:"linked_order_type,omitempty"` + LinkedCarrierType string `gorm:"column:linked_carrier_type;type:varchar(20)" json:"linked_carrier_type,omitempty"` + LinkedCarrierID *uint `gorm:"column:linked_carrier_id;type:bigint" json:"linked_carrier_id,omitempty"` + AutoPurchaseStatus string `gorm:"column:auto_purchase_status;type:varchar(20)" json:"auto_purchase_status,omitempty"` + AccumulatedRechargeSeries int64 `gorm:"column:accumulated_recharge_series;not null;default:0" json:"accumulated_recharge_series"` + PaymentConfigID *uint `gorm:"column:payment_config_id;index" json:"payment_config_id,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 (RechargeOrder) TableName() string { + return "tb_recharge_order" +} + +const ( + RechargeOrderStatusPending = 0 // 待支付 + RechargeOrderStatusPaid = 1 // 已支付 + RechargeOrderStatusClosed = 2 // 已关闭 + RechargeOrderStatusRefunded = 3 // 已退款 +) + +const ( + AutoPurchaseStatusPending = "pending" // 待处理 + AutoPurchaseStatusSuccess = "success" // 成功 + AutoPurchaseStatusFailed = "failed" // 失败 +) diff --git a/internal/routes/personal.go b/internal/routes/personal.go index 96ed62a..2752dac 100644 --- a/internal/routes/personal.go +++ b/internal/routes/personal.go @@ -190,6 +190,22 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator, Output: &dto.ClientRechargeListResponse{}, }) + Register(authGroup, doc, basePath, "GET", "/recharge-orders", handlers.ClientRechargeOrder.ListRechargeOrders, RouteSpec{ + Summary: "充值订单列表", + Tags: []string{"个人客户 - 充值订单"}, + Auth: true, + Input: &dto.ClientRechargeOrderListRequest{}, + Output: nil, + }) + + Register(authGroup, doc, basePath, "GET", "/recharge-orders/:id", handlers.ClientRechargeOrder.GetRechargeOrderDetail, RouteSpec{ + Summary: "充值订单详情", + Tags: []string{"个人客户 - 充值订单"}, + Auth: true, + Input: &dto.IDReq{}, + Output: nil, + }) + Register(authGroup, doc, basePath, "POST", "/orders/create", handlers.ClientOrder.CreateOrder, RouteSpec{ Summary: "创建订单", Tags: []string{"个人客户 - 订单"}, diff --git a/internal/service/client_order/service.go b/internal/service/client_order/service.go index 70e43c2..43e4b47 100644 --- a/internal/service/client_order/service.go +++ b/internal/service/client_order/service.go @@ -51,7 +51,8 @@ type Service struct { assetService *asset.Service purchaseValidationService *purchase_validation.Service orderStore *postgres.OrderStore - rechargeRecordStore *postgres.AssetRechargeStore + rechargeOrderStore *postgres.RechargeOrderStore + paymentStore *postgres.PaymentStore walletStore *postgres.AssetWalletStore personalDeviceStore *postgres.PersonalCustomerDeviceStore openIDStore *postgres.PersonalCustomerOpenIDStore @@ -73,7 +74,8 @@ func New( assetService *asset.Service, purchaseValidationService *purchase_validation.Service, orderStore *postgres.OrderStore, - rechargeRecordStore *postgres.AssetRechargeStore, + rechargeOrderStore *postgres.RechargeOrderStore, + paymentStore *postgres.PaymentStore, walletStore *postgres.AssetWalletStore, personalDeviceStore *postgres.PersonalCustomerDeviceStore, openIDStore *postgres.PersonalCustomerOpenIDStore, @@ -93,7 +95,8 @@ func New( assetService: assetService, purchaseValidationService: purchaseValidationService, orderStore: orderStore, - rechargeRecordStore: rechargeRecordStore, + rechargeOrderStore: rechargeOrderStore, + paymentStore: paymentStore, walletStore: walletStore, personalDeviceStore: personalDeviceStore, openIDStore: openIDStore, @@ -172,6 +175,27 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli if lockAcquired { _ = s.redis.Del(skipCtx, lockKey).Err() } + existingValue, err := s.redis.Get(skipCtx, redisKey).Result() + if err == nil && strings.HasPrefix(existingValue, "CRCH") { + existingOrder, queryErr := s.rechargeOrderStore.GetByRechargeOrderNo(skipCtx, existingValue) + if queryErr == nil && existingOrder.Status == model.RechargeOrderStatusPending { + return &dto.ClientCreateOrderResponse{ + OrderType: "recharge", + Recharge: &dto.ClientRechargeInfo{ + RechargeID: existingOrder.ID, + RechargeNo: existingOrder.RechargeOrderNo, + Amount: existingOrder.Amount, + Status: rechargeStatusToClientStatus(int(existingOrder.Status)), + StatusName: clientRechargeStatusName(rechargeStatusToClientStatus(int(existingOrder.Status))), + AutoPurchaseStatus: existingOrder.AutoPurchaseStatus, + }, + Idempotent: true, + }, nil + } + if queryErr == nil { + _ = s.redis.Del(skipCtx, redisKey).Err() + } + } return nil, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交") } @@ -360,17 +384,26 @@ func (s *Service) createForceRechargeOrder( return nil, errors.Wrap(errors.CodeInternalError, err, "序列化关联套餐失败") } - carrierID := resourceID - recharge := &model.AssetRechargeRecord{ + rechargeOrderNo := generateClientRechargeNo() + var iotCardID *uint + var deviceID *uint + if validationResult.Card != nil { + iotCardID = &validationResult.Card.ID + } + if validationResult.Device != nil { + deviceID = &validationResult.Device.ID + } + + rechargeOrder := &model.RechargeOrder{ + RechargeOrderNo: rechargeOrderNo, UserID: customerID, AssetWalletID: wallet.ID, ResourceType: resourceType, ResourceID: resourceID, - RechargeNo: generateClientRechargeNo(), + IotCardID: iotCardID, + DeviceID: deviceID, Amount: forceRecharge.ForceRechargeAmount, - PaymentMethod: model.PaymentMethodWechat, - PaymentConfigID: &activeConfig.ID, - Status: 1, + Status: model.RechargeOrderStatusPending, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, OperatorType: "personal_customer", @@ -378,33 +411,47 @@ func (s *Service) createForceRechargeOrder( LinkedPackageIDs: datatypes.JSON(linkedPackageIDs), LinkedOrderType: resolveOrderType(validationResult), LinkedCarrierType: assetInfo.AssetType, - LinkedCarrierID: &carrierID, - AutoPurchaseStatus: constants.AutoPurchaseStatusPending, + LinkedCarrierID: &resourceID, + AutoPurchaseStatus: model.AutoPurchaseStatusPending, } - if err := s.rechargeRecordStore.Create(ctx, recharge); err != nil { - return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值记录失败") + if err := s.rechargeOrderStore.Create(ctx, rechargeOrder); err != nil { + return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败") } - paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, recharge.RechargeNo, "余额充值", openID, int(recharge.Amount)) + paymentNo := generateClientRechargeNo() + payment := &model.Payment{ + PaymentNo: paymentNo, + OrderID: rechargeOrder.ID, + OrderType: model.PaymentOrderTypeRecharge, + PaymentMethod: model.PaymentByWechat, + Amount: forceRecharge.ForceRechargeAmount, + Status: model.PaymentRecordStatusPending, + PaymentConfigID: &activeConfig.ID, + } + + if err := s.paymentStore.Create(ctx, payment); err != nil { + return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败") + } + + paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, rechargeOrderNo, "余额充值", openID, int(rechargeOrder.Amount)) if err != nil { return nil, err } - // 微信支付创建成功后才标记幂等 key,确保支付失败时 defer 能正确清除 key - s.markClientPurchaseCreated(ctx, redisKey, recharge.RechargeNo) + s.markClientPurchaseCreated(ctx, redisKey, rechargeOrderNo) *created = true - clientStatus := rechargeStatusToClientStatus(recharge.Status) + clientStatus := rechargeStatusToClientStatus(int(rechargeOrder.Status)) return &dto.ClientCreateOrderResponse{ OrderType: "recharge", Recharge: &dto.ClientRechargeInfo{ - RechargeID: recharge.ID, - RechargeNo: recharge.RechargeNo, - Amount: recharge.Amount, + RechargeID: rechargeOrder.ID, + RechargeNo: rechargeOrderNo, + Amount: rechargeOrder.Amount, Status: clientStatus, StatusName: clientRechargeStatusName(clientStatus), - AutoPurchaseStatus: recharge.AutoPurchaseStatus, + AutoPurchaseStatus: rechargeOrder.AutoPurchaseStatus, }, PayConfig: buildClientPayConfigFromResult(paymentResult), LinkedPackageInfo: buildLinkedPackageInfo(validationResult, forceRecharge), diff --git a/internal/service/order/service.go b/internal/service/order/service.go index 7579b57..60f02d1 100644 --- a/internal/service/order/service.go +++ b/internal/service/order/service.go @@ -39,6 +39,7 @@ type Service struct { orderItemStore *postgres.OrderItemStore agentWalletStore *postgres.AgentWalletStore assetWalletStore *postgres.AssetWalletStore + paymentStore *postgres.PaymentStore purchaseValidationService *purchase_validation.Service shopPackageAllocationStore *postgres.ShopPackageAllocationStore shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore @@ -65,6 +66,7 @@ func New( orderItemStore *postgres.OrderItemStore, agentWalletStore *postgres.AgentWalletStore, assetWalletStore *postgres.AssetWalletStore, + paymentStore *postgres.PaymentStore, purchaseValidationService *purchase_validation.Service, shopPackageAllocationStore *postgres.ShopPackageAllocationStore, shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore, @@ -89,6 +91,7 @@ func New( orderItemStore: orderItemStore, agentWalletStore: agentWalletStore, assetWalletStore: assetWalletStore, + paymentStore: paymentStore, purchaseValidationService: purchaseValidationService, shopPackageAllocationStore: shopPackageAllocationStore, shopSeriesAllocationStore: shopSeriesAllocationStore, @@ -1188,6 +1191,18 @@ func (s *Service) unfreezeWalletForCancel(ctx context.Context, tx *gorm.DB, orde return nil } +func (s *Service) createWalletPaymentRecord(tx *gorm.DB, order *model.Order, paymentMethod string) error { + payment := &model.Payment{ + PaymentNo: order.OrderNo, + OrderID: order.ID, + OrderType: model.PaymentOrderTypePackage, + PaymentMethod: paymentMethod, + Amount: order.TotalAmount, + Status: model.PaymentRecordStatusPaid, + } + return tx.Create(payment).Error +} + func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error { order, err := s.orderStore.GetByID(ctx, orderID) if err != nil { @@ -1205,6 +1220,11 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, return errors.New(errors.CodeInvalidStatus, "代购订单无需支付") } + existingPayment, _ := s.paymentStore.GetByOrderIDAndStatus(ctx, orderID, model.PaymentRecordStatusPaid) + if existingPayment != nil { + return nil + } + var resourceType string var resourceID uint @@ -1286,6 +1306,10 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突") } + if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet); err != nil { + return err + } + return s.activatePackage(ctx, tx, order) }) } else { @@ -1370,6 +1394,10 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, return errors.Wrap(errors.CodeDatabaseError, err, "创建扣款流水失败") } + if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet); err != nil { + return err + } + return s.activatePackage(ctx, tx, order) }) } diff --git a/internal/service/recharge/service.go b/internal/service/recharge/service.go index 65af108..858188c 100644 --- a/internal/service/recharge/service.go +++ b/internal/service/recharge/service.go @@ -3,18 +3,12 @@ package recharge import ( "context" "fmt" - "math/rand" - "time" "github.com/break/junhong_cmp_fiber/internal/model" - "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/internal/store/postgres" - "github.com/break/junhong_cmp_fiber/internal/task" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/payment" - "github.com/break/junhong_cmp_fiber/pkg/queue" - "github.com/hibiken/asynq" "go.uber.org/zap" "gorm.io/gorm" ) @@ -40,7 +34,6 @@ type WechatConfigServiceInterface interface { // 负责资产钱包(IoT卡/设备)的充值订单创建、预检、支付回调处理等业务逻辑 type Service struct { db *gorm.DB - assetRechargeStore *postgres.AssetRechargeStore assetWalletStore *postgres.AssetWalletStore assetWalletTransactionStore *postgres.AssetWalletTransactionStore iotCardStore *postgres.IotCardStore @@ -50,14 +43,12 @@ type Service struct { commissionRecordStore *postgres.CommissionRecordStore wechatConfigService WechatConfigServiceInterface paymentLoader payment.PaymentConfigLoader - queueClient *queue.Client // 任务队列客户端,用于触发自动购包任务 logger *zap.Logger } // New 创建充值服务实例 func New( db *gorm.DB, - assetRechargeStore *postgres.AssetRechargeStore, assetWalletStore *postgres.AssetWalletStore, assetWalletTransactionStore *postgres.AssetWalletTransactionStore, iotCardStore *postgres.IotCardStore, @@ -68,11 +59,9 @@ func New( wechatConfigService WechatConfigServiceInterface, paymentLoader payment.PaymentConfigLoader, logger *zap.Logger, - queueClient *queue.Client, ) *Service { return &Service{ db: db, - assetRechargeStore: assetRechargeStore, assetWalletStore: assetWalletStore, assetWalletTransactionStore: assetWalletTransactionStore, iotCardStore: iotCardStore, @@ -82,96 +71,10 @@ func New( commissionRecordStore: commissionRecordStore, wechatConfigService: wechatConfigService, paymentLoader: paymentLoader, - queueClient: queueClient, logger: logger, } } -// Create 创建充值订单 -// 验证资源、金额范围、强充要求,生成订单号 -func (s *Service) Create(ctx context.Context, req *dto.CreateRechargeRequest, userID uint) (*dto.RechargeResponse, error) { - // 1. 验证金额范围 - if req.Amount < constants.AssetRechargeMinAmount { - return nil, errors.New(errors.CodeRechargeAmountInvalid, "充值金额不能低于1元") - } - if req.Amount > constants.AssetRechargeMaxAmount { - return nil, errors.New(errors.CodeRechargeAmountInvalid, "充值金额不能超过100000元") - } - - // 2. 获取资源(卡或设备)钱包 - var wallet *model.AssetWallet - var err error - - if req.ResourceType == "iot_card" { - wallet, err = s.assetWalletStore.GetByResourceTypeAndID(ctx, "iot_card", req.ResourceID) - } else if req.ResourceType == "device" { - wallet, err = s.assetWalletStore.GetByResourceTypeAndID(ctx, "device", req.ResourceID) - } else { - return nil, errors.New(errors.CodeInvalidParam, "无效的资源类型") - } - - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, errors.New(errors.CodeWalletNotFound, "钱包不存在") - } - return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败") - } - - // 3. 验证强充要求 - forceReq, err := s.checkForceRechargeRequirement(ctx, req.ResourceType, req.ResourceID) - if err != nil { - return nil, err - } - - if forceReq.NeedForceRecharge && req.Amount != forceReq.ForceRechargeAmount { - return nil, errors.New(errors.CodeForceRechargeAmountMismatch, - fmt.Sprintf("必须充值%d分才能满足强充要求", forceReq.ForceRechargeAmount)) - } - - // 4. 生成充值订单号 - rechargeNo := s.generateRechargeNo() - - // 5. 查询当前生效的支付配置 - var paymentConfigID *uint - if req.PaymentMethod == "wechat" || req.PaymentMethod == "alipay" { - activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx) - if err != nil { - s.logger.Warn("查询生效支付配置失败", zap.Error(err)) - } - if activeConfig != nil { - paymentConfigID = &activeConfig.ID - } - } - - // 6. 创建充值订单 - recharge := &model.AssetRechargeRecord{ - UserID: userID, - AssetWalletID: wallet.ID, - ResourceType: req.ResourceType, - ResourceID: req.ResourceID, - RechargeNo: rechargeNo, - Amount: req.Amount, - PaymentMethod: req.PaymentMethod, - PaymentConfigID: paymentConfigID, - Status: constants.RechargeStatusPending, - ShopIDTag: wallet.ShopIDTag, - EnterpriseIDTag: wallet.EnterpriseIDTag, - } - - if err := s.assetRechargeStore.Create(ctx, recharge); err != nil { - return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败") - } - - s.logger.Info("创建充值订单成功", - zap.Uint("recharge_id", recharge.ID), - zap.String("recharge_no", rechargeNo), - zap.Int64("amount", req.Amount), - zap.Uint("user_id", userID), - ) - - return s.buildRechargeResponse(recharge), nil -} - // GetRechargeCheck 充值预检 // 返回强充要求、金额限制等信息 func (s *Service) GetRechargeCheck(ctx context.Context, resourceType string, resourceID uint) (*ForceRechargeRequirement, error) { @@ -200,218 +103,6 @@ func (s *Service) GetRechargeCheck(ctx context.Context, resourceType string, res return s.checkForceRechargeRequirement(ctx, resourceType, resourceID) } -// GetByID 根据ID查询充值订单详情 -// 支持数据权限过滤 -func (s *Service) GetByID(ctx context.Context, id uint, userID uint) (*dto.RechargeResponse, error) { - recharge, err := s.assetRechargeStore.GetByID(ctx, id) - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, errors.New(errors.CodeRechargeNotFound, "充值订单不存在") - } - return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") - } - - // 数据权限检查:只能查看自己的充值订单 - if recharge.UserID != userID { - return nil, errors.New(errors.CodeForbidden, "无权查看此充值订单") - } - - return s.buildRechargeResponse(recharge), nil -} - -// List 查询充值订单列表 -// 支持分页、筛选、数据权限 -func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID uint) (*dto.RechargeListResponse, error) { - page := req.Page - pageSize := req.PageSize - if page == 0 { - page = 1 - } - if pageSize == 0 { - pageSize = constants.DefaultPageSize - } - - params := &postgres.ListAssetRechargeParams{ - Page: page, - PageSize: pageSize, - UserID: &userID, - } - - if req.Status != nil { - params.Status = req.Status - } - if req.WalletID != nil { - walletID := *req.WalletID - params.AssetWalletID = &walletID - } - if req.StartTime != nil { - params.StartTime = req.StartTime - } - if req.EndTime != nil { - params.EndTime = req.EndTime - } - - recharges, total, err := s.assetRechargeStore.List(ctx, params) - if err != nil { - return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单列表失败") - } - - var list []*dto.RechargeResponse - for _, r := range recharges { - list = append(list, s.buildRechargeResponse(r)) - } - - return &dto.RechargeListResponse{ - List: list, - Total: total, - Page: page, - PageSize: pageSize, - }, nil -} - -// HandlePaymentCallback 支付回调处理 -// 支持幂等性检查、事务处理、更新余额、触发佣金 -// 验签由外层 Handler 负责;如需按 payment_config_id 加载配置,可通过 s.paymentLoader 获取 -func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error { - // 1. 查询充值订单 - recharge, err := s.assetRechargeStore.GetByRechargeNo(ctx, rechargeNo) - if err != nil { - if err == gorm.ErrRecordNotFound { - return errors.New(errors.CodeRechargeNotFound, "充值订单不存在") - } - return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") - } - - // 2. 幂等性检查:已支付则直接返回成功 - if recharge.Status == constants.RechargeStatusPaid || recharge.Status == constants.RechargeStatusCompleted { - s.logger.Info("充值订单已支付,跳过处理", - zap.String("recharge_no", rechargeNo), - zap.Int("status", recharge.Status), - ) - return nil - } - - // 3. 检查订单状态是否允许支付 - if recharge.Status != constants.RechargeStatusPending { - return errors.New(errors.CodeInvalidStatus, "订单状态不允许支付") - } - - // 4. 获取钱包信息 - wallet, err := s.assetWalletStore.GetByID(ctx, recharge.AssetWalletID) - if err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败") - } - - // 5. 获取钱包对应的资源类型和ID(从充值记录中直接获取) - resourceType := recharge.ResourceType - resourceID := recharge.ResourceID - - // 6. 事务处理:更新订单状态、增加余额、更新累计充值、触发佣金 - now := time.Now() - err = s.db.Transaction(func(tx *gorm.DB) error { - // 6.1 更新充值订单状态(带状态检查,使用事务内 tx 确保原子性) - oldStatus := constants.RechargeStatusPending - if err := s.assetRechargeStore.UpdateStatusWithOptimisticLockDB(ctx, tx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil { - if err == gorm.ErrRecordNotFound { - return nil - } - return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单状态失败") - } - - // 6.2 更新支付信息(使用事务内 tx) - if err := s.assetRechargeStore.UpdatePaymentInfoWithDB(ctx, tx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "更新支付信息失败") - } - - // 6.3 增加钱包余额(使用乐观锁) - balanceBefore := wallet.Balance - result := tx.Model(&model.AssetWallet{}). - Where("id = ? AND version = ?", wallet.ID, wallet.Version). - Updates(map[string]any{ - "balance": gorm.Expr("balance + ?", recharge.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, "钱包版本冲突,请重试") - } - - // 6.4 创建钱包交易记录(reference_no 存储充值单号,便于前端跳转) - remark := "钱包充值" - refType := "recharge" - transaction := &model.AssetWalletTransaction{ - AssetWalletID: wallet.ID, - ResourceType: resourceType, - ResourceID: resourceID, - UserID: recharge.UserID, - TransactionType: "recharge", - Amount: recharge.Amount, - BalanceBefore: balanceBefore, - BalanceAfter: balanceBefore + recharge.Amount, - Status: 1, - ReferenceType: &refType, - ReferenceNo: &recharge.RechargeNo, - Remark: &remark, - ShopIDTag: wallet.ShopIDTag, - EnterpriseIDTag: wallet.EnterpriseIDTag, - } - if err := tx.Create(transaction).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败") - } - - // 6.5 更新累计充值 - if err := s.updateAccumulatedRechargeInTx(ctx, tx, resourceType, resourceID, recharge.Amount); err != nil { - return err - } - - // 6.6 触发一次性佣金判断 - if err := s.triggerOneTimeCommissionIfNeededInTx(ctx, tx, resourceType, resourceID, recharge.Amount, recharge.UserID); err != nil { - return err - } - - // 6.7 更新充值订单状态为已完成 - if err := tx.Model(&model.AssetRechargeRecord{}). - Where("id = ?", recharge.ID). - Update("status", constants.RechargeStatusCompleted).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单完成状态失败") - } - - return nil - }) - - if err != nil { - return err - } - - // 充值成功后,如有关联套餐则触发自动购包(强充绑套餐功能) - linkedIDs := recharge.LinkedPackageIDs - if len(linkedIDs) > 0 && string(linkedIDs) != "[]" && string(linkedIDs) != "null" { - if s.queueClient != nil { - taskPayload := task.AutoPurchasePayload{RechargeRecordID: recharge.ID} - if enqueueErr := s.queueClient.EnqueueTask(ctx, constants.TaskTypeAutoPurchaseAfterRecharge, taskPayload, - asynq.Queue(constants.QueueDefault), - asynq.MaxRetry(3), - ); enqueueErr != nil { - s.logger.Error("自动购包任务入队失败", - zap.Uint("recharge_id", recharge.ID), - zap.Error(enqueueErr)) - // 不影响充值成功,仅记录日志 - } - } - } - - s.logger.Info("充值支付回调处理成功", - zap.String("recharge_no", rechargeNo), - zap.Int64("amount", recharge.Amount), - zap.String("resource_type", resourceType), - zap.Uint("resource_id", resourceID), - ) - - return nil -} - // checkForceRechargeRequirement 检查强充要求 // 根据资源类型和ID检查是否需要强制充值指定金额 func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceType string, resourceID uint) (*ForceRechargeRequirement, error) { @@ -505,299 +196,3 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp // updateAccumulatedRechargeInTx 更新累计充值(事务内使用) // 同时更新旧的 accumulated_recharge 字段和新的 accumulated_recharge_by_series JSON 字段 -func (s *Service) updateAccumulatedRechargeInTx(ctx context.Context, tx *gorm.DB, resourceType string, resourceID uint, amount int64) error { - if resourceType == "iot_card" { - var card model.IotCard - if err := tx.First(&card, resourceID).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") - } - - if card.SeriesID != nil { - if err := card.AddAccumulatedRechargeBySeries(*card.SeriesID, amount); err != nil { - return errors.Wrap(errors.CodeInternalError, err, "更新卡按系列累计充值失败") - } - } - - result := tx.Model(&model.IotCard{}). - Where("id = ?", resourceID). - Updates(map[string]any{ - "accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount), - "accumulated_recharge_by_series": card.AccumulatedRechargeBySeriesJSON, - }) - if result.Error != nil { - return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新卡累计充值失败") - } - } else if resourceType == "device" { - var device model.Device - if err := tx.First(&device, resourceID).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") - } - - if device.SeriesID != nil { - if err := device.AddAccumulatedRechargeBySeries(*device.SeriesID, amount); err != nil { - return errors.Wrap(errors.CodeInternalError, err, "更新设备按系列累计充值失败") - } - } - - result := tx.Model(&model.Device{}). - Where("id = ?", resourceID). - Updates(map[string]any{ - "accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount), - "accumulated_recharge_by_series": device.AccumulatedRechargeBySeriesJSON, - }) - if result.Error != nil { - return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新设备累计充值失败") - } - } - return nil -} - -// triggerOneTimeCommissionIfNeededInTx 触发一次性佣金(事务内使用) -// 检查是否满足一次性佣金触发条件,满足则创建佣金记录并入账 -func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *gorm.DB, resourceType string, resourceID uint, rechargeAmount int64, userID uint) error { - var seriesID *uint - var accumulatedRecharge int64 - var firstCommissionPaid bool - var shopID *uint - - if resourceType == "iot_card" { - var card model.IotCard - if err := tx.First(&card, resourceID).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") - } - seriesID = card.SeriesID - shopID = card.ShopID - if seriesID != nil { - accumulatedRecharge = card.GetAccumulatedRechargeBySeries(*seriesID) - firstCommissionPaid = card.IsFirstRechargeTriggeredBySeries(*seriesID) - } - } else if resourceType == "device" { - var device model.Device - if err := tx.First(&device, resourceID).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") - } - seriesID = device.SeriesID - shopID = device.ShopID - if seriesID != nil { - accumulatedRecharge = device.GetAccumulatedRechargeBySeries(*seriesID) - firstCommissionPaid = device.IsFirstRechargeTriggeredBySeries(*seriesID) - } - } - - if seriesID == nil || firstCommissionPaid { - return nil - } - - if shopID == nil { - s.logger.Warn("资源未归属店铺,无法发放一次性佣金", - zap.String("resource_type", resourceType), - zap.Uint("resource_id", resourceID), - ) - return nil - } - - series, err := s.packageSeriesStore.GetByID(ctx, *seriesID) - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil - } - return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败") - } - - config, cfgErr := series.GetOneTimeCommissionConfig() - if cfgErr != nil || config == nil || !config.Enable { - return nil - } - - allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID) - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil - } - return errors.Wrap(errors.CodeDatabaseError, err, "查询系列分配失败") - } - - var rechargeAmountToCheck int64 - switch config.TriggerType { - case model.OneTimeCommissionTriggerFirstRecharge: - rechargeAmountToCheck = rechargeAmount - default: - rechargeAmountToCheck = accumulatedRecharge - } - - if rechargeAmountToCheck < config.Threshold { - return nil - } - - commissionAmount := allocation.OneTimeCommissionAmount - if commissionAmount <= 0 { - return nil - } - - 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("店铺佣金钱包不存在,跳过佣金发放", - zap.Uint("shop_id", *shopID), - ) - return nil - } - return errors.Wrap(errors.CodeDatabaseError, err, "查询店铺佣金钱包失败") - } - - var iotCardID, deviceID *uint - if resourceType == "iot_card" { - iotCardID = &resourceID - } else { - deviceID = &resourceID - } - - commissionRecord := &model.CommissionRecord{ - BaseModel: model.BaseModel{ - Creator: userID, - Updater: userID, - }, - ShopID: *shopID, - IotCardID: iotCardID, - DeviceID: deviceID, - CommissionSource: model.CommissionSourceOneTime, - Amount: commissionAmount, - Status: constants.CommissionStatusReleased, - Remark: "钱包充值触发一次性佣金", - } - - if err := tx.Create(commissionRecord).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金记录失败") - } - - // 11. 佣金入账到店铺佣金钱包 - balanceBefore := commissionWallet.Balance - result := tx.Model(&model.AgentWallet{}). - Where("id = ? AND version = ?", commissionWallet.ID, commissionWallet.Version). - Updates(map[string]any{ - "balance": gorm.Expr("balance + ?", commissionAmount), - "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, "佣金钱包版本冲突,请重试") - } - - // 12. 更新佣金记录的入账后余额 - now := time.Now() - if err := tx.Model(commissionRecord).Updates(map[string]any{ - "balance_after": balanceBefore + commissionAmount, - "released_at": now, - }).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "更新佣金记录失败") - } - - // 13. 创建佣金钱包交易记录 - remark := "一次性佣金入账(充值触发)" - refType := "commission" - commissionTransaction := &model.AgentWalletTransaction{ - AgentWalletID: commissionWallet.ID, - ShopID: *shopID, - UserID: userID, - TransactionType: "commission", - Amount: commissionAmount, - BalanceBefore: balanceBefore, - BalanceAfter: balanceBefore + commissionAmount, - Status: 1, - ReferenceType: &refType, - ReferenceID: &commissionRecord.ID, - Remark: &remark, - ShopIDTag: *shopID, - } - if err := tx.Create(commissionTransaction).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金钱包交易记录失败") - } - - // 14. 标记一次性佣金已发放 - if resourceType == "iot_card" { - var card model.IotCard - if err := tx.First(&card, resourceID).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") - } - if err := card.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil { - return errors.Wrap(errors.CodeInternalError, err, "设置卡佣金发放状态失败") - } - if err := tx.Model(&model.IotCard{}).Where("id = ?", resourceID). - Updates(map[string]any{ - "first_commission_paid": true, - "first_recharge_triggered_by_series": card.FirstRechargeTriggeredBySeriesJSON, - }).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败") - } - } else { - var device model.Device - if err := tx.First(&device, resourceID).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") - } - if err := device.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil { - return errors.Wrap(errors.CodeInternalError, err, "设置设备佣金发放状态失败") - } - if err := tx.Model(&model.Device{}).Where("id = ?", resourceID). - Updates(map[string]any{ - "first_commission_paid": true, - "first_recharge_triggered_by_series": device.FirstRechargeTriggeredBySeriesJSON, - }).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "更新设备佣金发放状态失败") - } - } - - s.logger.Info("一次性佣金发放成功", - zap.String("resource_type", resourceType), - zap.Uint("resource_id", resourceID), - zap.Uint("shop_id", *shopID), - zap.Int64("commission_amount", commissionAmount), - ) - - return nil -} - -// generateRechargeNo 生成充值订单号 -// 格式: RCH + 14位时间戳 + 6位随机数 -func (s *Service) generateRechargeNo() string { - now := time.Now() - timestamp := now.Format("20060102150405") - randomNum := rand.Intn(1000000) - return fmt.Sprintf("RCH%s%06d", timestamp, randomNum) -} - -// buildRechargeResponse 构建充值订单响应 -func (s *Service) buildRechargeResponse(recharge *model.AssetRechargeRecord) *dto.RechargeResponse { - statusText := "" - switch recharge.Status { - case constants.RechargeStatusPending: - statusText = "待支付" - case constants.RechargeStatusPaid: - statusText = "已支付" - case constants.RechargeStatusCompleted: - statusText = "已完成" - case constants.RechargeStatusClosed: - statusText = "已关闭" - case constants.RechargeStatusRefunded: - statusText = "已退款" - } - - return &dto.RechargeResponse{ - ID: recharge.ID, - RechargeNo: recharge.RechargeNo, - UserID: recharge.UserID, - WalletID: recharge.AssetWalletID, - Amount: recharge.Amount, - PaymentMethod: recharge.PaymentMethod, - PaymentChannel: recharge.PaymentChannel, - PaymentTransactionID: recharge.PaymentTransactionID, - Status: recharge.Status, - StatusText: statusText, - PaidAt: recharge.PaidAt, - CompletedAt: recharge.CompletedAt, - CreatedAt: recharge.CreatedAt, - UpdatedAt: recharge.UpdatedAt, - } -} diff --git a/internal/service/recharge_order/service.go b/internal/service/recharge_order/service.go new file mode 100644 index 0000000..d7b90b0 --- /dev/null +++ b/internal/service/recharge_order/service.go @@ -0,0 +1,433 @@ +package recharge_order + +import ( + "context" + "time" + + "github.com/break/junhong_cmp_fiber/internal/model" + "github.com/break/junhong_cmp_fiber/internal/store/postgres" + "github.com/break/junhong_cmp_fiber/internal/task" + "github.com/break/junhong_cmp_fiber/pkg/constants" + "github.com/break/junhong_cmp_fiber/pkg/errors" + "github.com/break/junhong_cmp_fiber/pkg/queue" + "github.com/hibiken/asynq" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type Service struct { + db *gorm.DB + rechargeOrderStore *postgres.RechargeOrderStore + paymentStore *postgres.PaymentStore + assetWalletStore *postgres.AssetWalletStore + assetWalletTransactionStore *postgres.AssetWalletTransactionStore + iotCardStore *postgres.IotCardStore + deviceStore *postgres.DeviceStore + packageSeriesStore *postgres.PackageSeriesStore + shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore + commissionRecordStore *postgres.CommissionRecordStore + queueClient *queue.Client + logger *zap.Logger +} + +func New( + db *gorm.DB, + rechargeOrderStore *postgres.RechargeOrderStore, + paymentStore *postgres.PaymentStore, + assetWalletStore *postgres.AssetWalletStore, + assetWalletTransactionStore *postgres.AssetWalletTransactionStore, + iotCardStore *postgres.IotCardStore, + deviceStore *postgres.DeviceStore, + packageSeriesStore *postgres.PackageSeriesStore, + shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore, + commissionRecordStore *postgres.CommissionRecordStore, + queueClient *queue.Client, + logger *zap.Logger, +) *Service { + return &Service{ + db: db, + rechargeOrderStore: rechargeOrderStore, + paymentStore: paymentStore, + assetWalletStore: assetWalletStore, + assetWalletTransactionStore: assetWalletTransactionStore, + iotCardStore: iotCardStore, + deviceStore: deviceStore, + packageSeriesStore: packageSeriesStore, + shopSeriesAllocationStore: shopSeriesAllocationStore, + commissionRecordStore: commissionRecordStore, + queueClient: queueClient, + logger: logger, + } +} + +func (s *Service) HandlePaymentCallback(ctx context.Context, paymentNo string, paymentMethod string, transactionID string) error { + payment, err := s.paymentStore.GetByPaymentNo(ctx, paymentNo) + if err != nil { + if err == gorm.ErrRecordNotFound { + return errors.New(errors.CodeNotFound, "支付记录不存在") + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") + } + + if payment.Status == model.PaymentRecordStatusPaid { + s.logger.Info("支付记录已处理,跳过", + zap.String("payment_no", paymentNo), + zap.Int("status", payment.Status), + ) + return nil + } + + if payment.Status != model.PaymentRecordStatusPending { + return errors.New(errors.CodeInvalidStatus, "支付状态不允许处理") + } + + rechargeOrder, err := s.rechargeOrderStore.GetByID(ctx, payment.OrderID) + if err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") + } + + wallet, err := s.assetWalletStore.GetByID(ctx, rechargeOrder.AssetWalletID) + if err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败") + } + + now := time.Now() + err = s.db.Transaction(func(tx *gorm.DB) error { + oldPaymentStatus := model.PaymentRecordStatusPending + if err := s.paymentStore.UpdateStatusWithOptimisticLockDB(ctx, tx, payment.ID, &oldPaymentStatus, model.PaymentRecordStatusPaid, &now); err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return errors.Wrap(errors.CodeDatabaseError, err, "更新支付状态失败") + } + + updates := map[string]interface{}{ + "third_party_trade_no": transactionID, + } + if err := tx.WithContext(ctx).Model(&model.Payment{}).Where("id = ?", payment.ID).Updates(updates).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "更新支付信息失败") + } + + oldRechargeStatus := model.RechargeOrderStatusPending + if err := s.rechargeOrderStore.UpdateStatusWithOptimisticLockDB(ctx, tx, rechargeOrder.ID, &oldRechargeStatus, model.RechargeOrderStatusPaid, &now); err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单状态失败") + } + + balanceBefore := wallet.Balance + result := tx.Model(&model.AssetWallet{}). + Where("id = ? AND version = ?", wallet.ID, wallet.Version). + Updates(map[string]any{ + "balance": gorm.Expr("balance + ?", rechargeOrder.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, "钱包版本冲突,请重试") + } + + refType := "recharge" + transaction := &model.AssetWalletTransaction{ + AssetWalletID: wallet.ID, + ResourceType: rechargeOrder.ResourceType, + ResourceID: rechargeOrder.ResourceID, + UserID: rechargeOrder.UserID, + TransactionType: "recharge", + Amount: rechargeOrder.Amount, + BalanceBefore: balanceBefore, + BalanceAfter: balanceBefore + rechargeOrder.Amount, + Status: 1, + ReferenceType: &refType, + ReferenceNo: &paymentNo, + ShopIDTag: wallet.ShopIDTag, + EnterpriseIDTag: wallet.EnterpriseIDTag, + } + if err := tx.Create(transaction).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败") + } + + if err := s.updateAccumulatedRechargeInTx(ctx, tx, rechargeOrder.ResourceType, rechargeOrder.ResourceID, rechargeOrder.Amount); err != nil { + return err + } + + if err := s.triggerOneTimeCommissionIfNeededInTx(ctx, tx, rechargeOrder, rechargeOrder.Amount); err != nil { + return err + } + + return nil + }) + + if err != nil { + return err + } + + linkedIDs := rechargeOrder.LinkedPackageIDs + if len(linkedIDs) > 0 { + taskPayload := task.AutoPurchasePayload{RechargeOrderID: rechargeOrder.ID} + if err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeAutoPurchaseAfterRecharge, taskPayload, + asynq.Queue(constants.QueueDefault), + asynq.MaxRetry(3), + ); err != nil { + s.logger.Error("自动购包任务入队失败", + zap.Uint("recharge_order_id", rechargeOrder.ID), + zap.Error(err)) + } + } + + s.logger.Info("充值支付回调处理成功", + zap.String("payment_no", paymentNo), + zap.Int64("amount", rechargeOrder.Amount), + zap.String("resource_type", rechargeOrder.ResourceType), + zap.Uint("resource_id", rechargeOrder.ResourceID), + ) + + return nil +} + +func (s *Service) updateAccumulatedRechargeInTx(ctx context.Context, tx *gorm.DB, resourceType string, resourceID uint, amount int64) error { + if resourceType == "iot_card" { + var card model.IotCard + if err := tx.First(&card, resourceID).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") + } + + if card.SeriesID != nil { + if err := card.AddAccumulatedRechargeBySeries(*card.SeriesID, amount); err != nil { + return errors.Wrap(errors.CodeInternalError, err, "更新卡按系列累计充值失败") + } + } + + result := tx.Model(&model.IotCard{}). + Where("id = ?", resourceID). + Updates(map[string]any{ + "accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount), + "accumulated_recharge_by_series": card.AccumulatedRechargeBySeriesJSON, + }) + if result.Error != nil { + return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新卡累计充值失败") + } + } else if resourceType == "device" { + var device model.Device + if err := tx.First(&device, resourceID).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") + } + + if device.SeriesID != nil { + if err := device.AddAccumulatedRechargeBySeries(*device.SeriesID, amount); err != nil { + return errors.Wrap(errors.CodeInternalError, err, "更新设备按系列累计充值失败") + } + } + + result := tx.Model(&model.Device{}). + Where("id = ?", resourceID). + Updates(map[string]any{ + "accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount), + "accumulated_recharge_by_series": device.AccumulatedRechargeBySeriesJSON, + }) + if result.Error != nil { + return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新设备累计充值失败") + } + } + return nil +} + +func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *gorm.DB, rechargeOrder *model.RechargeOrder, rechargeAmount int64) error { + var seriesID *uint + var accumulatedRecharge int64 + var firstCommissionPaid bool + var shopID *uint + + if rechargeOrder.ResourceType == "iot_card" { + var card model.IotCard + if err := tx.First(&card, rechargeOrder.ResourceID).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") + } + seriesID = card.SeriesID + shopID = card.ShopID + if seriesID != nil { + accumulatedRecharge = card.GetAccumulatedRechargeBySeries(*seriesID) + firstCommissionPaid = card.IsFirstRechargeTriggeredBySeries(*seriesID) + } + } else if rechargeOrder.ResourceType == "device" { + var device model.Device + if err := tx.First(&device, rechargeOrder.ResourceID).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") + } + seriesID = device.SeriesID + shopID = device.ShopID + if seriesID != nil { + accumulatedRecharge = device.GetAccumulatedRechargeBySeries(*seriesID) + firstCommissionPaid = device.IsFirstRechargeTriggeredBySeries(*seriesID) + } + } + + if seriesID == nil || firstCommissionPaid { + return nil + } + + if shopID == nil { + s.logger.Warn("资源未归属店铺,无法发放一次性佣金", + zap.String("resource_type", rechargeOrder.ResourceType), + zap.Uint("resource_id", rechargeOrder.ResourceID), + ) + return nil + } + + series, err := s.packageSeriesStore.GetByID(ctx, *seriesID) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败") + } + + config, cfgErr := series.GetOneTimeCommissionConfig() + if cfgErr != nil || config == nil || !config.Enable { + return nil + } + + allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询系列分配失败") + } + + var rechargeAmountToCheck int64 + switch config.TriggerType { + case model.OneTimeCommissionTriggerFirstRecharge: + rechargeAmountToCheck = rechargeAmount + default: + rechargeAmountToCheck = accumulatedRecharge + } + + if rechargeAmountToCheck < config.Threshold { + return nil + } + + commissionAmount := allocation.OneTimeCommissionAmount + if commissionAmount <= 0 { + return nil + } + + 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("店铺佣金钱包不存在,跳过佣金发放", zap.Uint("shop_id", *shopID)) + return nil + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询店铺佣金钱包失败") + } + + var iotCardID, deviceID *uint + if rechargeOrder.ResourceType == "iot_card" { + iotCardID = &rechargeOrder.ResourceID + } else { + deviceID = &rechargeOrder.ResourceID + } + + commissionRecord := &model.CommissionRecord{ + BaseModel: model.BaseModel{ + Creator: rechargeOrder.UserID, + Updater: rechargeOrder.UserID, + }, + ShopID: *shopID, + IotCardID: iotCardID, + DeviceID: deviceID, + CommissionSource: model.CommissionSourceOneTime, + Amount: commissionAmount, + Status: constants.CommissionStatusReleased, + Remark: "钱包充值触发一次性佣金", + } + + if err := tx.Create(commissionRecord).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金记录失败") + } + + balanceBefore := commissionWallet.Balance + result := tx.Model(&model.AgentWallet{}). + Where("id = ? AND version = ?", commissionWallet.ID, commissionWallet.Version). + Updates(map[string]any{ + "balance": gorm.Expr("balance + ?", commissionAmount), + "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, "佣金钱包版本冲突,请重试") + } + + now := time.Now() + if err := tx.Model(commissionRecord).Updates(map[string]any{ + "balance_after": balanceBefore + commissionAmount, + "released_at": now, + }).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "更新佣金记录失败") + } + + refType := "commission" + commissionTransaction := &model.AgentWalletTransaction{ + AgentWalletID: commissionWallet.ID, + ShopID: *shopID, + UserID: rechargeOrder.UserID, + TransactionType: "commission", + Amount: commissionAmount, + BalanceBefore: balanceBefore, + BalanceAfter: balanceBefore + commissionAmount, + Status: 1, + ReferenceType: &refType, + ReferenceID: &commissionRecord.ID, + ShopIDTag: *shopID, + } + if err := tx.Create(commissionTransaction).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金钱包交易记录失败") + } + + if rechargeOrder.ResourceType == "iot_card" { + var card model.IotCard + if err := tx.First(&card, rechargeOrder.ResourceID).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") + } + if err := card.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil { + return errors.Wrap(errors.CodeInternalError, err, "设置卡佣金发放状态失败") + } + if err := tx.Model(&model.IotCard{}).Where("id = ?", rechargeOrder.ResourceID). + Updates(map[string]any{ + "first_commission_paid": true, + "first_recharge_triggered_by_series": card.FirstRechargeTriggeredBySeriesJSON, + }).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败") + } + } else { + var device model.Device + if err := tx.First(&device, rechargeOrder.ResourceID).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") + } + if err := device.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil { + return errors.Wrap(errors.CodeInternalError, err, "设置设备佣金发放状态失败") + } + if err := tx.Model(&model.Device{}).Where("id = ?", rechargeOrder.ResourceID). + Updates(map[string]any{ + "first_commission_paid": true, + "first_recharge_triggered_by_series": device.FirstRechargeTriggeredBySeriesJSON, + }).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "更新设备佣金发放状态失败") + } + } + + s.logger.Info("一次性佣金发放成功", + zap.String("resource_type", rechargeOrder.ResourceType), + zap.Uint("resource_id", rechargeOrder.ResourceID), + zap.Uint("shop_id", *shopID), + zap.Int64("commission_amount", commissionAmount), + ) + + return nil +} diff --git a/internal/service/wechat_config/service.go b/internal/service/wechat_config/service.go index 2d9b580..68b3851 100644 --- a/internal/service/wechat_config/service.go +++ b/internal/service/wechat_config/service.go @@ -33,7 +33,7 @@ type AuditServiceInterface interface { type Service struct { store *postgres.WechatConfigStore orderStore *postgres.OrderStore - assetRechargeStore *postgres.AssetRechargeStore + rechargeOrderStore *postgres.RechargeOrderStore agentRechargeStore *postgres.AgentRechargeStore auditService AuditServiceInterface redis *redis.Client @@ -44,7 +44,7 @@ type Service struct { func New( store *postgres.WechatConfigStore, orderStore *postgres.OrderStore, - assetRechargeStore *postgres.AssetRechargeStore, + rechargeOrderStore *postgres.RechargeOrderStore, agentRechargeStore *postgres.AgentRechargeStore, auditService AuditServiceInterface, rdb *redis.Client, @@ -53,7 +53,7 @@ func New( return &Service{ store: store, orderStore: orderStore, - assetRechargeStore: assetRechargeStore, + rechargeOrderStore: rechargeOrderStore, agentRechargeStore: agentRechargeStore, auditService: auditService, redis: rdb, @@ -521,7 +521,7 @@ func (s *Service) resolvePaymentConfigID(ctx context.Context, orderNo string) (* return order.PaymentConfigID, nil case len(orderNo) >= 4 && orderNo[:4] == constants.AssetRechargeOrderPrefix: - record, err := s.assetRechargeStore.GetByRechargeNo(ctx, orderNo) + record, err := s.rechargeOrderStore.GetByRechargeOrderNo(ctx, orderNo) if err != nil { return nil, err } diff --git a/internal/store/postgres/asset_recharge_store.go b/internal/store/postgres/asset_recharge_store.go index 20a9168..f435a0a 100644 --- a/internal/store/postgres/asset_recharge_store.go +++ b/internal/store/postgres/asset_recharge_store.go @@ -1,250 +1,5 @@ package postgres -import ( - "context" - - "github.com/break/junhong_cmp_fiber/internal/model" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - // AssetRechargeStore 资产充值记录数据访问层 -type AssetRechargeStore struct { - db *gorm.DB - redis *redis.Client -} - -// NewAssetRechargeStore 创建资产充值记录 Store -func NewAssetRechargeStore(db *gorm.DB, redis *redis.Client) *AssetRechargeStore { - return &AssetRechargeStore{ - db: db, - redis: redis, - } -} - -// Create 创建充值记录 -func (s *AssetRechargeStore) Create(ctx context.Context, record *model.AssetRechargeRecord) error { - return s.db.WithContext(ctx).Create(record).Error -} - -// CreateWithTx 创建充值记录(带事务) -func (s *AssetRechargeStore) CreateWithTx(ctx context.Context, tx *gorm.DB, record *model.AssetRechargeRecord) error { - return tx.WithContext(ctx).Create(record).Error -} - -// GetByRechargeNo 根据充值订单号查询 -func (s *AssetRechargeStore) GetByRechargeNo(ctx context.Context, rechargeNo string) (*model.AssetRechargeRecord, error) { - var record model.AssetRechargeRecord - 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 *AssetRechargeStore) GetByID(ctx context.Context, id uint) (*model.AssetRechargeRecord, error) { - var record model.AssetRechargeRecord - if err := s.db.WithContext(ctx).First(&record, id).Error; err != nil { - return nil, err - } - return &record, nil -} - -// UpdateStatus 更新充值状态 -func (s *AssetRechargeStore) UpdateStatus(ctx context.Context, id uint, status int) error { - return s.db.WithContext(ctx). - Model(&model.AssetRechargeRecord{}). - Where("id = ?", id). - Update("status", status).Error -} - -// UpdateStatusWithTx 更新充值状态(带事务) -func (s *AssetRechargeStore) UpdateStatusWithTx(ctx context.Context, tx *gorm.DB, id uint, status int) error { - return tx.WithContext(ctx). - Model(&model.AssetRechargeRecord{}). - Where("id = ?", id). - Update("status", status).Error -} - -// Update 更新充值记录 -func (s *AssetRechargeStore) Update(ctx context.Context, record *model.AssetRechargeRecord) error { - return s.db.WithContext(ctx).Save(record).Error -} - -// UpdateWithTx 更新充值记录(带事务) -func (s *AssetRechargeStore) UpdateWithTx(ctx context.Context, tx *gorm.DB, record *model.AssetRechargeRecord) error { - return tx.WithContext(ctx).Save(record).Error -} - -// ListByResourceID 按资源查询充值记录(支持分页) -func (s *AssetRechargeStore) ListByResourceID(ctx context.Context, resourceType string, resourceID uint, offset, limit int) ([]*model.AssetRechargeRecord, error) { - var records []*model.AssetRechargeRecord - 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 *AssetRechargeStore) ListByUserID(ctx context.Context, userID uint, offset, limit int) ([]*model.AssetRechargeRecord, error) { - var records []*model.AssetRechargeRecord - 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 *AssetRechargeStore) ListByStatus(ctx context.Context, status int, offset, limit int) ([]*model.AssetRechargeRecord, error) { - var records []*model.AssetRechargeRecord - 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 -} - -// ListAssetRechargeParams 充值记录列表查询参数 -type ListAssetRechargeParams struct { - Page int - PageSize int - UserID *uint - AssetWalletID *uint - ResourceType *string - ResourceID *uint - Status *int - StartTime interface{} - EndTime interface{} -} - -// List 查询充值记录列表(支持分页和筛选) -func (s *AssetRechargeStore) List(ctx context.Context, params *ListAssetRechargeParams) ([]*model.AssetRechargeRecord, int64, error) { - var records []*model.AssetRechargeRecord - var total int64 - - query := s.db.WithContext(ctx).Model(&model.AssetRechargeRecord{}) - - if params.UserID != nil { - query = query.Where("user_id = ?", *params.UserID) - } - if params.AssetWalletID != nil { - query = query.Where("asset_wallet_id = ?", *params.AssetWalletID) - } - 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 *AssetRechargeStore) UpdatePaymentInfo(ctx context.Context, id uint, paymentMethod *string, paymentTransactionID *string) error { - return s.UpdatePaymentInfoWithDB(ctx, s.db, id, paymentMethod, paymentTransactionID) -} - -// UpdatePaymentInfoWithDB 更新支付信息(支持传入事务 tx) -func (s *AssetRechargeStore) UpdatePaymentInfoWithDB(ctx context.Context, db *gorm.DB, 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 := db.WithContext(ctx).Model(&model.AssetRechargeRecord{}).Where("id = ?", id).Updates(updates) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound - } - return nil -} - -// UpdateStatusWithOptimisticLock 更新充值状态(支持乐观锁) -func (s *AssetRechargeStore) UpdateStatusWithOptimisticLock(ctx context.Context, id uint, oldStatus *int, newStatus int, paidAt interface{}, completedAt interface{}) error { - return s.UpdateStatusWithOptimisticLockDB(ctx, s.db, id, oldStatus, newStatus, paidAt, completedAt) -} - -// UpdateStatusWithOptimisticLockDB 更新充值状态(支持传入事务 tx) -func (s *AssetRechargeStore) UpdateStatusWithOptimisticLockDB(ctx context.Context, db *gorm.DB, 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 := db.WithContext(ctx).Model(&model.AssetRechargeRecord{}).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 -} +// 已废弃 - 请使用 RechargeOrderStore 和 PaymentStore +type AssetRechargeStore struct{} diff --git a/internal/store/postgres/payment_store.go b/internal/store/postgres/payment_store.go new file mode 100644 index 0000000..698e4df --- /dev/null +++ b/internal/store/postgres/payment_store.go @@ -0,0 +1,152 @@ +package postgres + +import ( + "context" + + "github.com/break/junhong_cmp_fiber/internal/model" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type PaymentStore struct { + db *gorm.DB + redis *redis.Client +} + +func NewPaymentStore(db *gorm.DB, redis *redis.Client) *PaymentStore { + return &PaymentStore{ + db: db, + redis: redis, + } +} + +func (s *PaymentStore) Create(ctx context.Context, payment *model.Payment) error { + return s.db.WithContext(ctx).Create(payment).Error +} + +func (s *PaymentStore) CreateWithTx(ctx context.Context, tx *gorm.DB, payment *model.Payment) error { + return tx.WithContext(ctx).Create(payment).Error +} + +func (s *PaymentStore) GetByPaymentNo(ctx context.Context, paymentNo string) (*model.Payment, error) { + var payment model.Payment + err := s.db.WithContext(ctx). + Where("payment_no = ?", paymentNo). + First(&payment).Error + if err != nil { + return nil, err + } + return &payment, nil +} + +func (s *PaymentStore) GetByID(ctx context.Context, id uint) (*model.Payment, error) { + var payment model.Payment + err := s.db.WithContext(ctx).First(&payment, id).Error + if err != nil { + return nil, err + } + return &payment, nil +} + +func (s *PaymentStore) UpdateStatus(ctx context.Context, id uint, status int) error { + return s.db.WithContext(ctx). + Model(&model.Payment{}). + Where("id = ?", id). + Update("status", status).Error +} + +func (s *PaymentStore) UpdateStatusWithTx(ctx context.Context, tx *gorm.DB, id uint, status int) error { + return tx.WithContext(ctx). + Model(&model.Payment{}). + Where("id = ?", id). + Update("status", status).Error +} + +func (s *PaymentStore) UpdateStatusWithOptimisticLock(ctx context.Context, id uint, oldStatus *int, newStatus int, paidAt interface{}) error { + return s.UpdateStatusWithOptimisticLockDB(ctx, s.db, id, oldStatus, newStatus, paidAt) +} + +func (s *PaymentStore) UpdateStatusWithOptimisticLockDB(ctx context.Context, db *gorm.DB, id uint, oldStatus *int, newStatus int, paidAt interface{}) error { + updates := map[string]interface{}{ + "status": newStatus, + } + if paidAt != nil { + updates["paid_at"] = paidAt + } + + query := db.WithContext(ctx).Model(&model.Payment{}).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 +} + +func (s *PaymentStore) UpdatePaymentInfo(ctx context.Context, id uint, thirdPartyTradeNo string, paymentConfigID *uint) error { + return s.UpdatePaymentInfoWithDB(ctx, s.db, id, thirdPartyTradeNo, paymentConfigID) +} + +func (s *PaymentStore) UpdatePaymentInfoWithDB(ctx context.Context, db *gorm.DB, id uint, thirdPartyTradeNo string, paymentConfigID *uint) error { + updates := map[string]interface{}{} + if thirdPartyTradeNo != "" { + updates["third_party_trade_no"] = thirdPartyTradeNo + } + if paymentConfigID != nil { + updates["payment_config_id"] = *paymentConfigID + } + + if len(updates) == 0 { + return nil + } + + result := db.WithContext(ctx).Model(&model.Payment{}).Where("id = ?", id).Updates(updates) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + +func (s *PaymentStore) ListByOrderID(ctx context.Context, orderID uint, orderType string) ([]*model.Payment, error) { + var payments []*model.Payment + err := s.db.WithContext(ctx). + Where("order_id = ? AND order_type = ?", orderID, orderType). + Order("created_at DESC"). + Find(&payments).Error + if err != nil { + return nil, err + } + return payments, nil +} + +func (s *PaymentStore) ListByOrderIDAndStatus(ctx context.Context, orderID uint, orderType string, status int) (*model.Payment, error) { + var payment model.Payment + err := s.db.WithContext(ctx). + Where("order_id = ? AND order_type = ? AND status = ?", orderID, orderType, status). + First(&payment).Error + if err != nil { + return nil, err + } + return &payment, nil +} + +func (s *PaymentStore) GetByOrderIDAndStatus(ctx context.Context, orderID uint, status int) (*model.Payment, error) { + var payment model.Payment + err := s.db.WithContext(ctx). + Where("order_id = ? AND status = ?", orderID, status). + First(&payment).Error + if err != nil { + return nil, err + } + return &payment, nil +} diff --git a/internal/store/postgres/recharge_order_store.go b/internal/store/postgres/recharge_order_store.go new file mode 100644 index 0000000..81e66a9 --- /dev/null +++ b/internal/store/postgres/recharge_order_store.go @@ -0,0 +1,188 @@ +package postgres + +import ( + "context" + + "github.com/break/junhong_cmp_fiber/internal/model" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type RechargeOrderStore struct { + db *gorm.DB + redis *redis.Client +} + +func NewRechargeOrderStore(db *gorm.DB, redis *redis.Client) *RechargeOrderStore { + return &RechargeOrderStore{ + db: db, + redis: redis, + } +} + +func (s *RechargeOrderStore) Create(ctx context.Context, order *model.RechargeOrder) error { + return s.db.WithContext(ctx).Create(order).Error +} + +func (s *RechargeOrderStore) CreateWithTx(ctx context.Context, tx *gorm.DB, order *model.RechargeOrder) error { + return tx.WithContext(ctx).Create(order).Error +} + +func (s *RechargeOrderStore) GetByRechargeOrderNo(ctx context.Context, orderNo string) (*model.RechargeOrder, error) { + var order model.RechargeOrder + err := s.db.WithContext(ctx). + Where("recharge_order_no = ?", orderNo). + First(&order).Error + if err != nil { + return nil, err + } + return &order, nil +} + +func (s *RechargeOrderStore) GetByID(ctx context.Context, id uint) (*model.RechargeOrder, error) { + var order model.RechargeOrder + err := s.db.WithContext(ctx).First(&order, id).Error + if err != nil { + return nil, err + } + return &order, nil +} + +func (s *RechargeOrderStore) UpdateStatus(ctx context.Context, id uint, status int) error { + return s.db.WithContext(ctx). + Model(&model.RechargeOrder{}). + Where("id = ?", id). + Update("status", status).Error +} + +func (s *RechargeOrderStore) UpdateStatusWithTx(ctx context.Context, tx *gorm.DB, id uint, status int) error { + return tx.WithContext(ctx). + Model(&model.RechargeOrder{}). + Where("id = ?", id). + Update("status", status).Error +} + +func (s *RechargeOrderStore) UpdateStatusWithOptimisticLock(ctx context.Context, id uint, oldStatus *int, newStatus int, paidAt interface{}) error { + return s.UpdateStatusWithOptimisticLockDB(ctx, s.db, id, oldStatus, newStatus, paidAt) +} + +func (s *RechargeOrderStore) UpdateStatusWithOptimisticLockDB(ctx context.Context, db *gorm.DB, id uint, oldStatus *int, newStatus int, paidAt interface{}) error { + updates := map[string]interface{}{ + "status": newStatus, + } + if paidAt != nil { + updates["paid_at"] = paidAt + } + + query := db.WithContext(ctx).Model(&model.RechargeOrder{}).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 +} + +func (s *RechargeOrderStore) Update(ctx context.Context, order *model.RechargeOrder) error { + return s.db.WithContext(ctx).Save(order).Error +} + +func (s *RechargeOrderStore) ListByCard(ctx context.Context, iotCardID uint, generation int, offset, limit int) ([]*model.RechargeOrder, error) { + var orders []*model.RechargeOrder + query := s.db.WithContext(ctx). + Where("iot_card_id = ? AND generation = ?", iotCardID, generation) + + if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&orders).Error; err != nil { + return nil, err + } + return orders, nil +} + +func (s *RechargeOrderStore) ListByDevice(ctx context.Context, deviceID uint, generation int, offset, limit int) ([]*model.RechargeOrder, error) { + var orders []*model.RechargeOrder + query := s.db.WithContext(ctx). + Where("device_id = ? AND generation = ?", deviceID, generation) + + if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&orders).Error; err != nil { + return nil, err + } + return orders, nil +} + +func (s *RechargeOrderStore) ListByUserID(ctx context.Context, userID uint, offset, limit int) ([]*model.RechargeOrder, error) { + var orders []*model.RechargeOrder + err := s.db.WithContext(ctx). + Where("user_id = ?", userID). + Order("created_at DESC"). + Offset(offset). + Limit(limit). + Find(&orders).Error + if err != nil { + return nil, err + } + return orders, nil +} + +type ListRechargeOrderParams struct { + Page int + PageSize int + UserID *uint + IotCardID *uint + DeviceID *uint + Status *int + StartTime interface{} + EndTime interface{} +} + +func (s *RechargeOrderStore) List(ctx context.Context, params *ListRechargeOrderParams) ([]*model.RechargeOrder, int64, error) { + var orders []*model.RechargeOrder + var total int64 + + query := s.db.WithContext(ctx).Model(&model.RechargeOrder{}) + + if params.UserID != nil { + query = query.Where("user_id = ?", *params.UserID) + } + if params.IotCardID != nil { + query = query.Where("iot_card_id = ?", *params.IotCardID) + } + if params.DeviceID != nil { + query = query.Where("device_id = ?", *params.DeviceID) + } + 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(&orders).Error; err != nil { + return nil, 0, err + } + + return orders, total, nil +} diff --git a/internal/store/postgres/wechat_config_store.go b/internal/store/postgres/wechat_config_store.go index f85b618..b23427b 100644 --- a/internal/store/postgres/wechat_config_store.go +++ b/internal/store/postgres/wechat_config_store.go @@ -145,7 +145,7 @@ func (s *WechatConfigStore) CountPendingOrdersByConfigID(ctx context.Context, co func (s *WechatConfigStore) CountPendingRechargesByConfigID(ctx context.Context, configID uint) (int64, error) { var count int64 err := s.db.WithContext(ctx). - Table("tb_asset_recharge_record"). + Table("tb_recharge_order"). Where("payment_config_id = ? AND status = ? AND deleted_at IS NULL", configID, 1). Count(&count).Error return count, err diff --git a/internal/task/auto_purchase.go b/internal/task/auto_purchase.go index c6370be..b86f2a3 100644 --- a/internal/task/auto_purchase.go +++ b/internal/task/auto_purchase.go @@ -19,14 +19,15 @@ import ( // AutoPurchasePayload 充值后自动购包任务载荷 type AutoPurchasePayload struct { - RechargeRecordID uint `json:"recharge_record_id"` + RechargeOrderID uint `json:"recharge_order_id"` } // AutoPurchaseHandler 充值后自动购包任务处理器 type AutoPurchaseHandler struct { db *gorm.DB orderStore *postgres.OrderStore - rechargeRecordStore *postgres.AssetRechargeStore + rechargeOrderStore *postgres.RechargeOrderStore + paymentStore *postgres.PaymentStore walletStore *postgres.AssetWalletStore walletTransactionStore *postgres.AssetWalletTransactionStore packageUsageStore *postgres.PackageUsageStore @@ -42,7 +43,8 @@ type AutoPurchaseHandler struct { func NewAutoPurchaseHandler( db *gorm.DB, orderStore *postgres.OrderStore, - rechargeRecordStore *postgres.AssetRechargeStore, + rechargeOrderStore *postgres.RechargeOrderStore, + paymentStore *postgres.PaymentStore, walletStore *postgres.AssetWalletStore, walletTransactionStore *postgres.AssetWalletTransactionStore, packageUsageStore *postgres.PackageUsageStore, @@ -53,8 +55,11 @@ func NewAutoPurchaseHandler( if orderStore == nil { orderStore = postgres.NewOrderStore(db, redisClient) } - if rechargeRecordStore == nil { - rechargeRecordStore = postgres.NewAssetRechargeStore(db, redisClient) + if rechargeOrderStore == nil { + rechargeOrderStore = postgres.NewRechargeOrderStore(db, redisClient) + } + if paymentStore == nil { + paymentStore = postgres.NewPaymentStore(db, redisClient) } if walletStore == nil { walletStore = postgres.NewAssetWalletStore(db, redisClient) @@ -69,7 +74,8 @@ func NewAutoPurchaseHandler( return &AutoPurchaseHandler{ db: db, orderStore: orderStore, - rechargeRecordStore: rechargeRecordStore, + rechargeOrderStore: rechargeOrderStore, + paymentStore: paymentStore, walletStore: walletStore, walletTransactionStore: walletTransactionStore, packageUsageStore: packageUsageStore, @@ -89,64 +95,64 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) h.logger.Error("解析自动购包任务载荷失败", zap.Error(err)) return asynq.SkipRetry } - if payload.RechargeRecordID == 0 { - h.logger.Error("自动购包任务载荷无效", zap.Uint("recharge_record_id", payload.RechargeRecordID)) + if payload.RechargeOrderID == 0 { + h.logger.Error("自动购包任务载荷无效", zap.Uint("recharge_order_id", payload.RechargeOrderID)) return asynq.SkipRetry } - rechargeRecord, err := h.rechargeRecordStore.GetByID(ctx, payload.RechargeRecordID) + rechargeOrder, err := h.rechargeOrderStore.GetByID(ctx, payload.RechargeOrderID) if err != nil { if err == gorm.ErrRecordNotFound { - h.logger.Warn("充值记录不存在,跳过自动购包", zap.Uint("recharge_record_id", payload.RechargeRecordID)) + h.logger.Warn("充值订单不存在,跳过自动购包", zap.Uint("recharge_order_id", payload.RechargeOrderID)) return asynq.SkipRetry } - h.logger.Error("查询充值记录失败", zap.Uint("recharge_record_id", payload.RechargeRecordID), zap.Error(err)) + h.logger.Error("查询充值订单失败", zap.Uint("recharge_order_id", payload.RechargeOrderID), zap.Error(err)) return err } - if rechargeRecord.AutoPurchaseStatus == constants.AutoPurchaseStatusSuccess { + if rechargeOrder.AutoPurchaseStatus == constants.AutoPurchaseStatusSuccess { return nil } - if rechargeRecord.AutoPurchaseStatus == constants.AutoPurchaseStatusFailed { + if rechargeOrder.AutoPurchaseStatus == constants.AutoPurchaseStatusFailed { return nil } - packageIDs, err := parseLinkedPackageIDs(rechargeRecord.LinkedPackageIDs) + packageIDs, err := parseLinkedPackageIDs(rechargeOrder.LinkedPackageIDs) if err != nil { - h.logger.Error("解析关联套餐ID失败", zap.Uint("recharge_record_id", rechargeRecord.ID), zap.Error(err)) - h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeRecord.ID) + h.logger.Error("解析关联套餐ID失败", zap.Uint("recharge_order_id", rechargeOrder.ID), zap.Error(err)) + h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeOrder.ID) return asynq.SkipRetry } if len(packageIDs) == 0 { - h.logger.Error("关联套餐ID为空,无法自动购包", zap.Uint("recharge_record_id", rechargeRecord.ID)) - h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeRecord.ID) + h.logger.Error("关联套餐ID为空,无法自动购包", zap.Uint("recharge_order_id", rechargeOrder.ID)) + h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeOrder.ID) return asynq.SkipRetry } packages, totalAmount, err := h.loadPackages(ctx, packageIDs) if err != nil { - h.logger.Error("加载关联套餐失败", zap.Uint("recharge_record_id", rechargeRecord.ID), zap.Error(err)) - h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeRecord.ID) + h.logger.Error("加载关联套餐失败", zap.Uint("recharge_order_id", rechargeOrder.ID), zap.Error(err)) + h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeOrder.ID) return err } // 获取资产系列ID,用于差价佣金和一次性佣金计算 var seriesID *uint - if rechargeRecord.LinkedCarrierID != nil && *rechargeRecord.LinkedCarrierID > 0 { - if rechargeRecord.LinkedCarrierType == "card" || rechargeRecord.LinkedCarrierType == constants.AssetWalletResourceTypeIotCard { - if card, cardErr := h.iotCardStore.GetByID(ctx, *rechargeRecord.LinkedCarrierID); cardErr == nil { + if rechargeOrder.LinkedCarrierID != nil && *rechargeOrder.LinkedCarrierID > 0 { + if rechargeOrder.LinkedCarrierType == "card" || rechargeOrder.LinkedCarrierType == constants.AssetWalletResourceTypeIotCard { + if card, cardErr := h.iotCardStore.GetByID(ctx, *rechargeOrder.LinkedCarrierID); cardErr == nil { seriesID = card.SeriesID } else { h.logger.Warn("自动购包获取卡系列ID失败", - zap.Uint("card_id", *rechargeRecord.LinkedCarrierID), + zap.Uint("card_id", *rechargeOrder.LinkedCarrierID), zap.Error(cardErr)) } - } else if rechargeRecord.LinkedCarrierType == "device" || rechargeRecord.LinkedCarrierType == constants.AssetWalletResourceTypeDevice { - if device, deviceErr := h.deviceStore.GetByID(ctx, *rechargeRecord.LinkedCarrierID); deviceErr == nil { + } else if rechargeOrder.LinkedCarrierType == "device" || rechargeOrder.LinkedCarrierType == constants.AssetWalletResourceTypeDevice { + if device, deviceErr := h.deviceStore.GetByID(ctx, *rechargeOrder.LinkedCarrierID); deviceErr == nil { seriesID = device.SeriesID } else { h.logger.Warn("自动购包获取设备系列ID失败", - zap.Uint("device_id", *rechargeRecord.LinkedCarrierID), + zap.Uint("device_id", *rechargeOrder.LinkedCarrierID), zap.Error(deviceErr)) } } @@ -155,13 +161,13 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) // 获取卖家成本价,用于差价佣金链式计算的起点 // 平台直销(ShopIDTag == 0)时成本价为 0,差价佣金计算会直接跳过 var sellerCostPrice int64 - if rechargeRecord.ShopIDTag > 0 && len(packages) > 0 { - allocation, allocErr := h.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, rechargeRecord.ShopIDTag, packages[0].ID) + if rechargeOrder.ShopIDTag > 0 && len(packages) > 0 { + allocation, allocErr := h.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, rechargeOrder.ShopIDTag, packages[0].ID) if allocErr == nil { sellerCostPrice = allocation.CostPrice } else { h.logger.Warn("自动购包获取卖家成本价失败,差价佣金将计算为0", - zap.Uint("shop_id", rechargeRecord.ShopIDTag), + zap.Uint("shop_id", rechargeOrder.ShopIDTag), zap.Uint("package_id", packages[0].ID), zap.Error(allocErr)) } @@ -169,7 +175,7 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) var createdOrderID uint if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - wallet, walletErr := h.walletStore.GetByID(ctx, rechargeRecord.AssetWalletID) + wallet, walletErr := h.walletStore.GetByID(ctx, rechargeOrder.AssetWalletID) if walletErr != nil { if walletErr == gorm.ErrRecordNotFound { return errors.New("资产钱包不存在") @@ -185,7 +191,7 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) } now := time.Now() - order, orderItems, buildErr := h.buildOrderAndItems(rechargeRecord, packages, totalAmount, seriesID, sellerCostPrice, now) + order, orderItems, buildErr := h.buildOrderAndItems(rechargeOrder, packages, totalAmount, seriesID, sellerCostPrice, now) if buildErr != nil { return buildErr } @@ -202,12 +208,24 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) return err } + payment := &model.Payment{ + PaymentNo: order.OrderNo, + OrderID: order.ID, + OrderType: model.PaymentOrderTypePackage, + PaymentMethod: model.PaymentByWallet, + Amount: totalAmount, + Status: model.PaymentRecordStatusPaid, + } + if err = tx.Create(payment).Error; err != nil { + return err + } + refType := constants.ReferenceTypeOrder walletTx := &model.AssetWalletTransaction{ AssetWalletID: wallet.ID, ResourceType: wallet.ResourceType, ResourceID: wallet.ResourceID, - UserID: rechargeRecord.UserID, + UserID: rechargeOrder.UserID, TransactionType: constants.AssetTransactionTypeDeduct, Amount: -totalAmount, BalanceBefore: wallet.Balance, @@ -215,7 +233,7 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) Status: constants.TransactionStatusSuccess, ReferenceType: &refType, ReferenceNo: &order.OrderNo, - Creator: rechargeRecord.UserID, + Creator: rechargeOrder.UserID, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, } @@ -227,8 +245,8 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) return err } - if err = tx.Model(&model.AssetRechargeRecord{}). - Where("id = ?", rechargeRecord.ID). + if err = tx.Model(&model.RechargeOrder{}). + Where("id = ?", rechargeOrder.ID). Update("auto_purchase_status", constants.AutoPurchaseStatusSuccess).Error; err != nil { return err } @@ -236,10 +254,10 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) return nil }); err != nil { h.logger.Error("自动购包任务执行失败", - zap.Uint("recharge_record_id", rechargeRecord.ID), + zap.Uint("recharge_record_id", rechargeOrder.ID), zap.Error(err), ) - h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeRecord.ID) + h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeOrder.ID) return err } @@ -263,13 +281,13 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) } } - h.logger.Info("自动购包任务执行成功", zap.Uint("recharge_record_id", rechargeRecord.ID)) + h.logger.Info("自动购包任务执行成功", zap.Uint("recharge_record_id", rechargeOrder.ID)) return nil } // NewAutoPurchaseTask 创建充值后自动购包任务 -func NewAutoPurchaseTask(rechargeRecordID uint) (*asynq.Task, error) { - payloadBytes, err := sonic.Marshal(AutoPurchasePayload{RechargeRecordID: rechargeRecordID}) +func NewAutoPurchaseTask(rechargeOrderID uint) (*asynq.Task, error) { + payloadBytes, err := sonic.Marshal(AutoPurchasePayload{RechargeOrderID: rechargeOrderID}) if err != nil { return nil, err } @@ -281,7 +299,7 @@ func NewAutoPurchaseTask(rechargeRecordID uint) (*asynq.Task, error) { ), nil } -func (h *AutoPurchaseHandler) markAutoPurchaseFailedIfFinalRetry(ctx context.Context, rechargeRecordID uint) { +func (h *AutoPurchaseHandler) markAutoPurchaseFailedIfFinalRetry(ctx context.Context, rechargeOrderID uint) { retryCount, ok := asynq.GetRetryCount(ctx) if !ok { return @@ -295,17 +313,17 @@ func (h *AutoPurchaseHandler) markAutoPurchaseFailedIfFinalRetry(ctx context.Con } if err := h.db.WithContext(ctx). - Model(&model.AssetRechargeRecord{}). - Where("id = ?", rechargeRecordID). + Model(&model.RechargeOrder{}). + Where("id = ?", rechargeOrderID). Update("auto_purchase_status", constants.AutoPurchaseStatusFailed).Error; err != nil { h.logger.Error("更新自动购包失败状态失败", - zap.Uint("recharge_record_id", rechargeRecordID), + zap.Uint("recharge_record_id", rechargeOrderID), zap.Error(err), ) return } - h.logger.Warn("自动购包达到最大重试次数,已标记失败", zap.Uint("recharge_record_id", rechargeRecordID)) + h.logger.Warn("自动购包达到最大重试次数,已标记失败", zap.Uint("recharge_record_id", rechargeOrderID)) } func (h *AutoPurchaseHandler) loadPackages(ctx context.Context, packageIDs []uint) ([]*model.Package, int64, error) { @@ -330,39 +348,39 @@ func (h *AutoPurchaseHandler) loadPackages(ctx context.Context, packageIDs []uin } func (h *AutoPurchaseHandler) buildOrderAndItems( - rechargeRecord *model.AssetRechargeRecord, + rechargeOrder *model.RechargeOrder, packages []*model.Package, totalAmount int64, seriesID *uint, sellerCostPrice int64, now time.Time, ) (*model.Order, []*model.OrderItem, error) { - orderType, iotCardID, deviceID, err := parseLinkedCarrier(rechargeRecord.LinkedOrderType, rechargeRecord.LinkedCarrierType, rechargeRecord.LinkedCarrierID) + orderType, iotCardID, deviceID, err := parseLinkedCarrier(rechargeOrder.LinkedOrderType, rechargeOrder.LinkedCarrierType, rechargeOrder.LinkedCarrierID) if err != nil { return nil, nil, err } - generation := rechargeRecord.Generation + generation := rechargeOrder.Generation if generation <= 0 { generation = 1 } var sellerShopID *uint - if rechargeRecord.ShopIDTag > 0 { - shopID := rechargeRecord.ShopIDTag + if rechargeOrder.ShopIDTag > 0 { + shopID := rechargeOrder.ShopIDTag sellerShopID = &shopID } paidAmount := totalAmount order := &model.Order{ BaseModel: model.BaseModel{ - Creator: rechargeRecord.UserID, - Updater: rechargeRecord.UserID, + Creator: rechargeOrder.UserID, + Updater: rechargeOrder.UserID, }, OrderNo: h.orderStore.GenerateOrderNo(), OrderType: orderType, BuyerType: model.BuyerTypePersonal, - BuyerID: rechargeRecord.UserID, + BuyerID: rechargeOrder.UserID, IotCardID: iotCardID, DeviceID: deviceID, TotalAmount: totalAmount, @@ -383,8 +401,8 @@ func (h *AutoPurchaseHandler) buildOrderAndItems( for _, pkg := range packages { items = append(items, &model.OrderItem{ BaseModel: model.BaseModel{ - Creator: rechargeRecord.UserID, - Updater: rechargeRecord.UserID, + Creator: rechargeOrder.UserID, + Updater: rechargeOrder.UserID, }, PackageID: pkg.ID, PackageName: pkg.PackageName, diff --git a/migrations/000117_create_tb_recharge_order.down.sql b/migrations/000117_create_tb_recharge_order.down.sql new file mode 100644 index 0000000..2497918 --- /dev/null +++ b/migrations/000117_create_tb_recharge_order.down.sql @@ -0,0 +1,2 @@ +-- 回滚 tb_recharge_order 表 +DROP TABLE IF EXISTS tb_recharge_order; diff --git a/migrations/000117_create_tb_recharge_order.up.sql b/migrations/000117_create_tb_recharge_order.up.sql new file mode 100644 index 0000000..223b9c2 --- /dev/null +++ b/migrations/000117_create_tb_recharge_order.up.sql @@ -0,0 +1,37 @@ +-- 创建充值订单表 tb_recharge_order +-- 承接原 tb_asset_recharge_record 的业务字段 + 增加与资产的直接关联 +CREATE TABLE tb_recharge_order ( + id BIGSERIAL PRIMARY KEY, + recharge_order_no VARCHAR(40) NOT NULL UNIQUE, -- 沿用 CRCH 前缀 + user_id BIGINT NOT NULL, -- 发起用户(个人客户 ID) + asset_wallet_id BIGINT NOT NULL, + resource_type VARCHAR(20) NOT NULL, -- iot_card | device + resource_id BIGINT NOT NULL, + iot_card_id BIGINT, -- 单卡充值时有值(用于 ListOrders 查询) + device_id BIGINT, -- 设备充值时有值 + amount BIGINT NOT NULL, -- 充值金额(分) + status SMALLINT NOT NULL DEFAULT 1, -- 1待支付 2已支付 3已关闭 4已退款 + paid_at TIMESTAMPTZ, + shop_id_tag BIGINT NOT NULL DEFAULT 0, + enterprise_id_tag BIGINT, + operator_type VARCHAR(30) NOT NULL, -- personal_customer | admin_user + generation INT NOT NULL DEFAULT 1, + -- 强充专有字段(普通充值为 NULL) + linked_package_ids JSONB, + linked_order_type VARCHAR(20), + linked_carrier_type VARCHAR(20), + linked_carrier_id BIGINT, + auto_purchase_status VARCHAR(20), -- pending | success | failed + -- 累计充值(用于强充阈值判断) + accumulated_recharge_series BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- 索引 +CREATE INDEX idx_recharge_order_card ON tb_recharge_order(iot_card_id, generation) WHERE deleted_at IS NULL; +CREATE INDEX idx_recharge_order_device ON tb_recharge_order(device_id, generation) WHERE deleted_at IS NULL; +CREATE INDEX idx_recharge_order_user ON tb_recharge_order(user_id) WHERE deleted_at IS NULL; +CREATE INDEX idx_recharge_order_no ON tb_recharge_order(recharge_order_no) WHERE deleted_at IS NULL; +CREATE INDEX idx_recharge_order_status ON tb_recharge_order(status) WHERE deleted_at IS NULL; diff --git a/migrations/000118_create_tb_payment.down.sql b/migrations/000118_create_tb_payment.down.sql new file mode 100644 index 0000000..029e7b5 --- /dev/null +++ b/migrations/000118_create_tb_payment.down.sql @@ -0,0 +1,2 @@ +-- 回滚 tb_payment 表 +DROP TABLE IF EXISTS tb_payment; diff --git a/migrations/000118_create_tb_payment.up.sql b/migrations/000118_create_tb_payment.up.sql new file mode 100644 index 0000000..8c512af --- /dev/null +++ b/migrations/000118_create_tb_payment.up.sql @@ -0,0 +1,23 @@ +-- 创建支付记录表 tb_payment +-- 统一承接所有支付凭证(微信/支付宝/钱包) +CREATE TABLE tb_payment ( + id BIGSERIAL PRIMARY KEY, + payment_no VARCHAR(40) NOT NULL UNIQUE, -- 保持 CRCH/PAY 前缀兼容 + order_id BIGINT NOT NULL, + order_type VARCHAR(30) NOT NULL, -- 'package_order' | 'recharge_order' + payment_method VARCHAR(20) NOT NULL, -- wallet | wechat | alipay | offline + amount BIGINT NOT NULL, -- 分 + status SMALLINT NOT NULL DEFAULT 1, -- 1待支付 2已支付 3已失败 4已退款 + third_party_trade_no VARCHAR(100), + payment_config_id BIGINT, + payment_voucher_key VARCHAR(500), + paid_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- 索引 +CREATE INDEX idx_payment_order ON tb_payment(order_id, order_type); +CREATE INDEX idx_payment_no ON tb_payment(payment_no) WHERE deleted_at IS NULL; +CREATE INDEX idx_payment_status ON tb_payment(status) WHERE deleted_at IS NULL; diff --git a/migrations/000119_add_payment_config_id_to_recharge_order.down.sql b/migrations/000119_add_payment_config_id_to_recharge_order.down.sql new file mode 100644 index 0000000..dfa1ced --- /dev/null +++ b/migrations/000119_add_payment_config_id_to_recharge_order.down.sql @@ -0,0 +1,3 @@ +-- 回滚:删除 payment_config_id 列 +DROP INDEX IF EXISTS idx_recharge_order_payment_config_id; +ALTER TABLE tb_recharge_order DROP COLUMN IF EXISTS payment_config_id; \ No newline at end of file diff --git a/migrations/000119_add_payment_config_id_to_recharge_order.up.sql b/migrations/000119_add_payment_config_id_to_recharge_order.up.sql new file mode 100644 index 0000000..c72163a --- /dev/null +++ b/migrations/000119_add_payment_config_id_to_recharge_order.up.sql @@ -0,0 +1,4 @@ +-- tb_recharge_order 新增 payment_config_id 列(用于支付回调时获取微信配置) +ALTER TABLE tb_recharge_order ADD COLUMN payment_config_id BIGINT; +COMMENT ON COLUMN tb_recharge_order.payment_config_id IS '支付配置ID(关联tb_wechat_config.id)'; +CREATE INDEX idx_recharge_order_payment_config_id ON tb_recharge_order(payment_config_id) WHERE payment_config_id IS NOT NULL; \ No newline at end of file diff --git a/pkg/openapi/handlers.go b/pkg/openapi/handlers.go index 901ca42..cad9f9e 100644 --- a/pkg/openapi/handlers.go +++ b/pkg/openapi/handlers.go @@ -18,11 +18,12 @@ func BuildDocHandlers() *bootstrap.Handlers { PersonalCustomer: app.NewPersonalCustomerHandler(nil, nil), ClientAuth: app.NewClientAuthHandler(nil, nil), ClientAsset: app.NewClientAssetHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil), - ClientWallet: app.NewClientWalletHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil), + ClientWallet: app.NewClientWalletHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil), ClientOrder: app.NewClientOrderHandler(nil, nil), ClientExchange: app.NewClientExchangeHandler(nil), ClientRealname: app.NewClientRealnameHandler(nil, nil, nil, nil, nil, nil, nil, nil), ClientDevice: app.NewClientDeviceHandler(nil, nil, nil, nil, nil, nil, nil), + ClientRechargeOrder: app.NewClientRechargeOrderHandler(nil, nil, nil), Shop: admin.NewShopHandler(nil), ShopRole: admin.NewShopRoleHandler(nil), AdminAuth: admin.NewAuthHandler(nil, nil), @@ -48,7 +49,7 @@ func BuildDocHandlers() *bootstrap.Handlers { ShopSeriesGrant: admin.NewShopSeriesGrantHandler(nil), AdminOrder: admin.NewOrderHandler(nil, nil), AdminExchange: admin.NewExchangeHandler(nil, nil), - PaymentCallback: callback.NewPaymentHandler(nil, nil, nil, nil, nil, nil), + PaymentCallback: callback.NewPaymentHandler(nil, nil, nil, nil, nil, nil, nil, nil), PollingConfig: admin.NewPollingConfigHandler(nil), PollingConcurrency: admin.NewPollingConcurrencyHandler(nil), PollingMonitoring: admin.NewPollingMonitoringHandler(nil), diff --git a/pkg/queue/handler.go b/pkg/queue/handler.go index 3da6797..591ed3c 100644 --- a/pkg/queue/handler.go +++ b/pkg/queue/handler.go @@ -214,7 +214,8 @@ func (h *Handler) registerAutoPurchaseHandler() { autoPurchaseHandler := task.NewAutoPurchaseHandler( h.db, h.workerResult.Stores.Order, - nil, // AssetRechargeStore:在 NewAutoPurchaseHandler 内按需初始化 + nil, // RechargeOrderStore:在 NewAutoPurchaseHandler 内按需初始化 + nil, // PaymentStore:在 NewAutoPurchaseHandler 内按需初始化 h.workerResult.Stores.AssetWallet, nil, // AssetWalletTransactionStore:在 NewAutoPurchaseHandler 内按需初始化 h.workerResult.Stores.PackageUsage,