Compare commits

...

2 Commits

Author SHA1 Message Date
18f3d10518 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-12-12 15:27:17 +08:00
0d4953c6d3 微信支付 2025-12-12 15:27:15 +08:00
34 changed files with 1974 additions and 279 deletions

View File

@@ -362,6 +362,28 @@ alipay:
notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback" notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback"
return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return" 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"
# =========================================== # ===========================================
# 🔍 天眼查配置 # 🔍 天眼查配置
# =========================================== # ===========================================

View File

@@ -81,6 +81,27 @@ alipay:
return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return" 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: wallet:
@@ -114,34 +135,42 @@ development:
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS" cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id" cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
# ===========================================
# 🚦 开发环境全局限流(放宽或近似关闭)
# ===========================================
ratelimit:
requests: 1000000 # 每窗口允许的请求数,足够大,相当于关闭
window: 1s # 时间窗口
burst: 1000000 # 令牌桶突发容量
# =========================================== # ===========================================
# 🚀 开发环境频率限制配置(放宽限制) # 🚀 开发环境频率限制配置(放宽限制)
# =========================================== # ===========================================
daily_ratelimit: daily_ratelimit:
max_requests_per_day: 1000000 # 开发环境每日最大请求次数 max_requests_per_day: 1000000 # 开发环境每日最大请求次数
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数 max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
max_concurrent: 50 # 开发环境最大并发请求数 max_concurrent: 50 # 开发环境最大并发请求数
# 排除频率限制的路径 # 排除频率限制的路径
exclude_paths: exclude_paths:
- "/health" # 健康检查接口 - "/health" # 健康检查接口
- "/metrics" # 监控指标接口 - "/metrics" # 监控指标接口
# 排除频率限制的域名 # 排除频率限制的域名
exclude_domains: exclude_domains:
- "api.*" # API二级域名不受频率限制 - "api.*" # API二级域名不受频率限制
- "*.api.*" # 支持多级API域名 - "*.api.*" # 支持多级API域名
# 开发环境安全配置(放宽限制) # 开发环境安全配置(放宽限制)
enable_ip_whitelist: true # 启用IP白名单 enable_ip_whitelist: true # 启用IP白名单
ip_whitelist: # 开发环境IP白名单 ip_whitelist: # 开发环境IP白名单
- "127.0.0.1" # 本地回环 - "127.0.0.1" # 本地回环
- "localhost" # 本地主机 - "localhost" # 本地主机
- "192.168.*" # 内网IP段 - "192.168.*" # 内网IP段
- "10.*" # 内网IP段 - "10.*" # 内网IP段
- "172.16.*" # 内网IP段 - "172.16.*" # 内网IP段
enable_ip_blacklist: false # 开发环境禁用IP黑名单 enable_ip_blacklist: false # 开发环境禁用IP黑名单
enable_user_agent: false # 开发环境禁用User-Agent检查 enable_user_agent: false # 开发环境禁用User-Agent检查
enable_referer: false # 开发环境禁用Referer检查 enable_referer: false # 开发环境禁用Referer检查
enable_proxy_check: false # 开发环境禁用代理检查 enable_proxy_check: false # 开发环境禁用代理检查

View File

