Compare commits
2 Commits
3f64600f02
...
18f3d10518
| Author | SHA1 | Date | |
|---|---|---|---|
| 18f3d10518 | |||
| 0d4953c6d3 |
22
config.yaml
22
config.yaml
@@ -362,6 +362,28 @@ alipay:
|
||||
notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback"
|
||||
return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return"
|
||||
|
||||
# ===========================================
|
||||
# 💰 微信支付配置
|
||||
# ===========================================
|
||||
Wxpay:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
mch_id: "1683589176"
|
||||
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
|
||||
mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0"
|
||||
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
|
||||
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
|
||||
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
|
||||
notify_url: "https://console.tianyuanapi.com/api/v1/pay/wechat/callback"
|
||||
refund_notify_url: "https://console.tianyuanapi.com/api/v1/wechat/refund_callback"
|
||||
|
||||
# 微信小程序配置
|
||||
WechatMini:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
|
||||
# 微信H5配置
|
||||
WechatH5:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
|
||||
# ===========================================
|
||||
# 🔍 天眼查配置
|
||||
# ===========================================
|
||||
|
||||
@@ -81,6 +81,27 @@ alipay:
|
||||
return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return"
|
||||
|
||||
# ===========================================
|
||||
# 💰 微信支付配置
|
||||
# ===========================================
|
||||
Wxpay:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
mch_id: "1683589176"
|
||||
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
|
||||
mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0"
|
||||
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
|
||||
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
|
||||
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
|
||||
notify_url: "https://bx89915628g.vicp.fun/api/v1/pay/wechat/callback"
|
||||
refund_notify_url: "https://bx89915628g.vicp.fun/api/v1/wechat/refund_callback"
|
||||
|
||||
# 微信小程序配置
|
||||
WechatMini:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
|
||||
# 微信H5配置
|
||||
WechatH5:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
# ===========================================
|
||||
# 💰 钱包配置
|
||||
# ===========================================
|
||||
wallet:
|
||||
@@ -114,34 +135,42 @@ development:
|
||||
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
||||
|
||||
# ===========================================
|
||||
# 🚦 开发环境全局限流(放宽或近似关闭)
|
||||
# ===========================================
|
||||
ratelimit:
|
||||
requests: 1000000 # 每窗口允许的请求数,足够大,相当于关闭
|
||||
window: 1s # 时间窗口
|
||||
burst: 1000000 # 令牌桶突发容量
|
||||
|
||||
# ===========================================
|
||||
# 🚀 开发环境频率限制配置(放宽限制)
|
||||
# ===========================================
|
||||
daily_ratelimit:
|
||||
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
|
||||
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
|
||||
max_concurrent: 50 # 开发环境最大并发请求数
|
||||
|
||||
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
|
||||
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
|
||||
max_concurrent: 50 # 开发环境最大并发请求数
|
||||
|
||||
# 排除频率限制的路径
|
||||
exclude_paths:
|
||||
- "/health" # 健康检查接口
|
||||
- "/metrics" # 监控指标接口
|
||||
|
||||
- "/health" # 健康检查接口
|
||||
- "/metrics" # 监控指标接口
|
||||
|
||||
# 排除频率限制的域名
|
||||
exclude_domains:
|
||||
- "api.*" # API二级域名不受频率限制
|
||||
- "*.api.*" # 支持多级API域名
|
||||
|
||||
- "api.*" # API二级域名不受频率限制
|
||||
- "*.api.*" # 支持多级API域名
|
||||
|
||||
# 开发环境安全配置(放宽限制)
|
||||
enable_ip_whitelist: true # 启用IP白名单
|
||||
ip_whitelist: # 开发环境IP白名单
|
||||
- "127.0.0.1" # 本地回环
|
||||
- "localhost" # 本地主机
|
||||
- "192.168.*" # 内网IP段
|
||||
- "10.*" # 内网IP段
|
||||
- "172.16.*" # 内网IP段
|
||||
|
||||
enable_ip_blacklist: false # 开发环境禁用IP黑名单
|
||||
enable_user_agent: false # 开发环境禁用User-Agent检查
|
||||
enable_referer: false # 开发环境禁用Referer检查
|
||||
enable_proxy_check: false # 开发环境禁用代理检查
|
||||
enable_ip_whitelist: true # 启用IP白名单
|
||||
ip_whitelist: # 开发环境IP白名单
|
||||
- "127.0.0.1" # 本地回环
|
||||
- "localhost" # 本地主机
|
||||
- "192.168.*" # 内网IP段
|
||||
- "10.*" # 内网IP段
|
||||
- "172.16.*" # 内网IP段
|
||||
|
||||
enable_ip_blacklist: false # 开发环境禁用IP黑名单
|
||||
enable_user_agent: false # 开发环境禁用User-Agent检查
|
||||
enable_referer: false # 开发环境禁用Referer检查
|
||||
enable_proxy_check: false # 开发环境禁用代理检查
|
||||
|
||||
@@ -5,7 +5,6 @@ type CreateWalletCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
}
|
||||
|
||||
|
||||
// TransferRechargeCommand 对公转账充值命令
|
||||
type TransferRechargeCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
@@ -16,16 +15,24 @@ type TransferRechargeCommand struct {
|
||||
|
||||
// GiftRechargeCommand 赠送充值命令
|
||||
type GiftRechargeCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount string `json:"amount" binding:"required"`
|
||||
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount string `json:"amount" binding:"required"`
|
||||
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
||||
}
|
||||
|
||||
|
||||
// CreateAlipayRechargeCommand 创建支付宝充值订单命令
|
||||
type CreateAlipayRechargeCommand struct {
|
||||
UserID string `json:"-"` // 用户ID(从token获取)
|
||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||
Subject string `json:"-"` // 订单标题
|
||||
UserID string `json:"-"` // 用户ID(从token获取)
|
||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||
Subject string `json:"-"` // 订单标题
|
||||
Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台:app/h5/pc
|
||||
}
|
||||
|
||||
// CreateWechatRechargeCommand 创建微信充值订单命令
|
||||
type CreateWechatRechargeCommand struct {
|
||||
UserID string `json:"-"` // 用户ID(从token获取)
|
||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||
Subject string `json:"-"` // 订单标题
|
||||
Platform string `json:"platform" binding:"required,oneof=wx_native native wx_h5 h5"` // 仅支持微信Native扫码,兼容传入native/wx_h5/h5
|
||||
OpenID string `json:"openid" binding:"omitempty"` // 前端可直接传入的 openid(用于小程序/H5)
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ import (
|
||||
|
||||
// WalletResponse 钱包响应
|
||||
type WalletResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Balance decimal.Decimal `json:"balance"`
|
||||
BalanceStatus string `json:"balance_status"` // normal, low, arrears
|
||||
IsArrears bool `json:"is_arrears"` // 是否欠费
|
||||
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Balance decimal.Decimal `json:"balance"`
|
||||
BalanceStatus string `json:"balance_status"` // normal, low, arrears
|
||||
IsArrears bool `json:"is_arrears"` // 是否欠费
|
||||
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TransactionResponse 交易响应
|
||||
@@ -49,34 +49,36 @@ type WalletStatsResponse struct {
|
||||
|
||||
// RechargeRecordResponse 充值记录响应
|
||||
type RechargeRecordResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
RechargeType string `json:"recharge_type"`
|
||||
Status string `json:"status"`
|
||||
AlipayOrderID string `json:"alipay_order_id,omitempty"`
|
||||
TransferOrderID string `json:"transfer_order_id,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
OperatorID string `json:"operator_id,omitempty"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
RechargeType string `json:"recharge_type"`
|
||||
Status string `json:"status"`
|
||||
AlipayOrderID string `json:"alipay_order_id,omitempty"`
|
||||
WechatOrderID string `json:"wechat_order_id,omitempty"`
|
||||
TransferOrderID string `json:"transfer_order_id,omitempty"`
|
||||
Platform string `json:"platform,omitempty"` // 支付平台:pc/wx_native等
|
||||
Notes string `json:"notes,omitempty"`
|
||||
OperatorID string `json:"operator_id,omitempty"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// WalletTransactionResponse 钱包交易记录响应
|
||||
type WalletTransactionResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// WalletTransactionListResponse 钱包交易记录列表响应
|
||||
@@ -97,17 +99,17 @@ type RechargeRecordListResponse struct {
|
||||
|
||||
// AlipayRechargeOrderResponse 支付宝充值订单响应
|
||||
type AlipayRechargeOrderResponse struct {
|
||||
PayURL string `json:"pay_url"` // 支付链接
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
PayURL string `json:"pay_url"` // 支付链接
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
}
|
||||
|
||||
// RechargeConfigResponse 充值配置响应
|
||||
type RechargeConfigResponse struct {
|
||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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"` // 是否可以重试
|
||||
}
|
||||
@@ -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参数)
|
||||
}
|
||||
@@ -17,13 +17,14 @@ type FinanceApplicationService interface {
|
||||
|
||||
// 充值管理
|
||||
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error)
|
||||
CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error)
|
||||
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
|
||||
// 交易记录
|
||||
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
|
||||
|
||||
// 导出功能
|
||||
ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
|
||||
ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
|
||||
@@ -33,12 +34,15 @@ type FinanceApplicationService interface {
|
||||
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
|
||||
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
|
||||
|
||||
// 微信支付回调处理
|
||||
HandleWechatPayCallback(ctx context.Context, r *http.Request) error
|
||||
HandleWechatRefundCallback(ctx context.Context, r *http.Request) error
|
||||
GetWechatOrderStatus(ctx context.Context, outTradeNo string) (*responses.WechatOrderStatusResponse, error)
|
||||
|
||||
// 充值记录
|
||||
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
|
||||
// 获取充值配置
|
||||
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
"tyapi-server/internal/application/finance/dto/commands"
|
||||
"tyapi-server/internal/application/finance/dto/queries"
|
||||
"tyapi-server/internal/application/finance/dto/responses"
|
||||
@@ -19,16 +20,20 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// FinanceApplicationServiceImpl 财务应用服务实现
|
||||
type FinanceApplicationServiceImpl struct {
|
||||
aliPayClient *payment.AliPayService
|
||||
wechatPayService *payment.WechatPayService
|
||||
walletService finance_services.WalletAggregateService
|
||||
rechargeRecordService finance_services.RechargeRecordService
|
||||
walletTransactionRepository finance_repositories.WalletTransactionRepository
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
txManager *database.TransactionManager
|
||||
exportManager *export.ExportManager
|
||||
@@ -39,10 +44,13 @@ type FinanceApplicationServiceImpl struct {
|
||||
// NewFinanceApplicationService 创建财务应用服务
|
||||
func NewFinanceApplicationService(
|
||||
aliPayClient *payment.AliPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
rechargeRecordService finance_services.RechargeRecordService,
|
||||
walletTransactionRepository finance_repositories.WalletTransactionRepository,
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository,
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository,
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
@@ -51,10 +59,13 @@ func NewFinanceApplicationService(
|
||||
) FinanceApplicationService {
|
||||
return &FinanceApplicationServiceImpl{
|
||||
aliPayClient: aliPayClient,
|
||||
wechatPayService: wechatPayService,
|
||||
walletService: walletService,
|
||||
rechargeRecordService: rechargeRecordService,
|
||||
walletTransactionRepository: walletTransactionRepository,
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
wechatOrderRepo: wechatOrderRepo,
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
userRepo: userRepo,
|
||||
txManager: txManager,
|
||||
exportManager: exportManager,
|
||||
@@ -100,8 +111,9 @@ func (s *FinanceApplicationServiceImpl) GetWallet(ctx context.Context, query *qu
|
||||
BalanceStatus: wallet.GetBalanceStatus(),
|
||||
IsArrears: wallet.IsArrears(),
|
||||
IsLowBalance: wallet.IsLowBalance(),
|
||||
CreatedAt: wallet.CreatedAt,
|
||||
UpdatedAt: wallet.UpdatedAt,
|
||||
|
||||
CreatedAt: wallet.CreatedAt,
|
||||
UpdatedAt: wallet.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -188,6 +200,168 @@ func (s *FinanceApplicationServiceImpl) CreateAlipayRechargeOrder(ctx context.Co
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateWechatRechargeOrder 创建微信充值订单(完整流程编排)
|
||||
func (s *FinanceApplicationServiceImpl) CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error) {
|
||||
cmd.Subject = "天远数据API充值"
|
||||
amount, err := decimal.NewFromString(cmd.Amount)
|
||||
if err != nil {
|
||||
s.logger.Error("金额格式错误", zap.String("amount", cmd.Amount), zap.Error(err))
|
||||
return nil, fmt.Errorf("金额格式错误: %w", err)
|
||||
}
|
||||
|
||||
if amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("充值金额必须大于0")
|
||||
}
|
||||
|
||||
minAmount, err := decimal.NewFromString(s.config.Wallet.MinAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("配置中的最低充值金额格式错误", zap.String("min_amount", s.config.Wallet.MinAmount), zap.Error(err))
|
||||
return nil, fmt.Errorf("系统配置错误: %w", err)
|
||||
}
|
||||
maxAmount, err := decimal.NewFromString(s.config.Wallet.MaxAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("配置中的最高充值金额格式错误", zap.String("max_amount", s.config.Wallet.MaxAmount), zap.Error(err))
|
||||
return nil, fmt.Errorf("系统配置错误: %w", err)
|
||||
}
|
||||
|
||||
if amount.LessThan(minAmount) {
|
||||
return nil, fmt.Errorf("充值金额不能少于%s元", minAmount.String())
|
||||
}
|
||||
if amount.GreaterThan(maxAmount) {
|
||||
return nil, fmt.Errorf("单次充值金额不能超过%s元", maxAmount.String())
|
||||
}
|
||||
|
||||
platform := normalizeWechatPlatform(cmd.Platform)
|
||||
if platform != payment.PlatformWxNative && platform != payment.PlatformWxH5 {
|
||||
return nil, fmt.Errorf("不支持的支付平台: %s", cmd.Platform)
|
||||
}
|
||||
if s.wechatPayService == nil {
|
||||
return nil, fmt.Errorf("微信支付服务未初始化")
|
||||
}
|
||||
|
||||
outTradeNo := s.wechatPayService.GenerateOutTradeNo()
|
||||
|
||||
s.logger.Info("开始创建微信充值订单",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("platform", cmd.Platform),
|
||||
zap.String("subject", cmd.Subject),
|
||||
)
|
||||
|
||||
var prepayData interface{}
|
||||
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 创建微信充值记录
|
||||
rechargeRecord := finance_entities.NewWechatRechargeRecord(cmd.UserID, amount, outTradeNo)
|
||||
createdRecord, createErr := s.rechargeRecordRepo.Create(txCtx, *rechargeRecord)
|
||||
if createErr != nil {
|
||||
s.logger.Error("创建微信充值记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.Error(createErr),
|
||||
)
|
||||
return fmt.Errorf("创建微信充值记录失败: %w", createErr)
|
||||
}
|
||||
|
||||
s.logger.Info("创建微信充值记录成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", createdRecord.ID),
|
||||
zap.String("user_id", cmd.UserID),
|
||||
)
|
||||
|
||||
// 创建微信订单本地记录
|
||||
wechatOrder := finance_entities.NewWechatOrder(createdRecord.ID, outTradeNo, cmd.Subject, amount, platform)
|
||||
createdOrder, orderErr := s.wechatOrderRepo.Create(txCtx, *wechatOrder)
|
||||
if orderErr != nil {
|
||||
s.logger.Error("创建微信订单记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", createdRecord.ID),
|
||||
zap.Error(orderErr),
|
||||
)
|
||||
return fmt.Errorf("创建微信订单记录失败: %w", orderErr)
|
||||
}
|
||||
|
||||
s.logger.Info("创建微信订单记录成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("order_id", createdOrder.ID),
|
||||
zap.String("recharge_id", createdRecord.ID),
|
||||
)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payCtx := context.WithValue(ctx, "platform", platform)
|
||||
payCtx = context.WithValue(payCtx, "user_id", cmd.UserID)
|
||||
|
||||
s.logger.Info("调用微信支付接口创建订单",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("platform", platform),
|
||||
)
|
||||
|
||||
prepayData, err = s.wechatPayService.CreateWechatOrder(payCtx, amount.InexactFloat64(), cmd.Subject, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("微信下单失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
// 回写失败状态
|
||||
_ = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
order, getErr := s.wechatOrderRepo.GetByOutTradeNo(txCtx, outTradeNo)
|
||||
if getErr == nil && order != nil {
|
||||
order.MarkFailed("create_failed", err.Error())
|
||||
updateErr := s.wechatOrderRepo.Update(txCtx, *order)
|
||||
if updateErr != nil {
|
||||
s.logger.Error("回写微信订单失败状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(updateErr),
|
||||
)
|
||||
} else {
|
||||
s.logger.Info("回写微信订单失败状态成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil, fmt.Errorf("创建微信支付订单失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("微信充值订单创建成功",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("platform", cmd.Platform),
|
||||
)
|
||||
|
||||
return &responses.WechatRechargeOrderResponse{
|
||||
OutTradeNo: outTradeNo,
|
||||
Amount: amount,
|
||||
Platform: platform,
|
||||
Subject: cmd.Subject,
|
||||
PrepayData: prepayData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// normalizeWechatPlatform 将兼容写法(h5/mini)转换为系统内使用的wx_h5/wx_mini
|
||||
func normalizeWechatPlatform(p string) string {
|
||||
switch p {
|
||||
case "h5", payment.PlatformWxH5:
|
||||
return payment.PlatformWxNative
|
||||
case "native":
|
||||
return payment.PlatformWxNative
|
||||
default:
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
// TransferRecharge 对公转账充值
|
||||
func (s *FinanceApplicationServiceImpl) TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error) {
|
||||
// 将字符串金额转换为 decimal.Decimal
|
||||
@@ -507,8 +681,8 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
|
||||
}
|
||||
|
||||
// 准备导出数据
|
||||
headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "转账订单号", "备注", "充值时间"}
|
||||
columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20}
|
||||
headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "微信订单号", "转账订单号", "备注", "充值时间"}
|
||||
columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20, 20}
|
||||
|
||||
data := make([][]interface{}, len(allRecords))
|
||||
for i, record := range allRecords {
|
||||
@@ -523,6 +697,10 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
|
||||
if record.AlipayOrderID != nil && *record.AlipayOrderID != "" {
|
||||
alipayOrderID = *record.AlipayOrderID
|
||||
}
|
||||
wechatOrderID := ""
|
||||
if record.WechatOrderID != nil && *record.WechatOrderID != "" {
|
||||
wechatOrderID = *record.WechatOrderID
|
||||
}
|
||||
transferOrderID := ""
|
||||
if record.TransferOrderID != nil && *record.TransferOrderID != "" {
|
||||
transferOrderID = *record.TransferOrderID
|
||||
@@ -543,6 +721,7 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
|
||||
translateRechargeType(record.RechargeType),
|
||||
translateRechargeStatus(record.Status),
|
||||
alipayOrderID,
|
||||
wechatOrderID,
|
||||
transferOrderID,
|
||||
notes,
|
||||
createdAt,
|
||||
@@ -566,6 +745,8 @@ func translateRechargeType(rechargeType finance_entities.RechargeType) string {
|
||||
switch rechargeType {
|
||||
case finance_entities.RechargeTypeAlipay:
|
||||
return "支付宝充值"
|
||||
case finance_entities.RechargeTypeWechat:
|
||||
return "微信充值"
|
||||
case finance_entities.RechargeTypeTransfer:
|
||||
return "对公转账"
|
||||
case finance_entities.RechargeTypeGift:
|
||||
@@ -890,15 +1071,27 @@ func (s *FinanceApplicationServiceImpl) GetAlipayOrderStatus(ctx context.Context
|
||||
|
||||
// GetUserRechargeRecords 获取用户充值记录
|
||||
func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) {
|
||||
// 查询用户充值记录
|
||||
records, err := s.rechargeRecordService.GetByUserID(ctx, userID)
|
||||
// 确保 filters 不为 nil
|
||||
if filters == nil {
|
||||
filters = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// 添加 user_id 筛选条件,确保只能查询当前用户的记录
|
||||
filters["user_id"] = userID
|
||||
|
||||
// 查询用户充值记录(使用筛选和分页功能)
|
||||
records, err := s.rechargeRecordService.GetAll(ctx, filters, options)
|
||||
if err != nil {
|
||||
s.logger.Error("查询用户充值记录失败", zap.Error(err), zap.String("userID", userID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
total := int64(len(records))
|
||||
// 获取总数(使用筛选条件)
|
||||
total, err := s.rechargeRecordService.Count(ctx, filters)
|
||||
if err != nil {
|
||||
s.logger.Error("统计用户充值记录失败", zap.Error(err), zap.String("userID", userID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应DTO
|
||||
var items []responses.RechargeRecordResponse
|
||||
@@ -914,9 +1107,20 @@ func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Conte
|
||||
UpdatedAt: record.UpdatedAt,
|
||||
}
|
||||
|
||||
// 根据充值类型设置相应的订单号
|
||||
// 根据充值类型设置相应的订单号和平台信息
|
||||
if record.AlipayOrderID != nil {
|
||||
item.AlipayOrderID = *record.AlipayOrderID
|
||||
// 通过订单号获取平台信息
|
||||
if alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, *record.AlipayOrderID); err == nil && alipayOrder != nil {
|
||||
item.Platform = alipayOrder.Platform
|
||||
}
|
||||
}
|
||||
if record.WechatOrderID != nil {
|
||||
item.WechatOrderID = *record.WechatOrderID
|
||||
// 通过订单号获取平台信息
|
||||
if wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, *record.WechatOrderID); err == nil && wechatOrder != nil {
|
||||
item.Platform = wechatOrder.Platform
|
||||
}
|
||||
}
|
||||
if record.TransferOrderID != nil {
|
||||
item.TransferOrderID = *record.TransferOrderID
|
||||
@@ -963,9 +1167,20 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont
|
||||
UpdatedAt: record.UpdatedAt,
|
||||
}
|
||||
|
||||
// 根据充值类型设置相应的订单号
|
||||
// 根据充值类型设置相应的订单号和平台信息
|
||||
if record.AlipayOrderID != nil {
|
||||
item.AlipayOrderID = *record.AlipayOrderID
|
||||
// 通过订单号获取平台信息
|
||||
if alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, *record.AlipayOrderID); err == nil && alipayOrder != nil {
|
||||
item.Platform = alipayOrder.Platform
|
||||
}
|
||||
}
|
||||
if record.WechatOrderID != nil {
|
||||
item.WechatOrderID = *record.WechatOrderID
|
||||
// 通过订单号获取平台信息
|
||||
if wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, *record.WechatOrderID); err == nil && wechatOrder != nil {
|
||||
item.Platform = wechatOrder.Platform
|
||||
}
|
||||
}
|
||||
if record.TransferOrderID != nil {
|
||||
item.TransferOrderID = *record.TransferOrderID
|
||||
@@ -1012,3 +1227,445 @@ func (s *FinanceApplicationServiceImpl) GetRechargeConfig(ctx context.Context) (
|
||||
AlipayRechargeBonus: bonus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWechatOrderStatus 获取微信订单状态
|
||||
func (s *FinanceApplicationServiceImpl) GetWechatOrderStatus(ctx context.Context, outTradeNo string) (*responses.WechatOrderStatusResponse, error) {
|
||||
if outTradeNo == "" {
|
||||
return nil, fmt.Errorf("缺少商户订单号")
|
||||
}
|
||||
|
||||
// 查找微信订单
|
||||
wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查找微信订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err))
|
||||
return nil, fmt.Errorf("查找微信订单失败: %w", err)
|
||||
}
|
||||
|
||||
if wechatOrder == nil {
|
||||
s.logger.Error("微信订单不存在", zap.String("out_trade_no", outTradeNo))
|
||||
return nil, fmt.Errorf("微信订单不存在")
|
||||
}
|
||||
|
||||
// 如果订单状态为pending,主动查询微信订单状态
|
||||
if wechatOrder.Status == finance_entities.WechatOrderStatusPending {
|
||||
s.logger.Info("订单状态为pending,主动查询微信订单状态",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
|
||||
// 调用微信查询接口
|
||||
transaction, err := s.wechatPayService.QueryOrderStatus(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查询微信订单状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 查询失败不影响返回,继续使用数据库中的状态
|
||||
} else {
|
||||
// 解析微信返回的状态
|
||||
tradeState := ""
|
||||
transactionID := ""
|
||||
if transaction.TradeState != nil {
|
||||
tradeState = *transaction.TradeState
|
||||
}
|
||||
if transaction.TransactionId != nil {
|
||||
transactionID = *transaction.TransactionId
|
||||
}
|
||||
|
||||
s.logger.Info("微信查询订单状态返回",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_state", tradeState),
|
||||
zap.String("transaction_id", transactionID),
|
||||
)
|
||||
|
||||
// 使用公共方法更新订单状态
|
||||
err = s.updateWechatOrderStatus(ctx, outTradeNo, tradeState, transaction)
|
||||
if err != nil {
|
||||
s.logger.Error("更新微信订单状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_state", tradeState),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
// 重新获取更新后的订单信息
|
||||
updatedOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err == nil && updatedOrder != nil {
|
||||
wechatOrder = updatedOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否处理中
|
||||
isProcessing := wechatOrder.Status == finance_entities.WechatOrderStatusPending
|
||||
|
||||
// 判断是否可以重试(失败状态可以重试)
|
||||
canRetry := wechatOrder.Status == finance_entities.WechatOrderStatusFailed
|
||||
|
||||
// 转换为响应DTO
|
||||
response := &responses.WechatOrderStatusResponse{
|
||||
OutTradeNo: wechatOrder.OutTradeNo,
|
||||
TransactionID: wechatOrder.TradeNo,
|
||||
Status: string(wechatOrder.Status),
|
||||
Amount: wechatOrder.Amount,
|
||||
Subject: wechatOrder.Subject,
|
||||
Platform: wechatOrder.Platform,
|
||||
CreatedAt: wechatOrder.CreatedAt,
|
||||
UpdatedAt: wechatOrder.UpdatedAt,
|
||||
NotifyTime: wechatOrder.NotifyTime,
|
||||
ReturnTime: wechatOrder.ReturnTime,
|
||||
ErrorCode: &wechatOrder.ErrorCode,
|
||||
ErrorMessage: &wechatOrder.ErrorMessage,
|
||||
IsProcessing: isProcessing,
|
||||
CanRetry: canRetry,
|
||||
}
|
||||
|
||||
// 如果错误码为空,设置为nil
|
||||
if wechatOrder.ErrorCode == "" {
|
||||
response.ErrorCode = nil
|
||||
}
|
||||
if wechatOrder.ErrorMessage == "" {
|
||||
response.ErrorMessage = nil
|
||||
}
|
||||
|
||||
s.logger.Info("查询微信订单状态完成",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("status", string(wechatOrder.Status)),
|
||||
zap.Bool("is_processing", isProcessing),
|
||||
zap.Bool("can_retry", canRetry),
|
||||
)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// updateWechatOrderStatus 根据微信状态更新本地订单状态
|
||||
func (s *FinanceApplicationServiceImpl) updateWechatOrderStatus(ctx context.Context, outTradeNo string, tradeState string, transaction *payments.Transaction) error {
|
||||
// 查找微信订单
|
||||
wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查找微信订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err))
|
||||
return fmt.Errorf("查找微信订单失败: %w", err)
|
||||
}
|
||||
|
||||
if wechatOrder == nil {
|
||||
s.logger.Error("微信订单不存在", zap.String("out_trade_no", outTradeNo))
|
||||
return fmt.Errorf("微信订单不存在")
|
||||
}
|
||||
|
||||
switch tradeState {
|
||||
case payment.TradeStateSuccess:
|
||||
// 支付成功,调用公共处理逻辑
|
||||
transactionID := ""
|
||||
if transaction.TransactionId != nil {
|
||||
transactionID = *transaction.TransactionId
|
||||
}
|
||||
payAmount := decimal.Zero
|
||||
if transaction.Amount != nil && transaction.Amount.Total != nil {
|
||||
// 将分转换为元
|
||||
payAmount = decimal.NewFromInt(*transaction.Amount.Total).Div(decimal.NewFromInt(100))
|
||||
}
|
||||
return s.processWechatPaymentSuccess(ctx, outTradeNo, transactionID, payAmount)
|
||||
case payment.TradeStateClosed:
|
||||
// 交易关闭
|
||||
s.logger.Info("微信订单交易关闭",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
wechatOrder.MarkClosed()
|
||||
err = s.wechatOrderRepo.Update(ctx, *wechatOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("更新微信订单关闭状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
s.logger.Info("微信订单关闭状态更新成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
case payment.TradeStateNotPay:
|
||||
// 未支付,保持pending状态
|
||||
s.logger.Info("微信订单未支付",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
default:
|
||||
// 其他状态,记录日志
|
||||
s.logger.Info("微信订单其他状态",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_state", tradeState),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleWechatPayCallback 处理微信支付回调
|
||||
func (s *FinanceApplicationServiceImpl) HandleWechatPayCallback(ctx context.Context, r *http.Request) error {
|
||||
if s.wechatPayService == nil {
|
||||
s.logger.Error("微信支付服务未初始化")
|
||||
return fmt.Errorf("微信支付服务未初始化")
|
||||
}
|
||||
|
||||
// 解析并验证微信支付回调通知
|
||||
transaction, err := s.wechatPayService.HandleWechatPayNotification(ctx, r)
|
||||
if err != nil {
|
||||
s.logger.Error("微信支付回调验证失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 提取回调数据
|
||||
outTradeNo := ""
|
||||
if transaction.OutTradeNo != nil {
|
||||
outTradeNo = *transaction.OutTradeNo
|
||||
}
|
||||
transactionID := ""
|
||||
if transaction.TransactionId != nil {
|
||||
transactionID = *transaction.TransactionId
|
||||
}
|
||||
tradeState := ""
|
||||
if transaction.TradeState != nil {
|
||||
tradeState = *transaction.TradeState
|
||||
}
|
||||
totalAmount := decimal.Zero
|
||||
if transaction.Amount != nil && transaction.Amount.Total != nil {
|
||||
// 将分转换为元
|
||||
totalAmount = decimal.NewFromInt(*transaction.Amount.Total).Div(decimal.NewFromInt(100))
|
||||
}
|
||||
|
||||
// 记录回调数据
|
||||
s.logger.Info("微信支付回调数据",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("trade_state", tradeState),
|
||||
zap.String("total_amount", totalAmount.String()),
|
||||
)
|
||||
|
||||
// 检查交易状态
|
||||
if tradeState != payment.TradeStateSuccess {
|
||||
s.logger.Warn("微信支付交易未成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_state", tradeState),
|
||||
)
|
||||
return nil // 不返回错误,因为这是正常的业务状态
|
||||
}
|
||||
|
||||
// 处理支付成功逻辑
|
||||
err = s.processWechatPaymentSuccess(ctx, outTradeNo, transactionID, totalAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("处理微信支付成功失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", totalAmount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processWechatPaymentSuccess 处理微信支付成功的公共逻辑
|
||||
func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.Context, outTradeNo, transactionID string, amount decimal.Decimal) error {
|
||||
// 查找微信订单
|
||||
wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查找微信订单失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("查找微信订单失败: %w", err)
|
||||
}
|
||||
|
||||
if wechatOrder == nil {
|
||||
s.logger.Error("微信订单不存在",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
return fmt.Errorf("微信订单不存在")
|
||||
}
|
||||
|
||||
// 查找对应的充值记录
|
||||
rechargeRecord, err := s.rechargeRecordService.GetByID(ctx, wechatOrder.RechargeID)
|
||||
if err != nil {
|
||||
s.logger.Error("查找充值记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", wechatOrder.RechargeID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("查找充值记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查订单和充值记录状态,如果都已成功则跳过(只记录一次日志)
|
||||
if wechatOrder.Status == finance_entities.WechatOrderStatusSuccess && rechargeRecord.Status == finance_entities.RechargeStatusSuccess {
|
||||
s.logger.Info("微信支付订单已处理成功,跳过重复处理",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("order_id", wechatOrder.ID),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算充值赠送金额(复用支付宝的赠送逻辑)
|
||||
bonusAmount := decimal.Zero
|
||||
if len(s.config.Wallet.AliPayRechargeBonus) > 0 {
|
||||
for i := len(s.config.Wallet.AliPayRechargeBonus) - 1; i >= 0; i-- {
|
||||
rule := s.config.Wallet.AliPayRechargeBonus[i]
|
||||
if amount.GreaterThanOrEqual(decimal.NewFromFloat(rule.RechargeAmount)) {
|
||||
bonusAmount = decimal.NewFromFloat(rule.BonusAmount)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录开始处理支付成功
|
||||
s.logger.Info("开始处理微信支付成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
)
|
||||
|
||||
// 在事务中处理支付成功逻辑
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 更新微信订单状态
|
||||
wechatOrder.MarkSuccess(transactionID, "", "", amount, amount)
|
||||
now := time.Now()
|
||||
wechatOrder.NotifyTime = &now
|
||||
err := s.wechatOrderRepo.Update(txCtx, *wechatOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("更新微信订单状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新充值记录状态为成功
|
||||
rechargeRecord.MarkSuccess()
|
||||
err = s.rechargeRecordRepo.Update(txCtx, *rechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("更新充值记录状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果有赠送金额,创建赠送充值记录
|
||||
if bonusAmount.GreaterThan(decimal.Zero) {
|
||||
giftRechargeRecord := finance_entities.NewGiftRechargeRecord(rechargeRecord.UserID, bonusAmount, "充值活动赠送")
|
||||
createdGift, err := s.rechargeRecordRepo.Create(txCtx, *giftRechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("创建赠送充值记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
s.logger.Info("创建赠送充值记录成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("gift_recharge_id", createdGift.ID),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
)
|
||||
}
|
||||
|
||||
// 充值到钱包(包含赠送金额)
|
||||
totalRechargeAmount := amount.Add(bonusAmount)
|
||||
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalRechargeAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("充值到钱包失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("total_amount", totalRechargeAmount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("处理微信支付成功失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("微信支付成功处理完成",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleWechatRefundCallback 处理微信退款回调
|
||||
func (s *FinanceApplicationServiceImpl) HandleWechatRefundCallback(ctx context.Context, r *http.Request) error {
|
||||
if s.wechatPayService == nil {
|
||||
s.logger.Error("微信支付服务未初始化")
|
||||
return fmt.Errorf("微信支付服务未初始化")
|
||||
}
|
||||
|
||||
// 解析并验证微信退款回调通知
|
||||
refund, err := s.wechatPayService.HandleRefundNotification(ctx, r)
|
||||
if err != nil {
|
||||
s.logger.Error("微信退款回调验证失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录回调数据
|
||||
s.logger.Info("微信退款回调数据",
|
||||
zap.String("out_trade_no", func() string {
|
||||
if refund.OutTradeNo != nil {
|
||||
return *refund.OutTradeNo
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
zap.String("out_refund_no", func() string {
|
||||
if refund.OutRefundNo != nil {
|
||||
return *refund.OutRefundNo
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
zap.String("refund_id", func() string {
|
||||
if refund.RefundId != nil {
|
||||
return *refund.RefundId
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
zap.Any("status", func() interface{} {
|
||||
if refund.Status != nil {
|
||||
return *refund.Status
|
||||
}
|
||||
return nil
|
||||
}()),
|
||||
)
|
||||
|
||||
// 处理退款逻辑
|
||||
// 这里可以根据实际业务需求实现退款处理逻辑
|
||||
s.logger.Info("微信退款回调处理完成",
|
||||
zap.String("out_trade_no", func() string {
|
||||
if refund.OutTradeNo != nil {
|
||||
return *refund.OutTradeNo
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
zap.String("refund_id", func() string {
|
||||
if refund.RefundId != nil {
|
||||
return *refund.RefundId
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
|
||||
// 3. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -408,7 +408,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
||||
// 5. 构建响应DTO
|
||||
return &dto.AvailableAmountResponse{
|
||||
AvailableAmount: availableAmount,
|
||||
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+对公转账)
|
||||
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+微信充值+对公转账)
|
||||
TotalGifted: totalGifted,
|
||||
TotalInvoiced: totalInvoiced,
|
||||
PendingApplications: pendingAmount,
|
||||
@@ -417,7 +417,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
||||
|
||||
// calculateAvailableAmount 计算可开票金额(私有方法)
|
||||
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||
// 1. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
|
||||
// 1. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||
if err != nil {
|
||||
return decimal.Zero, err
|
||||
@@ -433,7 +433,7 @@ func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Con
|
||||
fmt.Println("totalInvoiced", totalInvoiced)
|
||||
fmt.Println("pendingAmount", pendingAmount)
|
||||
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
|
||||
// 可开票金额 = 真实充值金额(支付宝充值+对公转账) - 已开票金额 - 待处理申请金额
|
||||
// 可开票金额 = 真实充值金额(支付宝充值+微信充值+对公转账) - 已开票金额 - 待处理申请金额
|
||||
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
|
||||
fmt.Println("availableAmount", availableAmount)
|
||||
// 确保可开票金额不为负数
|
||||
@@ -452,16 +452,16 @@ func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, us
|
||||
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 计算真实充值金额(支付宝充值 + 对公转账)和总赠送金额
|
||||
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 对公转账
|
||||
// 2. 计算真实充值金额(支付宝充值 + 微信充值 + 对公转账)和总赠送金额
|
||||
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 微信充值 + 对公转账
|
||||
var totalGifted decimal.Decimal // 总赠送金额
|
||||
for _, record := range rechargeRecords {
|
||||
if record.IsSuccess() {
|
||||
if record.RechargeType == entities.RechargeTypeGift {
|
||||
// 赠送金额不计入可开票金额
|
||||
totalGifted = totalGifted.Add(record.Amount)
|
||||
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeTransfer {
|
||||
// 只有支付宝充值和对公转账计入可开票金额
|
||||
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeWechat || record.RechargeType == entities.RechargeTypeTransfer {
|
||||
// 支付宝充值、微信充值和对公转账计入可开票金额
|
||||
realRecharged = realRecharged.Add(record.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ type Config struct {
|
||||
Zhicha ZhichaConfig `mapstructure:"zhicha"`
|
||||
Muzi MuziConfig `mapstructure:"muzi"`
|
||||
AliPay AliPayConfig `mapstructure:"alipay"`
|
||||
Wxpay WxpayConfig `mapstructure:"wxpay"`
|
||||
WechatMini WechatMiniConfig `mapstructure:"wechat_mini"`
|
||||
WechatH5 WechatH5Config `mapstructure:"wechat_h5"`
|
||||
Yushan YushanConfig `mapstructure:"yushan"`
|
||||
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
|
||||
Alicloud AlicloudConfig `mapstructure:"alicloud"`
|
||||
@@ -429,6 +432,29 @@ type AliPayConfig struct {
|
||||
ReturnURL string `mapstructure:"return_url"`
|
||||
}
|
||||
|
||||
// WxpayConfig 微信支付配置
|
||||
type WxpayConfig struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
MchID string `mapstructure:"mch_id"`
|
||||
MchCertificateSerialNumber string `mapstructure:"mch_certificate_serial_number"`
|
||||
MchApiv3Key string `mapstructure:"mch_apiv3_key"`
|
||||
MchPrivateKeyPath string `mapstructure:"mch_private_key_path"`
|
||||
MchPublicKeyID string `mapstructure:"mch_public_key_id"`
|
||||
MchPublicKeyPath string `mapstructure:"mch_public_key_path"`
|
||||
NotifyUrl string `mapstructure:"notify_url"`
|
||||
RefundNotifyUrl string `mapstructure:"refund_notify_url"`
|
||||
}
|
||||
|
||||
// WechatMiniConfig 微信小程序配置
|
||||
type WechatMiniConfig struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
}
|
||||
|
||||
// WechatH5Config 微信H5配置
|
||||
type WechatH5Config struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
}
|
||||
|
||||
// YushanConfig 羽山配置
|
||||
type YushanConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
|
||||
@@ -307,6 +307,16 @@ func NewContainer() *Container {
|
||||
}
|
||||
return payment.NewAliPayService(config)
|
||||
},
|
||||
// 微信支付服务
|
||||
func(cfg *config.Config, logger *zap.Logger) *payment.WechatPayService {
|
||||
// 根据配置选择初始化方式,默认使用平台证书方式
|
||||
initType := payment.InitTypePlatformCert
|
||||
// 如果配置了公钥ID,使用公钥方式
|
||||
if cfg.Wxpay.MchPublicKeyID != "" {
|
||||
initType = payment.InitTypeWxPayPubKey
|
||||
}
|
||||
return payment.NewWechatPayService(*cfg, initType, logger)
|
||||
},
|
||||
// 导出管理器
|
||||
func(logger *zap.Logger) *export.ExportManager {
|
||||
return export.NewExportManager(logger)
|
||||
@@ -512,6 +522,11 @@ func NewContainer() *Container {
|
||||
finance_repo.NewGormAlipayOrderRepository,
|
||||
fx.As(new(domain_finance_repo.AlipayOrderRepository)),
|
||||
),
|
||||
// 微信订单仓储
|
||||
fx.Annotate(
|
||||
finance_repo.NewGormWechatOrderRepository,
|
||||
fx.As(new(domain_finance_repo.WechatOrderRepository)),
|
||||
),
|
||||
// 发票申请仓储
|
||||
fx.Annotate(
|
||||
finance_repo.NewGormInvoiceApplicationRepository,
|
||||
@@ -855,10 +870,13 @@ func NewContainer() *Container {
|
||||
fx.Annotate(
|
||||
func(
|
||||
aliPayClient *payment.AliPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
rechargeRecordService finance_services.RechargeRecordService,
|
||||
walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
|
||||
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||
userRepo domain_user_repo.UserRepository,
|
||||
txManager *shared_database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
@@ -867,10 +885,13 @@ func NewContainer() *Container {
|
||||
) finance.FinanceApplicationService {
|
||||
return finance.NewFinanceApplicationService(
|
||||
aliPayClient,
|
||||
wechatPayService,
|
||||
walletService,
|
||||
rechargeRecordService,
|
||||
walletTransactionRepo,
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
rechargeRecordRepo,
|
||||
userRepo,
|
||||
txManager,
|
||||
logger,
|
||||
|
||||
@@ -1,140 +1,28 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
import "github.com/shopspring/decimal"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AlipayOrderStatus 支付宝订单状态枚举
|
||||
type AlipayOrderStatus string
|
||||
// AlipayOrderStatus 支付宝订单状态枚举(别名)
|
||||
type AlipayOrderStatus = PayOrderStatus
|
||||
|
||||
const (
|
||||
AlipayOrderStatusPending AlipayOrderStatus = "pending" // 待支付
|
||||
AlipayOrderStatusSuccess AlipayOrderStatus = "success" // 支付成功
|
||||
AlipayOrderStatusFailed AlipayOrderStatus = "failed" // 支付失败
|
||||
AlipayOrderStatusCancelled AlipayOrderStatus = "cancelled" // 已取消
|
||||
AlipayOrderStatusClosed AlipayOrderStatus = "closed" // 已关闭
|
||||
AlipayOrderStatusPending AlipayOrderStatus = PayOrderStatusPending // 待支付
|
||||
AlipayOrderStatusSuccess AlipayOrderStatus = PayOrderStatusSuccess // 支付成功
|
||||
AlipayOrderStatusFailed AlipayOrderStatus = PayOrderStatusFailed // 支付失败
|
||||
AlipayOrderStatusCancelled AlipayOrderStatus = PayOrderStatusCancelled // 已取消
|
||||
AlipayOrderStatusClosed AlipayOrderStatus = PayOrderStatusClosed // 已关闭
|
||||
)
|
||||
|
||||
const (
|
||||
AlipayOrderPlatformApp = "app" // 支付宝APP支付
|
||||
AlipayOrderPlatformH5 = "h5" // 支付宝H5支付
|
||||
AlipayOrderPlatformPC = "pc" // 支付宝PC支付
|
||||
AlipayOrderPlatformApp = "app" // 支付宝APP支付
|
||||
AlipayOrderPlatformH5 = "h5" // 支付宝H5支付
|
||||
AlipayOrderPlatformPC = "pc" // 支付宝PC支付
|
||||
)
|
||||
|
||||
// AlipayOrder 支付宝订单详情实体
|
||||
type AlipayOrder struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"支付宝订单唯一标识"`
|
||||
RechargeID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"recharge_id" comment:"关联充值记录ID"`
|
||||
OutTradeNo string `gorm:"type:varchar(64);not null;uniqueIndex" json:"out_trade_no" comment:"商户订单号"`
|
||||
TradeNo *string `gorm:"type:varchar(64);uniqueIndex" json:"trade_no,omitempty" comment:"支付宝交易号"`
|
||||
|
||||
// 订单信息
|
||||
Subject string `gorm:"type:varchar(200);not null" json:"subject" comment:"订单标题"`
|
||||
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"订单金额"`
|
||||
Platform string `gorm:"type:varchar(20);not null" json:"platform" comment:"支付平台:app/h5/pc"`
|
||||
Status AlipayOrderStatus `gorm:"type:varchar(20);not null;default:'pending';index" json:"status" comment:"订单状态"`
|
||||
|
||||
// 支付宝返回信息
|
||||
BuyerID string `gorm:"type:varchar(64)" json:"buyer_id,omitempty" comment:"买家支付宝用户ID"`
|
||||
SellerID string `gorm:"type:varchar(64)" json:"seller_id,omitempty" comment:"卖家支付宝用户ID"`
|
||||
PayAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"pay_amount,omitempty" comment:"实际支付金额"`
|
||||
ReceiptAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"receipt_amount,omitempty" comment:"实收金额"`
|
||||
|
||||
// 回调信息
|
||||
NotifyTime *time.Time `gorm:"index" json:"notify_time,omitempty" comment:"异步通知时间"`
|
||||
ReturnTime *time.Time `gorm:"index" json:"return_time,omitempty" comment:"同步返回时间"`
|
||||
|
||||
// 错误信息
|
||||
ErrorCode string `gorm:"type:varchar(64)" json:"error_code,omitempty" comment:"错误码"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message,omitempty" comment:"错误信息"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (AlipayOrder) TableName() string {
|
||||
return "alipay_orders"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (a *AlipayOrder) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPending 检查是否为待支付状态
|
||||
func (a *AlipayOrder) IsPending() bool {
|
||||
return a.Status == AlipayOrderStatusPending
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否为支付成功状态
|
||||
func (a *AlipayOrder) IsSuccess() bool {
|
||||
return a.Status == AlipayOrderStatusSuccess
|
||||
}
|
||||
|
||||
// IsFailed 检查是否为支付失败状态
|
||||
func (a *AlipayOrder) IsFailed() bool {
|
||||
return a.Status == AlipayOrderStatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled 检查是否为已取消状态
|
||||
func (a *AlipayOrder) IsCancelled() bool {
|
||||
return a.Status == AlipayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// IsClosed 检查是否为已关闭状态
|
||||
func (a *AlipayOrder) IsClosed() bool {
|
||||
return a.Status == AlipayOrderStatusClosed
|
||||
}
|
||||
|
||||
// MarkSuccess 标记为支付成功
|
||||
func (a *AlipayOrder) MarkSuccess(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) {
|
||||
a.Status = AlipayOrderStatusSuccess
|
||||
a.TradeNo = &tradeNo
|
||||
a.BuyerID = buyerID
|
||||
a.SellerID = sellerID
|
||||
a.PayAmount = payAmount
|
||||
a.ReceiptAmount = receiptAmount
|
||||
now := time.Now()
|
||||
a.NotifyTime = &now
|
||||
}
|
||||
|
||||
// MarkFailed 标记为支付失败
|
||||
func (a *AlipayOrder) MarkFailed(errorCode, errorMessage string) {
|
||||
a.Status = AlipayOrderStatusFailed
|
||||
a.ErrorCode = errorCode
|
||||
a.ErrorMessage = errorMessage
|
||||
}
|
||||
|
||||
// MarkCancelled 标记为已取消
|
||||
func (a *AlipayOrder) MarkCancelled() {
|
||||
a.Status = AlipayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// MarkClosed 标记为已关闭
|
||||
func (a *AlipayOrder) MarkClosed() {
|
||||
a.Status = AlipayOrderStatusClosed
|
||||
}
|
||||
// AlipayOrder 支付宝订单实体(统一表 typay_orders,兼容多支付渠道)
|
||||
type AlipayOrder = PayOrder
|
||||
|
||||
// NewAlipayOrder 工厂方法 - 创建支付宝订单
|
||||
func NewAlipayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *AlipayOrder {
|
||||
return &AlipayOrder{
|
||||
ID: uuid.New().String(),
|
||||
RechargeID: rechargeID,
|
||||
OutTradeNo: outTradeNo,
|
||||
Subject: subject,
|
||||
Amount: amount,
|
||||
Platform: platform,
|
||||
Status: AlipayOrderStatusPending,
|
||||
}
|
||||
return NewPayOrder(rechargeID, outTradeNo, subject, amount, platform, "alipay")
|
||||
}
|
||||
|
||||
136
internal/domains/finance/entities/pay_order.go
Normal file
136
internal/domains/finance/entities/pay_order.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ type RechargeType string
|
||||
|
||||
const (
|
||||
RechargeTypeAlipay RechargeType = "alipay" // 支付宝充值
|
||||
RechargeTypeWechat RechargeType = "wechat" // 微信充值
|
||||
RechargeTypeTransfer RechargeType = "transfer" // 对公转账
|
||||
RechargeTypeGift RechargeType = "gift" // 赠送
|
||||
)
|
||||
@@ -42,6 +43,7 @@ type RechargeRecord struct {
|
||||
|
||||
// 订单号字段(根据充值类型使用不同字段)
|
||||
AlipayOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"alipay_order_id,omitempty" comment:"支付宝订单号"`
|
||||
WechatOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"wechat_order_id,omitempty" comment:"微信订单号"`
|
||||
TransferOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"transfer_order_id,omitempty" comment:"转账订单号"`
|
||||
|
||||
// 通用字段
|
||||
@@ -104,14 +106,24 @@ func (r *RechargeRecord) MarkCancelled() {
|
||||
// ValidatePaymentMethod 验证支付方式:支付宝订单号和转账订单号只能有一个存在
|
||||
func (r *RechargeRecord) ValidatePaymentMethod() error {
|
||||
hasAlipay := r.AlipayOrderID != nil && *r.AlipayOrderID != ""
|
||||
hasWechat := r.WechatOrderID != nil && *r.WechatOrderID != ""
|
||||
hasTransfer := r.TransferOrderID != nil && *r.TransferOrderID != ""
|
||||
|
||||
if hasAlipay && hasTransfer {
|
||||
return errors.New("支付宝订单号和转账订单号不能同时存在")
|
||||
count := 0
|
||||
if hasAlipay {
|
||||
count++
|
||||
}
|
||||
|
||||
if !hasAlipay && !hasTransfer {
|
||||
return errors.New("必须提供支付宝订单号或转账订单号")
|
||||
if hasWechat {
|
||||
count++
|
||||
}
|
||||
if hasTransfer {
|
||||
count++
|
||||
}
|
||||
if count > 1 {
|
||||
return errors.New("支付宝、微信或转账订单号只能存在一个")
|
||||
}
|
||||
if count == 0 {
|
||||
return errors.New("必须提供支付宝、微信或转账订单号")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -124,6 +136,10 @@ func (r *RechargeRecord) GetOrderID() string {
|
||||
if r.AlipayOrderID != nil {
|
||||
return *r.AlipayOrderID
|
||||
}
|
||||
case RechargeTypeWechat:
|
||||
if r.WechatOrderID != nil {
|
||||
return *r.WechatOrderID
|
||||
}
|
||||
case RechargeTypeTransfer:
|
||||
if r.TransferOrderID != nil {
|
||||
return *r.TransferOrderID
|
||||
@@ -137,6 +153,11 @@ func (r *RechargeRecord) SetAlipayOrderID(orderID string) {
|
||||
r.AlipayOrderID = &orderID
|
||||
}
|
||||
|
||||
// SetWechatOrderID 设置微信订单号
|
||||
func (r *RechargeRecord) SetWechatOrderID(orderID string) {
|
||||
r.WechatOrderID = &orderID
|
||||
}
|
||||
|
||||
// SetTransferOrderID 设置转账订单号
|
||||
func (r *RechargeRecord) SetTransferOrderID(orderID string) {
|
||||
r.TransferOrderID = &orderID
|
||||
@@ -153,6 +174,17 @@ func NewAlipayRechargeRecord(userID string, amount decimal.Decimal, alipayOrderI
|
||||
}
|
||||
}
|
||||
|
||||
// NewWechatRechargeRecord 工厂方法 - 创建微信充值记录
|
||||
func NewWechatRechargeRecord(userID string, amount decimal.Decimal, wechatOrderID string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
RechargeType: RechargeTypeWechat,
|
||||
Status: RechargeStatusPending,
|
||||
WechatOrderID: &wechatOrderID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTransferRechargeRecord 工厂方法 - 创建对公转账充值记录
|
||||
func NewTransferRechargeRecord(userID string, amount decimal.Decimal, transferOrderID, notes string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
|
||||
33
internal/domains/finance/entities/wechat_order.go
Normal file
33
internal/domains/finance/entities/wechat_order.go
Normal 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)
|
||||
}
|
||||
@@ -17,4 +17,4 @@ type AlipayOrderRepository interface {
|
||||
UpdateStatus(ctx context.Context, id string, status entities.AlipayOrderStatus) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
Exists(ctx context.Context, id string) (bool, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,20 +17,20 @@ type RechargeRecordRepository interface {
|
||||
GetByTransferOrderID(ctx context.Context, transferOrderID string) (*entities.RechargeRecord, error)
|
||||
Update(ctx context.Context, record entities.RechargeRecord) error
|
||||
UpdateStatus(ctx context.Context, id string, status entities.RechargeStatus) error
|
||||
|
||||
|
||||
// 管理员查询方法
|
||||
List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error)
|
||||
Count(ctx context.Context, options interfaces.CountOptions) (int64, error)
|
||||
|
||||
|
||||
// 统计相关方法
|
||||
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
|
||||
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error)
|
||||
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalAmount(ctx context.Context) (float64, error)
|
||||
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -20,7 +20,6 @@ type UserStats struct {
|
||||
// UserRepository 用户仓储接口
|
||||
type UserRepository interface {
|
||||
interfaces.Repository[entities.User]
|
||||
|
||||
// 基础查询 - 直接使用实体
|
||||
GetByPhone(ctx context.Context, phone string) (*entities.User, error)
|
||||
GetByUsername(ctx context.Context, username string) (*entities.User, error)
|
||||
@@ -48,7 +47,7 @@ type UserRepository interface {
|
||||
// 统计信息
|
||||
GetStats(ctx context.Context) (*UserStats, error)
|
||||
GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*UserStats, error)
|
||||
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemUserStats(ctx context.Context) (*UserStats, error)
|
||||
GetSystemUserStatsByDateRange(ctx context.Context, startDate, endDate time.Time) (*UserStats, error)
|
||||
@@ -56,7 +55,7 @@ type UserRepository interface {
|
||||
GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemDailyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
|
||||
// 排行榜查询方法
|
||||
GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
GetUserCallRankingByConsumption(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
@@ -119,4 +118,4 @@ type EnterpriseInfoRepository interface {
|
||||
Count(ctx context.Context, options interfaces.CountOptions) (int64, error)
|
||||
List(ctx context.Context, options interfaces.ListOptions) ([]entities.EnterpriseInfo, error)
|
||||
Exists(ctx context.Context, id string) (bool, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
AlipayOrdersTable = "alipay_orders"
|
||||
AlipayOrdersTable = "typay_orders"
|
||||
)
|
||||
|
||||
type GormAlipayOrderRepository struct {
|
||||
@@ -72,9 +72,9 @@ func (r *GormAlipayOrderRepository) GetByRechargeID(ctx context.Context, recharg
|
||||
func (r *GormAlipayOrderRepository) GetByUserID(ctx context.Context, userID string) ([]entities.AlipayOrder, error) {
|
||||
var orders []entities.AlipayOrder
|
||||
err := r.GetDB(ctx).
|
||||
Joins("JOIN recharge_records ON alipay_orders.recharge_id = recharge_records.id").
|
||||
Joins("JOIN recharge_records ON typay_orders.recharge_id = recharge_records.id").
|
||||
Where("recharge_records.user_id = ?", userID).
|
||||
Order("alipay_orders.created_at DESC").
|
||||
Order("typay_orders.created_at DESC").
|
||||
Find(&orders).Error
|
||||
return orders, err
|
||||
}
|
||||
@@ -95,4 +95,4 @@ func (r *GormAlipayOrderRepository) Exists(ctx context.Context, id string) (bool
|
||||
var count int64
|
||||
err := r.GetDB(ctx).Model(&entities.AlipayOrder{}).Where("id = ?", id).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,11 +163,11 @@ func (r *GormRechargeRecordRepository) Count(ctx context.Context, options interf
|
||||
}
|
||||
if options.Search != "" {
|
||||
if hasCompanyNameFilter {
|
||||
query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ? OR rr.wechat_order_id LIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
} else {
|
||||
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ? OR wechat_order_id LIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
}
|
||||
}
|
||||
return count, query.Count(&count).Error
|
||||
@@ -267,11 +267,11 @@ func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfa
|
||||
|
||||
if options.Search != "" {
|
||||
if hasCompanyNameFilter {
|
||||
query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ? OR rr.wechat_order_id LIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
} else {
|
||||
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ? OR wechat_order_id LIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -19,12 +21,12 @@ import (
|
||||
|
||||
// FinanceHandler 财务HTTP处理器
|
||||
type FinanceHandler struct {
|
||||
appService finance.FinanceApplicationService
|
||||
invoiceAppService finance.InvoiceApplicationService
|
||||
adminInvoiceAppService finance.AdminInvoiceApplicationService
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
appService finance.FinanceApplicationService
|
||||
invoiceAppService finance.InvoiceApplicationService
|
||||
adminInvoiceAppService finance.AdminInvoiceApplicationService
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewFinanceHandler 创建财务HTTP处理器
|
||||
@@ -201,6 +203,123 @@ func (h *FinanceHandler) HandleAlipayCallback(c *gin.Context) {
|
||||
c.String(200, "success")
|
||||
}
|
||||
|
||||
// HandleWechatPayCallback 处理微信支付回调
|
||||
// @Summary 微信支付回调
|
||||
// @Description 处理微信支付异步通知
|
||||
// @Tags 支付管理
|
||||
// @Accept application/json
|
||||
// @Produce text/plain
|
||||
// @Success 200 {string} string "success"
|
||||
// @Failure 400 {string} string "fail"
|
||||
// @Router /api/v1/pay/wechat/callback [post]
|
||||
func (h *FinanceHandler) HandleWechatPayCallback(c *gin.Context) {
|
||||
// 记录回调请求信息
|
||||
h.logger.Info("收到微信支付回调请求",
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("url", c.Request.URL.String()),
|
||||
zap.String("remote_addr", c.ClientIP()),
|
||||
zap.String("user_agent", c.GetHeader("User-Agent")),
|
||||
zap.String("content_type", c.GetHeader("Content-Type")),
|
||||
)
|
||||
|
||||
// 读取请求体内容用于调试(注意:读取后需要重新设置,否则后续解析会失败)
|
||||
bodyBytes, err := c.GetRawData()
|
||||
if err == nil && len(bodyBytes) > 0 {
|
||||
h.logger.Info("微信支付回调请求体",
|
||||
zap.String("body", string(bodyBytes)),
|
||||
zap.Int("body_size", len(bodyBytes)),
|
||||
)
|
||||
// 重新设置请求体,供后续解析使用
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
}
|
||||
|
||||
// 通过应用服务处理微信支付回调
|
||||
err = h.appService.HandleWechatPayCallback(c.Request.Context(), c.Request)
|
||||
if err != nil {
|
||||
h.logger.Error("微信支付回调处理失败",
|
||||
zap.Error(err),
|
||||
zap.String("remote_addr", c.ClientIP()),
|
||||
)
|
||||
c.String(400, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("微信支付回调处理成功", zap.String("remote_addr", c.ClientIP()))
|
||||
// 返回成功响应(微信要求返回success)
|
||||
c.String(200, "success")
|
||||
}
|
||||
|
||||
// HandleWechatRefundCallback 处理微信退款回调
|
||||
// @Summary 微信退款回调
|
||||
// @Description 处理微信退款异步通知
|
||||
// @Tags 支付管理
|
||||
// @Accept application/json
|
||||
// @Produce text/plain
|
||||
// @Success 200 {string} string "success"
|
||||
// @Failure 400 {string} string "fail"
|
||||
// @Router /api/v1/wechat/refund_callback [post]
|
||||
func (h *FinanceHandler) HandleWechatRefundCallback(c *gin.Context) {
|
||||
// 记录回调请求信息
|
||||
h.logger.Info("收到微信退款回调请求",
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("url", c.Request.URL.String()),
|
||||
zap.String("remote_addr", c.ClientIP()),
|
||||
zap.String("user_agent", c.GetHeader("User-Agent")),
|
||||
)
|
||||
|
||||
// 通过应用服务处理微信退款回调
|
||||
err := h.appService.HandleWechatRefundCallback(c.Request.Context(), c.Request)
|
||||
if err != nil {
|
||||
h.logger.Error("微信退款回调处理失败", zap.Error(err))
|
||||
c.String(400, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回成功响应(微信要求返回success)
|
||||
c.String(200, "success")
|
||||
}
|
||||
|
||||
// GetWechatOrderStatus 获取微信订单状态
|
||||
// @Summary 获取微信订单状态
|
||||
// @Description 根据商户订单号查询微信订单状态
|
||||
// @Tags 钱包管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param out_trade_no query string true "商户订单号"
|
||||
// @Success 200 {object} responses.WechatOrderStatusResponse "获取订单状态成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 404 {object} map[string]interface{} "订单不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/finance/wallet/wechat-order-status [get]
|
||||
func (h *FinanceHandler) GetWechatOrderStatus(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
outTradeNo := c.Query("out_trade_no")
|
||||
if outTradeNo == "" {
|
||||
h.responseBuilder.BadRequest(c, "缺少商户订单号")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.appService.GetWechatOrderStatus(c.Request.Context(), outTradeNo)
|
||||
if err != nil {
|
||||
h.logger.Error("获取微信订单状态失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
h.responseBuilder.BadRequest(c, "获取订单状态失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取订单状态成功")
|
||||
}
|
||||
|
||||
// HandleAlipayReturn 处理支付宝同步回调
|
||||
// @Summary 支付宝同步回调
|
||||
// @Description 处理支付宝同步支付通知,跳转到前端成功页面
|
||||
@@ -240,7 +359,7 @@ func (h *FinanceHandler) HandleAlipayReturn(c *gin.Context) {
|
||||
// 通过应用服务处理同步回调,查询订单状态
|
||||
orderStatus, err := h.appService.HandleAlipayReturn(c.Request.Context(), outTradeNo)
|
||||
if err != nil {
|
||||
h.logger.Error("支付宝同步回调处理失败",
|
||||
h.logger.Error("支付宝同步回调处理失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err))
|
||||
h.redirectToFailPage(c, outTradeNo, "订单处理失败")
|
||||
@@ -257,7 +376,7 @@ func (h *FinanceHandler) HandleAlipayReturn(c *gin.Context) {
|
||||
switch orderStatus {
|
||||
case "TRADE_SUCCESS":
|
||||
// 支付成功,跳转到前端成功页面
|
||||
successURL := fmt.Sprintf("%s/finance/wallet/success?out_trade_no=%s&trade_no=%s&amount=%s",
|
||||
successURL := fmt.Sprintf("%s/finance/wallet/success?out_trade_no=%s&trade_no=%s&amount=%s",
|
||||
frontendDomain, outTradeNo, tradeNo, totalAmount)
|
||||
c.Redirect(http.StatusFound, successURL)
|
||||
case "WAIT_BUYER_PAY":
|
||||
@@ -275,8 +394,8 @@ func (h *FinanceHandler) redirectToFailPage(c *gin.Context, outTradeNo, reason s
|
||||
if gin.Mode() == gin.DebugMode {
|
||||
frontendDomain = "http://localhost:5173"
|
||||
}
|
||||
|
||||
failURL := fmt.Sprintf("%s/finance/wallet/fail?out_trade_no=%s&reason=%s",
|
||||
|
||||
failURL := fmt.Sprintf("%s/finance/wallet/fail?out_trade_no=%s&reason=%s",
|
||||
frontendDomain, outTradeNo, reason)
|
||||
c.Redirect(http.StatusFound, failURL)
|
||||
}
|
||||
@@ -287,8 +406,8 @@ func (h *FinanceHandler) redirectToProcessingPage(c *gin.Context, outTradeNo, am
|
||||
if gin.Mode() == gin.DebugMode {
|
||||
frontendDomain = "http://localhost:5173"
|
||||
}
|
||||
|
||||
processingURL := fmt.Sprintf("%s/finance/wallet/processing?out_trade_no=%s&amount=%s",
|
||||
|
||||
processingURL := fmt.Sprintf("%s/finance/wallet/processing?out_trade_no=%s&amount=%s",
|
||||
frontendDomain, outTradeNo, amount)
|
||||
c.Redirect(http.StatusFound, processingURL)
|
||||
}
|
||||
@@ -319,7 +438,6 @@ func (h *FinanceHandler) CreateAlipayRecharge(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 调用应用服务进行完整的业务流程编排
|
||||
result, err := h.appService.CreateAlipayRechargeOrder(c.Request.Context(), &cmd)
|
||||
if err != nil {
|
||||
@@ -343,6 +461,53 @@ func (h *FinanceHandler) CreateAlipayRecharge(c *gin.Context) {
|
||||
h.responseBuilder.Success(c, result, "支付宝充值订单创建成功")
|
||||
}
|
||||
|
||||
// CreateWechatRecharge 创建微信充值订单
|
||||
// @Summary 创建微信充值订单
|
||||
// @Description 创建微信充值订单并返回预支付数据
|
||||
// @Tags 钱包管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body commands.CreateWechatRechargeCommand true "微信充值请求"
|
||||
// @Success 200 {object} responses.WechatRechargeOrderResponse "创建充值订单成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/finance/wallet/wechat-recharge [post]
|
||||
func (h *FinanceHandler) CreateWechatRecharge(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
var cmd commands.CreateWechatRechargeCommand
|
||||
cmd.UserID = userID
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.appService.CreateWechatRechargeOrder(c.Request.Context(), &cmd)
|
||||
if err != nil {
|
||||
h.logger.Error("创建微信充值订单失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", cmd.Amount),
|
||||
zap.Error(err),
|
||||
)
|
||||
h.responseBuilder.BadRequest(c, "创建微信充值订单失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("微信充值订单创建成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("out_trade_no", result.OutTradeNo),
|
||||
zap.String("amount", cmd.Amount),
|
||||
zap.String("platform", cmd.Platform),
|
||||
)
|
||||
|
||||
h.responseBuilder.Success(c, result, "微信充值订单创建成功")
|
||||
}
|
||||
|
||||
// TransferRecharge 管理员对公转账充值
|
||||
func (h *FinanceHandler) TransferRecharge(c *gin.Context) {
|
||||
var cmd commands.TransferRechargeCommand
|
||||
@@ -849,8 +1014,6 @@ func (h *FinanceHandler) ApproveInvoiceApplication(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "通过发票申请成功")
|
||||
}
|
||||
|
||||
@@ -932,14 +1095,14 @@ func (h *FinanceHandler) AdminDownloadInvoiceFile(c *gin.Context) {
|
||||
// @Router /api/v1/debug/event-system [post]
|
||||
func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
|
||||
h.logger.Info("🔍 请求事件系统调试信息")
|
||||
|
||||
|
||||
// 这里可以添加事件系统的状态信息
|
||||
// 暂时返回基本信息
|
||||
debugInfo := map[string]interface{}{
|
||||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"message": "事件系统调试端点已启用",
|
||||
"handler": "FinanceHandler",
|
||||
"message": "事件系统调试端点已启用",
|
||||
"handler": "FinanceHandler",
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, debugInfo, "事件系统调试信息")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -11,9 +13,6 @@ import (
|
||||
"tyapi-server/internal/application/product/dto/queries"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ProductAdminHandler 产品管理员HTTP处理器
|
||||
@@ -1338,7 +1337,7 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) {
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param user_id query string false "用户ID"
|
||||
// @Param recharge_type query string false "充值类型" Enums(alipay, transfer, gift)
|
||||
// @Param recharge_type query string false "充值类型" Enums(alipay, wechat, transfer, gift)
|
||||
// @Param status query string false "状态" Enums(pending, success, failed)
|
||||
// @Param min_amount query string false "最小金额"
|
||||
// @Param max_amount query string false "最大金额"
|
||||
@@ -1425,7 +1424,7 @@ func (h *ProductAdminHandler) GetAdminRechargeRecords(c *gin.Context) {
|
||||
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv
|
||||
// @Security Bearer
|
||||
// @Param user_ids query string false "用户ID列表,逗号分隔"
|
||||
// @Param recharge_type query string false "充值类型" Enums(alipay, transfer, gift)
|
||||
// @Param recharge_type query string false "充值类型" Enums(alipay, wechat, transfer, gift)
|
||||
// @Param status query string false "状态" Enums(pending, success, failed)
|
||||
// @Param start_time query string false "开始时间" format(date-time)
|
||||
// @Param end_time query string false "结束时间" format(date-time)
|
||||
|
||||
@@ -42,6 +42,18 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
alipayGroup.GET("/return", r.financeHandler.HandleAlipayReturn) // 支付宝同步回调
|
||||
}
|
||||
|
||||
// 微信支付回调路由(不需要认证)
|
||||
wechatPayGroup := engine.Group("/api/v1/pay/wechat")
|
||||
{
|
||||
wechatPayGroup.POST("/callback", r.financeHandler.HandleWechatPayCallback) // 微信支付异步回调
|
||||
}
|
||||
|
||||
// 微信退款回调路由(不需要认证)
|
||||
wechatRefundGroup := engine.Group("/api/v1/wechat")
|
||||
{
|
||||
wechatRefundGroup.POST("/refund_callback", r.financeHandler.HandleWechatRefundCallback) // 微信退款异步回调
|
||||
}
|
||||
|
||||
// 财务路由组,需要用户认证
|
||||
financeGroup := engine.Group("/api/v1/finance")
|
||||
financeGroup.Use(r.authMiddleware.Handle())
|
||||
@@ -49,12 +61,14 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
// 钱包相关路由
|
||||
walletGroup := financeGroup.Group("/wallet")
|
||||
{
|
||||
walletGroup.GET("", r.financeHandler.GetWallet) // 获取钱包信息
|
||||
walletGroup.GET("/transactions", r.financeHandler.GetUserWalletTransactions) // 获取钱包交易记录
|
||||
walletGroup.GET("/recharge-config", r.financeHandler.GetRechargeConfig) // 获取充值配置
|
||||
walletGroup.POST("/alipay-recharge", r.financeHandler.CreateAlipayRecharge) // 创建支付宝充值订单
|
||||
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页
|
||||
walletGroup.GET("", r.financeHandler.GetWallet) // 获取钱包信息
|
||||
walletGroup.GET("/transactions", r.financeHandler.GetUserWalletTransactions) // 获取钱包交易记录
|
||||
walletGroup.GET("/recharge-config", r.financeHandler.GetRechargeConfig) // 获取充值配置
|
||||
walletGroup.POST("/alipay-recharge", r.financeHandler.CreateAlipayRecharge) // 创建支付宝充值订单
|
||||
walletGroup.POST("/wechat-recharge", r.financeHandler.CreateWechatRecharge) // 创建微信充值订单
|
||||
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页
|
||||
walletGroup.GET("/alipay-order-status", r.financeHandler.GetAlipayOrderStatus) // 获取支付宝订单状态
|
||||
walletGroup.GET("/wechat-order-status", r.financeHandler.GetWechatOrderStatus) // 获取微信订单状态
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,11 +76,11 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
invoiceGroup := engine.Group("/api/v1/invoices")
|
||||
invoiceGroup.Use(r.authMiddleware.Handle())
|
||||
{
|
||||
invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票
|
||||
invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息
|
||||
invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息
|
||||
invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录
|
||||
invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额
|
||||
invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票
|
||||
invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息
|
||||
invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息
|
||||
invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录
|
||||
invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额
|
||||
invoiceGroup.GET("/:application_id/download", r.financeHandler.DownloadInvoiceFile) // 下载发票文件
|
||||
}
|
||||
|
||||
@@ -74,8 +88,8 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
adminFinanceGroup := engine.Group("/api/v1/admin/finance")
|
||||
adminFinanceGroup.Use(r.adminAuthMiddleware.Handle())
|
||||
{
|
||||
adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值
|
||||
adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值
|
||||
adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值
|
||||
adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值
|
||||
adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页
|
||||
}
|
||||
|
||||
@@ -83,7 +97,7 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
adminInvoiceGroup := engine.Group("/api/v1/admin/invoices")
|
||||
adminInvoiceGroup.Use(r.adminAuthMiddleware.Handle())
|
||||
{
|
||||
adminInvoiceGroup.GET("/pending", r.financeHandler.GetPendingApplications) // 获取待处理申请列表
|
||||
adminInvoiceGroup.GET("/pending", r.financeHandler.GetPendingApplications) // 获取待处理申请列表
|
||||
adminInvoiceGroup.POST("/:application_id/approve", r.financeHandler.ApproveInvoiceApplication) // 通过发票申请
|
||||
adminInvoiceGroup.POST("/:application_id/reject", r.financeHandler.RejectInvoiceApplication) // 拒绝发票申请
|
||||
adminInvoiceGroup.GET("/:application_id/download", r.financeHandler.AdminDownloadInvoiceFile) // 下载发票文件
|
||||
|
||||
25
internal/shared/payment/context.go
Normal file
25
internal/shared/payment/context.go
Normal 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
|
||||
}
|
||||
48
internal/shared/payment/user_auth_model.go
Normal file
48
internal/shared/payment/user_auth_model.go
Normal 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未实现,请注入真实的实现")
|
||||
}
|
||||
7
internal/shared/payment/utils.go
Normal file
7
internal/shared/payment/utils.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package payment
|
||||
|
||||
// ToWechatAmount 将金额转换为微信支付金额(单位:分)
|
||||
// 微信支付金额以分为单位,需要将元转换为分
|
||||
func ToWechatAmount(amount float64) int64 {
|
||||
return int64(amount * 100)
|
||||
}
|
||||
353
internal/shared/payment/wechatpay.go
Normal file
353
internal/shared/payment/wechatpay.go
Normal 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
|
||||
}
|
||||
BIN
resources/etc/wxetc_cert/apiclient_cert.p12
Normal file
BIN
resources/etc/wxetc_cert/apiclient_cert.p12
Normal file
Binary file not shown.
25
resources/etc/wxetc_cert/apiclient_cert.pem
Normal file
25
resources/etc/wxetc_cert/apiclient_cert.pem
Normal 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-----
|
||||
28
resources/etc/wxetc_cert/apiclient_key.pem
Normal file
28
resources/etc/wxetc_cert/apiclient_key.pem
Normal 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-----
|
||||
9
resources/etc/wxetc_cert/pub_key.pem
Normal file
9
resources/etc/wxetc_cert/pub_key.pem
Normal 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-----
|
||||
18
resources/etc/wxetc_cert/璇佷功浣跨敤璇存槑.txt
Normal file
18
resources/etc/wxetc_cert/璇佷功浣跨敤璇存槑.txt
Normal 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)下载
|
||||
Reference in New Issue
Block a user