From 0d4953c6d350ad1d956d11c6e0edbb0411c7ffed Mon Sep 17 00:00:00 2001 From: 18278715334 <18278715334@163.com> Date: Fri, 12 Dec 2025 15:27:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.yaml | 22 + configs/env.development.yaml | 73 +- .../finance/dto/commands/finance_commands.go | 23 +- .../dto/responses/finance_responses.go | 80 ++- .../responses/wechat_order_status_response.go | 25 + .../wechat_recharge_order_response.go | 12 + .../finance/finance_application_service.go | 10 +- .../finance_application_service_impl.go | 677 +++++++++++++++++- .../finance/invoice_application_service.go | 16 +- internal/config/config.go | 26 + internal/container/container.go | 21 + .../domains/finance/entities/alipay_order.go | 140 +--- .../domains/finance/entities/pay_order.go | 136 ++++ .../finance/entities/recharge_record.go | 42 +- .../domains/finance/entities/wechat_order.go | 33 + .../alipay_order_repository_interface.go | 2 +- .../recharge_record_repository_interface.go | 8 +- .../wechat_order_repository_interface.go | 20 + .../repositories/user_repository_interface.go | 7 +- .../finance/gorm_alipay_order_repository.go | 8 +- .../gorm_recharge_record_repository.go | 16 +- .../finance/gorm_wechat_order_repository.go | 93 +++ .../http/handlers/finance_handler.go | 201 +++++- .../http/handlers/product_admin_handler.go | 9 +- .../http/routes/finance_routes.go | 40 +- internal/shared/payment/context.go | 25 + internal/shared/payment/user_auth_model.go | 48 ++ internal/shared/payment/utils.go | 7 + internal/shared/payment/wechatpay.go | 353 +++++++++ resources/etc/wxetc_cert/apiclient_cert.p12 | Bin 0 -> 2766 bytes resources/etc/wxetc_cert/apiclient_cert.pem | 25 + resources/etc/wxetc_cert/apiclient_key.pem | 28 + resources/etc/wxetc_cert/pub_key.pem | 9 + .../etc/wxetc_cert/璇佷功浣跨敤璇存槑.txt | 18 + 34 files changed, 1974 insertions(+), 279 deletions(-) create mode 100644 internal/application/finance/dto/responses/wechat_order_status_response.go create mode 100644 internal/application/finance/dto/responses/wechat_recharge_order_response.go create mode 100644 internal/domains/finance/entities/pay_order.go create mode 100644 internal/domains/finance/entities/wechat_order.go create mode 100644 internal/domains/finance/repositories/wechat_order_repository_interface.go create mode 100644 internal/infrastructure/database/repositories/finance/gorm_wechat_order_repository.go create mode 100644 internal/shared/payment/context.go create mode 100644 internal/shared/payment/user_auth_model.go create mode 100644 internal/shared/payment/utils.go create mode 100644 internal/shared/payment/wechatpay.go create mode 100644 resources/etc/wxetc_cert/apiclient_cert.p12 create mode 100644 resources/etc/wxetc_cert/apiclient_cert.pem create mode 100644 resources/etc/wxetc_cert/apiclient_key.pem create mode 100644 resources/etc/wxetc_cert/pub_key.pem create mode 100644 resources/etc/wxetc_cert/璇佷功浣跨敤璇存槑.txt diff --git a/config.yaml b/config.yaml index 290ec12..9057e10 100644 --- a/config.yaml +++ b/config.yaml @@ -362,6 +362,28 @@ alipay: notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback" return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return" +# =========================================== +# 💰 微信支付配置 +# =========================================== +Wxpay: + app_id: "wxa581992dc74d860e" + mch_id: "1683589176" + mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D" + mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0" + mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem" + mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800" + mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem" + notify_url: "https://console.tianyuanapi.com/api/v1/pay/wechat/callback" + refund_notify_url: "https://console.tianyuanapi.com/api/v1/wechat/refund_callback" + +# 微信小程序配置 +WechatMini: + app_id: "wxa581992dc74d860e" + +# 微信H5配置 +WechatH5: + app_id: "wxa581992dc74d860e" + # =========================================== # 🔍 天眼查配置 # =========================================== diff --git a/configs/env.development.yaml b/configs/env.development.yaml index b04850f..ec2fe6d 100644 --- a/configs/env.development.yaml +++ b/configs/env.development.yaml @@ -81,6 +81,27 @@ alipay: return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return" # =========================================== +# 💰 微信支付配置 +# =========================================== +Wxpay: + app_id: "wxa581992dc74d860e" + mch_id: "1683589176" + mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D" + mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0" + mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem" + mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800" + mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem" + notify_url: "https://bx89915628g.vicp.fun/api/v1/pay/wechat/callback" + refund_notify_url: "https://bx89915628g.vicp.fun/api/v1/wechat/refund_callback" + +# 微信小程序配置 +WechatMini: + app_id: "wxa581992dc74d860e" + +# 微信H5配置 +WechatH5: + app_id: "wxa581992dc74d860e" +# =========================================== # 💰 钱包配置 # =========================================== wallet: @@ -114,34 +135,42 @@ development: cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS" cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id" +# =========================================== +# 🚦 开发环境全局限流(放宽或近似关闭) +# =========================================== +ratelimit: + requests: 1000000 # 每窗口允许的请求数,足够大,相当于关闭 + window: 1s # 时间窗口 + burst: 1000000 # 令牌桶突发容量 + # =========================================== # 🚀 开发环境频率限制配置(放宽限制) # =========================================== daily_ratelimit: - max_requests_per_day: 1000000 # 开发环境每日最大请求次数 - max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数 - max_concurrent: 50 # 开发环境最大并发请求数 - + max_requests_per_day: 1000000 # 开发环境每日最大请求次数 + max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数 + max_concurrent: 50 # 开发环境最大并发请求数 + # 排除频率限制的路径 exclude_paths: - - "/health" # 健康检查接口 - - "/metrics" # 监控指标接口 - + - "/health" # 健康检查接口 + - "/metrics" # 监控指标接口 + # 排除频率限制的域名 exclude_domains: - - "api.*" # API二级域名不受频率限制 - - "*.api.*" # 支持多级API域名 - + - "api.*" # API二级域名不受频率限制 + - "*.api.*" # 支持多级API域名 + # 开发环境安全配置(放宽限制) - enable_ip_whitelist: true # 启用IP白名单 - ip_whitelist: # 开发环境IP白名单 - - "127.0.0.1" # 本地回环 - - "localhost" # 本地主机 - - "192.168.*" # 内网IP段 - - "10.*" # 内网IP段 - - "172.16.*" # 内网IP段 - - enable_ip_blacklist: false # 开发环境禁用IP黑名单 - enable_user_agent: false # 开发环境禁用User-Agent检查 - enable_referer: false # 开发环境禁用Referer检查 - enable_proxy_check: false # 开发环境禁用代理检查 \ No newline at end of file + enable_ip_whitelist: true # 启用IP白名单 + ip_whitelist: # 开发环境IP白名单 + - "127.0.0.1" # 本地回环 + - "localhost" # 本地主机 + - "192.168.*" # 内网IP段 + - "10.*" # 内网IP段 + - "172.16.*" # 内网IP段 + + enable_ip_blacklist: false # 开发环境禁用IP黑名单 + enable_user_agent: false # 开发环境禁用User-Agent检查 + enable_referer: false # 开发环境禁用Referer检查 + enable_proxy_check: false # 开发环境禁用代理检查 diff --git a/internal/application/finance/dto/commands/finance_commands.go b/internal/application/finance/dto/commands/finance_commands.go index eec79c6..9dc628f 100644 --- a/internal/application/finance/dto/commands/finance_commands.go +++ b/internal/application/finance/dto/commands/finance_commands.go @@ -5,7 +5,6 @@ type CreateWalletCommand struct { UserID string `json:"user_id" binding:"required,uuid"` } - // TransferRechargeCommand 对公转账充值命令 type TransferRechargeCommand struct { UserID string `json:"user_id" binding:"required,uuid"` @@ -16,16 +15,24 @@ type TransferRechargeCommand struct { // GiftRechargeCommand 赠送充值命令 type GiftRechargeCommand struct { - UserID string `json:"user_id" binding:"required,uuid"` - Amount string `json:"amount" binding:"required"` - Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"` + UserID string `json:"user_id" binding:"required,uuid"` + Amount string `json:"amount" binding:"required"` + Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"` } - // CreateAlipayRechargeCommand 创建支付宝充值订单命令 type CreateAlipayRechargeCommand struct { - UserID string `json:"-"` // 用户ID(从token获取) - Amount string `json:"amount" binding:"required"` // 充值金额 - Subject string `json:"-"` // 订单标题 + UserID string `json:"-"` // 用户ID(从token获取) + Amount string `json:"amount" binding:"required"` // 充值金额 + Subject string `json:"-"` // 订单标题 Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台:app/h5/pc } + +// CreateWechatRechargeCommand 创建微信充值订单命令 +type CreateWechatRechargeCommand struct { + UserID string `json:"-"` // 用户ID(从token获取) + Amount string `json:"amount" binding:"required"` // 充值金额 + Subject string `json:"-"` // 订单标题 + Platform string `json:"platform" binding:"required,oneof=wx_native native wx_h5 h5"` // 仅支持微信Native扫码,兼容传入native/wx_h5/h5 + OpenID string `json:"openid" binding:"omitempty"` // 前端可直接传入的 openid(用于小程序/H5) +} diff --git a/internal/application/finance/dto/responses/finance_responses.go b/internal/application/finance/dto/responses/finance_responses.go index b0b772b..fdb0f10 100644 --- a/internal/application/finance/dto/responses/finance_responses.go +++ b/internal/application/finance/dto/responses/finance_responses.go @@ -8,15 +8,15 @@ import ( // WalletResponse 钱包响应 type WalletResponse struct { - ID string `json:"id"` - UserID string `json:"user_id"` - IsActive bool `json:"is_active"` - Balance decimal.Decimal `json:"balance"` - BalanceStatus string `json:"balance_status"` // normal, low, arrears - IsArrears bool `json:"is_arrears"` // 是否欠费 - IsLowBalance bool `json:"is_low_balance"` // 是否余额较低 - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + UserID string `json:"user_id"` + IsActive bool `json:"is_active"` + Balance decimal.Decimal `json:"balance"` + BalanceStatus string `json:"balance_status"` // normal, low, arrears + IsArrears bool `json:"is_arrears"` // 是否欠费 + IsLowBalance bool `json:"is_low_balance"` // 是否余额较低 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TransactionResponse 交易响应 @@ -49,34 +49,36 @@ type WalletStatsResponse struct { // RechargeRecordResponse 充值记录响应 type RechargeRecordResponse struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Amount decimal.Decimal `json:"amount"` - RechargeType string `json:"recharge_type"` - Status string `json:"status"` - AlipayOrderID string `json:"alipay_order_id,omitempty"` - TransferOrderID string `json:"transfer_order_id,omitempty"` - Notes string `json:"notes,omitempty"` - OperatorID string `json:"operator_id,omitempty"` - CompanyName string `json:"company_name,omitempty"` - User *UserSimpleResponse `json:"user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + UserID string `json:"user_id"` + Amount decimal.Decimal `json:"amount"` + RechargeType string `json:"recharge_type"` + Status string `json:"status"` + AlipayOrderID string `json:"alipay_order_id,omitempty"` + WechatOrderID string `json:"wechat_order_id,omitempty"` + TransferOrderID string `json:"transfer_order_id,omitempty"` + Platform string `json:"platform,omitempty"` // 支付平台:pc/wx_native等 + Notes string `json:"notes,omitempty"` + OperatorID string `json:"operator_id,omitempty"` + CompanyName string `json:"company_name,omitempty"` + User *UserSimpleResponse `json:"user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // WalletTransactionResponse 钱包交易记录响应 type WalletTransactionResponse struct { - ID string `json:"id"` - UserID string `json:"user_id"` - ApiCallID string `json:"api_call_id"` - TransactionID string `json:"transaction_id"` - ProductID string `json:"product_id"` - ProductName string `json:"product_name"` - Amount decimal.Decimal `json:"amount"` - CompanyName string `json:"company_name,omitempty"` + ID string `json:"id"` + UserID string `json:"user_id"` + ApiCallID string `json:"api_call_id"` + TransactionID string `json:"transaction_id"` + ProductID string `json:"product_id"` + ProductName string `json:"product_name"` + Amount decimal.Decimal `json:"amount"` + CompanyName string `json:"company_name,omitempty"` User *UserSimpleResponse `json:"user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // WalletTransactionListResponse 钱包交易记录列表响应 @@ -97,17 +99,17 @@ type RechargeRecordListResponse struct { // AlipayRechargeOrderResponse 支付宝充值订单响应 type AlipayRechargeOrderResponse struct { - PayURL string `json:"pay_url"` // 支付链接 - OutTradeNo string `json:"out_trade_no"` // 商户订单号 - Amount decimal.Decimal `json:"amount"` // 充值金额 - Platform string `json:"platform"` // 支付平台 - Subject string `json:"subject"` // 订单标题 + PayURL string `json:"pay_url"` // 支付链接 + OutTradeNo string `json:"out_trade_no"` // 商户订单号 + Amount decimal.Decimal `json:"amount"` // 充值金额 + Platform string `json:"platform"` // 支付平台 + Subject string `json:"subject"` // 订单标题 } // RechargeConfigResponse 充值配置响应 type RechargeConfigResponse struct { - MinAmount string `json:"min_amount"` // 最低充值金额 - MaxAmount string `json:"max_amount"` // 最高充值金额 + MinAmount string `json:"min_amount"` // 最低充值金额 + MaxAmount string `json:"max_amount"` // 最高充值金额 AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"` } diff --git a/internal/application/finance/dto/responses/wechat_order_status_response.go b/internal/application/finance/dto/responses/wechat_order_status_response.go new file mode 100644 index 0000000..8bfa6e9 --- /dev/null +++ b/internal/application/finance/dto/responses/wechat_order_status_response.go @@ -0,0 +1,25 @@ +package responses + +import ( + "time" + + "github.com/shopspring/decimal" +) + +// WechatOrderStatusResponse 微信订单状态响应 +type WechatOrderStatusResponse struct { + OutTradeNo string `json:"out_trade_no"` // 商户订单号 + TransactionID *string `json:"transaction_id"` // 微信支付交易号 + Status string `json:"status"` // 订单状态 + Amount decimal.Decimal `json:"amount"` // 订单金额 + Subject string `json:"subject"` // 订单标题 + Platform string `json:"platform"` // 支付平台 + CreatedAt time.Time `json:"created_at"` // 创建时间 + UpdatedAt time.Time `json:"updated_at"` // 更新时间 + NotifyTime *time.Time `json:"notify_time"` // 异步通知时间 + ReturnTime *time.Time `json:"return_time"` // 同步返回时间 + ErrorCode *string `json:"error_code"` // 错误码 + ErrorMessage *string `json:"error_message"` // 错误信息 + IsProcessing bool `json:"is_processing"` // 是否处理中 + CanRetry bool `json:"can_retry"` // 是否可以重试 +} diff --git a/internal/application/finance/dto/responses/wechat_recharge_order_response.go b/internal/application/finance/dto/responses/wechat_recharge_order_response.go new file mode 100644 index 0000000..89ef08a --- /dev/null +++ b/internal/application/finance/dto/responses/wechat_recharge_order_response.go @@ -0,0 +1,12 @@ +package responses + +import "github.com/shopspring/decimal" + +// WechatRechargeOrderResponse 微信充值下单响应 +type WechatRechargeOrderResponse struct { + OutTradeNo string `json:"out_trade_no"` // 商户订单号 + Amount decimal.Decimal `json:"amount"` // 充值金额 + Platform string `json:"platform"` // 支付平台 + Subject string `json:"subject"` // 订单标题 + PrepayData interface{} `json:"prepay_data"` // 预支付数据(APP预支付ID或JSAPI参数) +} diff --git a/internal/application/finance/finance_application_service.go b/internal/application/finance/finance_application_service.go index a41471d..27c9dc1 100644 --- a/internal/application/finance/finance_application_service.go +++ b/internal/application/finance/finance_application_service.go @@ -17,13 +17,14 @@ type FinanceApplicationService interface { // 充值管理 CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error) + CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error) TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error) GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error) // 交易记录 GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error) GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error) - + // 导出功能 ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error) ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error) @@ -33,12 +34,15 @@ type FinanceApplicationService interface { HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error) GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error) + // 微信支付回调处理 + HandleWechatPayCallback(ctx context.Context, r *http.Request) error + HandleWechatRefundCallback(ctx context.Context, r *http.Request) error + GetWechatOrderStatus(ctx context.Context, outTradeNo string) (*responses.WechatOrderStatusResponse, error) + // 充值记录 GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) // 获取充值配置 GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error) - - } diff --git a/internal/application/finance/finance_application_service_impl.go b/internal/application/finance/finance_application_service_impl.go index 2dff5b6..2cb017e 100644 --- a/internal/application/finance/finance_application_service_impl.go +++ b/internal/application/finance/finance_application_service_impl.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "time" "tyapi-server/internal/application/finance/dto/commands" "tyapi-server/internal/application/finance/dto/queries" "tyapi-server/internal/application/finance/dto/responses" @@ -19,16 +20,20 @@ import ( "github.com/shopspring/decimal" "github.com/smartwalle/alipay/v3" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments" "go.uber.org/zap" ) // FinanceApplicationServiceImpl 财务应用服务实现 type FinanceApplicationServiceImpl struct { aliPayClient *payment.AliPayService + wechatPayService *payment.WechatPayService walletService finance_services.WalletAggregateService rechargeRecordService finance_services.RechargeRecordService walletTransactionRepository finance_repositories.WalletTransactionRepository alipayOrderRepo finance_repositories.AlipayOrderRepository + wechatOrderRepo finance_repositories.WechatOrderRepository + rechargeRecordRepo finance_repositories.RechargeRecordRepository userRepo user_repositories.UserRepository txManager *database.TransactionManager exportManager *export.ExportManager @@ -39,10 +44,13 @@ type FinanceApplicationServiceImpl struct { // NewFinanceApplicationService 创建财务应用服务 func NewFinanceApplicationService( aliPayClient *payment.AliPayService, + wechatPayService *payment.WechatPayService, walletService finance_services.WalletAggregateService, rechargeRecordService finance_services.RechargeRecordService, walletTransactionRepository finance_repositories.WalletTransactionRepository, alipayOrderRepo finance_repositories.AlipayOrderRepository, + wechatOrderRepo finance_repositories.WechatOrderRepository, + rechargeRecordRepo finance_repositories.RechargeRecordRepository, userRepo user_repositories.UserRepository, txManager *database.TransactionManager, logger *zap.Logger, @@ -51,10 +59,13 @@ func NewFinanceApplicationService( ) FinanceApplicationService { return &FinanceApplicationServiceImpl{ aliPayClient: aliPayClient, + wechatPayService: wechatPayService, walletService: walletService, rechargeRecordService: rechargeRecordService, walletTransactionRepository: walletTransactionRepository, alipayOrderRepo: alipayOrderRepo, + wechatOrderRepo: wechatOrderRepo, + rechargeRecordRepo: rechargeRecordRepo, userRepo: userRepo, txManager: txManager, exportManager: exportManager, @@ -100,8 +111,9 @@ func (s *FinanceApplicationServiceImpl) GetWallet(ctx context.Context, query *qu BalanceStatus: wallet.GetBalanceStatus(), IsArrears: wallet.IsArrears(), IsLowBalance: wallet.IsLowBalance(), - CreatedAt: wallet.CreatedAt, - UpdatedAt: wallet.UpdatedAt, + + CreatedAt: wallet.CreatedAt, + UpdatedAt: wallet.UpdatedAt, }, nil } @@ -188,6 +200,168 @@ func (s *FinanceApplicationServiceImpl) CreateAlipayRechargeOrder(ctx context.Co }, nil } +// CreateWechatRechargeOrder 创建微信充值订单(完整流程编排) +func (s *FinanceApplicationServiceImpl) CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error) { + cmd.Subject = "天远数据API充值" + amount, err := decimal.NewFromString(cmd.Amount) + if err != nil { + s.logger.Error("金额格式错误", zap.String("amount", cmd.Amount), zap.Error(err)) + return nil, fmt.Errorf("金额格式错误: %w", err) + } + + if amount.LessThanOrEqual(decimal.Zero) { + return nil, fmt.Errorf("充值金额必须大于0") + } + + minAmount, err := decimal.NewFromString(s.config.Wallet.MinAmount) + if err != nil { + s.logger.Error("配置中的最低充值金额格式错误", zap.String("min_amount", s.config.Wallet.MinAmount), zap.Error(err)) + return nil, fmt.Errorf("系统配置错误: %w", err) + } + maxAmount, err := decimal.NewFromString(s.config.Wallet.MaxAmount) + if err != nil { + s.logger.Error("配置中的最高充值金额格式错误", zap.String("max_amount", s.config.Wallet.MaxAmount), zap.Error(err)) + return nil, fmt.Errorf("系统配置错误: %w", err) + } + + if amount.LessThan(minAmount) { + return nil, fmt.Errorf("充值金额不能少于%s元", minAmount.String()) + } + if amount.GreaterThan(maxAmount) { + return nil, fmt.Errorf("单次充值金额不能超过%s元", maxAmount.String()) + } + + platform := normalizeWechatPlatform(cmd.Platform) + if platform != payment.PlatformWxNative && platform != payment.PlatformWxH5 { + return nil, fmt.Errorf("不支持的支付平台: %s", cmd.Platform) + } + if s.wechatPayService == nil { + return nil, fmt.Errorf("微信支付服务未初始化") + } + + outTradeNo := s.wechatPayService.GenerateOutTradeNo() + + s.logger.Info("开始创建微信充值订单", + zap.String("user_id", cmd.UserID), + zap.String("out_trade_no", outTradeNo), + zap.String("amount", amount.String()), + zap.String("platform", cmd.Platform), + zap.String("subject", cmd.Subject), + ) + + var prepayData interface{} + + err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { + // 创建微信充值记录 + rechargeRecord := finance_entities.NewWechatRechargeRecord(cmd.UserID, amount, outTradeNo) + createdRecord, createErr := s.rechargeRecordRepo.Create(txCtx, *rechargeRecord) + if createErr != nil { + s.logger.Error("创建微信充值记录失败", + zap.String("out_trade_no", outTradeNo), + zap.String("user_id", cmd.UserID), + zap.String("amount", amount.String()), + zap.Error(createErr), + ) + return fmt.Errorf("创建微信充值记录失败: %w", createErr) + } + + s.logger.Info("创建微信充值记录成功", + zap.String("out_trade_no", outTradeNo), + zap.String("recharge_id", createdRecord.ID), + zap.String("user_id", cmd.UserID), + ) + + // 创建微信订单本地记录 + wechatOrder := finance_entities.NewWechatOrder(createdRecord.ID, outTradeNo, cmd.Subject, amount, platform) + createdOrder, orderErr := s.wechatOrderRepo.Create(txCtx, *wechatOrder) + if orderErr != nil { + s.logger.Error("创建微信订单记录失败", + zap.String("out_trade_no", outTradeNo), + zap.String("recharge_id", createdRecord.ID), + zap.Error(orderErr), + ) + return fmt.Errorf("创建微信订单记录失败: %w", orderErr) + } + + s.logger.Info("创建微信订单记录成功", + zap.String("out_trade_no", outTradeNo), + zap.String("order_id", createdOrder.ID), + zap.String("recharge_id", createdRecord.ID), + ) + return nil + }) + if err != nil { + return nil, err + } + + payCtx := context.WithValue(ctx, "platform", platform) + payCtx = context.WithValue(payCtx, "user_id", cmd.UserID) + + s.logger.Info("调用微信支付接口创建订单", + zap.String("out_trade_no", outTradeNo), + zap.String("platform", platform), + ) + + prepayData, err = s.wechatPayService.CreateWechatOrder(payCtx, amount.InexactFloat64(), cmd.Subject, outTradeNo) + if err != nil { + s.logger.Error("微信下单失败", + zap.String("out_trade_no", outTradeNo), + zap.String("user_id", cmd.UserID), + zap.String("amount", amount.String()), + zap.Error(err), + ) + + // 回写失败状态 + _ = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { + order, getErr := s.wechatOrderRepo.GetByOutTradeNo(txCtx, outTradeNo) + if getErr == nil && order != nil { + order.MarkFailed("create_failed", err.Error()) + updateErr := s.wechatOrderRepo.Update(txCtx, *order) + if updateErr != nil { + s.logger.Error("回写微信订单失败状态失败", + zap.String("out_trade_no", outTradeNo), + zap.Error(updateErr), + ) + } else { + s.logger.Info("回写微信订单失败状态成功", + zap.String("out_trade_no", outTradeNo), + ) + } + } + return nil + }) + + return nil, fmt.Errorf("创建微信支付订单失败: %w", err) + } + + s.logger.Info("微信充值订单创建成功", + zap.String("user_id", cmd.UserID), + zap.String("out_trade_no", outTradeNo), + zap.String("amount", amount.String()), + zap.String("platform", cmd.Platform), + ) + + return &responses.WechatRechargeOrderResponse{ + OutTradeNo: outTradeNo, + Amount: amount, + Platform: platform, + Subject: cmd.Subject, + PrepayData: prepayData, + }, nil +} + +// normalizeWechatPlatform 将兼容写法(h5/mini)转换为系统内使用的wx_h5/wx_mini +func normalizeWechatPlatform(p string) string { + switch p { + case "h5", payment.PlatformWxH5: + return payment.PlatformWxNative + case "native": + return payment.PlatformWxNative + default: + return p + } +} + // TransferRecharge 对公转账充值 func (s *FinanceApplicationServiceImpl) TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error) { // 将字符串金额转换为 decimal.Decimal @@ -507,8 +681,8 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C } // 准备导出数据 - headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "转账订单号", "备注", "充值时间"} - columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20} + headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "微信订单号", "转账订单号", "备注", "充值时间"} + columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20, 20} data := make([][]interface{}, len(allRecords)) for i, record := range allRecords { @@ -523,6 +697,10 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C if record.AlipayOrderID != nil && *record.AlipayOrderID != "" { alipayOrderID = *record.AlipayOrderID } + wechatOrderID := "" + if record.WechatOrderID != nil && *record.WechatOrderID != "" { + wechatOrderID = *record.WechatOrderID + } transferOrderID := "" if record.TransferOrderID != nil && *record.TransferOrderID != "" { transferOrderID = *record.TransferOrderID @@ -543,6 +721,7 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C translateRechargeType(record.RechargeType), translateRechargeStatus(record.Status), alipayOrderID, + wechatOrderID, transferOrderID, notes, createdAt, @@ -566,6 +745,8 @@ func translateRechargeType(rechargeType finance_entities.RechargeType) string { switch rechargeType { case finance_entities.RechargeTypeAlipay: return "支付宝充值" + case finance_entities.RechargeTypeWechat: + return "微信充值" case finance_entities.RechargeTypeTransfer: return "对公转账" case finance_entities.RechargeTypeGift: @@ -890,15 +1071,27 @@ func (s *FinanceApplicationServiceImpl) GetAlipayOrderStatus(ctx context.Context // GetUserRechargeRecords 获取用户充值记录 func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) { - // 查询用户充值记录 - records, err := s.rechargeRecordService.GetByUserID(ctx, userID) + // 确保 filters 不为 nil + if filters == nil { + filters = make(map[string]interface{}) + } + + // 添加 user_id 筛选条件,确保只能查询当前用户的记录 + filters["user_id"] = userID + + // 查询用户充值记录(使用筛选和分页功能) + records, err := s.rechargeRecordService.GetAll(ctx, filters, options) if err != nil { s.logger.Error("查询用户充值记录失败", zap.Error(err), zap.String("userID", userID)) return nil, err } - // 计算总数 - total := int64(len(records)) + // 获取总数(使用筛选条件) + total, err := s.rechargeRecordService.Count(ctx, filters) + if err != nil { + s.logger.Error("统计用户充值记录失败", zap.Error(err), zap.String("userID", userID)) + return nil, err + } // 转换为响应DTO var items []responses.RechargeRecordResponse @@ -914,9 +1107,20 @@ func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Conte UpdatedAt: record.UpdatedAt, } - // 根据充值类型设置相应的订单号 + // 根据充值类型设置相应的订单号和平台信息 if record.AlipayOrderID != nil { item.AlipayOrderID = *record.AlipayOrderID + // 通过订单号获取平台信息 + if alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, *record.AlipayOrderID); err == nil && alipayOrder != nil { + item.Platform = alipayOrder.Platform + } + } + if record.WechatOrderID != nil { + item.WechatOrderID = *record.WechatOrderID + // 通过订单号获取平台信息 + if wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, *record.WechatOrderID); err == nil && wechatOrder != nil { + item.Platform = wechatOrder.Platform + } } if record.TransferOrderID != nil { item.TransferOrderID = *record.TransferOrderID @@ -963,9 +1167,20 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont UpdatedAt: record.UpdatedAt, } - // 根据充值类型设置相应的订单号 + // 根据充值类型设置相应的订单号和平台信息 if record.AlipayOrderID != nil { item.AlipayOrderID = *record.AlipayOrderID + // 通过订单号获取平台信息 + if alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, *record.AlipayOrderID); err == nil && alipayOrder != nil { + item.Platform = alipayOrder.Platform + } + } + if record.WechatOrderID != nil { + item.WechatOrderID = *record.WechatOrderID + // 通过订单号获取平台信息 + if wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, *record.WechatOrderID); err == nil && wechatOrder != nil { + item.Platform = wechatOrder.Platform + } } if record.TransferOrderID != nil { item.TransferOrderID = *record.TransferOrderID @@ -1012,3 +1227,445 @@ func (s *FinanceApplicationServiceImpl) GetRechargeConfig(ctx context.Context) ( AlipayRechargeBonus: bonus, }, nil } + +// GetWechatOrderStatus 获取微信订单状态 +func (s *FinanceApplicationServiceImpl) GetWechatOrderStatus(ctx context.Context, outTradeNo string) (*responses.WechatOrderStatusResponse, error) { + if outTradeNo == "" { + return nil, fmt.Errorf("缺少商户订单号") + } + + // 查找微信订单 + wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo) + if err != nil { + s.logger.Error("查找微信订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err)) + return nil, fmt.Errorf("查找微信订单失败: %w", err) + } + + if wechatOrder == nil { + s.logger.Error("微信订单不存在", zap.String("out_trade_no", outTradeNo)) + return nil, fmt.Errorf("微信订单不存在") + } + + // 如果订单状态为pending,主动查询微信订单状态 + if wechatOrder.Status == finance_entities.WechatOrderStatusPending { + s.logger.Info("订单状态为pending,主动查询微信订单状态", + zap.String("out_trade_no", outTradeNo), + ) + + // 调用微信查询接口 + transaction, err := s.wechatPayService.QueryOrderStatus(ctx, outTradeNo) + if err != nil { + s.logger.Error("查询微信订单状态失败", + zap.String("out_trade_no", outTradeNo), + zap.Error(err), + ) + // 查询失败不影响返回,继续使用数据库中的状态 + } else { + // 解析微信返回的状态 + tradeState := "" + transactionID := "" + if transaction.TradeState != nil { + tradeState = *transaction.TradeState + } + if transaction.TransactionId != nil { + transactionID = *transaction.TransactionId + } + + s.logger.Info("微信查询订单状态返回", + zap.String("out_trade_no", outTradeNo), + zap.String("trade_state", tradeState), + zap.String("transaction_id", transactionID), + ) + + // 使用公共方法更新订单状态 + err = s.updateWechatOrderStatus(ctx, outTradeNo, tradeState, transaction) + if err != nil { + s.logger.Error("更新微信订单状态失败", + zap.String("out_trade_no", outTradeNo), + zap.String("trade_state", tradeState), + zap.Error(err), + ) + } + + // 重新获取更新后的订单信息 + updatedOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo) + if err == nil && updatedOrder != nil { + wechatOrder = updatedOrder + } + } + } + + // 判断是否处理中 + isProcessing := wechatOrder.Status == finance_entities.WechatOrderStatusPending + + // 判断是否可以重试(失败状态可以重试) + canRetry := wechatOrder.Status == finance_entities.WechatOrderStatusFailed + + // 转换为响应DTO + response := &responses.WechatOrderStatusResponse{ + OutTradeNo: wechatOrder.OutTradeNo, + TransactionID: wechatOrder.TradeNo, + Status: string(wechatOrder.Status), + Amount: wechatOrder.Amount, + Subject: wechatOrder.Subject, + Platform: wechatOrder.Platform, + CreatedAt: wechatOrder.CreatedAt, + UpdatedAt: wechatOrder.UpdatedAt, + NotifyTime: wechatOrder.NotifyTime, + ReturnTime: wechatOrder.ReturnTime, + ErrorCode: &wechatOrder.ErrorCode, + ErrorMessage: &wechatOrder.ErrorMessage, + IsProcessing: isProcessing, + CanRetry: canRetry, + } + + // 如果错误码为空,设置为nil + if wechatOrder.ErrorCode == "" { + response.ErrorCode = nil + } + if wechatOrder.ErrorMessage == "" { + response.ErrorMessage = nil + } + + s.logger.Info("查询微信订单状态完成", + zap.String("out_trade_no", outTradeNo), + zap.String("status", string(wechatOrder.Status)), + zap.Bool("is_processing", isProcessing), + zap.Bool("can_retry", canRetry), + ) + + return response, nil +} + +// updateWechatOrderStatus 根据微信状态更新本地订单状态 +func (s *FinanceApplicationServiceImpl) updateWechatOrderStatus(ctx context.Context, outTradeNo string, tradeState string, transaction *payments.Transaction) error { + // 查找微信订单 + wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo) + if err != nil { + s.logger.Error("查找微信订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err)) + return fmt.Errorf("查找微信订单失败: %w", err) + } + + if wechatOrder == nil { + s.logger.Error("微信订单不存在", zap.String("out_trade_no", outTradeNo)) + return fmt.Errorf("微信订单不存在") + } + + switch tradeState { + case payment.TradeStateSuccess: + // 支付成功,调用公共处理逻辑 + transactionID := "" + if transaction.TransactionId != nil { + transactionID = *transaction.TransactionId + } + payAmount := decimal.Zero + if transaction.Amount != nil && transaction.Amount.Total != nil { + // 将分转换为元 + payAmount = decimal.NewFromInt(*transaction.Amount.Total).Div(decimal.NewFromInt(100)) + } + return s.processWechatPaymentSuccess(ctx, outTradeNo, transactionID, payAmount) + case payment.TradeStateClosed: + // 交易关闭 + s.logger.Info("微信订单交易关闭", + zap.String("out_trade_no", outTradeNo), + ) + wechatOrder.MarkClosed() + err = s.wechatOrderRepo.Update(ctx, *wechatOrder) + if err != nil { + s.logger.Error("更新微信订单关闭状态失败", + zap.String("out_trade_no", outTradeNo), + zap.Error(err), + ) + return err + } + s.logger.Info("微信订单关闭状态更新成功", + zap.String("out_trade_no", outTradeNo), + ) + case payment.TradeStateNotPay: + // 未支付,保持pending状态 + s.logger.Info("微信订单未支付", + zap.String("out_trade_no", outTradeNo), + ) + default: + // 其他状态,记录日志 + s.logger.Info("微信订单其他状态", + zap.String("out_trade_no", outTradeNo), + zap.String("trade_state", tradeState), + ) + } + + return nil +} + +// HandleWechatPayCallback 处理微信支付回调 +func (s *FinanceApplicationServiceImpl) HandleWechatPayCallback(ctx context.Context, r *http.Request) error { + if s.wechatPayService == nil { + s.logger.Error("微信支付服务未初始化") + return fmt.Errorf("微信支付服务未初始化") + } + + // 解析并验证微信支付回调通知 + transaction, err := s.wechatPayService.HandleWechatPayNotification(ctx, r) + if err != nil { + s.logger.Error("微信支付回调验证失败", zap.Error(err)) + return err + } + + // 提取回调数据 + outTradeNo := "" + if transaction.OutTradeNo != nil { + outTradeNo = *transaction.OutTradeNo + } + transactionID := "" + if transaction.TransactionId != nil { + transactionID = *transaction.TransactionId + } + tradeState := "" + if transaction.TradeState != nil { + tradeState = *transaction.TradeState + } + totalAmount := decimal.Zero + if transaction.Amount != nil && transaction.Amount.Total != nil { + // 将分转换为元 + totalAmount = decimal.NewFromInt(*transaction.Amount.Total).Div(decimal.NewFromInt(100)) + } + + // 记录回调数据 + s.logger.Info("微信支付回调数据", + zap.String("out_trade_no", outTradeNo), + zap.String("transaction_id", transactionID), + zap.String("trade_state", tradeState), + zap.String("total_amount", totalAmount.String()), + ) + + // 检查交易状态 + if tradeState != payment.TradeStateSuccess { + s.logger.Warn("微信支付交易未成功", + zap.String("out_trade_no", outTradeNo), + zap.String("trade_state", tradeState), + ) + return nil // 不返回错误,因为这是正常的业务状态 + } + + // 处理支付成功逻辑 + err = s.processWechatPaymentSuccess(ctx, outTradeNo, transactionID, totalAmount) + if err != nil { + s.logger.Error("处理微信支付成功失败", + zap.String("out_trade_no", outTradeNo), + zap.String("transaction_id", transactionID), + zap.String("amount", totalAmount.String()), + zap.Error(err), + ) + return err + } + + return nil +} + +// processWechatPaymentSuccess 处理微信支付成功的公共逻辑 +func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.Context, outTradeNo, transactionID string, amount decimal.Decimal) error { + // 查找微信订单 + wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo) + if err != nil { + s.logger.Error("查找微信订单失败", + zap.String("out_trade_no", outTradeNo), + zap.Error(err), + ) + return fmt.Errorf("查找微信订单失败: %w", err) + } + + if wechatOrder == nil { + s.logger.Error("微信订单不存在", + zap.String("out_trade_no", outTradeNo), + ) + return fmt.Errorf("微信订单不存在") + } + + // 查找对应的充值记录 + rechargeRecord, err := s.rechargeRecordService.GetByID(ctx, wechatOrder.RechargeID) + if err != nil { + s.logger.Error("查找充值记录失败", + zap.String("out_trade_no", outTradeNo), + zap.String("recharge_id", wechatOrder.RechargeID), + zap.Error(err), + ) + return fmt.Errorf("查找充值记录失败: %w", err) + } + + // 检查订单和充值记录状态,如果都已成功则跳过(只记录一次日志) + if wechatOrder.Status == finance_entities.WechatOrderStatusSuccess && rechargeRecord.Status == finance_entities.RechargeStatusSuccess { + s.logger.Info("微信支付订单已处理成功,跳过重复处理", + zap.String("out_trade_no", outTradeNo), + zap.String("transaction_id", transactionID), + zap.String("order_id", wechatOrder.ID), + zap.String("recharge_id", rechargeRecord.ID), + ) + return nil + } + + // 计算充值赠送金额(复用支付宝的赠送逻辑) + bonusAmount := decimal.Zero + if len(s.config.Wallet.AliPayRechargeBonus) > 0 { + for i := len(s.config.Wallet.AliPayRechargeBonus) - 1; i >= 0; i-- { + rule := s.config.Wallet.AliPayRechargeBonus[i] + if amount.GreaterThanOrEqual(decimal.NewFromFloat(rule.RechargeAmount)) { + bonusAmount = decimal.NewFromFloat(rule.BonusAmount) + break + } + } + } + + // 记录开始处理支付成功 + s.logger.Info("开始处理微信支付成功", + zap.String("out_trade_no", outTradeNo), + zap.String("transaction_id", transactionID), + zap.String("amount", amount.String()), + zap.String("user_id", rechargeRecord.UserID), + zap.String("bonus_amount", bonusAmount.String()), + ) + + // 在事务中处理支付成功逻辑 + err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { + // 更新微信订单状态 + wechatOrder.MarkSuccess(transactionID, "", "", amount, amount) + now := time.Now() + wechatOrder.NotifyTime = &now + err := s.wechatOrderRepo.Update(txCtx, *wechatOrder) + if err != nil { + s.logger.Error("更新微信订单状态失败", + zap.String("out_trade_no", outTradeNo), + zap.Error(err), + ) + return err + } + + // 更新充值记录状态为成功 + rechargeRecord.MarkSuccess() + err = s.rechargeRecordRepo.Update(txCtx, *rechargeRecord) + if err != nil { + s.logger.Error("更新充值记录状态失败", + zap.String("out_trade_no", outTradeNo), + zap.String("recharge_id", rechargeRecord.ID), + zap.Error(err), + ) + return err + } + + // 如果有赠送金额,创建赠送充值记录 + if bonusAmount.GreaterThan(decimal.Zero) { + giftRechargeRecord := finance_entities.NewGiftRechargeRecord(rechargeRecord.UserID, bonusAmount, "充值活动赠送") + createdGift, err := s.rechargeRecordRepo.Create(txCtx, *giftRechargeRecord) + if err != nil { + s.logger.Error("创建赠送充值记录失败", + zap.String("out_trade_no", outTradeNo), + zap.String("user_id", rechargeRecord.UserID), + zap.String("bonus_amount", bonusAmount.String()), + zap.Error(err), + ) + return err + } + s.logger.Info("创建赠送充值记录成功", + zap.String("out_trade_no", outTradeNo), + zap.String("gift_recharge_id", createdGift.ID), + zap.String("bonus_amount", bonusAmount.String()), + ) + } + + // 充值到钱包(包含赠送金额) + totalRechargeAmount := amount.Add(bonusAmount) + err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalRechargeAmount) + if err != nil { + s.logger.Error("充值到钱包失败", + zap.String("out_trade_no", outTradeNo), + zap.String("user_id", rechargeRecord.UserID), + zap.String("total_amount", totalRechargeAmount.String()), + zap.Error(err), + ) + return err + } + + return nil + }) + + if err != nil { + s.logger.Error("处理微信支付成功失败", + zap.String("out_trade_no", outTradeNo), + zap.String("transaction_id", transactionID), + zap.String("amount", amount.String()), + zap.Error(err), + ) + return err + } + + s.logger.Info("微信支付成功处理完成", + zap.String("out_trade_no", outTradeNo), + zap.String("transaction_id", transactionID), + zap.String("amount", amount.String()), + zap.String("bonus_amount", bonusAmount.String()), + zap.String("user_id", rechargeRecord.UserID), + ) + + return nil +} + +// HandleWechatRefundCallback 处理微信退款回调 +func (s *FinanceApplicationServiceImpl) HandleWechatRefundCallback(ctx context.Context, r *http.Request) error { + if s.wechatPayService == nil { + s.logger.Error("微信支付服务未初始化") + return fmt.Errorf("微信支付服务未初始化") + } + + // 解析并验证微信退款回调通知 + refund, err := s.wechatPayService.HandleRefundNotification(ctx, r) + if err != nil { + s.logger.Error("微信退款回调验证失败", zap.Error(err)) + return err + } + + // 记录回调数据 + s.logger.Info("微信退款回调数据", + zap.String("out_trade_no", func() string { + if refund.OutTradeNo != nil { + return *refund.OutTradeNo + } + return "" + }()), + zap.String("out_refund_no", func() string { + if refund.OutRefundNo != nil { + return *refund.OutRefundNo + } + return "" + }()), + zap.String("refund_id", func() string { + if refund.RefundId != nil { + return *refund.RefundId + } + return "" + }()), + zap.Any("status", func() interface{} { + if refund.Status != nil { + return *refund.Status + } + return nil + }()), + ) + + // 处理退款逻辑 + // 这里可以根据实际业务需求实现退款处理逻辑 + s.logger.Info("微信退款回调处理完成", + zap.String("out_trade_no", func() string { + if refund.OutTradeNo != nil { + return *refund.OutTradeNo + } + return "" + }()), + zap.String("refund_id", func() string { + if refund.RefundId != nil { + return *refund.RefundId + } + return "" + }()), + ) + + return nil +} diff --git a/internal/application/finance/invoice_application_service.go b/internal/application/finance/invoice_application_service.go index 67fab6d..9647108 100644 --- a/internal/application/finance/invoice_application_service.go +++ b/internal/application/finance/invoice_application_service.go @@ -393,7 +393,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context, return nil, err } - // 3. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额 + // 3. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额 realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID) if err != nil { return nil, err @@ -408,7 +408,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context, // 5. 构建响应DTO return &dto.AvailableAmountResponse{ AvailableAmount: availableAmount, - TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+对公转账) + TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+微信充值+对公转账) TotalGifted: totalGifted, TotalInvoiced: totalInvoiced, PendingApplications: pendingAmount, @@ -417,7 +417,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context, // calculateAvailableAmount 计算可开票金额(私有方法) func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) { - // 1. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额 + // 1. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额 realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID) if err != nil { return decimal.Zero, err @@ -433,7 +433,7 @@ func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Con fmt.Println("totalInvoiced", totalInvoiced) fmt.Println("pendingAmount", pendingAmount) // 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请 - // 可开票金额 = 真实充值金额(支付宝充值+对公转账) - 已开票金额 - 待处理申请金额 + // 可开票金额 = 真实充值金额(支付宝充值+微信充值+对公转账) - 已开票金额 - 待处理申请金额 availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount) fmt.Println("availableAmount", availableAmount) // 确保可开票金额不为负数 @@ -452,16 +452,16 @@ func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, us return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err) } - // 2. 计算真实充值金额(支付宝充值 + 对公转账)和总赠送金额 - var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 对公转账 + // 2. 计算真实充值金额(支付宝充值 + 微信充值 + 对公转账)和总赠送金额 + var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 微信充值 + 对公转账 var totalGifted decimal.Decimal // 总赠送金额 for _, record := range rechargeRecords { if record.IsSuccess() { if record.RechargeType == entities.RechargeTypeGift { // 赠送金额不计入可开票金额 totalGifted = totalGifted.Add(record.Amount) - } else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeTransfer { - // 只有支付宝充值和对公转账计入可开票金额 + } else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeWechat || record.RechargeType == entities.RechargeTypeTransfer { + // 支付宝充值、微信充值和对公转账计入可开票金额 realRecharged = realRecharged.Add(record.Amount) } } diff --git a/internal/config/config.go b/internal/config/config.go index ba63e76..505f6a9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,6 +31,9 @@ type Config struct { Zhicha ZhichaConfig `mapstructure:"zhicha"` Muzi MuziConfig `mapstructure:"muzi"` AliPay AliPayConfig `mapstructure:"alipay"` + Wxpay WxpayConfig `mapstructure:"wxpay"` + WechatMini WechatMiniConfig `mapstructure:"wechat_mini"` + WechatH5 WechatH5Config `mapstructure:"wechat_h5"` Yushan YushanConfig `mapstructure:"yushan"` TianYanCha TianYanChaConfig `mapstructure:"tianyancha"` Alicloud AlicloudConfig `mapstructure:"alicloud"` @@ -429,6 +432,29 @@ type AliPayConfig struct { ReturnURL string `mapstructure:"return_url"` } +// WxpayConfig 微信支付配置 +type WxpayConfig struct { + AppID string `mapstructure:"app_id"` + MchID string `mapstructure:"mch_id"` + MchCertificateSerialNumber string `mapstructure:"mch_certificate_serial_number"` + MchApiv3Key string `mapstructure:"mch_apiv3_key"` + MchPrivateKeyPath string `mapstructure:"mch_private_key_path"` + MchPublicKeyID string `mapstructure:"mch_public_key_id"` + MchPublicKeyPath string `mapstructure:"mch_public_key_path"` + NotifyUrl string `mapstructure:"notify_url"` + RefundNotifyUrl string `mapstructure:"refund_notify_url"` +} + +// WechatMiniConfig 微信小程序配置 +type WechatMiniConfig struct { + AppID string `mapstructure:"app_id"` +} + +// WechatH5Config 微信H5配置 +type WechatH5Config struct { + AppID string `mapstructure:"app_id"` +} + // YushanConfig 羽山配置 type YushanConfig struct { URL string `mapstructure:"url"` diff --git a/internal/container/container.go b/internal/container/container.go index 72a3fc3..9ea0c62 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -307,6 +307,16 @@ func NewContainer() *Container { } return payment.NewAliPayService(config) }, + // 微信支付服务 + func(cfg *config.Config, logger *zap.Logger) *payment.WechatPayService { + // 根据配置选择初始化方式,默认使用平台证书方式 + initType := payment.InitTypePlatformCert + // 如果配置了公钥ID,使用公钥方式 + if cfg.Wxpay.MchPublicKeyID != "" { + initType = payment.InitTypeWxPayPubKey + } + return payment.NewWechatPayService(*cfg, initType, logger) + }, // 导出管理器 func(logger *zap.Logger) *export.ExportManager { return export.NewExportManager(logger) @@ -512,6 +522,11 @@ func NewContainer() *Container { finance_repo.NewGormAlipayOrderRepository, fx.As(new(domain_finance_repo.AlipayOrderRepository)), ), + // 微信订单仓储 + fx.Annotate( + finance_repo.NewGormWechatOrderRepository, + fx.As(new(domain_finance_repo.WechatOrderRepository)), + ), // 发票申请仓储 fx.Annotate( finance_repo.NewGormInvoiceApplicationRepository, @@ -855,10 +870,13 @@ func NewContainer() *Container { fx.Annotate( func( aliPayClient *payment.AliPayService, + wechatPayService *payment.WechatPayService, walletService finance_services.WalletAggregateService, rechargeRecordService finance_services.RechargeRecordService, walletTransactionRepo domain_finance_repo.WalletTransactionRepository, alipayOrderRepo domain_finance_repo.AlipayOrderRepository, + wechatOrderRepo domain_finance_repo.WechatOrderRepository, + rechargeRecordRepo domain_finance_repo.RechargeRecordRepository, userRepo domain_user_repo.UserRepository, txManager *shared_database.TransactionManager, logger *zap.Logger, @@ -867,10 +885,13 @@ func NewContainer() *Container { ) finance.FinanceApplicationService { return finance.NewFinanceApplicationService( aliPayClient, + wechatPayService, walletService, rechargeRecordService, walletTransactionRepo, alipayOrderRepo, + wechatOrderRepo, + rechargeRecordRepo, userRepo, txManager, logger, diff --git a/internal/domains/finance/entities/alipay_order.go b/internal/domains/finance/entities/alipay_order.go index 8a63c25..5f68905 100644 --- a/internal/domains/finance/entities/alipay_order.go +++ b/internal/domains/finance/entities/alipay_order.go @@ -1,140 +1,28 @@ package entities -import ( - "time" +import "github.com/shopspring/decimal" - "github.com/google/uuid" - "github.com/shopspring/decimal" - "gorm.io/gorm" -) - -// AlipayOrderStatus 支付宝订单状态枚举 -type AlipayOrderStatus string +// AlipayOrderStatus 支付宝订单状态枚举(别名) +type AlipayOrderStatus = PayOrderStatus const ( - AlipayOrderStatusPending AlipayOrderStatus = "pending" // 待支付 - AlipayOrderStatusSuccess AlipayOrderStatus = "success" // 支付成功 - AlipayOrderStatusFailed AlipayOrderStatus = "failed" // 支付失败 - AlipayOrderStatusCancelled AlipayOrderStatus = "cancelled" // 已取消 - AlipayOrderStatusClosed AlipayOrderStatus = "closed" // 已关闭 + AlipayOrderStatusPending AlipayOrderStatus = PayOrderStatusPending // 待支付 + AlipayOrderStatusSuccess AlipayOrderStatus = PayOrderStatusSuccess // 支付成功 + AlipayOrderStatusFailed AlipayOrderStatus = PayOrderStatusFailed // 支付失败 + AlipayOrderStatusCancelled AlipayOrderStatus = PayOrderStatusCancelled // 已取消 + AlipayOrderStatusClosed AlipayOrderStatus = PayOrderStatusClosed // 已关闭 ) const ( - AlipayOrderPlatformApp = "app" // 支付宝APP支付 - AlipayOrderPlatformH5 = "h5" // 支付宝H5支付 - AlipayOrderPlatformPC = "pc" // 支付宝PC支付 + AlipayOrderPlatformApp = "app" // 支付宝APP支付 + AlipayOrderPlatformH5 = "h5" // 支付宝H5支付 + AlipayOrderPlatformPC = "pc" // 支付宝PC支付 ) -// AlipayOrder 支付宝订单详情实体 -type AlipayOrder struct { - // 基础标识 - ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"支付宝订单唯一标识"` - RechargeID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"recharge_id" comment:"关联充值记录ID"` - OutTradeNo string `gorm:"type:varchar(64);not null;uniqueIndex" json:"out_trade_no" comment:"商户订单号"` - TradeNo *string `gorm:"type:varchar(64);uniqueIndex" json:"trade_no,omitempty" comment:"支付宝交易号"` - - // 订单信息 - Subject string `gorm:"type:varchar(200);not null" json:"subject" comment:"订单标题"` - Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"订单金额"` - Platform string `gorm:"type:varchar(20);not null" json:"platform" comment:"支付平台:app/h5/pc"` - Status AlipayOrderStatus `gorm:"type:varchar(20);not null;default:'pending';index" json:"status" comment:"订单状态"` - - // 支付宝返回信息 - BuyerID string `gorm:"type:varchar(64)" json:"buyer_id,omitempty" comment:"买家支付宝用户ID"` - SellerID string `gorm:"type:varchar(64)" json:"seller_id,omitempty" comment:"卖家支付宝用户ID"` - PayAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"pay_amount,omitempty" comment:"实际支付金额"` - ReceiptAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"receipt_amount,omitempty" comment:"实收金额"` - - // 回调信息 - NotifyTime *time.Time `gorm:"index" json:"notify_time,omitempty" comment:"异步通知时间"` - ReturnTime *time.Time `gorm:"index" json:"return_time,omitempty" comment:"同步返回时间"` - - // 错误信息 - ErrorCode string `gorm:"type:varchar(64)" json:"error_code,omitempty" comment:"错误码"` - ErrorMessage string `gorm:"type:text" json:"error_message,omitempty" comment:"错误信息"` - - // 时间戳字段 - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"` -} - -// TableName 指定数据库表名 -func (AlipayOrder) TableName() string { - return "alipay_orders" -} - -// BeforeCreate GORM钩子:创建前自动生成UUID -func (a *AlipayOrder) BeforeCreate(tx *gorm.DB) error { - if a.ID == "" { - a.ID = uuid.New().String() - } - return nil -} - -// IsPending 检查是否为待支付状态 -func (a *AlipayOrder) IsPending() bool { - return a.Status == AlipayOrderStatusPending -} - -// IsSuccess 检查是否为支付成功状态 -func (a *AlipayOrder) IsSuccess() bool { - return a.Status == AlipayOrderStatusSuccess -} - -// IsFailed 检查是否为支付失败状态 -func (a *AlipayOrder) IsFailed() bool { - return a.Status == AlipayOrderStatusFailed -} - -// IsCancelled 检查是否为已取消状态 -func (a *AlipayOrder) IsCancelled() bool { - return a.Status == AlipayOrderStatusCancelled -} - -// IsClosed 检查是否为已关闭状态 -func (a *AlipayOrder) IsClosed() bool { - return a.Status == AlipayOrderStatusClosed -} - -// MarkSuccess 标记为支付成功 -func (a *AlipayOrder) MarkSuccess(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) { - a.Status = AlipayOrderStatusSuccess - a.TradeNo = &tradeNo - a.BuyerID = buyerID - a.SellerID = sellerID - a.PayAmount = payAmount - a.ReceiptAmount = receiptAmount - now := time.Now() - a.NotifyTime = &now -} - -// MarkFailed 标记为支付失败 -func (a *AlipayOrder) MarkFailed(errorCode, errorMessage string) { - a.Status = AlipayOrderStatusFailed - a.ErrorCode = errorCode - a.ErrorMessage = errorMessage -} - -// MarkCancelled 标记为已取消 -func (a *AlipayOrder) MarkCancelled() { - a.Status = AlipayOrderStatusCancelled -} - -// MarkClosed 标记为已关闭 -func (a *AlipayOrder) MarkClosed() { - a.Status = AlipayOrderStatusClosed -} +// AlipayOrder 支付宝订单实体(统一表 typay_orders,兼容多支付渠道) +type AlipayOrder = PayOrder // NewAlipayOrder 工厂方法 - 创建支付宝订单 func NewAlipayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *AlipayOrder { - return &AlipayOrder{ - ID: uuid.New().String(), - RechargeID: rechargeID, - OutTradeNo: outTradeNo, - Subject: subject, - Amount: amount, - Platform: platform, - Status: AlipayOrderStatusPending, - } + return NewPayOrder(rechargeID, outTradeNo, subject, amount, platform, "alipay") } diff --git a/internal/domains/finance/entities/pay_order.go b/internal/domains/finance/entities/pay_order.go new file mode 100644 index 0000000..63c3d1a --- /dev/null +++ b/internal/domains/finance/entities/pay_order.go @@ -0,0 +1,136 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "github.com/shopspring/decimal" + "gorm.io/gorm" +) + +// PayOrderStatus 支付订单状态枚举(通用) +type PayOrderStatus string + +const ( + PayOrderStatusPending PayOrderStatus = "pending" // 待支付 + PayOrderStatusSuccess PayOrderStatus = "success" // 支付成功 + PayOrderStatusFailed PayOrderStatus = "failed" // 支付失败 + PayOrderStatusCancelled PayOrderStatus = "cancelled" // 已取消 + PayOrderStatusClosed PayOrderStatus = "closed" // 已关闭 +) + +// PayOrder 支付订单详情实体(统一表 typay_orders,兼容多支付渠道) +type PayOrder struct { + // 基础标识 + ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"支付订单唯一标识"` + RechargeID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"recharge_id" comment:"关联充值记录ID"` + OutTradeNo string `gorm:"type:varchar(64);not null;uniqueIndex" json:"out_trade_no" comment:"商户订单号"` + TradeNo *string `gorm:"type:varchar(64);uniqueIndex" json:"trade_no,omitempty" comment:"第三方支付交易号"` + + // 订单信息 + Subject string `gorm:"type:varchar(200);not null" json:"subject" comment:"订单标题"` + Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"订单金额"` + Platform string `gorm:"type:varchar(20);not null" json:"platform" comment:"支付平台:app/h5/pc/wx_h5/wx_mini等"` + PayChannel string `gorm:"type:varchar(20);not null;default:'alipay';index" json:"pay_channel" comment:"支付渠道:alipay/wechat"` + Status PayOrderStatus `gorm:"type:varchar(20);not null;default:'pending';index" json:"status" comment:"订单状态"` + + // 支付渠道返回信息 + BuyerID string `gorm:"type:varchar(64)" json:"buyer_id,omitempty" comment:"买家ID(支付渠道方)"` + SellerID string `gorm:"type:varchar(64)" json:"seller_id,omitempty" comment:"卖家ID(支付渠道方)"` + PayAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"pay_amount,omitempty" comment:"实际支付金额"` + ReceiptAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"receipt_amount,omitempty" comment:"实收金额"` + + // 回调信息 + NotifyTime *time.Time `gorm:"index" json:"notify_time,omitempty" comment:"异步通知时间"` + ReturnTime *time.Time `gorm:"index" json:"return_time,omitempty" comment:"同步返回时间"` + + // 错误信息 + ErrorCode string `gorm:"type:varchar(64)" json:"error_code,omitempty" comment:"错误码"` + ErrorMessage string `gorm:"type:text" json:"error_message,omitempty" comment:"错误信息"` + + // 时间戳字段 + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"` +} + +// TableName 指定数据库表名 +func (PayOrder) TableName() string { + return "typay_orders" +} + +// BeforeCreate GORM钩子:创建前自动生成UUID +func (p *PayOrder) BeforeCreate(tx *gorm.DB) error { + if p.ID == "" { + p.ID = uuid.New().String() + } + return nil +} + +// IsPending 检查是否为待支付状态 +func (p *PayOrder) IsPending() bool { + return p.Status == PayOrderStatusPending +} + +// IsSuccess 检查是否为支付成功状态 +func (p *PayOrder) IsSuccess() bool { + return p.Status == PayOrderStatusSuccess +} + +// IsFailed 检查是否为支付失败状态 +func (p *PayOrder) IsFailed() bool { + return p.Status == PayOrderStatusFailed +} + +// IsCancelled 检查是否为已取消状态 +func (p *PayOrder) IsCancelled() bool { + return p.Status == PayOrderStatusCancelled +} + +// IsClosed 检查是否为已关闭状态 +func (p *PayOrder) IsClosed() bool { + return p.Status == PayOrderStatusClosed +} + +// MarkSuccess 标记为支付成功 +func (p *PayOrder) MarkSuccess(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) { + p.Status = PayOrderStatusSuccess + p.TradeNo = &tradeNo + p.BuyerID = buyerID + p.SellerID = sellerID + p.PayAmount = payAmount + p.ReceiptAmount = receiptAmount + now := time.Now() + p.NotifyTime = &now +} + +// MarkFailed 标记为支付失败 +func (p *PayOrder) MarkFailed(errorCode, errorMessage string) { + p.Status = PayOrderStatusFailed + p.ErrorCode = errorCode + p.ErrorMessage = errorMessage +} + +// MarkCancelled 标记为已取消 +func (p *PayOrder) MarkCancelled() { + p.Status = PayOrderStatusCancelled +} + +// MarkClosed 标记为已关闭 +func (p *PayOrder) MarkClosed() { + p.Status = PayOrderStatusClosed +} + +// NewPayOrder 通用工厂方法 - 创建支付订单(支持多支付渠道) +func NewPayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform, payChannel string) *PayOrder { + return &PayOrder{ + ID: uuid.New().String(), + RechargeID: rechargeID, + OutTradeNo: outTradeNo, + Subject: subject, + Amount: amount, + Platform: platform, + PayChannel: payChannel, + Status: PayOrderStatusPending, + } +} diff --git a/internal/domains/finance/entities/recharge_record.go b/internal/domains/finance/entities/recharge_record.go index e4e13b8..5563c29 100644 --- a/internal/domains/finance/entities/recharge_record.go +++ b/internal/domains/finance/entities/recharge_record.go @@ -14,6 +14,7 @@ type RechargeType string const ( RechargeTypeAlipay RechargeType = "alipay" // 支付宝充值 + RechargeTypeWechat RechargeType = "wechat" // 微信充值 RechargeTypeTransfer RechargeType = "transfer" // 对公转账 RechargeTypeGift RechargeType = "gift" // 赠送 ) @@ -42,6 +43,7 @@ type RechargeRecord struct { // 订单号字段(根据充值类型使用不同字段) AlipayOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"alipay_order_id,omitempty" comment:"支付宝订单号"` + WechatOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"wechat_order_id,omitempty" comment:"微信订单号"` TransferOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"transfer_order_id,omitempty" comment:"转账订单号"` // 通用字段 @@ -104,14 +106,24 @@ func (r *RechargeRecord) MarkCancelled() { // ValidatePaymentMethod 验证支付方式:支付宝订单号和转账订单号只能有一个存在 func (r *RechargeRecord) ValidatePaymentMethod() error { hasAlipay := r.AlipayOrderID != nil && *r.AlipayOrderID != "" + hasWechat := r.WechatOrderID != nil && *r.WechatOrderID != "" hasTransfer := r.TransferOrderID != nil && *r.TransferOrderID != "" - if hasAlipay && hasTransfer { - return errors.New("支付宝订单号和转账订单号不能同时存在") + count := 0 + if hasAlipay { + count++ } - - if !hasAlipay && !hasTransfer { - return errors.New("必须提供支付宝订单号或转账订单号") + if hasWechat { + count++ + } + if hasTransfer { + count++ + } + if count > 1 { + return errors.New("支付宝、微信或转账订单号只能存在一个") + } + if count == 0 { + return errors.New("必须提供支付宝、微信或转账订单号") } return nil @@ -124,6 +136,10 @@ func (r *RechargeRecord) GetOrderID() string { if r.AlipayOrderID != nil { return *r.AlipayOrderID } + case RechargeTypeWechat: + if r.WechatOrderID != nil { + return *r.WechatOrderID + } case RechargeTypeTransfer: if r.TransferOrderID != nil { return *r.TransferOrderID @@ -137,6 +153,11 @@ func (r *RechargeRecord) SetAlipayOrderID(orderID string) { r.AlipayOrderID = &orderID } +// SetWechatOrderID 设置微信订单号 +func (r *RechargeRecord) SetWechatOrderID(orderID string) { + r.WechatOrderID = &orderID +} + // SetTransferOrderID 设置转账订单号 func (r *RechargeRecord) SetTransferOrderID(orderID string) { r.TransferOrderID = &orderID @@ -153,6 +174,17 @@ func NewAlipayRechargeRecord(userID string, amount decimal.Decimal, alipayOrderI } } +// NewWechatRechargeRecord 工厂方法 - 创建微信充值记录 +func NewWechatRechargeRecord(userID string, amount decimal.Decimal, wechatOrderID string) *RechargeRecord { + return &RechargeRecord{ + UserID: userID, + Amount: amount, + RechargeType: RechargeTypeWechat, + Status: RechargeStatusPending, + WechatOrderID: &wechatOrderID, + } +} + // NewTransferRechargeRecord 工厂方法 - 创建对公转账充值记录 func NewTransferRechargeRecord(userID string, amount decimal.Decimal, transferOrderID, notes string) *RechargeRecord { return &RechargeRecord{ diff --git a/internal/domains/finance/entities/wechat_order.go b/internal/domains/finance/entities/wechat_order.go new file mode 100644 index 0000000..1c4af7a --- /dev/null +++ b/internal/domains/finance/entities/wechat_order.go @@ -0,0 +1,33 @@ +package entities + +import "github.com/shopspring/decimal" + +// WechatOrderStatus 微信订单状态枚举(别名) +type WechatOrderStatus = PayOrderStatus + +const ( + WechatOrderStatusPending WechatOrderStatus = PayOrderStatusPending // 待支付 + WechatOrderStatusSuccess WechatOrderStatus = PayOrderStatusSuccess // 支付成功 + WechatOrderStatusFailed WechatOrderStatus = PayOrderStatusFailed // 支付失败 + WechatOrderStatusCancelled WechatOrderStatus = PayOrderStatusCancelled // 已取消 + WechatOrderStatusClosed WechatOrderStatus = PayOrderStatusClosed // 已关闭 +) + +const ( + WechatOrderPlatformApp = "app" // 微信APP支付 + WechatOrderPlatformH5 = "h5" // 微信H5支付 + WechatOrderPlatformMini = "mini" // 微信小程序支付 +) + +// WechatOrder 微信订单实体(统一表 typay_orders,兼容多支付渠道) +type WechatOrder = PayOrder + +// NewWechatOrder 工厂方法 - 创建微信订单(统一表 typay_orders) +func NewWechatOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *WechatOrder { + return NewPayOrder(rechargeID, outTradeNo, subject, amount, platform, "wechat") +} + +// NewWechatPayOrder 工厂方法 - 创建微信支付订单(别名,保持向后兼容) +func NewWechatPayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *WechatOrder { + return NewWechatOrder(rechargeID, outTradeNo, subject, amount, platform) +} diff --git a/internal/domains/finance/repositories/alipay_order_repository_interface.go b/internal/domains/finance/repositories/alipay_order_repository_interface.go index ea3ef14..6cb3837 100644 --- a/internal/domains/finance/repositories/alipay_order_repository_interface.go +++ b/internal/domains/finance/repositories/alipay_order_repository_interface.go @@ -17,4 +17,4 @@ type AlipayOrderRepository interface { UpdateStatus(ctx context.Context, id string, status entities.AlipayOrderStatus) error Delete(ctx context.Context, id string) error Exists(ctx context.Context, id string) (bool, error) -} \ No newline at end of file +} diff --git a/internal/domains/finance/repositories/recharge_record_repository_interface.go b/internal/domains/finance/repositories/recharge_record_repository_interface.go index c1c3f2b..ed285e3 100644 --- a/internal/domains/finance/repositories/recharge_record_repository_interface.go +++ b/internal/domains/finance/repositories/recharge_record_repository_interface.go @@ -17,20 +17,20 @@ type RechargeRecordRepository interface { GetByTransferOrderID(ctx context.Context, transferOrderID string) (*entities.RechargeRecord, error) Update(ctx context.Context, record entities.RechargeRecord) error UpdateStatus(ctx context.Context, id string, status entities.RechargeStatus) error - + // 管理员查询方法 List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) - + // 统计相关方法 GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error) GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) - + // 系统级别统计方法 GetSystemTotalAmount(ctx context.Context) (float64, error) GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) -} \ No newline at end of file +} diff --git a/internal/domains/finance/repositories/wechat_order_repository_interface.go b/internal/domains/finance/repositories/wechat_order_repository_interface.go new file mode 100644 index 0000000..f4f8ef1 --- /dev/null +++ b/internal/domains/finance/repositories/wechat_order_repository_interface.go @@ -0,0 +1,20 @@ +package repositories + +import ( + "context" + + "tyapi-server/internal/domains/finance/entities" +) + +// WechatOrderRepository 微信订单仓储接口 +type WechatOrderRepository interface { + Create(ctx context.Context, order entities.WechatOrder) (entities.WechatOrder, error) + GetByID(ctx context.Context, id string) (entities.WechatOrder, error) + GetByOutTradeNo(ctx context.Context, outTradeNo string) (*entities.WechatOrder, error) + GetByRechargeID(ctx context.Context, rechargeID string) (*entities.WechatOrder, error) + GetByUserID(ctx context.Context, userID string) ([]entities.WechatOrder, error) + Update(ctx context.Context, order entities.WechatOrder) error + UpdateStatus(ctx context.Context, id string, status entities.WechatOrderStatus) error + Delete(ctx context.Context, id string) error + Exists(ctx context.Context, id string) (bool, error) +} diff --git a/internal/domains/user/repositories/user_repository_interface.go b/internal/domains/user/repositories/user_repository_interface.go index 8386ff6..482d1df 100644 --- a/internal/domains/user/repositories/user_repository_interface.go +++ b/internal/domains/user/repositories/user_repository_interface.go @@ -20,7 +20,6 @@ type UserStats struct { // UserRepository 用户仓储接口 type UserRepository interface { interfaces.Repository[entities.User] - // 基础查询 - 直接使用实体 GetByPhone(ctx context.Context, phone string) (*entities.User, error) GetByUsername(ctx context.Context, username string) (*entities.User, error) @@ -48,7 +47,7 @@ type UserRepository interface { // 统计信息 GetStats(ctx context.Context) (*UserStats, error) GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*UserStats, error) - + // 系统级别统计方法 GetSystemUserStats(ctx context.Context) (*UserStats, error) GetSystemUserStatsByDateRange(ctx context.Context, startDate, endDate time.Time) (*UserStats, error) @@ -56,7 +55,7 @@ type UserRepository interface { GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) GetSystemDailyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) GetSystemMonthlyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) - + // 排行榜查询方法 GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) GetUserCallRankingByConsumption(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) @@ -119,4 +118,4 @@ type EnterpriseInfoRepository interface { Count(ctx context.Context, options interfaces.CountOptions) (int64, error) List(ctx context.Context, options interfaces.ListOptions) ([]entities.EnterpriseInfo, error) Exists(ctx context.Context, id string) (bool, error) -} \ No newline at end of file +} diff --git a/internal/infrastructure/database/repositories/finance/gorm_alipay_order_repository.go b/internal/infrastructure/database/repositories/finance/gorm_alipay_order_repository.go index fe053d7..26f51db 100644 --- a/internal/infrastructure/database/repositories/finance/gorm_alipay_order_repository.go +++ b/internal/infrastructure/database/repositories/finance/gorm_alipay_order_repository.go @@ -13,7 +13,7 @@ import ( ) const ( - AlipayOrdersTable = "alipay_orders" + AlipayOrdersTable = "typay_orders" ) type GormAlipayOrderRepository struct { @@ -72,9 +72,9 @@ func (r *GormAlipayOrderRepository) GetByRechargeID(ctx context.Context, recharg func (r *GormAlipayOrderRepository) GetByUserID(ctx context.Context, userID string) ([]entities.AlipayOrder, error) { var orders []entities.AlipayOrder err := r.GetDB(ctx). - Joins("JOIN recharge_records ON alipay_orders.recharge_id = recharge_records.id"). + Joins("JOIN recharge_records ON typay_orders.recharge_id = recharge_records.id"). Where("recharge_records.user_id = ?", userID). - Order("alipay_orders.created_at DESC"). + Order("typay_orders.created_at DESC"). Find(&orders).Error return orders, err } @@ -95,4 +95,4 @@ func (r *GormAlipayOrderRepository) Exists(ctx context.Context, id string) (bool var count int64 err := r.GetDB(ctx).Model(&entities.AlipayOrder{}).Where("id = ?", id).Count(&count).Error return count > 0, err -} \ No newline at end of file +} diff --git a/internal/infrastructure/database/repositories/finance/gorm_recharge_record_repository.go b/internal/infrastructure/database/repositories/finance/gorm_recharge_record_repository.go index 0e2c775..2588867 100644 --- a/internal/infrastructure/database/repositories/finance/gorm_recharge_record_repository.go +++ b/internal/infrastructure/database/repositories/finance/gorm_recharge_record_repository.go @@ -163,11 +163,11 @@ func (r *GormRechargeRecordRepository) Count(ctx context.Context, options interf } if options.Search != "" { if hasCompanyNameFilter { - query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ? OR rr.wechat_order_id LIKE ?", + "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") } else { - query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ? OR wechat_order_id LIKE ?", + "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") } } return count, query.Count(&count).Error @@ -267,11 +267,11 @@ func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfa if options.Search != "" { if hasCompanyNameFilter { - query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ? OR rr.wechat_order_id LIKE ?", + "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") } else { - query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ? OR wechat_order_id LIKE ?", + "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") } } diff --git a/internal/infrastructure/database/repositories/finance/gorm_wechat_order_repository.go b/internal/infrastructure/database/repositories/finance/gorm_wechat_order_repository.go new file mode 100644 index 0000000..7a079a9 --- /dev/null +++ b/internal/infrastructure/database/repositories/finance/gorm_wechat_order_repository.go @@ -0,0 +1,93 @@ +package repositories + +import ( + "context" + "errors" + + "tyapi-server/internal/domains/finance/entities" + domain_finance_repo "tyapi-server/internal/domains/finance/repositories" + "tyapi-server/internal/shared/database" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +const ( + WechatOrdersTable = "typay_orders" +) + +type GormWechatOrderRepository struct { + *database.CachedBaseRepositoryImpl +} + +var _ domain_finance_repo.WechatOrderRepository = (*GormWechatOrderRepository)(nil) + +func NewGormWechatOrderRepository(db *gorm.DB, logger *zap.Logger) domain_finance_repo.WechatOrderRepository { + return &GormWechatOrderRepository{ + CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, WechatOrdersTable), + } +} + +func (r *GormWechatOrderRepository) Create(ctx context.Context, order entities.WechatOrder) (entities.WechatOrder, error) { + err := r.CreateEntity(ctx, &order) + return order, err +} + +func (r *GormWechatOrderRepository) GetByID(ctx context.Context, id string) (entities.WechatOrder, error) { + var order entities.WechatOrder + err := r.SmartGetByID(ctx, id, &order) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return entities.WechatOrder{}, gorm.ErrRecordNotFound + } + return entities.WechatOrder{}, err + } + return order, nil +} + +func (r *GormWechatOrderRepository) GetByOutTradeNo(ctx context.Context, outTradeNo string) (*entities.WechatOrder, error) { + var order entities.WechatOrder + err := r.GetDB(ctx).Where("out_trade_no = ?", outTradeNo).First(&order).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &order, nil +} + +func (r *GormWechatOrderRepository) GetByRechargeID(ctx context.Context, rechargeID string) (*entities.WechatOrder, error) { + var order entities.WechatOrder + err := r.GetDB(ctx).Where("recharge_id = ?", rechargeID).First(&order).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &order, nil +} + +func (r *GormWechatOrderRepository) GetByUserID(ctx context.Context, userID string) ([]entities.WechatOrder, error) { + var orders []entities.WechatOrder + // 需要通过充值记录关联查询,这里简化处理 + err := r.GetDB(ctx).Find(&orders).Error + return orders, err +} + +func (r *GormWechatOrderRepository) Update(ctx context.Context, order entities.WechatOrder) error { + return r.UpdateEntity(ctx, &order) +} + +func (r *GormWechatOrderRepository) UpdateStatus(ctx context.Context, id string, status entities.WechatOrderStatus) error { + return r.GetDB(ctx).Model(&entities.WechatOrder{}).Where("id = ?", id).Update("status", status).Error +} + +func (r *GormWechatOrderRepository) Delete(ctx context.Context, id string) error { + return r.DeleteEntity(ctx, id, &entities.WechatOrder{}) +} + +func (r *GormWechatOrderRepository) Exists(ctx context.Context, id string) (bool, error) { + return r.ExistsEntity(ctx, id, &entities.WechatOrder{}) +} diff --git a/internal/infrastructure/http/handlers/finance_handler.go b/internal/infrastructure/http/handlers/finance_handler.go index 693a276..1ba3f78 100644 --- a/internal/infrastructure/http/handlers/finance_handler.go +++ b/internal/infrastructure/http/handlers/finance_handler.go @@ -2,7 +2,9 @@ package handlers import ( + "bytes" "fmt" + "io" "net/http" "strconv" "time" @@ -19,12 +21,12 @@ import ( // FinanceHandler 财务HTTP处理器 type FinanceHandler struct { - appService finance.FinanceApplicationService - invoiceAppService finance.InvoiceApplicationService - adminInvoiceAppService finance.AdminInvoiceApplicationService - responseBuilder interfaces.ResponseBuilder - validator interfaces.RequestValidator - logger *zap.Logger + appService finance.FinanceApplicationService + invoiceAppService finance.InvoiceApplicationService + adminInvoiceAppService finance.AdminInvoiceApplicationService + responseBuilder interfaces.ResponseBuilder + validator interfaces.RequestValidator + logger *zap.Logger } // NewFinanceHandler 创建财务HTTP处理器 @@ -201,6 +203,123 @@ func (h *FinanceHandler) HandleAlipayCallback(c *gin.Context) { c.String(200, "success") } +// HandleWechatPayCallback 处理微信支付回调 +// @Summary 微信支付回调 +// @Description 处理微信支付异步通知 +// @Tags 支付管理 +// @Accept application/json +// @Produce text/plain +// @Success 200 {string} string "success" +// @Failure 400 {string} string "fail" +// @Router /api/v1/pay/wechat/callback [post] +func (h *FinanceHandler) HandleWechatPayCallback(c *gin.Context) { + // 记录回调请求信息 + h.logger.Info("收到微信支付回调请求", + zap.String("method", c.Request.Method), + zap.String("url", c.Request.URL.String()), + zap.String("remote_addr", c.ClientIP()), + zap.String("user_agent", c.GetHeader("User-Agent")), + zap.String("content_type", c.GetHeader("Content-Type")), + ) + + // 读取请求体内容用于调试(注意:读取后需要重新设置,否则后续解析会失败) + bodyBytes, err := c.GetRawData() + if err == nil && len(bodyBytes) > 0 { + h.logger.Info("微信支付回调请求体", + zap.String("body", string(bodyBytes)), + zap.Int("body_size", len(bodyBytes)), + ) + // 重新设置请求体,供后续解析使用 + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + + // 通过应用服务处理微信支付回调 + err = h.appService.HandleWechatPayCallback(c.Request.Context(), c.Request) + if err != nil { + h.logger.Error("微信支付回调处理失败", + zap.Error(err), + zap.String("remote_addr", c.ClientIP()), + ) + c.String(400, "fail") + return + } + + h.logger.Info("微信支付回调处理成功", zap.String("remote_addr", c.ClientIP())) + // 返回成功响应(微信要求返回success) + c.String(200, "success") +} + +// HandleWechatRefundCallback 处理微信退款回调 +// @Summary 微信退款回调 +// @Description 处理微信退款异步通知 +// @Tags 支付管理 +// @Accept application/json +// @Produce text/plain +// @Success 200 {string} string "success" +// @Failure 400 {string} string "fail" +// @Router /api/v1/wechat/refund_callback [post] +func (h *FinanceHandler) HandleWechatRefundCallback(c *gin.Context) { + // 记录回调请求信息 + h.logger.Info("收到微信退款回调请求", + zap.String("method", c.Request.Method), + zap.String("url", c.Request.URL.String()), + zap.String("remote_addr", c.ClientIP()), + zap.String("user_agent", c.GetHeader("User-Agent")), + ) + + // 通过应用服务处理微信退款回调 + err := h.appService.HandleWechatRefundCallback(c.Request.Context(), c.Request) + if err != nil { + h.logger.Error("微信退款回调处理失败", zap.Error(err)) + c.String(400, "fail") + return + } + + // 返回成功响应(微信要求返回success) + c.String(200, "success") +} + +// GetWechatOrderStatus 获取微信订单状态 +// @Summary 获取微信订单状态 +// @Description 根据商户订单号查询微信订单状态 +// @Tags 钱包管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param out_trade_no query string true "商户订单号" +// @Success 200 {object} responses.WechatOrderStatusResponse "获取订单状态成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 404 {object} map[string]interface{} "订单不存在" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/finance/wallet/wechat-order-status [get] +func (h *FinanceHandler) GetWechatOrderStatus(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + h.responseBuilder.Unauthorized(c, "用户未登录") + return + } + + outTradeNo := c.Query("out_trade_no") + if outTradeNo == "" { + h.responseBuilder.BadRequest(c, "缺少商户订单号") + return + } + + result, err := h.appService.GetWechatOrderStatus(c.Request.Context(), outTradeNo) + if err != nil { + h.logger.Error("获取微信订单状态失败", + zap.String("user_id", userID), + zap.String("out_trade_no", outTradeNo), + zap.Error(err), + ) + h.responseBuilder.BadRequest(c, "获取订单状态失败: "+err.Error()) + return + } + + h.responseBuilder.Success(c, result, "获取订单状态成功") +} + // HandleAlipayReturn 处理支付宝同步回调 // @Summary 支付宝同步回调 // @Description 处理支付宝同步支付通知,跳转到前端成功页面 @@ -240,7 +359,7 @@ func (h *FinanceHandler) HandleAlipayReturn(c *gin.Context) { // 通过应用服务处理同步回调,查询订单状态 orderStatus, err := h.appService.HandleAlipayReturn(c.Request.Context(), outTradeNo) if err != nil { - h.logger.Error("支付宝同步回调处理失败", + h.logger.Error("支付宝同步回调处理失败", zap.String("out_trade_no", outTradeNo), zap.Error(err)) h.redirectToFailPage(c, outTradeNo, "订单处理失败") @@ -257,7 +376,7 @@ func (h *FinanceHandler) HandleAlipayReturn(c *gin.Context) { switch orderStatus { case "TRADE_SUCCESS": // 支付成功,跳转到前端成功页面 - successURL := fmt.Sprintf("%s/finance/wallet/success?out_trade_no=%s&trade_no=%s&amount=%s", + successURL := fmt.Sprintf("%s/finance/wallet/success?out_trade_no=%s&trade_no=%s&amount=%s", frontendDomain, outTradeNo, tradeNo, totalAmount) c.Redirect(http.StatusFound, successURL) case "WAIT_BUYER_PAY": @@ -275,8 +394,8 @@ func (h *FinanceHandler) redirectToFailPage(c *gin.Context, outTradeNo, reason s if gin.Mode() == gin.DebugMode { frontendDomain = "http://localhost:5173" } - - failURL := fmt.Sprintf("%s/finance/wallet/fail?out_trade_no=%s&reason=%s", + + failURL := fmt.Sprintf("%s/finance/wallet/fail?out_trade_no=%s&reason=%s", frontendDomain, outTradeNo, reason) c.Redirect(http.StatusFound, failURL) } @@ -287,8 +406,8 @@ func (h *FinanceHandler) redirectToProcessingPage(c *gin.Context, outTradeNo, am if gin.Mode() == gin.DebugMode { frontendDomain = "http://localhost:5173" } - - processingURL := fmt.Sprintf("%s/finance/wallet/processing?out_trade_no=%s&amount=%s", + + processingURL := fmt.Sprintf("%s/finance/wallet/processing?out_trade_no=%s&amount=%s", frontendDomain, outTradeNo, amount) c.Redirect(http.StatusFound, processingURL) } @@ -319,7 +438,6 @@ func (h *FinanceHandler) CreateAlipayRecharge(c *gin.Context) { return } - // 调用应用服务进行完整的业务流程编排 result, err := h.appService.CreateAlipayRechargeOrder(c.Request.Context(), &cmd) if err != nil { @@ -343,6 +461,53 @@ func (h *FinanceHandler) CreateAlipayRecharge(c *gin.Context) { h.responseBuilder.Success(c, result, "支付宝充值订单创建成功") } +// CreateWechatRecharge 创建微信充值订单 +// @Summary 创建微信充值订单 +// @Description 创建微信充值订单并返回预支付数据 +// @Tags 钱包管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body commands.CreateWechatRechargeCommand true "微信充值请求" +// @Success 200 {object} responses.WechatRechargeOrderResponse "创建充值订单成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/finance/wallet/wechat-recharge [post] +func (h *FinanceHandler) CreateWechatRecharge(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + h.responseBuilder.Unauthorized(c, "用户未登录") + return + } + + var cmd commands.CreateWechatRechargeCommand + cmd.UserID = userID + if err := h.validator.BindAndValidate(c, &cmd); err != nil { + return + } + + result, err := h.appService.CreateWechatRechargeOrder(c.Request.Context(), &cmd) + if err != nil { + h.logger.Error("创建微信充值订单失败", + zap.String("user_id", userID), + zap.String("amount", cmd.Amount), + zap.Error(err), + ) + h.responseBuilder.BadRequest(c, "创建微信充值订单失败: "+err.Error()) + return + } + + h.logger.Info("微信充值订单创建成功", + zap.String("user_id", userID), + zap.String("out_trade_no", result.OutTradeNo), + zap.String("amount", cmd.Amount), + zap.String("platform", cmd.Platform), + ) + + h.responseBuilder.Success(c, result, "微信充值订单创建成功") +} + // TransferRecharge 管理员对公转账充值 func (h *FinanceHandler) TransferRecharge(c *gin.Context) { var cmd commands.TransferRechargeCommand @@ -849,8 +1014,6 @@ func (h *FinanceHandler) ApproveInvoiceApplication(c *gin.Context) { return } - - h.responseBuilder.Success(c, nil, "通过发票申请成功") } @@ -932,14 +1095,14 @@ func (h *FinanceHandler) AdminDownloadInvoiceFile(c *gin.Context) { // @Router /api/v1/debug/event-system [post] func (h *FinanceHandler) DebugEventSystem(c *gin.Context) { h.logger.Info("🔍 请求事件系统调试信息") - + // 这里可以添加事件系统的状态信息 // 暂时返回基本信息 debugInfo := map[string]interface{}{ "timestamp": time.Now().Format("2006-01-02 15:04:05"), - "message": "事件系统调试端点已启用", - "handler": "FinanceHandler", + "message": "事件系统调试端点已启用", + "handler": "FinanceHandler", } - + h.responseBuilder.Success(c, debugInfo, "事件系统调试信息") } diff --git a/internal/infrastructure/http/handlers/product_admin_handler.go b/internal/infrastructure/http/handlers/product_admin_handler.go index 89c7a65..9b701b4 100644 --- a/internal/infrastructure/http/handlers/product_admin_handler.go +++ b/internal/infrastructure/http/handlers/product_admin_handler.go @@ -1,6 +1,8 @@ package handlers import ( + "github.com/gin-gonic/gin" + "go.uber.org/zap" "strconv" "strings" "time" @@ -11,9 +13,6 @@ import ( "tyapi-server/internal/application/product/dto/queries" "tyapi-server/internal/application/product/dto/responses" "tyapi-server/internal/shared/interfaces" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" ) // ProductAdminHandler 产品管理员HTTP处理器 @@ -1338,7 +1337,7 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) { // @Param page query int false "页码" default(1) // @Param page_size query int false "每页数量" default(10) // @Param user_id query string false "用户ID" -// @Param recharge_type query string false "充值类型" Enums(alipay, transfer, gift) +// @Param recharge_type query string false "充值类型" Enums(alipay, wechat, transfer, gift) // @Param status query string false "状态" Enums(pending, success, failed) // @Param min_amount query string false "最小金额" // @Param max_amount query string false "最大金额" @@ -1425,7 +1424,7 @@ func (h *ProductAdminHandler) GetAdminRechargeRecords(c *gin.Context) { // @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv // @Security Bearer // @Param user_ids query string false "用户ID列表,逗号分隔" -// @Param recharge_type query string false "充值类型" Enums(alipay, transfer, gift) +// @Param recharge_type query string false "充值类型" Enums(alipay, wechat, transfer, gift) // @Param status query string false "状态" Enums(pending, success, failed) // @Param start_time query string false "开始时间" format(date-time) // @Param end_time query string false "结束时间" format(date-time) diff --git a/internal/infrastructure/http/routes/finance_routes.go b/internal/infrastructure/http/routes/finance_routes.go index 251b2fe..90dd150 100644 --- a/internal/infrastructure/http/routes/finance_routes.go +++ b/internal/infrastructure/http/routes/finance_routes.go @@ -42,6 +42,18 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) { alipayGroup.GET("/return", r.financeHandler.HandleAlipayReturn) // 支付宝同步回调 } + // 微信支付回调路由(不需要认证) + wechatPayGroup := engine.Group("/api/v1/pay/wechat") + { + wechatPayGroup.POST("/callback", r.financeHandler.HandleWechatPayCallback) // 微信支付异步回调 + } + + // 微信退款回调路由(不需要认证) + wechatRefundGroup := engine.Group("/api/v1/wechat") + { + wechatRefundGroup.POST("/refund_callback", r.financeHandler.HandleWechatRefundCallback) // 微信退款异步回调 + } + // 财务路由组,需要用户认证 financeGroup := engine.Group("/api/v1/finance") financeGroup.Use(r.authMiddleware.Handle()) @@ -49,12 +61,14 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) { // 钱包相关路由 walletGroup := financeGroup.Group("/wallet") { - walletGroup.GET("", r.financeHandler.GetWallet) // 获取钱包信息 - walletGroup.GET("/transactions", r.financeHandler.GetUserWalletTransactions) // 获取钱包交易记录 - walletGroup.GET("/recharge-config", r.financeHandler.GetRechargeConfig) // 获取充值配置 - walletGroup.POST("/alipay-recharge", r.financeHandler.CreateAlipayRecharge) // 创建支付宝充值订单 - walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页 + walletGroup.GET("", r.financeHandler.GetWallet) // 获取钱包信息 + walletGroup.GET("/transactions", r.financeHandler.GetUserWalletTransactions) // 获取钱包交易记录 + walletGroup.GET("/recharge-config", r.financeHandler.GetRechargeConfig) // 获取充值配置 + walletGroup.POST("/alipay-recharge", r.financeHandler.CreateAlipayRecharge) // 创建支付宝充值订单 + walletGroup.POST("/wechat-recharge", r.financeHandler.CreateWechatRecharge) // 创建微信充值订单 + walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页 walletGroup.GET("/alipay-order-status", r.financeHandler.GetAlipayOrderStatus) // 获取支付宝订单状态 + walletGroup.GET("/wechat-order-status", r.financeHandler.GetWechatOrderStatus) // 获取微信订单状态 } } @@ -62,11 +76,11 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) { invoiceGroup := engine.Group("/api/v1/invoices") invoiceGroup.Use(r.authMiddleware.Handle()) { - invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票 - invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息 - invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息 - invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录 - invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额 + invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票 + invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息 + invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息 + invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录 + invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额 invoiceGroup.GET("/:application_id/download", r.financeHandler.DownloadInvoiceFile) // 下载发票文件 } @@ -74,8 +88,8 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) { adminFinanceGroup := engine.Group("/api/v1/admin/finance") adminFinanceGroup.Use(r.adminAuthMiddleware.Handle()) { - adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值 - adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值 + adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值 + adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值 adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页 } @@ -83,7 +97,7 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) { adminInvoiceGroup := engine.Group("/api/v1/admin/invoices") adminInvoiceGroup.Use(r.adminAuthMiddleware.Handle()) { - adminInvoiceGroup.GET("/pending", r.financeHandler.GetPendingApplications) // 获取待处理申请列表 + adminInvoiceGroup.GET("/pending", r.financeHandler.GetPendingApplications) // 获取待处理申请列表 adminInvoiceGroup.POST("/:application_id/approve", r.financeHandler.ApproveInvoiceApplication) // 通过发票申请 adminInvoiceGroup.POST("/:application_id/reject", r.financeHandler.RejectInvoiceApplication) // 拒绝发票申请 adminInvoiceGroup.GET("/:application_id/download", r.financeHandler.AdminDownloadInvoiceFile) // 下载发票文件 diff --git a/internal/shared/payment/context.go b/internal/shared/payment/context.go new file mode 100644 index 0000000..7fcdf52 --- /dev/null +++ b/internal/shared/payment/context.go @@ -0,0 +1,25 @@ +package payment + +import ( + "context" + "fmt" +) + +// GetUidFromCtx 从context中获取用户ID +func GetUidFromCtx(ctx context.Context) (string, error) { + userID := ctx.Value("user_id") + if userID == nil { + return "", fmt.Errorf("用户ID不存在于上下文中") + } + + id, ok := userID.(string) + if !ok { + return "", fmt.Errorf("用户ID类型错误") + } + + if id == "" { + return "", fmt.Errorf("用户ID为空") + } + + return id, nil +} diff --git a/internal/shared/payment/user_auth_model.go b/internal/shared/payment/user_auth_model.go new file mode 100644 index 0000000..d7b3a4c --- /dev/null +++ b/internal/shared/payment/user_auth_model.go @@ -0,0 +1,48 @@ +package payment + +import ( + "context" + "fmt" +) + +// UserAuthModel 用户认证模型接口 +// 用于存储和管理用户的第三方认证信息(如微信OpenID) +type UserAuthModel interface { + FindOneByUserIdAuthType(ctx context.Context, userID string, authType string) (*UserAuth, error) + UpsertUserAuth(ctx context.Context, userID, authType, authKey string) error +} + +// UserAuth 用户认证信息 +type UserAuth struct { + UserID string // 用户ID + AuthType string // 认证类型 + AuthKey string // 认证密钥(如OpenID) +} + +// Platform 支付平台常量 +const ( + PlatformWxMini = "wx_mini" // 微信小程序 + PlatformWxH5 = "wx_h5" // 微信H5 + PlatformApp = "app" // APP + PlatformWxNative = "wx_native" // 微信Native扫码 +) + +// UserAuthType 用户认证类型常量 +const ( + UserAuthTypeWxMiniOpenID = "wx_mini_openid" // 微信小程序OpenID + UserAuthTypeWxh5OpenID = "wx_h5_openid" // 微信H5 OpenID +) + +// DefaultUserAuthModel 默认实现(如果不需要实际数据库查询,可以返回错误) +type DefaultUserAuthModel struct{} + +// FindOneByUserIdAuthType 查找用户认证信息 +// 注意:这是一个占位实现,实际使用时需要注入真实的实现 +func (m *DefaultUserAuthModel) FindOneByUserIdAuthType(ctx context.Context, userID string, authType string) (*UserAuth, error) { + return nil, fmt.Errorf("UserAuthModel未实现,请注入真实的实现") +} + +// UpsertUserAuth 占位实现 +func (m *DefaultUserAuthModel) UpsertUserAuth(ctx context.Context, userID, authType, authKey string) error { + return fmt.Errorf("UserAuthModel未实现,请注入真实的实现") +} diff --git a/internal/shared/payment/utils.go b/internal/shared/payment/utils.go new file mode 100644 index 0000000..38d27ca --- /dev/null +++ b/internal/shared/payment/utils.go @@ -0,0 +1,7 @@ +package payment + +// ToWechatAmount 将金额转换为微信支付金额(单位:分) +// 微信支付金额以分为单位,需要将元转换为分 +func ToWechatAmount(amount float64) int64 { + return int64(amount * 100) +} diff --git a/internal/shared/payment/wechatpay.go b/internal/shared/payment/wechatpay.go new file mode 100644 index 0000000..d4d8afe --- /dev/null +++ b/internal/shared/payment/wechatpay.go @@ -0,0 +1,353 @@ +package payment + +import ( + "context" + "fmt" + "net/http" + "os" + "path/filepath" + "strconv" + "time" + "tyapi-server/internal/config" + + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" + "github.com/wechatpay-apiv3/wechatpay-go/core/downloader" + "github.com/wechatpay-apiv3/wechatpay-go/core/notify" + "github.com/wechatpay-apiv3/wechatpay-go/core/option" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native" + "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" + "github.com/wechatpay-apiv3/wechatpay-go/utils" + "go.uber.org/zap" +) + +const ( + TradeStateSuccess = "SUCCESS" // 支付成功 + TradeStateRefund = "REFUND" // 转入退款 + TradeStateNotPay = "NOTPAY" // 未支付 + TradeStateClosed = "CLOSED" // 已关闭 + TradeStateRevoked = "REVOKED" // 已撤销(付款码支付) + TradeStateUserPaying = "USERPAYING" // 用户支付中(付款码支付) + TradeStatePayError = "PAYERROR" // 支付失败(其他原因,如银行返回失败) +) + +// resolveCertPath 解析证书文件路径,支持相对路径和绝对路径 +// 如果是相对路径,会从多个候选位置查找文件 +func resolveCertPath(relativePath string, logger *zap.Logger) (string, error) { + if relativePath == "" { + return "", fmt.Errorf("证书路径为空") + } + + // 如果已经是绝对路径,直接返回 + if filepath.IsAbs(relativePath) { + if _, err := os.Stat(relativePath); err == nil { + return relativePath, nil + } + return "", fmt.Errorf("证书文件不存在: %s", relativePath) + } + + // 候选路径列表(按优先级排序) + var candidatePaths []string + + // 优先级1: 从可执行文件所在目录查找(生产环境) + if execPath, err := os.Executable(); err == nil { + execDir := filepath.Dir(execPath) + // 处理符号链接 + if realPath, err := filepath.EvalSymlinks(execPath); err == nil { + execDir = filepath.Dir(realPath) + } + candidatePaths = append(candidatePaths, filepath.Join(execDir, relativePath)) + } + + // 优先级2: 从工作目录查找(开发环境) + if workDir, err := os.Getwd(); err == nil { + candidatePaths = append(candidatePaths, + filepath.Join(workDir, relativePath), + filepath.Join(workDir, "tyapi-server", relativePath), + ) + } + + // 尝试每个候选路径 + for _, candidatePath := range candidatePaths { + absPath, err := filepath.Abs(candidatePath) + if err != nil { + continue + } + + if logger != nil { + logger.Debug("尝试查找证书文件", zap.String("path", absPath)) + } + + // 检查文件是否存在 + if info, err := os.Stat(absPath); err == nil && !info.IsDir() { + if logger != nil { + logger.Info("找到证书文件", zap.String("path", absPath)) + } + return absPath, nil + } + } + + // 所有候选路径都不存在,返回错误 + return "", fmt.Errorf("证书文件不存在,已尝试的路径: %v", candidatePaths) +} + +// InitType 初始化类型 +type InitType string + +const ( + InitTypePlatformCert InitType = "platform_cert" // 平台证书初始化 + InitTypeWxPayPubKey InitType = "wxpay_pubkey" // 微信支付公钥初始化 +) + +type WechatPayService struct { + config config.Config + wechatClient *core.Client + notifyHandler *notify.Handler + logger *zap.Logger +} + +// NewWechatPayService 创建微信支付服务实例 +func NewWechatPayService(c config.Config, initType InitType, logger *zap.Logger) *WechatPayService { + switch initType { + case InitTypePlatformCert: + return newWechatPayServiceWithPlatformCert(c, logger) + case InitTypeWxPayPubKey: + return newWechatPayServiceWithWxPayPubKey(c, logger) + default: + logger.Error("不支持的初始化类型", zap.String("init_type", string(initType))) + panic(fmt.Sprintf("初始化失败,服务停止: %s", initType)) + } +} + +// newWechatPayServiceWithPlatformCert 使用平台证书初始化微信支付服务 +func newWechatPayServiceWithPlatformCert(c config.Config, logger *zap.Logger) *WechatPayService { + // 从配置中加载商户信息 + mchID := c.Wxpay.MchID + mchCertificateSerialNumber := c.Wxpay.MchCertificateSerialNumber + mchAPIv3Key := c.Wxpay.MchApiv3Key + + // 解析证书路径 + privateKeyPath, err := resolveCertPath(c.Wxpay.MchPrivateKeyPath, logger) + if err != nil { + logger.Error("解析商户私钥路径失败", zap.Error(err), zap.String("path", c.Wxpay.MchPrivateKeyPath)) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 从文件中加载商户私钥 + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(privateKeyPath) + if err != nil { + logger.Error("加载商户私钥失败", zap.Error(err), zap.String("path", privateKeyPath)) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 使用商户私钥和其他参数初始化微信支付客户端 + opts := []core.ClientOption{ + option.WithWechatPayAutoAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchAPIv3Key), + } + client, err := core.NewClient(context.Background(), opts...) + if err != nil { + logger.Error("创建微信支付客户端失败", zap.Error(err)) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 在初始化时获取证书访问器并创建 notifyHandler + certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID) + notifyHandler, err := notify.NewRSANotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor)) + if err != nil { + logger.Error("获取证书访问器失败", zap.Error(err)) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + logger.Info("微信支付客户端初始化成功(平台证书方式)") + return &WechatPayService{ + config: c, + wechatClient: client, + notifyHandler: notifyHandler, + logger: logger, + } +} + +// newWechatPayServiceWithWxPayPubKey 使用微信支付公钥初始化微信支付服务 +func newWechatPayServiceWithWxPayPubKey(c config.Config, logger *zap.Logger) *WechatPayService { + // 从配置中加载商户信息 + mchID := c.Wxpay.MchID + mchCertificateSerialNumber := c.Wxpay.MchCertificateSerialNumber + mchAPIv3Key := c.Wxpay.MchApiv3Key + mchPublicKeyID := c.Wxpay.MchPublicKeyID + + // 解析证书路径 + privateKeyPath, err := resolveCertPath(c.Wxpay.MchPrivateKeyPath, logger) + if err != nil { + logger.Error("解析商户私钥路径失败", zap.Error(err), zap.String("path", c.Wxpay.MchPrivateKeyPath)) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + publicKeyPath, err := resolveCertPath(c.Wxpay.MchPublicKeyPath, logger) + if err != nil { + logger.Error("解析微信支付平台证书路径失败", zap.Error(err), zap.String("path", c.Wxpay.MchPublicKeyPath)) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 从文件中加载商户私钥 + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(privateKeyPath) + if err != nil { + logger.Error("加载商户私钥失败", zap.Error(err), zap.String("path", privateKeyPath)) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 从文件中加载微信支付平台证书 + mchPublicKey, err := utils.LoadPublicKeyWithPath(publicKeyPath) + if err != nil { + logger.Error("加载微信支付平台证书失败", zap.Error(err), zap.String("path", publicKeyPath)) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 使用商户私钥和其他参数初始化微信支付客户端 + opts := []core.ClientOption{ + option.WithWechatPayPublicKeyAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchPublicKeyID, mchPublicKey), + } + client, err := core.NewClient(context.Background(), opts...) + if err != nil { + logger.Error("创建微信支付客户端失败", zap.Error(err)) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 初始化 notify.Handler + certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID) + notifyHandler := notify.NewNotifyHandler( + mchAPIv3Key, + verifiers.NewSHA256WithRSACombinedVerifier(certificateVisitor, mchPublicKeyID, *mchPublicKey)) + + logger.Info("微信支付客户端初始化成功(微信支付公钥方式)") + return &WechatPayService{ + config: c, + wechatClient: client, + notifyHandler: notifyHandler, + logger: logger, + } +} + +// CreateWechatNativeOrder 创建微信Native(扫码)支付订单 +func (w *WechatPayService) CreateWechatNativeOrder(ctx context.Context, amount float64, description string, outTradeNo string) (interface{}, error) { + totalAmount := ToWechatAmount(amount) + + req := native.PrepayRequest{ + Appid: core.String(w.config.Wxpay.AppID), + Mchid: core.String(w.config.Wxpay.MchID), + Description: core.String(description), + OutTradeNo: core.String(outTradeNo), + NotifyUrl: core.String(w.config.Wxpay.NotifyUrl), + Amount: &native.Amount{ + Total: core.Int64(totalAmount), + }, + } + + svc := native.NativeApiService{Client: w.wechatClient} + resp, result, err := svc.Prepay(ctx, req) + if err != nil { + statusCode := 0 + if result != nil && result.Response != nil { + statusCode = result.Response.StatusCode + } + return "", fmt.Errorf("微信扫码下单失败: %v, 状态码: %d", err, statusCode) + } + + if resp.CodeUrl == nil || *resp.CodeUrl == "" { + return "", fmt.Errorf("微信扫码下单成功但未返回code_url") + } + + // 返回二维码链接,由前端生成二维码 + return map[string]string{"code_url": *resp.CodeUrl}, nil + +} + +// CreateWechatOrder 创建微信支付订单(仅 Native 扫码) +func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64, description string, outTradeNo string) (interface{}, error) { + return w.CreateWechatNativeOrder(ctx, amount, description, outTradeNo) +} + +// HandleWechatPayNotification 处理微信支付回调 +func (w *WechatPayService) HandleWechatPayNotification(ctx context.Context, req *http.Request) (*payments.Transaction, error) { + transaction := new(payments.Transaction) + _, err := w.notifyHandler.ParseNotifyRequest(ctx, req, transaction) + if err != nil { + return nil, fmt.Errorf("微信支付通知处理失败: %v", err) + } + // 返回交易信息 + return transaction, nil +} + +// HandleRefundNotification 处理微信退款回调 +func (w *WechatPayService) HandleRefundNotification(ctx context.Context, req *http.Request) (*refunddomestic.Refund, error) { + refund := new(refunddomestic.Refund) + _, err := w.notifyHandler.ParseNotifyRequest(ctx, req, refund) + if err != nil { + return nil, fmt.Errorf("微信退款回调通知处理失败: %v", err) + } + return refund, nil +} + +// QueryOrderStatus 主动查询订单状态(根据商户订单号) +func (w *WechatPayService) QueryOrderStatus(ctx context.Context, outTradeNo string) (*payments.Transaction, error) { + svc := native.NativeApiService{Client: w.wechatClient} + + // 调用 QueryOrderByOutTradeNo 方法查询订单状态 + resp, result, err := svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{ + OutTradeNo: core.String(outTradeNo), + Mchid: core.String(w.config.Wxpay.MchID), + }) + if err != nil { + return nil, fmt.Errorf("订单查询失败: %v, 状态码: %d", err, result.Response.StatusCode) + } + return resp, nil +} + +// WeChatRefund 申请微信退款 +func (w *WechatPayService) WeChatRefund(ctx context.Context, outTradeNo string, refundAmount float64, totalAmount float64) error { + // 生成唯一的退款单号 + outRefundNo := fmt.Sprintf("%s-refund", outTradeNo) + + // 初始化退款服务 + svc := refunddomestic.RefundsApiService{Client: w.wechatClient} + + // 创建退款请求 + resp, result, err := svc.Create(ctx, refunddomestic.CreateRequest{ + OutTradeNo: core.String(outTradeNo), + OutRefundNo: core.String(outRefundNo), + NotifyUrl: core.String(w.config.Wxpay.RefundNotifyUrl), + Amount: &refunddomestic.AmountReq{ + Currency: core.String("CNY"), + Refund: core.Int64(ToWechatAmount(refundAmount)), + Total: core.Int64(ToWechatAmount(totalAmount)), + }, + }) + if err != nil { + return fmt.Errorf("微信订单申请退款错误: %v", err) + } + // 打印退款结果 + w.logger.Info("退款申请成功", + zap.Int("status_code", result.Response.StatusCode), + zap.String("out_refund_no", *resp.OutRefundNo), + zap.String("refund_id", *resp.RefundId)) + return nil +} + +// GenerateOutTradeNo 生成唯一订单号 +func (w *WechatPayService) GenerateOutTradeNo() string { + length := 16 + timestamp := time.Now().UnixNano() + timeStr := strconv.FormatInt(timestamp, 10) + randomPart := strconv.Itoa(int(timestamp % 1e6)) + combined := timeStr + randomPart + + if len(combined) >= length { + return combined[:length] + } + + for len(combined) < length { + combined += strconv.Itoa(int(timestamp % 10)) + } + + return combined +} diff --git a/resources/etc/wxetc_cert/apiclient_cert.p12 b/resources/etc/wxetc_cert/apiclient_cert.p12 new file mode 100644 index 0000000000000000000000000000000000000000..81e5d95187e7428f0af76c3be234d439f8353049 GIT binary patch literal 2766 zcmY+EX*3j!8pmhG7>qIYU5pa1wXua5%2=|7ERie`vXo`Ul6?@#lF(4HWv!4b5wd09 z#yUobv1ClPvSh#BbMC$Gy&wMPInVF^oaf^YiG%infHX)PG!4Qa@%Z}VJtiO>Fc$}n z0pp;NXE+pz1E2m=L2_~6%`@~91O%LorGFYAQUC(|_Xijdf`o(V{T_3eP9=LVfmxk1+Cu!d1>x-n4*PQRe?Vybo&$(Io^*H zITKcqV#FfuxVCoFqu-q^aQ>F7BL3!KMwz&3BP$d4up)g$>s#`~rr@_S)^LRQBkJqA zP1UU^^}mK_2&rE-2zniRwTlDA4rzB#*%BVA&oP=TZ1~nT7mF{34oh`U%anx=$%)QI zxCu=WHr_3s4Sc&JAPswQH`Cn%HIA7#Ma%doghlpJH1L(2J04k<^d0lQB>DFyEcG^h zsNbg}85Mm18ENO?BDmcXXkE^wfJVPU*&|C@JJZEf;hR7E7NaUQ^A|A1 zEe(VH<#TcI>RXQtQ1c~+#I21|SNqoJvV3NhA+l%cL%VBZKM=8Ek1!_jmX0G$4liK0 z&5Xz5-&AX|_FU0=z?_g$;2Z1*)o%TnEze079~7IIy1mSJ<3k-LuUYaH$3SB#Yjl;I zCv&sVfm~(ec8*M{r@$%wRKqtJRMzIINvww2*82Oz=I_dx7BNnTQ|I1>qg|WDoa`+*(%VU;8U{UYkmS< zTwvx2dT2;`u<=6Q$jM5+0@sm~c~xHPM;sh4rDRG8%+nCWU-9dJOVvb7VoEnJ?Gp!E zUffNx7HfI+;O6YM9u(O>(%kzLsjfA(yP`gfdU(EhJLKyqDz^MjgX)KbTw_}P>3x-u zXIe`A)eU9N8UglbSkm*)I5Ri>l%u||J-2I58xN<8qWn!1KaW#Z#l^`fXG zxw{L@LY(|izhV5kEW##B(A5WWPRBW0Tn^;ab)x<@TL%=7IfO=k{PA(Gv-d5FlGpW$ z-SETjAEUUgB`wr3&*Is>_omtYmHlhEI^|BlK!aFVLbbN@IZ=VNey5__R;E+A&`hg> zV;U^lpl@C`WShA&67a#P;RX9@5L*eK9|y$$+XUyYNjb#@CmT=dw#(>%E@gn(e=RN3 zi!;!Cjxc-8TR!5}QZNdMqxJtE(dFW3UBNh7r!#DSmfH}<|7Z^i1fBVD{Y;Fj|7Sy* zzik*gb8xh16lL?b4Z%2YcW>;UoCG#uhlEyPOz^eg>vbNUS_(v9~+vgG{4W*eq(?|@Ga3%9@IjE_6R5OY^9_?jCh zJ`kg{CMwRi7j2Z9?VnmoS%Z(Zi7aNcst!~)fyRORW}vA+-Uy=+w?c89b7*;PfARr; zNM|?Yeo@^WhCSZ7sz(fu3|^9b)+w@4H5FJ@SPq6||IS%QN}FNJ+TFAzw@MEk z1VR4Ni_YUr;(hnh9x%jhisSp!=iK{fNPGVL0i|&8>il&& zp(C@bP@b%mnR-0dUHY3|DPOTP=H0ypOI2?iJLOtDpDcLm%eLu4Xadkiwsq`te;KHK z*X1)iEU4TEI?7t@)@Vqg724#5>(Nal_u-_IdY%lDZ5=zWt^`*}X=uDQWc~9=6O@vq1B*0RWWv0*{X;oiSJlyyr@OlU%1J9>q1BT7Q zpu_tD<(qW=tJR@Z?O{vZhJV-*=KW+-wCax6dYePy2AAR2KACs2tL2lTtGxn@>K9YJ zA4aM=?iIhBe@hRi}Cvb?GycMTS9PS92an%ELCF zM^Ro_HB_mCQ|P4g2~Tr%dp9GWzJw zQRj)d=+Z#W^I5NRvNjR@Z`qa)u+y;+1?Pc+IM=A!W=y&5lXs{j4O2b+Ay$rWxj$)U zSI&npuOy_yQ6yORQPNzqTz^}9!4!J}?|>mqJ4lwMSX#plUqWg7mf7AuDik4ql|KxQ zADtkMw=y2C