@@ -5,7 +5,6 @@ type CreateWalletCommand struct {
UserID string `json:"user_id" binding:"required,uuid"` UserID string `json:"user_id" binding:"required,uuid"`
} }
// TransferRechargeCommand 对公转账充值命令 // TransferRechargeCommand 对公转账充值命令
type TransferRechargeCommand struct { type TransferRechargeCommand struct {
UserID string `json:"user_id" binding:"required,uuid"` UserID string `json:"user_id" binding:"required,uuid"`
@@ -16,16 +15,24 @@ type TransferRechargeCommand struct {
// GiftRechargeCommand 赠送充值命令 // GiftRechargeCommand 赠送充值命令
type GiftRechargeCommand struct { type GiftRechargeCommand struct {
UserID string `json:"user_id" binding:"required,uuid"` UserID string `json:"user_id" binding:"required,uuid"`
Amount string `json:"amount" binding:"required"` Amount string `json:"amount" binding:"required"`
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"` Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
} }
// CreateAlipayRechargeCommand 创建支付宝充值订单命令 // CreateAlipayRechargeCommand 创建支付宝充值订单命令
type CreateAlipayRechargeCommand struct { type CreateAlipayRechargeCommand struct {
UserID string `json:"-"` // 用户ID从token获取 UserID string `json:"-"` // 用户ID从token获取
Amount string `json:"amount" binding:"required"` // 充值金额 Amount string `json:"amount" binding:"required"` // 充值金额
Subject string `json:"-"` // 订单标题 Subject string `json:"-"` // 订单标题
Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台app/h5/pc 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
}

View File

@@ -8,15 +8,15 @@ import (
// WalletResponse 钱包响应 // WalletResponse 钱包响应
type WalletResponse struct { type WalletResponse struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
Balance decimal.Decimal `json:"balance"` Balance decimal.Decimal `json:"balance"`
BalanceStatus string `json:"balance_status"` // normal, low, arrears BalanceStatus string `json:"balance_status"` // normal, low, arrears
IsArrears bool `json:"is_arrears"` // 是否欠费 IsArrears bool `json:"is_arrears"` // 是否欠费
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低 IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
// TransactionResponse 交易响应 // TransactionResponse 交易响应
@@ -49,34 +49,36 @@ type WalletStatsResponse struct {
// RechargeRecordResponse 充值记录响应 // RechargeRecordResponse 充值记录响应
type RechargeRecordResponse struct { type RechargeRecordResponse struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
Amount decimal.Decimal `json:"amount"` Amount decimal.Decimal `json:"amount"`
RechargeType string `json:"recharge_type"` RechargeType string `json:"recharge_type"`
Status string `json:"status"` Status string `json:"status"`
AlipayOrderID string `json:"alipay_order_id,omitempty"` AlipayOrderID string `json:"alipay_order_id,omitempty"`
TransferOrderID string `json:"transfer_order_id,omitempty"` WechatOrderID string `json:"wechat_order_id,omitempty"`
Notes string `json:"notes,omitempty"` TransferOrderID string `json:"transfer_order_id,omitempty"`
OperatorID string `json:"operator_id,omitempty"` Platform string `json:"platform,omitempty"` // 支付平台pc/wx_native等
CompanyName string `json:"company_name,omitempty"` Notes string `json:"notes,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"` OperatorID string `json:"operator_id,omitempty"`
CreatedAt time.Time `json:"created_at"` CompanyName string `json:"company_name,omitempty"`
UpdatedAt time.Time `json:"updated_at"` User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
// WalletTransactionResponse 钱包交易记录响应 // WalletTransactionResponse 钱包交易记录响应
type WalletTransactionResponse struct { type WalletTransactionResponse struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
ApiCallID string `json:"api_call_id"` ApiCallID string `json:"api_call_id"`
TransactionID string `json:"transaction_id"` TransactionID string `json:"transaction_id"`
ProductID string `json:"product_id"` ProductID string `json:"product_id"`
ProductName string `json:"product_name"` ProductName string `json:"product_name"`
Amount decimal.Decimal `json:"amount"` Amount decimal.Decimal `json:"amount"`
CompanyName string `json:"company_name,omitempty"` CompanyName string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"` User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
// WalletTransactionListResponse 钱包交易记录列表响应 // WalletTransactionListResponse 钱包交易记录列表响应
@@ -97,17 +99,17 @@ type RechargeRecordListResponse struct {
// AlipayRechargeOrderResponse 支付宝充值订单响应 // AlipayRechargeOrderResponse 支付宝充值订单响应
type AlipayRechargeOrderResponse struct { type AlipayRechargeOrderResponse struct {
PayURL string `json:"pay_url"` // 支付链接 PayURL string `json:"pay_url"` // 支付链接
OutTradeNo string `json:"out_trade_no"` // 商户订单号 OutTradeNo string `json:"out_trade_no"` // 商户订单号
Amount decimal.Decimal `json:"amount"` // 充值金额 Amount decimal.Decimal `json:"amount"` // 充值金额
Platform string `json:"platform"` // 支付平台 Platform string `json:"platform"` // 支付平台
Subject string `json:"subject"` // 订单标题 Subject string `json:"subject"` // 订单标题
} }
// RechargeConfigResponse 充值配置响应 // RechargeConfigResponse 充值配置响应
type RechargeConfigResponse struct { type RechargeConfigResponse struct {
MinAmount string `json:"min_amount"` // 最低充值金额 MinAmount string `json:"min_amount"` // 最低充值金额
MaxAmount string `json:"max_amount"` // 最高充值金额 MaxAmount string `json:"max_amount"` // 最高充值金额
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"` AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
} }

View File

@@ -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"` // 是否可以重试
}

View File

@@ -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参数
}

View File

@@ -17,13 +17,14 @@ type FinanceApplicationService interface {
// 充值管理 // 充值管理
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error) 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) TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*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) 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) 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) ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
ExportAdminRechargeRecords(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) HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, 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) 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) GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
// 获取充值配置 // 获取充值配置
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error) GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"time"
"tyapi-server/internal/application/finance/dto/commands" "tyapi-server/internal/application/finance/dto/commands"
"tyapi-server/internal/application/finance/dto/queries" "tyapi-server/internal/application/finance/dto/queries"
"tyapi-server/internal/application/finance/dto/responses" "tyapi-server/internal/application/finance/dto/responses"
@@ -19,16 +20,20 @@ import (
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/smartwalle/alipay/v3" "github.com/smartwalle/alipay/v3"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
"go.uber.org/zap" "go.uber.org/zap"
) )
// FinanceApplicationServiceImpl 财务应用服务实现 // FinanceApplicationServiceImpl 财务应用服务实现
type FinanceApplicationServiceImpl struct { type FinanceApplicationServiceImpl struct {
aliPayClient *payment.AliPayService aliPayClient *payment.AliPayService
wechatPayService *payment.WechatPayService
walletService finance_services.WalletAggregateService walletService finance_services.WalletAggregateService
rechargeRecordService finance_services.RechargeRecordService rechargeRecordService finance_services.RechargeRecordService
walletTransactionRepository finance_repositories.WalletTransactionRepository walletTransactionRepository finance_repositories.WalletTransactionRepository
alipayOrderRepo finance_repositories.AlipayOrderRepository alipayOrderRepo finance_repositories.AlipayOrderRepository
wechatOrderRepo finance_repositories.WechatOrderRepository
rechargeRecordRepo finance_repositories.RechargeRecordRepository
userRepo user_repositories.UserRepository userRepo user_repositories.UserRepository
txManager *database.TransactionManager txManager *database.TransactionManager
exportManager *export.ExportManager exportManager *export.ExportManager
@@ -39,10 +44,13 @@ type FinanceApplicationServiceImpl struct {
// NewFinanceApplicationService 创建财务应用服务 // NewFinanceApplicationService 创建财务应用服务
func NewFinanceApplicationService( func NewFinanceApplicationService(
aliPayClient *payment.AliPayService, aliPayClient *payment.AliPayService,
wechatPayService *payment.WechatPayService,
walletService finance_services.WalletAggregateService, walletService finance_services.WalletAggregateService,
rechargeRecordService finance_services.RechargeRecordService, rechargeRecordService finance_services.RechargeRecordService,
walletTransactionRepository finance_repositories.WalletTransactionRepository, walletTransactionRepository finance_repositories.WalletTransactionRepository,
alipayOrderRepo finance_repositories.AlipayOrderRepository, alipayOrderRepo finance_repositories.AlipayOrderRepository,
wechatOrderRepo finance_repositories.WechatOrderRepository,
rechargeRecordRepo finance_repositories.RechargeRecordRepository,
userRepo user_repositories.UserRepository, userRepo user_repositories.UserRepository,
txManager *database.TransactionManager, txManager *database.TransactionManager,
logger *zap.Logger, logger *zap.Logger,
@@ -51,10 +59,13 @@ func NewFinanceApplicationService(
) FinanceApplicationService { ) FinanceApplicationService {
return &FinanceApplicationServiceImpl{ return &FinanceApplicationServiceImpl{
aliPayClient: aliPayClient, aliPayClient: aliPayClient,
wechatPayService: wechatPayService,
walletService: walletService, walletService: walletService,
rechargeRecordService: rechargeRecordService, rechargeRecordService: rechargeRecordService,
walletTransactionRepository: walletTransactionRepository, walletTransactionRepository: walletTransactionRepository,
alipayOrderRepo: alipayOrderRepo, alipayOrderRepo: alipayOrderRepo,
wechatOrderRepo: wechatOrderRepo,
rechargeRecordRepo: rechargeRecordRepo,
userRepo: userRepo, userRepo: userRepo,
txManager: txManager, txManager: txManager,
exportManager: exportManager, exportManager: exportManager,
@@ -100,8 +111,9 @@ func (s *FinanceApplicationServiceImpl) GetWallet(ctx context.Context, query *qu
BalanceStatus: wallet.GetBalanceStatus(), BalanceStatus: wallet.GetBalanceStatus(),
IsArrears: wallet.IsArrears(), IsArrears: wallet.IsArrears(),
IsLowBalance: wallet.IsLowBalance(), IsLowBalance: wallet.IsLowBalance(),
CreatedAt: wallet.CreatedAt,
UpdatedAt: wallet.UpdatedAt, CreatedAt: wallet.CreatedAt,
UpdatedAt: wallet.UpdatedAt,
}, nil }, nil
} }
@@ -188,6 +200,168 @@ func (s *FinanceApplicationServiceImpl) CreateAlipayRechargeOrder(ctx context.Co
}, nil }, 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 对公转账充值 // TransferRecharge 对公转账充值
func (s *FinanceApplicationServiceImpl) TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error) { func (s *FinanceApplicationServiceImpl) TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error) {
// 将字符串金额转换为 decimal.Decimal // 将字符串金额转换为 decimal.Decimal
@@ -507,8 +681,8 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
} }
// 准备导出数据 // 准备导出数据
headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "转账订单号", "备注", "充值时间"} headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "微信订单号", "转账订单号", "备注", "充值时间"}
columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20} columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20, 20}
data := make([][]interface{}, len(allRecords)) data := make([][]interface{}, len(allRecords))
for i, record := range allRecords { for i, record := range allRecords {
@@ -523,6 +697,10 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
if record.AlipayOrderID != nil && *record.AlipayOrderID != "" { if record.AlipayOrderID != nil && *record.AlipayOrderID != "" {
alipayOrderID = *record.AlipayOrderID alipayOrderID = *record.AlipayOrderID
} }
wechatOrderID := ""
if record.WechatOrderID != nil && *record.WechatOrderID != "" {
wechatOrderID = *record.WechatOrderID
}
transferOrderID := "" transferOrderID := ""
if record.TransferOrderID != nil && *record.TransferOrderID != "" { if record.TransferOrderID != nil && *record.TransferOrderID != "" {
transferOrderID = *record.TransferOrderID transferOrderID = *record.TransferOrderID
@@ -543,6 +721,7 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
translateRechargeType(record.RechargeType), translateRechargeType(record.RechargeType),
translateRechargeStatus(record.Status), translateRechargeStatus(record.Status),
alipayOrderID, alipayOrderID,
wechatOrderID,
transferOrderID, transferOrderID,
notes, notes,
createdAt, createdAt,
@@ -566,6 +745,8 @@ func translateRechargeType(rechargeType finance_entities.RechargeType) string {
switch rechargeType { switch rechargeType {
case finance_entities.RechargeTypeAlipay: case finance_entities.RechargeTypeAlipay:
return "支付宝充值" return "支付宝充值"
case finance_entities.RechargeTypeWechat:
return "微信充值"
case finance_entities.RechargeTypeTransfer: case finance_entities.RechargeTypeTransfer:
return "对公转账" return "对公转账"
case finance_entities.RechargeTypeGift: case finance_entities.RechargeTypeGift:
@@ -890,15 +1071,27 @@ func (s *FinanceApplicationServiceImpl) GetAlipayOrderStatus(ctx context.Context
// GetUserRechargeRecords 获取用户充值记录 // GetUserRechargeRecords 获取用户充值记录
func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) { func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) {
// 查询用户充值记录 // 确保 filters 不为 nil
records, err := s.rechargeRecordService.GetByUserID(ctx, userID) 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 { if err != nil {
s.logger.Error("查询用户充值记录失败", zap.Error(err), zap.String("userID", userID)) s.logger.Error("查询用户充值记录失败", zap.Error(err), zap.String("userID", userID))
return nil, err 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 // 转换为响应DTO
var items []responses.RechargeRecordResponse var items []responses.RechargeRecordResponse
@@ -914,9 +1107,20 @@ func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Conte
UpdatedAt: record.UpdatedAt, UpdatedAt: record.UpdatedAt,
} }
// 根据充值类型设置相应的订单号 // 根据充值类型设置相应的订单号和平台信息
if record.AlipayOrderID != nil { if record.AlipayOrderID != nil {
item.AlipayOrderID = *record.AlipayOrderID 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 { if record.TransferOrderID != nil {
item.TransferOrderID = *record.TransferOrderID item.TransferOrderID = *record.TransferOrderID
@@ -963,9 +1167,20 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont
UpdatedAt: record.UpdatedAt, UpdatedAt: record.UpdatedAt,
} }
// 根据充值类型设置相应的订单号 // 根据充值类型设置相应的订单号和平台信息
if record.AlipayOrderID != nil { if record.AlipayOrderID != nil {
item.AlipayOrderID = *record.AlipayOrderID 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 { if record.TransferOrderID != nil {
item.TransferOrderID = *record.TransferOrderID item.TransferOrderID = *record.TransferOrderID
@@ -1012,3 +1227,445 @@ func (s *FinanceApplicationServiceImpl) GetRechargeConfig(ctx context.Context) (
AlipayRechargeBonus: bonus, AlipayRechargeBonus: bonus,
}, nil }, 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
}

View File

@@ -393,7 +393,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
return nil, err return nil, err
} }
// 3. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额 // 3. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID) realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -408,7 +408,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
// 5. 构建响应DTO // 5. 构建响应DTO
return &dto.AvailableAmountResponse{ return &dto.AvailableAmountResponse{
AvailableAmount: availableAmount, AvailableAmount: availableAmount,
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+对公转账) TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+微信充值+对公转账)
TotalGifted: totalGifted, TotalGifted: totalGifted,
TotalInvoiced: totalInvoiced, TotalInvoiced: totalInvoiced,
PendingApplications: pendingAmount, PendingApplications: pendingAmount,
@@ -417,7 +417,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
// calculateAvailableAmount 计算可开票金额(私有方法) // calculateAvailableAmount 计算可开票金额(私有方法)
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) { func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
// 1. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额 // 1. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID) realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
if err != nil { if err != nil {
return decimal.Zero, err return decimal.Zero, err
@@ -433,7 +433,7 @@ func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Con
fmt.Println("totalInvoiced", totalInvoiced) fmt.Println("totalInvoiced", totalInvoiced)
fmt.Println("pendingAmount", pendingAmount) fmt.Println("pendingAmount", pendingAmount)
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请 // 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
// 可开票金额 = 真实充值金额(支付宝充值+对公转账) - 已开票金额 - 待处理申请金额 // 可开票金额 = 真实充值金额(支付宝充值+微信充值+对公转账) - 已开票金额 - 待处理申请金额
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount) availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
fmt.Println("availableAmount", availableAmount) 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) return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
} }
// 2. 计算真实充值金额(支付宝充值 + 对公转账)和总赠送金额 // 2. 计算真实充值金额(支付宝充值 + 微信充值 + 对公转账)和总赠送金额
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 对公转账 var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 微信充值 + 对公转账
var totalGifted decimal.Decimal // 总赠送金额 var totalGifted decimal.Decimal // 总赠送金额
for _, record := range rechargeRecords { for _, record := range rechargeRecords {
if record.IsSuccess() { if record.IsSuccess() {
if record.RechargeType == entities.RechargeTypeGift { if record.RechargeType == entities.RechargeTypeGift {
// 赠送金额不计入可开票金额 // 赠送金额不计入可开票金额
totalGifted = totalGifted.Add(record.Amount) 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) realRecharged = realRecharged.Add(record.Amount)
} }
} }

View File

@@ -31,6 +31,9 @@ type Config struct {
Zhicha ZhichaConfig `mapstructure:"zhicha"` Zhicha ZhichaConfig `mapstructure:"zhicha"`
Muzi MuziConfig `mapstructure:"muzi"` Muzi MuziConfig `mapstructure:"muzi"`
AliPay AliPayConfig `mapstructure:"alipay"` AliPay AliPayConfig `mapstructure:"alipay"`
Wxpay WxpayConfig `mapstructure:"wxpay"`
WechatMini WechatMiniConfig `mapstructure:"wechat_mini"`
WechatH5 WechatH5Config `mapstructure:"wechat_h5"`
Yushan YushanConfig `mapstructure:"yushan"` Yushan YushanConfig `mapstructure:"yushan"`
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"` TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
Alicloud AlicloudConfig `mapstructure:"alicloud"` Alicloud AlicloudConfig `mapstructure:"alicloud"`
@@ -429,6 +432,29 @@ type AliPayConfig struct {
ReturnURL string `mapstructure:"return_url"` 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 羽山配置 // YushanConfig 羽山配置
type YushanConfig struct { type YushanConfig struct {
URL string `mapstructure:"url"` URL string `mapstructure:"url"`

View File

@@ -307,6 +307,16 @@ func NewContainer() *Container {
} }
return payment.NewAliPayService(config) 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 { func(logger *zap.Logger) *export.ExportManager {
return export.NewExportManager(logger) return export.NewExportManager(logger)
@@ -512,6 +522,11 @@ func NewContainer() *Container {
finance_repo.NewGormAlipayOrderRepository, finance_repo.NewGormAlipayOrderRepository,
fx.As(new(domain_finance_repo.AlipayOrderRepository)), fx.As(new(domain_finance_repo.AlipayOrderRepository)),
), ),
// 微信订单仓储
fx.Annotate(
finance_repo.NewGormWechatOrderRepository,
fx.As(new(domain_finance_repo.WechatOrderRepository)),
),
// 发票申请仓储 // 发票申请仓储
fx.Annotate( fx.Annotate(
finance_repo.NewGormInvoiceApplicationRepository, finance_repo.NewGormInvoiceApplicationRepository,
@@ -855,10 +870,13 @@ func NewContainer() *Container {
fx.Annotate( fx.Annotate(
func( func(
aliPayClient *payment.AliPayService, aliPayClient *payment.AliPayService,
wechatPayService *payment.WechatPayService,
walletService finance_services.WalletAggregateService, walletService finance_services.WalletAggregateService,
rechargeRecordService finance_services.RechargeRecordService, rechargeRecordService finance_services.RechargeRecordService,
walletTransactionRepo domain_finance_repo.WalletTransactionRepository, walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
alipayOrderRepo domain_finance_repo.AlipayOrderRepository, alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
userRepo domain_user_repo.UserRepository, userRepo domain_user_repo.UserRepository,
txManager *shared_database.TransactionManager, txManager *shared_database.TransactionManager,
logger *zap.Logger, logger *zap.Logger,
@@ -867,10 +885,13 @@ func NewContainer() *Container {
) finance.FinanceApplicationService { ) finance.FinanceApplicationService {
return finance.NewFinanceApplicationService( return finance.NewFinanceApplicationService(
aliPayClient, aliPayClient,
wechatPayService,
walletService, walletService,
rechargeRecordService, rechargeRecordService,
walletTransactionRepo, walletTransactionRepo,
alipayOrderRepo, alipayOrderRepo,
wechatOrderRepo,
rechargeRecordRepo,
userRepo, userRepo,
txManager, txManager,
logger, logger,

View File

@@ -1,140 +1,28 @@
package entities package entities
import ( import "github.com/shopspring/decimal"
"time"
"github.com/google/uuid" // AlipayOrderStatus 支付宝订单状态枚举(别名)
"github.com/shopspring/decimal" type AlipayOrderStatus = PayOrderStatus
"gorm.io/gorm"
)
// AlipayOrderStatus 支付宝订单状态枚举
type AlipayOrderStatus string
const ( const (
AlipayOrderStatusPending AlipayOrderStatus = "pending" // 待支付 AlipayOrderStatusPending AlipayOrderStatus = PayOrderStatusPending // 待支付
AlipayOrderStatusSuccess AlipayOrderStatus = "success" // 支付成功 AlipayOrderStatusSuccess AlipayOrderStatus = PayOrderStatusSuccess // 支付成功
AlipayOrderStatusFailed AlipayOrderStatus = "failed" // 支付失败 AlipayOrderStatusFailed AlipayOrderStatus = PayOrderStatusFailed // 支付失败
AlipayOrderStatusCancelled AlipayOrderStatus = "cancelled" // 已取消 AlipayOrderStatusCancelled AlipayOrderStatus = PayOrderStatusCancelled // 已取消
AlipayOrderStatusClosed AlipayOrderStatus = "closed" // 已关闭 AlipayOrderStatusClosed AlipayOrderStatus = PayOrderStatusClosed // 已关闭
) )
const ( const (
AlipayOrderPlatformApp = "app" // 支付宝APP支付 AlipayOrderPlatformApp = "app" // 支付宝APP支付
AlipayOrderPlatformH5 = "h5" // 支付宝H5支付 AlipayOrderPlatformH5 = "h5" // 支付宝H5支付
AlipayOrderPlatformPC = "pc" // 支付宝PC支付 AlipayOrderPlatformPC = "pc" // 支付宝PC支付
) )
// AlipayOrder 支付宝订单详情实体 // AlipayOrder 支付宝订单实体(统一表 typay_orders兼容多支付渠道
type AlipayOrder struct { type AlipayOrder = PayOrder
// 基础标识
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
}
// NewAlipayOrder 工厂方法 - 创建支付宝订单 // NewAlipayOrder 工厂方法 - 创建支付宝订单
func NewAlipayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *AlipayOrder { func NewAlipayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *AlipayOrder {
return &AlipayOrder{ return NewPayOrder(rechargeID, outTradeNo, subject, amount, platform, "alipay")
ID: uuid.New().String(),
RechargeID: rechargeID,
OutTradeNo: outTradeNo,
Subject: subject,
Amount: amount,
Platform: platform,
Status: AlipayOrderStatusPending,
}
} }

View File

@@ -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,
}
}

View File

@@ -14,6 +14,7 @@ type RechargeType string
const ( const (
RechargeTypeAlipay RechargeType = "alipay" // 支付宝充值 RechargeTypeAlipay RechargeType = "alipay" // 支付宝充值
RechargeTypeWechat RechargeType = "wechat" // 微信充值
RechargeTypeTransfer RechargeType = "transfer" // 对公转账 RechargeTypeTransfer RechargeType = "transfer" // 对公转账
RechargeTypeGift RechargeType = "gift" // 赠送 RechargeTypeGift RechargeType = "gift" // 赠送
) )
@@ -42,6 +43,7 @@ type RechargeRecord struct {
// 订单号字段(根据充值类型使用不同字段) // 订单号字段(根据充值类型使用不同字段)
AlipayOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"alipay_order_id,omitempty" comment:"支付宝订单号"` 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:"转账订单号"` TransferOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"transfer_order_id,omitempty" comment:"转账订单号"`
// 通用字段 // 通用字段
@@ -104,14 +106,24 @@ func (r *RechargeRecord) MarkCancelled() {
// ValidatePaymentMethod 验证支付方式:支付宝订单号和转账订单号只能有一个存在 // ValidatePaymentMethod 验证支付方式:支付宝订单号和转账订单号只能有一个存在
func (r *RechargeRecord) ValidatePaymentMethod() error { func (r *RechargeRecord) ValidatePaymentMethod() error {
hasAlipay := r.AlipayOrderID != nil && *r.AlipayOrderID != "" hasAlipay := r.AlipayOrderID != nil && *r.AlipayOrderID != ""
hasWechat := r.WechatOrderID != nil && *r.WechatOrderID != ""
hasTransfer := r.TransferOrderID != nil && *r.TransferOrderID != "" hasTransfer := r.TransferOrderID != nil && *r.TransferOrderID != ""
if hasAlipay && hasTransfer { count := 0
return errors.New("支付宝订单号和转账订单号不能同时存在") if hasAlipay {
count++
} }
if hasWechat {
if !hasAlipay && !hasTransfer { count++
return errors.New("必须提供支付宝订单号或转账订单号") }
if hasTransfer {
count++
}
if count > 1 {
return errors.New("支付宝、微信或转账订单号只能存在一个")
}
if count == 0 {
return errors.New("必须提供支付宝、微信或转账订单号")
} }
return nil return nil
@@ -124,6 +136,10 @@ func (r *RechargeRecord) GetOrderID() string {
if r.AlipayOrderID != nil { if r.AlipayOrderID != nil {
return *r.AlipayOrderID return *r.AlipayOrderID
} }
case RechargeTypeWechat:
if r.WechatOrderID != nil {
return *r.WechatOrderID
}
case RechargeTypeTransfer: case RechargeTypeTransfer:
if r.TransferOrderID != nil { if r.TransferOrderID != nil {
return *r.TransferOrderID return *r.TransferOrderID
@@ -137,6 +153,11 @@ func (r *RechargeRecord) SetAlipayOrderID(orderID string) {
r.AlipayOrderID = &orderID r.AlipayOrderID = &orderID
} }
// SetWechatOrderID 设置微信订单号
func (r *RechargeRecord) SetWechatOrderID(orderID string) {
r.WechatOrderID = &orderID
}
// SetTransferOrderID 设置转账订单号 // SetTransferOrderID 设置转账订单号
func (r *RechargeRecord) SetTransferOrderID(orderID string) { func (r *RechargeRecord) SetTransferOrderID(orderID string) {
r.TransferOrderID = &orderID 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 工厂方法 - 创建对公转账充值记录 // NewTransferRechargeRecord 工厂方法 - 创建对公转账充值记录
func NewTransferRechargeRecord(userID string, amount decimal.Decimal, transferOrderID, notes string) *RechargeRecord { func NewTransferRechargeRecord(userID string, amount decimal.Decimal, transferOrderID, notes string) *RechargeRecord {
return &RechargeRecord{ return &RechargeRecord{

View File

@@ -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)
}

View File

@@ -17,4 +17,4 @@ type AlipayOrderRepository interface {
UpdateStatus(ctx context.Context, id string, status entities.AlipayOrderStatus) error UpdateStatus(ctx context.Context, id string, status entities.AlipayOrderStatus) error
Delete(ctx context.Context, id string) error Delete(ctx context.Context, id string) error
Exists(ctx context.Context, id string) (bool, error) Exists(ctx context.Context, id string) (bool, error)
} }

View File

@@ -17,20 +17,20 @@ type RechargeRecordRepository interface {
GetByTransferOrderID(ctx context.Context, transferOrderID string) (*entities.RechargeRecord, error) GetByTransferOrderID(ctx context.Context, transferOrderID string) (*entities.RechargeRecord, error)
Update(ctx context.Context, record entities.RechargeRecord) error Update(ctx context.Context, record entities.RechargeRecord) error
UpdateStatus(ctx context.Context, id string, status entities.RechargeStatus) error UpdateStatus(ctx context.Context, id string, status entities.RechargeStatus) error
// 管理员查询方法 // 管理员查询方法
List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error) List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error)
Count(ctx context.Context, options interfaces.CountOptions) (int64, error) Count(ctx context.Context, options interfaces.CountOptions) (int64, error)
// 统计相关方法 // 统计相关方法
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error) GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (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) 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) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
// 系统级别统计方法 // 系统级别统计方法
GetSystemTotalAmount(ctx context.Context) (float64, error) GetSystemTotalAmount(ctx context.Context) (float64, error)
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error) GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, 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) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
} }

View File

@@ -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)
}

View File

@@ -20,7 +20,6 @@ type UserStats struct {
// UserRepository 用户仓储接口 // UserRepository 用户仓储接口
type UserRepository interface { type UserRepository interface {
interfaces.Repository[entities.User] interfaces.Repository[entities.User]
// 基础查询 - 直接使用实体 // 基础查询 - 直接使用实体
GetByPhone(ctx context.Context, phone string) (*entities.User, error) GetByPhone(ctx context.Context, phone string) (*entities.User, error)
GetByUsername(ctx context.Context, username 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) GetStats(ctx context.Context) (*UserStats, error)
GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*UserStats, error) GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*UserStats, error)
// 系统级别统计方法 // 系统级别统计方法
GetSystemUserStats(ctx context.Context) (*UserStats, error) GetSystemUserStats(ctx context.Context) (*UserStats, error)
GetSystemUserStatsByDateRange(ctx context.Context, startDate, endDate time.Time) (*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) GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
GetSystemDailyCertificationStats(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) GetSystemMonthlyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
// 排行榜查询方法 // 排行榜查询方法
GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]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) 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) Count(ctx context.Context, options interfaces.CountOptions) (int64, error)
List(ctx context.Context, options interfaces.ListOptions) ([]entities.EnterpriseInfo, error) List(ctx context.Context, options interfaces.ListOptions) ([]entities.EnterpriseInfo, error)
Exists(ctx context.Context, id string) (bool, error) Exists(ctx context.Context, id string) (bool, error)
} }

View File

@@ -13,7 +13,7 @@ import (
) )
const ( const (
AlipayOrdersTable = "alipay_orders" AlipayOrdersTable = "typay_orders"
) )
type GormAlipayOrderRepository struct { 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) { func (r *GormAlipayOrderRepository) GetByUserID(ctx context.Context, userID string) ([]entities.AlipayOrder, error) {
var orders []entities.AlipayOrder var orders []entities.AlipayOrder
err := r.GetDB(ctx). 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). Where("recharge_records.user_id = ?", userID).
Order("alipay_orders.created_at DESC"). Order("typay_orders.created_at DESC").
Find(&orders).Error Find(&orders).Error
return orders, err return orders, err
} }
@@ -95,4 +95,4 @@ func (r *GormAlipayOrderRepository) Exists(ctx context.Context, id string) (bool
var count int64 var count int64
err := r.GetDB(ctx).Model(&entities.AlipayOrder{}).Where("id = ?", id).Count(&count).Error err := r.GetDB(ctx).Model(&entities.AlipayOrder{}).Where("id = ?", id).Count(&count).Error
return count > 0, err return count > 0, err
} }

View File

@@ -163,11 +163,11 @@ func (r *GormRechargeRecordRepository) Count(ctx context.Context, options interf
} }
if options.Search != "" { if options.Search != "" {
if hasCompanyNameFilter { if hasCompanyNameFilter {
query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ?", 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+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
} else { } else {
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?", 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+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
} }
} }
return count, query.Count(&count).Error return count, query.Count(&count).Error
@@ -267,11 +267,11 @@ func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfa
if options.Search != "" { if options.Search != "" {
if hasCompanyNameFilter { if hasCompanyNameFilter {
query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ?", 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+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
} else { } else {
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?", 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+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
} }
} }

View File

@@ -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{})
}

View File

@@ -2,7 +2,9 @@
package handlers package handlers
import ( import (
"bytes"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@@ -19,12 +21,12 @@ import (
// FinanceHandler 财务HTTP处理器 // FinanceHandler 财务HTTP处理器
type FinanceHandler struct { type FinanceHandler struct {
appService finance.FinanceApplicationService appService finance.FinanceApplicationService
invoiceAppService finance.InvoiceApplicationService invoiceAppService finance.InvoiceApplicationService
adminInvoiceAppService finance.AdminInvoiceApplicationService adminInvoiceAppService finance.AdminInvoiceApplicationService
responseBuilder interfaces.ResponseBuilder responseBuilder interfaces.ResponseBuilder
validator interfaces.RequestValidator validator interfaces.RequestValidator
logger *zap.Logger logger *zap.Logger
} }
// NewFinanceHandler 创建财务HTTP处理器 // NewFinanceHandler 创建财务HTTP处理器
@@ -201,6 +203,123 @@ func (h *FinanceHandler) HandleAlipayCallback(c *gin.Context) {
c.String(200, "success") 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 处理支付宝同步回调 // HandleAlipayReturn 处理支付宝同步回调
// @Summary 支付宝同步回调 // @Summary 支付宝同步回调
// @Description 处理支付宝同步支付通知,跳转到前端成功页面 // @Description 处理支付宝同步支付通知,跳转到前端成功页面
@@ -240,7 +359,7 @@ func (h *FinanceHandler) HandleAlipayReturn(c *gin.Context) {
// 通过应用服务处理同步回调,查询订单状态 // 通过应用服务处理同步回调,查询订单状态
orderStatus, err := h.appService.HandleAlipayReturn(c.Request.Context(), outTradeNo) orderStatus, err := h.appService.HandleAlipayReturn(c.Request.Context(), outTradeNo)
if err != nil { if err != nil {
h.logger.Error("支付宝同步回调处理失败", h.logger.Error("支付宝同步回调处理失败",
zap.String("out_trade_no", outTradeNo), zap.String("out_trade_no", outTradeNo),
zap.Error(err)) zap.Error(err))
h.redirectToFailPage(c, outTradeNo, "订单处理失败") h.redirectToFailPage(c, outTradeNo, "订单处理失败")
@@ -257,7 +376,7 @@ func (h *FinanceHandler) HandleAlipayReturn(c *gin.Context) {
switch orderStatus { switch orderStatus {
case "TRADE_SUCCESS": 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) frontendDomain, outTradeNo, tradeNo, totalAmount)
c.Redirect(http.StatusFound, successURL) c.Redirect(http.StatusFound, successURL)
case "WAIT_BUYER_PAY": case "WAIT_BUYER_PAY":
@@ -275,8 +394,8 @@ func (h *FinanceHandler) redirectToFailPage(c *gin.Context, outTradeNo, reason s
if gin.Mode() == gin.DebugMode { if gin.Mode() == gin.DebugMode {
frontendDomain = "http://localhost:5173" 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) frontendDomain, outTradeNo, reason)
c.Redirect(http.StatusFound, failURL) c.Redirect(http.StatusFound, failURL)
} }
@@ -287,8 +406,8 @@ func (h *FinanceHandler) redirectToProcessingPage(c *gin.Context, outTradeNo, am
if gin.Mode() == gin.DebugMode { if gin.Mode() == gin.DebugMode {
frontendDomain = "http://localhost:5173" 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) frontendDomain, outTradeNo, amount)
c.Redirect(http.StatusFound, processingURL) c.Redirect(http.StatusFound, processingURL)
} }
@@ -319,7 +438,6 @@ func (h *FinanceHandler) CreateAlipayRecharge(c *gin.Context) {
return return
} }
// 调用应用服务进行完整的业务流程编排 // 调用应用服务进行完整的业务流程编排
result, err := h.appService.CreateAlipayRechargeOrder(c.Request.Context(), &cmd) result, err := h.appService.CreateAlipayRechargeOrder(c.Request.Context(), &cmd)
if err != nil { if err != nil {
@@ -343,6 +461,53 @@ func (h *FinanceHandler) CreateAlipayRecharge(c *gin.Context) {
h.responseBuilder.Success(c, result, "支付宝充值订单创建成功") 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 管理员对公转账充值 // TransferRecharge 管理员对公转账充值
func (h *FinanceHandler) TransferRecharge(c *gin.Context) { func (h *FinanceHandler) TransferRecharge(c *gin.Context) {
var cmd commands.TransferRechargeCommand var cmd commands.TransferRechargeCommand
@@ -849,8 +1014,6 @@ func (h *FinanceHandler) ApproveInvoiceApplication(c *gin.Context) {
return return
} }
h.responseBuilder.Success(c, nil, "通过发票申请成功") h.responseBuilder.Success(c, nil, "通过发票申请成功")
} }
@@ -932,14 +1095,14 @@ func (h *FinanceHandler) AdminDownloadInvoiceFile(c *gin.Context) {
// @Router /api/v1/debug/event-system [post] // @Router /api/v1/debug/event-system [post]
func (h *FinanceHandler) DebugEventSystem(c *gin.Context) { func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
h.logger.Info("🔍 请求事件系统调试信息") h.logger.Info("🔍 请求事件系统调试信息")
// 这里可以添加事件系统的状态信息 // 这里可以添加事件系统的状态信息
// 暂时返回基本信息 // 暂时返回基本信息
debugInfo := map[string]interface{}{ debugInfo := map[string]interface{}{
"timestamp": time.Now().Format("2006-01-02 15:04:05"), "timestamp": time.Now().Format("2006-01-02 15:04:05"),
"message": "事件系统调试端点已启用", "message": "事件系统调试端点已启用",
"handler": "FinanceHandler", "handler": "FinanceHandler",
} }
h.responseBuilder.Success(c, debugInfo, "事件系统调试信息") h.responseBuilder.Success(c, debugInfo, "事件系统调试信息")
} }

View File

@@ -1,6 +1,8 @@
package handlers package handlers
import ( import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -11,9 +13,6 @@ import (
"tyapi-server/internal/application/product/dto/queries" "tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses" "tyapi-server/internal/application/product/dto/responses"
"tyapi-server/internal/shared/interfaces" "tyapi-server/internal/shared/interfaces"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
) )
// ProductAdminHandler 产品管理员HTTP处理器 // ProductAdminHandler 产品管理员HTTP处理器
@@ -1338,7 +1337,7 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) {
// @Param page query int false "页码" default(1) // @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10) // @Param page_size query int false "每页数量" default(10)
// @Param user_id query string false "用户ID" // @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 status query string false "状态" Enums(pending, success, failed)
// @Param min_amount query string false "最小金额" // @Param min_amount query string false "最小金额"
// @Param max_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 // @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv
// @Security Bearer // @Security Bearer
// @Param user_ids query string false "用户ID列表逗号分隔" // @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 status query string false "状态" Enums(pending, success, failed)
// @Param start_time query string false "开始时间" format(date-time) // @Param start_time query string false "开始时间" format(date-time)
// @Param end_time query string false "结束时间" format(date-time) // @Param end_time query string false "结束时间" format(date-time)

View File

@@ -42,6 +42,18 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
alipayGroup.GET("/return", r.financeHandler.HandleAlipayReturn) // 支付宝同步回调 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 := engine.Group("/api/v1/finance")
financeGroup.Use(r.authMiddleware.Handle()) financeGroup.Use(r.authMiddleware.Handle())
@@ -49,12 +61,14 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
// 钱包相关路由 // 钱包相关路由
walletGroup := financeGroup.Group("/wallet") walletGroup := financeGroup.Group("/wallet")
{ {
walletGroup.GET("", r.financeHandler.GetWallet) // 获取钱包信息 walletGroup.GET("", r.financeHandler.GetWallet) // 获取钱包信息
walletGroup.GET("/transactions", r.financeHandler.GetUserWalletTransactions) // 获取钱包交易记录 walletGroup.GET("/transactions", r.financeHandler.GetUserWalletTransactions) // 获取钱包交易记录
walletGroup.GET("/recharge-config", r.financeHandler.GetRechargeConfig) // 获取充值配置 walletGroup.GET("/recharge-config", r.financeHandler.GetRechargeConfig) // 获取充值配置
walletGroup.POST("/alipay-recharge", r.financeHandler.CreateAlipayRecharge) // 创建支付宝充值订单 walletGroup.POST("/alipay-recharge", r.financeHandler.CreateAlipayRecharge) // 创建支付宝充值订单
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页 walletGroup.POST("/wechat-recharge", r.financeHandler.CreateWechatRecharge) // 创建微信充值订单
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页
walletGroup.GET("/alipay-order-status", r.financeHandler.GetAlipayOrderStatus) // 获取支付宝订单状态 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 := engine.Group("/api/v1/invoices")
invoiceGroup.Use(r.authMiddleware.Handle()) invoiceGroup.Use(r.authMiddleware.Handle())
{ {
invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票 invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票
invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息 invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息
invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息 invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息
invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录 invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录
invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额 invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额
invoiceGroup.GET("/:application_id/download", r.financeHandler.DownloadInvoiceFile) // 下载发票文件 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 := engine.Group("/api/v1/admin/finance")
adminFinanceGroup.Use(r.adminAuthMiddleware.Handle()) adminFinanceGroup.Use(r.adminAuthMiddleware.Handle())
{ {
adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值 adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值
adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值 adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值
adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页 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 := engine.Group("/api/v1/admin/invoices")
adminInvoiceGroup.Use(r.adminAuthMiddleware.Handle()) 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/approve", r.financeHandler.ApproveInvoiceApplication) // 通过发票申请
adminInvoiceGroup.POST("/:application_id/reject", r.financeHandler.RejectInvoiceApplication) // 拒绝发票申请 adminInvoiceGroup.POST("/:application_id/reject", r.financeHandler.RejectInvoiceApplication) // 拒绝发票申请
adminInvoiceGroup.GET("/:application_id/download", r.financeHandler.AdminDownloadInvoiceFile) // 下载发票文件 adminInvoiceGroup.GET("/:application_id/download", r.financeHandler.AdminDownloadInvoiceFile) // 下载发票文件

View File

@@ -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
}

View File

@@ -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未实现请注入真实的实现")
}

View File

@@ -0,0 +1,7 @@
package payment
// ToWechatAmount 将金额转换为微信支付金额(单位:分)
// 微信支付金额以分为单位,需要将元转换为分
func ToWechatAmount(amount float64) int64 {
return int64(amount * 100)
}

View File

@@ -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
}

Binary file not shown.

View File

@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEJDCCAwygAwIBAgIUH06LPDnGADXUzBVPJ20D2cwsYD0wDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUxMjExMDYxMjQ4WhcNMzAxMjEwMDYxMjQ4WjB+MRMwEQYDVQQDDAox
NjgzNTg5MTc2MRswGQYDVQQKDBLlvq7kv6HllYbmiLfns7vnu58xKjAoBgNVBAsM
Iea1t+WNl+a1t+Wuh+Wkp+aVsOaNruaciemZkOWFrOWPuDELMAkGA1UEBhMCQ04x
ETAPBgNVBAcMCFNoZW5aaGVuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAn7zhFOO7aFq0Zd5L0xf+rnhJl3ELFhhSDgHTo2wk9f1K7U0PWsdu6RWjtQiu
MS6u4gOPtYXgVAAue37KzyTs9nWfdOFpm9Q/CI/lLfyFs9/JV61sDO18+t4apr0D
ML0enRxrzE6dqlgBdjm7FGcfWLOnVcnBSbxskp2vSji230HFcBDOwVTlELApoDzJ
6zkfaoKfKJkhk1b+ZHB70ikyRg0f8z+qeNyFkmJecPzRXGn6QlrXldX0Or10ZMss
HBMuDDqCihl0mom20phRbUgLVj7/dlRSslrhQfh0MD9Mn55g8dok4YV68s+hZpIC
l0EfzCGCvppDvGnkVFcYLwoDdwIDAQABo4G5MIG2MAkGA1UdEwQCMAAwCwYDVR0P
BAQDAgP4MIGbBgNVHR8EgZMwgZAwgY2ggYqggYeGgYRodHRwOi8vZXZjYS5pdHJ1
cy5jb20uY24vcHVibGljL2l0cnVzY3JsP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFE
Mzk3NTQ5ODQ2QzAxQzNFOEVCRDImc2c9SEFDQzQ3MUI2NTQyMkUxMkIyN0E5RDMz
QTg3QUQxQ0RGNTkyNkUxNDAzNzEwDQYJKoZIhvcNAQELBQADggEBAKzb7i8F/jJ3
yDUphme5IpOl14HXYWwuIqWMnD2Sk8YemMcjAEvxFMvXR5WmwWymnfcYhrQWYBn6
iWMzfT2hovOo+DBUjn01XTzzWGAS0WwOJ5ewwFIvyW5BYODvqBcWd1dF9pCXhpH6
fk0dUKi6t9PbErLEtqf3CDSsM9muh8Lb81ks80VfHz/IV24Su2ZKShJJIMbqK+cW
UqrBMnwpd9CqrzkKb4RPll3wRyG7CZ/DMfWXx7uz3UDULSlaRIfNFw2v/w4WSX3H
1Sy1MzDERvfq3CjWXGwtuI7OQE1AWxdH+FEik8dKm81U8yR/bX+rPjjFM4CJg3MD
M8N+ymic4rs=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCfvOEU47toWrRl
3kvTF/6ueEmXcQsWGFIOAdOjbCT1/UrtTQ9ax27pFaO1CK4xLq7iA4+1heBUAC57
fsrPJOz2dZ904Wmb1D8Ij+Ut/IWz38lXrWwM7Xz63hqmvQMwvR6dHGvMTp2qWAF2
ObsUZx9Ys6dVycFJvGySna9KOLbfQcVwEM7BVOUQsCmgPMnrOR9qgp8omSGTVv5k
cHvSKTJGDR/zP6p43IWSYl5w/NFcafpCWteV1fQ6vXRkyywcEy4MOoKKGXSaibbS
mFFtSAtWPv92VFKyWuFB+HQwP0yfnmDx2iThhXryz6FmkgKXQR/MIYK+mkO8aeRU
VxgvCgN3AgMBAAECggEAP6qfp5zREFm+ty9v11Yj+1QUONkkiwzsf4q42NT8slLf
b0+chBkjGqG2Wyx3iUDLEWhL+hS/AZwE6tHxcbiM/fqJsKM7XZGuAfKgbMDOZZAX
huunOkvZ2X927eg+AkoOjp5KVOcsrj1fb8i4yPwFIWyRkH7WnFYOjC1vNUz/jmHe
ZHos/T+ZGOrP/Q9fpzyCKKtDwC0oMpx1l6hsQjU14MNbWIgc/eiWmnyAbUe5PmS3
M5Aj2xFBoFCiRS95P8lG2d/0rdq2XmNh1L1MqqEJ0uc5iAAma2FTjpVbbey3N1hM
csfq/s2olPExO8v13W4UJDFBPwTvCcAC1JPyb6WoGQKBgQDLwARt3N3rdo61GZSo
HF9vUHRJ3+7OkF5mTYV0+y4LyKYTxa8GiyOrCD9XQbRnfcGG74hK02HNzyPDdbD/
XDBmr3DxHx3hG7wmrajkLr0+Pum7ajjaqiC990bneBhof5odz28PPo/Vkk66QKJD
RWucTloHdZosQBPLAMENtmLNUwKBgQDIs4CbvZSKNDw9sXZFC3cSKg5eREGIftVt
gUiBT5yBcu7pVA6aAp73JYsDPzyWxlLbQ+6dT4gMVeE6uLs5DnYiLDzEm6X8XrVp
kXIS5M+xzBWCTtUgUmZtWHbTH6nxTmNFTzQEd/9TPhYTRTVJF4V3jTYRDevBSwJ8
HDcX1VsIzQKBgQC2GXab7hOVV4+yAhvfqAQPi7tzLyXTDiqgilZlt/xuYbU05LBK
S97kBGoABWREPpvRipGoNoYqGCChl7VKdU146QIrLqFYyJ3/f6P71F4knLvvWb9Y
h0beIXwIckh2VN0cGYHsAQEyYyHjytJ7BzdnKovCMPRK6jYGcDUamVByqwKBgE1V
xZe9XFBIwnGvQPhn65DHPdQbDvlujgTtDSguqgrDY8XqytmTavemssMkic87SlAN
BBP/wleme+wppJLevKx8SUolA7eUMukjz0Xyfwlur1cP01XqCmfV76t4hv5hiyT4
2P1j07GaudzhDSBF/PrNIek+aPqJUcLLCHuZjcN9AoGAfpWmZ1PivWZ3K99nWj3H
u0P7mgENWAuuOXCoVMJ+42Ce8siBsCovkZJynbVhd1TYqto6F15KvwdOjLKKucDx
3K5yACAL9fxbBqO+gel2t6Lkd145kwLly3ChJxF9Y+GfxkrQC5XedHENmb+20Ryb
qc7u6TBrGPF1ceeEK3HBvzw=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArw9V+Nc7LZ/2Sul64PWT
rIpnWKAILD5Mt+lStWBm48sWxGsDDXcZVlp8Pk58Otrxl/d1yuGOWDa3WAp6W1cs
xWnx4jfG5V9sh/xWWEMnGTnOYC+KwtOADFLqIXPbkNeieDjaIxoVyDQEQFxIjN6W
lNdHbA0iWH8rqzFPtLwlP1U4X/xXpZvN/vwfEbuC/+tDhMROYbi1uGCEoYVpT8i4
cd6UfO46CG40VuT2V+ZWGC0Ulu5dxjG/MSmIwhFhSoaF8Ec9wxR+yumTUhRG4Ahv
ZRBylfZrJFk95LYWVEXf7dbJvbc5wYpWTOH4k3A4Nvo5ILzN4KQoA5WoULLCHUeu
vQIDAQAB
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,18 @@
欢迎使用微信支付!
附件中的三份文件证书pkcs12格式、证书pem格式、证书密钥pem格式,为接口中强制要求时需携带的证书文件。
证书属于敏感信息,请妥善保管不要泄露和被他人复制。
不同开发语言下的证书格式不同,以下为说明指引:
证书pkcs12格式apiclient_cert.p12
包含了私钥信息的证书文件为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份
部分安全性要求较高的API需要使用该证书来确认您的调用身份
windows上可以直接双击导入系统导入过程中会提示输入证书密码证书密码默认为您的商户号1900006031
证书pem格式apiclient_cert.pem
从apiclient_cert.p12中导出证书部分的文件为pem格式请妥善保管不要泄漏和被他人复制
部分开发语言和环境不能直接使用p12文件而需要使用pem所以为了方便您使用已为您直接提供
您也可以使用openssl命令来自己导出openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem
证书密钥pem格式apiclient_key.pem
从apiclient_cert.p12中导出密钥部分的文件为pem格式
部分开发语言和环境不能直接使用p12文件而需要使用pem所以为了方便您使用已为您直接提供
您也可以使用openssl命令来自己导出openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem
备注说明:
由于绝大部分操作系统已内置了微信支付服务器证书的根CA证书, 2018年3月6日后, 不再提供CA证书文件rootca.pem下载