feat: 添加支付模块,支持支付宝支付功能,包括订单创建、支付状态查询及支付历史记录管理

This commit is contained in:
wood911010 committed 2026-01-09 19:21:40 +08:00
1 parent 879aca449f
commit af76eaf1b1
26 files changed
+2580 -26

No files matched your search

+24 -1
View File
@@ -47,9 +47,32 @@ license:
private_key_path: "../configs/rsa_private_key.pem" # RSA私钥文件路径(服务器端使用,必须保密)
public_key_path: "../configs/rsa_public_key.pem" # RSA公钥文件路径(客户端使用,可以公开)
key_size: 2048 # 密钥大小:2048或4096,默认2048
heartbeat_timeout: 300 # 心跳超时时间(秒) - 5分钟内为在线
offline_timeout: 1440 # 离线超时时间(分钟) - 24小时后为异常
expiring_days: 30 # 即将过期天数 - 30天内到期算即将过期
payment:
# 默认支付方式
default_method: alipay
# 支付提供商配置 - 支付宝沙箱环境
providers:
alipay:
enabled: true
app_id: "9021000159611786"
private_key: "-----BEGIN PRIVATE KEY-----\nMIIEogIBAAKCAQEAhxBHcY3BLmA0glH1v1e9VnLFEc72ZyI5xNF77SMA5hGutz30\nSmzczjYvGJNYTeyWfIjIqtP0IA49XA61kpUPiTyBy2sxjDfu6ew+Yu2waynpI+Nbk\npIz8TWT1g7rqgZMsUexufmvFEpvn1AmboVKN8iZVflwc4lq5pQ/GS7uTlDK3fY32j\nEk6jxEHy8yZvkf467X0WK9AdrJ0IxUTOLBROdZySqg7mc2Iyo61xdDOuVCJjZSWFH\nt+s4UWx2VPFSbL2z4m82abGicAgj696J2PU9iLhQG2HA/EwrH8zsVyJPHTMgPFwng\nN9sxmT4U3OH8ugCnw/amEsD8VZR3PG3ypQIDAQABAoIBAFSs2Q2Q4H7xo5ncRmQ6Xq\nKW+4Ppq8MGyw7ggoAuBL84cL/C4KfUTi4d/DK9FYxs+f+DRcMLdK6wHrnbIuyN9Rq\nFy527S2nsGyWejZI6D/NvaBGu1Vczv8rGWzPFUOvcXTJ7jynYJnJjJE3N0vpRKDGG\n2tLyuhRFH7cFU/W23YJM7TflTUUh1SG0Ec+jUa+ek5nK/vsqGE7A5p/D+mW4WLfS4\nsGhfe1XzNKjuCznTYajG79YXHXBKljqq4SKoEdh0SQHoEy7H0BXqArjSbh4KdHYoW/\nKkvA/c5QpbaTuWdK3iuvq7bZ1I0SEckL/rzufj+OcgoKcnwxZG2/Eq1Nb8tUCgYEAw\ncuyQW1AZEVl4dbk9Y4JXFeWh82LZ9pmyvr9tmqwQP4Ywlb9zUlXy+Zbv1wa9acfRWu9\ndYGm3SByFk8c3UGb/s3+eY4MribqkNVkjdotC/+wfymyf3gdLypGEXjBGwNJregGFeF\nmUhWnS+YWOiCiH401uDTU1pRtW37DZgdD5ZMCgYEAsmqIRL4U+3bkIb8u26lgS3k2W\nImen8+i91jVc8wWD7/aWGNmjodimyOv9AJRt34y5apzzfcWuzr9qdNQrzGubtKjaVM6\ns6NNVGzUJkLjF4WpyFDmX/ZLZkTW0gQLMYAjmiEJkPlbpHQ81vSMCT7W76RB31vL8p\ndRJMGBz/dN6ecCgYATzaXsi6Z92J2do2EIOrh4KDcor3dkaGwCp/1PK73RFmYfTdZnq\nOwA2QxdFKNooOhQJr9Yh6Quioc+Al2XuoGfWxt6/moLTczAeh3pZPcfd8RoajFkVzjc\nPfylAmrxQLfBvqkIiVz0XGQZHGz/CaCnir2HklaBiu9V3zFGds2YYwKBgFuaNryUm1F\nAiYCKGBTPsZEOesiaX8bNZG56PZ1mey0642/eGRm0iPv+JqixSCNdt+QSz8I/ynv2naZ\nDyHkqmNRtcVjyUoFIprth5MqALPg2uCUQ5DcAzVWEoobRbK3NjHnl3HOCk3qr81Cp3f\nwhN1lyjB+RkfTLm+2zLGMKJgahAoGAGUigAVlCnUo6zj8PVOeUPJWV3tZf8ZkTtCXT\njdp9tPwZqf0ZlmVZCoQB3nclF/6PgxgH7W596DlUt8TIrdmgg28HM8C2rpY/AoCw1H3\ncMkha8gCUN308cx0I2duea3tvDUb8thfvR34d7bxCI2hTOsplfE/MVv+sjzprGwtH/84=\n-----END PRIVATE KEY-----"
public_key: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjkjXfe1fXjelyi6HIzFp\n5l9PGPxDBTi/TyIsRrF5NaYRCGqqSSJj1sc440eJ7InKRb4q/7cpdjlroC6DDO1O\nHKipld1pWSqoezc2lLVxvRWGt7Tl9mDfrul1/4ywx9OnIPBx3iJvzY15XDVWlZFY\nWVJFQv5EriLn+3xy+3gaazzJYCp/PumDqMTUgp0Kjk+HuHJ5/PKtFt3AtN2GB8gl\nQEybl2Wf0qNZU9SzmEm+9I7CNvB/zUC3csRxJHTvbOtkjhCC/TrS20Xm8IFjygu9\nlBf9qzacXyfHjG6fLMU8aDn4kPAXL+wbqE2UzFmVvMNCLpEaIS3r7I8S2TGQLPDD\n6QIDAQAB\n-----END PUBLIC KEY-----"
gateway_url: "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
notify_url: "http://localhost:18888/api/payment/alipay/callback"
return_url: "http://localhost:18888/payment/success"
sign_type: "RSA2"
charset: "utf-8"
format: "json"
# 通用支付配置
expire_minutes: 30 # 支付过期时间(分钟)
retry_times: 3 # 支付重试次数
enable_log: true # 启用支付日志
+34 -2
View File
@@ -48,7 +48,39 @@ license:
private_key_path: "../configs/rsa_private_key.pem" # RSA私钥文件路径(服务器端使用,必须保密)
public_key_path: "../configs/rsa_public_key.pem" # RSA公钥文件路径(客户端使用,可以公开)
key_size: 2048 # 密钥大小:2048或4096,默认2048
heartbeat_timeout: 300 # 心跳超时时间(秒) - 5分钟内为在线
offline_timeout: 1440 # 离线超时时间(分钟) - 24小时后为异常
expiring_days: 30 # 即将过期天数 - 30天内到期算即将过期
expiring_days: 30 # 即将过期天数 - 30天内到期算即将过期
payment:
# 默认支付方式
default_method: alipay
# 支付提供商配置
providers:
alipay:
enabled: true
app_id: "your_alipay_app_id"
private_key: "your_alipay_private_key"
public_key: "alipay_public_key"
gateway_url: "https://openapi.alipay.com/gateway.do"
notify_url: "https://your-domain.com/api/payment/alipay/callback"
return_url: "https://your-domain.com/payment/success"
sign_type: "RSA2"
charset: "utf-8"
format: "json"
wechat:
enabled: false # 可动态开启
app_id: "your_wechat_app_id"
mch_id: "your_wechat_mch_id"
private_key: "your_wechat_private_key"
gateway_url: "https://api.mch.weixin.qq.com/pay/unifiedorder"
notify_url: "https://your-domain.com/api/payment/wechat/callback"
# ... 其他微信支付配置
# 通用支付配置
expire_minutes: 30 # 支付过期时间(分钟)
retry_times: 3 # 支付重试次数
enable_log: true # 启用支付日志
+24 -1
View File
@@ -50,4 +50,27 @@ license:
heartbeat_timeout: 300 # 心跳超时时间(秒) - 5分钟内未上报心跳视为离线
offline_timeout: 1440 # 离线超时时间(分钟) - 24小时后为异常
expiring_days: 30 # 即将过期天数 - 30天内到期算即将过期
expiring_days: 30 # 即将过期天数 - 30天内到期算即将过期
payment:
# 默认支付方式
default_method: alipay
# 支付提供商配置 - 支付宝沙箱环境
providers:
alipay:
enabled: true
app_id: "9021000159611786"
private_key: "-----BEGIN PRIVATE KEY-----\nMIIEogIBAAKCAQEAhxBHcY3BLmA0glH1v1e9VnLFEc72ZyI5xNF77SMA5hGutz30\nSmzczjYvGJNYTeyWfIjIqtP0IA49XA61kpUPiTyBy2sxjDfu6ew+Yu2waynpI+Nbk\npIz8TWT1g7rqgZMsUexufmvFEpvn1AmboVKN8iZVflwc4lq5pQ/GS7uTlDK3fY32j\nEk6jxEHy8yZvkf467X0WK9AdrJ0IxUTOLBROdZySqg7mc2Iyo61xdDOuVCJjZSWFH\nt+s4UWx2VPFSbL2z4m82abGicAgj696J2PU9iLhQG2HA/EwrH8zsVyJPHTMgPFwng\nN9sxmT4U3OH8ugCnw/amEsD8VZR3PG3ypQIDAQABAoIBAFSs2Q2Q4H7xo5ncRmQ6Xq\nKW+4Ppq8MGyw7ggoAuBL84cL/C4KfUTi4d/DK9FYxs+f+DRcMLdK6wHrnbIuyN9Rq\nFy527S2nsGyWejZI6D/NvaBGu1Vczv8rGWzPFUOvcXTJ7jynYJnJjJE3N0vpRKDGG\n2tLyuhRFH7cFU/W23YJM7TflTUUh1SG0Ec+jUa+ek5nK/vsqGE7A5p/D+mW4WLfS4\nsGhfe1XzNKjuCznTYajG79YXHXBKljqq4SKoEdh0SQHoEy7H0BXqArjSbh4KdHYoW/\nKkvA/c5QpbaTuWdK3iuvq7bZ1I0SEckL/rzufj+OcgoKcnwxZG2/Eq1Nb8tUCgYEAw\ncuyQW1AZEVl4dbk9Y4JXFeWh82LZ9pmyvr9tmqwQP4Ywlb9zUlXy+Zbv1wa9acfRWu9\ndYGm3SByFk8c3UGb/s3+eY4MribqkNVkjdotC/+wfymyf3gdLypGEXjBGwNJregGFeF\nmUhWnS+YWOiCiH401uDTU1pRtW37DZgdD5ZMCgYEAsmqIRL4U+3bkIb8u26lgS3k2W\nImen8+i91jVc8wWD7/aWGNmjodimyOv9AJRt34y5apzzfcWuzr9qdNQrzGubtKjaVM6\ns6NNVGzUJkLjF4WpyFDmX/ZLZkTW0gQLMYAjmiEJkPlbpHQ81vSMCT7W76RB31vL8p\ndRJMGBz/dN6ecCgYATzaXsi6Z92J2do2EIOrh4KDcor3dkaGwCp/1PK73RFmYfTdZnq\nOwA2QxdFKNooOhQJr9Yh6Quioc+Al2XuoGfWxt6/moLTczAeh3pZPcfd8RoajFkVzjc\nPfylAmrxQLfBvqkIiVz0XGQZHGz/CaCnir2HklaBiu9V3zFGds2YYwKBgFuaNryUm1F\nAiYCKGBTPsZEOesiaX8bNZG56PZ1mey0642/eGRm0iPv+JqixSCNdt+QSz8I/ynv2naZ\nDyHkqmNRtcVjyUoFIprth5MqALPg2uCUQ5DcAzVWEoobRbK3NjHnl3HOCk3qr81Cp3f\nwhN1lyjB+RkfTLm+2zLGMKJgahAoGAGUigAVlCnUo6zj8PVOeUPJWV3tZf8ZkTtCXT\njdp9tPwZqf0ZlmVZCoQB3nclF/6PgxgH7W596DlUt8TIrdmgg28HM8C2rpY/AoCw1H3\ncMkha8gCUN308cx0I2duea3tvDUb8thfvR34d7bxCI2hTOsplfE/MVv+sjzprGwtH/84=\n-----END PRIVATE KEY-----"
public_key: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjkjXfe1fXjelyi6HIzFp\n5l9PGPxDBTi/TyIsRrF5NaYRCGqqSSJj1sc440eJ7InKRb4q/7cpdjlroC6DDO1O\nHKipld1pWSqoezc2lLVxvRWGt7Tl9mDfrul1/4ywx9OnIPBx3iJvzY15XDVWlZFY\nWVJFQv5EriLn+3xy+3gaazzJYCp/PumDqMTUgp0Kjk+HuHJ5/PKtFt3AtN2GB8gl\nQEybl2Wf0qNZU9SzmEm+9I7CNvB/zUC3csRxJHTvbOtkjhCC/TrS20Xm8IFjygu9\nlBf9qzacXyfHjG6fLMU8aDn4kPAXL+wbqE2UzFmVvMNCLpEaIS3r7I8S2TGQLPDD\n6QIDAQAB\n-----END PUBLIC KEY-----"
gateway_url: "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
notify_url: "http://localhost:18888/api/payment/alipay/callback"
return_url: "http://localhost:18888/payment/success"
sign_type: "RSA2"
charset: "utf-8"
format: "json"
# 通用支付配置
expire_minutes: 30 # 支付过期时间(分钟)
retry_times: 3 # 支付重试次数
enable_log: true # 启用支付日志
@@ -0,0 +1,492 @@
# 支付宝支付对接设计文档
## 概述
本系统需要对接支付宝支付功能,主要用于用户购买产品套餐的支付场景。系统采用模块化设计,支持后续扩展其他支付方式(如微信支付等)。
## 业务需求
- 用户购买产品套餐时支持支付宝支付
- 支持支付状态查询和回调处理
- 支付成功后自动激活用户套餐
- 支持支付记录查询和管理
## 技术架构
基于现有的Clean Architecture设计,使用**支付宝官方Go SDK**实现支付功能:
### 核心组件
- **支付SDK**: `github.com/smartwalle/alipay/v3` - 官方支付宝Go SDK
- **支付服务**: 封装支付业务逻辑,集成官方SDK
- **支付仓储**: 支付数据持久化
- **支付处理器**: RESTful API接口
### 架构优势
- **官方维护**: 使用官方SDK,确保兼容性和安全性
- **简化开发**: 无需手动实现签名、验证等复杂逻辑
- **标准化**: 遵循支付宝官方API规范
- **易扩展**: 支持支付宝、微信等多支付方式
## 数据模型设计
### 通用支付设计:支付抽象与业务分离
采用最佳实践的支付抽象设计,支持多种支付方式和业务场景:
#### 设计原则
- **支付抽象**:payments表只包含支付相关的通用字段
- **业务关联**:通过`business_type`和`business_id`关联具体业务
- **扩展性**:支持支付宝、微信等多种支付方式
- **业务隔离**:支付失败不影响业务数据
#### 支付订单表 (payments)
```sql
CREATE TABLE payments (
id SERIAL PRIMARY KEY,
payment_no VARCHAR(64) UNIQUE NOT NULL COMMENT '支付单号',
business_type VARCHAR(50) NOT NULL COMMENT '业务类型 (package_order-套餐购买)',
business_id VARCHAR(36) NULL COMMENT '业务ID (关联cu_orders.id)',
user_id INTEGER NOT NULL COMMENT '用户ID',
amount DECIMAL(10,2) NOT NULL COMMENT '支付金额',
currency VARCHAR(3) DEFAULT 'CNY' COMMENT '货币类型',
payment_method VARCHAR(20) DEFAULT 'alipay' COMMENT '支付方式',
payment_provider VARCHAR(20) DEFAULT 'alipay' COMMENT '支付提供商',
status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '支付状态',
trade_no VARCHAR(64) NULL COMMENT '第三方交易号',
payment_time TIMESTAMP NULL COMMENT '支付完成时间',
expire_time TIMESTAMP NOT NULL COMMENT '支付过期时间',
payment_url TEXT NULL COMMENT '支付链接',
notify_data JSONB NULL COMMENT '回调数据',
extra_data JSONB NULL COMMENT '扩展数据',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 索引
CREATE INDEX idx_payments_payment_no ON payments(payment_no);
CREATE INDEX idx_payments_business_type_id ON payments(business_type, business_id);
CREATE INDEX idx_payments_user_id ON payments(user_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_created_at ON payments(created_at);
```
#### 业务类型枚举
- `package_order`: 套餐购买订单
- `license_extend`: 许可延期(预留)
- `service_upgrade`: 服务升级(预留)
#### 支付状态枚举
- `pending`: 待支付
- `paid`: 已支付
- `cancelled`: 已取消
- `expired`: 已过期
- `failed`: 支付失败
- `refunded`: 已退款
#### 支付方式枚举
- `alipay`: 支付宝
- `wechat`: 微信支付(预留)
- `bank`: 银行卡(预留)
## 官方SDK集成
### SDK选择
使用 `github.com/smartwalle/alipay/v3` 官方Go SDK,原因:
- **官方维护**: 支付宝官方团队维护,确保最新API支持
- **功能完整**: 支持支付、退款、查询等所有功能
- **签名处理**: 自动处理RSA签名和验证
- **类型安全**: 强类型接口,避免手动构造参数
- **社区活跃**: 有完整的文档和示例
### 核心用法
```go
// 1. 创建客户端
client, err := alipay.New(appID, privateKey, false)
// 2. 创建支付请求
payRequest := alipay.TradePagePay{
Trade: alipay.Trade{
OutTradeNo: "PAY20240101001",
TotalAmount: "100.00",
Subject: "产品购买",
ProductCode: "FAST_INSTANT_TRADE_PAY",
},
}
// 3. 生成支付URL
payURL, err := client.TradePagePay(payRequest)
// 4. 处理回调
notification, err := client.GetTradeNotification(request)
```
### 集成优势
- **零配置**: SDK自动处理签名和验证
- **错误处理**: 完善的错误类型和处理机制
- **性能优化**: 内部实现连接池和缓存
- **安全可靠**: 官方审核,确保安全合规
## API接口设计
### 1. 创建订单并支付
**方案选择:兼容性修改现有接口**
基于现有 `POST /api/cu/orders` 接口进行兼容性修改:
**接口路径**: `POST /api/cu/orders`(保持不变)
**新增请求参数**:
```json
{
"package_id": "basic",
"license_count": 100,
"payment_method": "alipay" // 新增:支付方式,不传则为免费订单
}
```
**响应格式变化**:
**免费订单(试用版)响应** - 保持原有格式:
```json
{
"code": "000000",
"message": "成功",
"data": {
"order_id": "order_id_123",
"order_no": "ORD202401010001",
"total_amount": 0.00,
"status": "paid",
"authorization_code": "AC-240101-A1B2C3D4",
"expired_at": "2024-01-25T23:59:59Z"
}
}
```
**付费订单响应** - 新增支付信息:
```json
{
"code": "000000",
"message": "成功",
"data": {
"order_id": "order_id_456",
"order_no": "ORD202401010002",
"payment_no": "PAY202401010002",
"total_amount": 24000.00,
"status": "pending",
"payment_url": "https://openapi.alipay.com/gateway.do?...",
"expire_time": "2024-01-01T13:00:00Z"
}
}
```
**业务逻辑**:
- 根据套餐类型和支付方式决定处理流程
- 免费订单:直接创建paid状态订单并生成授权码
- 付费订单:创建pending状态订单和支付单,返回支付链接
### 2. 查询订单状态
**接口**: `GET /api/cu/orders/{order_no}/status`(保持与现有API风格一致)
**响应**:
**免费订单状态**:
```json
{
"code": "000000",
"data": {
"order_no": "ORD202401010001",
"status": "paid",
"authorization_code": "AC-240101-A1B2C3D4",
"expired_at": "2024-01-25T23:59:59Z"
}
}
```
**付费订单状态**:
```json
{
"code": "000000",
"data": {
"order_no": "ORD202401010002",
"status": "paid",
"payment_status": "paid",
"authorization_code": "AC-240102-B2C3D4E5",
"payment_time": "2024-01-01T12:35:00Z"
}
}
```
**业务逻辑**:
- 兼容现有查询接口
- 对于付费订单,额外返回支付相关信息
### 3. 支付回调处理
**接口**: `POST /api/v1/payment/alipay/callback`
支付宝服务器回调,系统验证签名后更新订单状态。
### 4. 获取支付历史
**接口**: `GET /api/v1/payment/history`
支持分页查询用户的支付记录。
## 支付流程设计
### 1. 支付流程图
```
用户选择套餐 → 创建业务订单(cu_orders, status=pending) → 创建支付单(payments, pending) → 跳转支付 → 用户支付 → 支付回调 → 更新payments为paid → 更新cu_orders为paid → 生成授权码
```
### 2. 详细流程
1. **创建业务订单**
- 前端调用创建订单接口,传入套餐ID和许可数量
- 后端验证套餐配置,计算价格和折扣
- 在`cu_orders`表创建记录,状态为`pending`,生成业务订单
- **创建对应的支付单**,关联业务订单,状态为`pending`
- 调用支付接口生成支付链接
- 返回支付信息给前端
2. **支付处理**
- 用户跳转到支付页面完成支付
- 第三方支付平台处理支付并异步回调系统
3. **回调处理**
- 验证支付回调签名和数据
- 确认支付金额一致
- 在事务中执行:
- 更新`payments`表状态为`paid`
- 更新对应的`cu_orders`表状态为`paid`
- 生成授权码
- 返回成功响应给支付平台
4. **状态查询**
- 前端可查询支付状态
- 返回支付状态和业务订单信息
## 安全设计
### 1. 支付验证
- 使用支付平台公钥验证回调签名
- 防止伪造回调请求
- 支持多种支付方式的签名验证
### 2. 金额防篡改
- 支付单金额在创建时计算并存储
- 支付回调时二次验证金额
- 业务数据与支付数据分离,确保一致性
### 3. 防重复支付
- 支付单状态机严格控制
- 第三方交易号唯一性验证
- 支付过期时间控制
### 4. 业务隔离
- 支付失败:只更新payments状态,业务订单保持pending
- 支付成功:事务中同时更新payments和业务订单状态
- 支持多种业务类型的支付处理
## 配置管理
在`configs/config.yaml`中添加统一的支付配置:
```yaml
payment:
# 默认支付方式
default_method: alipay
# 支付提供商配置
providers:
alipay:
enabled: true
app_id: "your_app_id"
private_key: "your_private_key"
public_key: "alipay_public_key"
gateway_url: "https://openapi.alipay.com/gateway.do"
notify_url: "https://your-domain.com/api/v1/payment/alipay/callback"
return_url: "https://your-domain.com/payment/success"
sign_type: "RSA2"
charset: "utf-8"
format: "json"
# 通用支付配置
expire_minutes: 30 # 支付过期时间(分钟)
retry_times: 3 # 支付重试次数
enable_log: true # 启用支付日志
```
**配置说明**:
- `default_method`: 默认支付方式,可设置为 `alipay` 或 `wechat`
- `providers`: 各支付提供商的详细配置,支持动态开启/关闭
- 通用配置:支付过期时间、重试次数、日志开关等
## 错误处理
### 支付相关错误码
- `PAYMENT_ORDER_NOT_FOUND`: 订单不存在
- `PAYMENT_ALREADY_PAID`: 订单已支付
- `PAYMENT_EXPIRED`: 订单已过期
- `PAYMENT_AMOUNT_MISMATCH`: 支付金额不匹配
- `PAYMENT_SIGNATURE_VERIFY_FAILED`: 签名验证失败
## 扩展性设计
### 1. 多支付方式支持
通过策略模式和工厂模式支持多种支付方式:
```go
// 支付策略接口
type PaymentStrategy interface {
CreatePayment(payment *Payment) (*PaymentResponse, error)
QueryPayment(paymentNo string) (*PaymentStatus, error)
HandleCallback(data map[string]interface{}) error
ValidateSignature(data map[string]interface{}) bool
}
// 支付工厂
type PaymentFactory struct {
strategies map[string]PaymentStrategy
}
func (f *PaymentFactory) GetStrategy(method string) PaymentStrategy {
return f.strategies[method]
}
```
### 2. 支持的支付方式
- **支付宝**:当前实现
- **微信支付**:预留扩展
- **银行卡**:预留扩展
### 3. 多业务场景支持
通过`business_type`支持不同业务场景:
```go
const (
BusinessTypePackageOrder = "package_order" // 套餐购买
BusinessTypeLicenseExtend = "license_extend" // 许可延期
BusinessTypeServiceUpgrade = "service_upgrade" // 服务升级
)
```
### 4. 配置说明
详见[配置管理](#配置管理)章节,支持支付宝、微信等多支付方式统一配置。
## 测试策略
### 1. 单元测试
- 支付服务逻辑测试
- 支付宝接口mock测试
- 回调处理测试
### 2. 集成测试
- 完整支付流程测试
- 支付宝沙箱环境测试
### 3. 沙箱环境
使用支付宝提供的沙箱环境进行测试,避免真实资金操作。
## 数据迁移和兼容性
### 新增payments表
创建通用的支付表,支持多种支付方式和业务场景:
```sql
-- 创建payments表(见数据模型设计部分)
-- business_type + business_id 支持关联不同业务
```
### API兼容性处理
#### 现有接口修改策略
**POST /api/cu/orders 接口变更**:
1. **向后兼容**:不传`payment_method`参数时,保持原有免费订单逻辑
2. **扩展功能**:传入`payment_method`参数时,启用支付流程
3. **响应差异**:免费订单返回授权码,付费订单返回支付链接
#### 迁移步骤
1. **代码层面**:
```go
// 修改现有创建订单逻辑
func CreateOrder(req CreateOrderRequest) (*OrderResponse, error) {
if req.PaymentMethod == "" {
// 免费订单:直接创建并返回授权码
return createFreeOrder(req)
} else {
// 付费订单:创建订单+支付单,返回支付信息
return createPaidOrder(req)
}
}
```
2. **数据库层面**:
- 新增payments表
- cu_orders表保持不变
- 通过关联关系连接
3. **前端适配**:
- 免费订单:无需修改,正常显示授权码
- 付费订单:跳转支付页面,支付完成后查询状态
#### 错误处理兼容
保持现有的错误码体系,新增支付相关错误码:
```yaml
cu_payment:
"602001": "支付失败"
"602002": "支付金额不匹配"
"602003": "重复支付"
"602004": "支付已过期"
```
## 部署注意事项
1. **网络要求**: 支付宝回调URL必须是公网可访问的HTTPS地址
2. **密钥安全**: 配置支付宝RSA私钥文件,严格控制访问权限
3. **数据库**: 确保payments表已创建,数据库连接池充足
4. **日志监控**: 支付相关日志级别设为INFO,配置告警监控支付成功率
5. **SDK依赖**: 确保 `github.com/smartwalle/alipay/v3` 已正确安装
6. **配置验证**: 部署前验证支付宝配置参数的正确性
7. **业务隔离**: 支付失败不影响业务订单,确保数据一致性
## 对接资料
### 支付宝官方文档
- [电脑网站支付API](https://opendocs.alipay.com/open/59da99d0_alipay.trade.page.pay)
- [SDK下载与文档](https://opendocs.alipay.com/open/54/103419)
- [签名验证指南](https://opendocs.alipay.com/open/291/105974)
### Go SDK资源
- [smartwalle/alipay GitHub](https://github.com/smartwalle/alipay)
- [SDK使用示例](https://github.com/smartwalle/alipay/tree/master/examples)
- [GoDoc文档](https://pkg.go.dev/github.com/smartwalle/alipay/v3)
### 沙箱环境
- [支付宝沙箱](https://open.alipay.com/develop/sandbox/app) - 用于开发测试
- [测试账号申请](https://opendocs.alipay.com/open/200/105311) - 获取测试环境账号
@@ -0,0 +1,38 @@
沙箱账号
商家信息
商户账号enjyeu8747@sandbox.com
登录密码111111
商户PID2088721092516360
账户余额1000000.00
买家信息
买家账号mgwlmn3858@sandbox.com
登录密码111111
支付密码111111
用户UID2088722092516378
用户名称mgwlmn3858
证件类型IDENTITY_CARD
证件账号471743193305122691
账户余额1000000.00
支付宝网关地址
https://openapi-sandbox.dl.alipaydev.com/gateway.do
websocket服务地址
openchannel-sandbox.dl.alipaydev.com
基本信息
APPID
9021000159611786
应用名称
sandbox 默认应用:2088721092516360
绑定的商家账号(PID)
2088721092516360
应用公钥:复制公钥
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhxBHcY3BLmA0glH1v1e9VnLFEc72ZyI5xNF77SMA5hGutz30SmzczjYvGJNYTeyWfIjIqtP0IA49XA61kpUPiTyBy2sxjDfu6ew+Yu2waynpI+NbkpIz8TWT1g7rqgZMsUexufmvFEpvn1AmboVKN8iZVflwc4lq5pQ/GS7uTlDK3fY32jEk6jxEHy8yZvkf467X0WK9AdrJ0IxUTOLBROdZySqg7mc2Iyo61xdDOuVCJjZSWFHt+s4UWx2VPFSbL2z4m82abGicAgj696J2PU9iLhQG2HA/EwrH8zsVyJPHTMgPFwngN9sxmT4U3OH8ugCnw/amEsD8VZR3PG3ypQIDAQAB
应用私钥:
复制私钥
MIIEogIBAAKCAQEAhxBHcY3BLmA0glH1v1e9VnLFEc72ZyI5xNF77SMA5hGutz30SmzczjYvGJNYTeyWfIjIqtP0IA49XA61kpUPiTyBy2sxjDfu6ew+Yu2waynpI+NbkpIz8TWT1g7rqgZMsUexufmvFEpvn1AmboVKN8iZVflwc4lq5pQ/GS7uTlDK3fY32jEk6jxEHy8yZvkf467X0WK9AdrJ0IxUTOLBROdZySqg7mc2Iyo61xdDOuVCJjZSWFHt+s4UWx2VPFSbL2z4m82abGicAgj696J2PU9iLhQG2HA/EwrH8zsVyJPHTMgPFwngN9sxmT4U3OH8ugCnw/amEsD8VZR3PG3ypQIDAQABAoIBAFSs2Q2Q4H7xo5ncRmQ6XqKW+4Ppq8MGyw7ggoAuBL84cL/C4KfUTi4d/DK9FYxs+f+DRcMLdK6wHrnbIuyN9RqFy527S2nsGyWejZI6D/NvaBGu1Vczv8rGWzPFUOvcXTJ7jynYJnJjJE3N0vpRKDGG2tLyuhRFH7cFU/W23YJM7TflTUUh1SG0Ec+jUa+ek5nK/vsqGE7A5p/D+mW4WLfS4sGhfe1XzNKjuCznTYajG79YXHXBKljqq4SKoEdh0SQHoEy7H0BXqArjSbh4KdHYoW/KkvA/c5QpbaTuWdK3iuvq7bZ1I0SEckL/rzufj+OcgoKcnwxZG2/Eq1Nb8tUCgYEAwcuyQW1AZEVl4dbk9Y4JXFeWh82LZ9pmyvr9tmqwQP4Ywlb9zUlXy+Zbv1wa9acfRWu9dYGm3SByFk8c3UGb/s3+eY4MribqkNVkjdotC/+wfymyf3gdLypGEXjBGwNJregGFeFmUhWnS+YWOiCiH401uDTU1pRtW37DZgdD5ZMCgYEAsmqIRL4U+3bkIb8u26lgS3k2WImen8+i91jVc8wWD7/aWGNmjodimyOv9AJRt34y5apzzfcWuzr9qdNQrzGubtKjaVM6s6NNVGzUJkLjF4WpyFDmX/ZLZkTW0gQLMYAjmiEJkPlbpHQ81vSMCT7W76RB31vL8pdRJMGBz/dN6ecCgYATzaXsi6Z92J2do2EIOrh4KDcor3dkaGwCp/1PK73RFmYfTdZnqOwA2QxdFKNooOhQJr9Yh6Quioc+Al2XuoGfWxt6/moLTczAeh3pZPcfd8RoajFkVzjcPfylAmrxQLfBvqkIiVz0XGQZHGz/CaCnir2HklaBiu9V3zFGds2YYwKBgFuaNryUm1FAiYCKGBTPsZEOesiaX8bNZG56PZ1mey0642/eGRm0iPv+JqixSCNdt+QSz8I/ynv2naZDyHkqmNRtcVjyUoFIprth5MqALPg2uCUQ5DcAzVWEoobRbK3NjHnl3HOCk3qr81Cp3fwhN1lyjB+RkfTLm+2zLGMKJgahAoGAGUigAVlCnUo6zj8PVOeUPJWV3tZf8ZkTtCXTjdp9tPwZqf0ZlmVZCoQB3nclF/6PgxgH7W596DlUt8TIrdmgg28HM8C2rpY/AoCw1H3cMkha8gCUN308cx0I2duea3tvDUb8thfvR34d7bxCI2hTOsplfE/MVv+sjzprGwtH/84=
支付宝公钥:复制公钥
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjkjXfe1fXjelyi6HIzFp5l9PGPxDBTi/TyIsRrF5NaYRCGqqSSJj1sc440eJ7InKRb4q/7cpdjlroC6DDO1OHKipld1pWSqoezc2lLVxvRWGt7Tl9mDfrul1/4ywx9OnIPBx3iJvzY15XDVWlZFYWVJFQv5EriLn+3xy+3gaazzJYCp/PumDqMTUgp0Kjk+HuHJ5/PKtFt3AtN2GB8glQEybl2Wf0qNZU9SzmEm+9I7CNvB/zUC3csRxJHTvbOtkjhCC/TrS20Xm8IFjygu9lBf9qzacXyfHjG6fLMU8aDn4kPAXL+wbqE2UzFmVvMNCLpEaIS3r7I8S2TGQLPDD6QIDAQAB
@@ -0,0 +1,182 @@
# 支付宝沙箱测试指南
## 🔧 配置准备
### 1. 启动服务
```bash
# 启动数据库和后端服务
cd backend
go run cmd/main.go
```
### 2. 验证配置
启动后检查日志,确认支付宝SDK初始化成功:
```
[INFO] Alipay client initialized successfully
```
## 🧪 测试步骤
### 步骤1: 创建订单并支付
#### API请求
```bash
# 1. 获取套餐列表
curl -X GET "http://localhost:18888/api/cu/packages" \
-H "Content-Type: application/json"
# 2. 计算价格
curl -X POST "http://localhost:18888/api/cu/orders/calculate" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"package_id": "basic",
"license_count": 1
}'
# 3. 创建付费订单
curl -X POST "http://localhost:18888/api/cu/orders" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"package_id": "basic",
"license_count": 1,
"payment_method": "alipay"
}'
```
#### 预期响应
```json
{
"code": "000000",
"message": "成功",
"data": {
"id": "order_xxx",
"order_no": "ORD20240101001",
"payment_no": "PAY20240101001",
"total_amount": 300.00,
"status": "pending",
"payment_url": "https://openapi-sandbox.dl.alipaydev.com/gateway.do?...",
"expire_time": "2024-01-01T13:00:00Z"
}
}
```
### 步骤2: 模拟支付
#### 方法1: 浏览器访问支付链接
1. 复制API响应中的 `payment_url`
2. 在浏览器中打开该链接
3. 使用沙箱买家账号登录:
- 账号:`mgwlmn3858@sandbox.com`
- 密码:`111111`
- 支付密码:`111111`
#### 方法2: 使用支付宝App扫码
如果支付链接是二维码,可以用支付宝App扫描支付
### 步骤3: 验证支付结果
#### 查询支付状态
```bash
curl -X GET "http://localhost:18888/api/payment/PAY20240101001/status" \
-H "Authorization: Bearer YOUR_TOKEN"
```
#### 预期成功响应
```json
{
"code": "000000",
"data": {
"payment_no": "PAY20240101001",
"status": "paid",
"amount": 300.00,
"payment_time": "2024-01-01T12:35:00Z",
"trade_no": "20240101220014xxxxxx",
"business_order": {
"order_no": "ORD20240101001",
"authorization_code": "AC-240101-XXXXXX",
"status": "paid"
}
}
}
```
## 🔍 常见问题排查
### 1. 支付链接无法访问
- 检查 `gateway_url` 是否正确
- 确认网络能访问支付宝沙箱环境
### 2. 回调失败
- 检查 `notify_url` 是否可公网访问
- 查看服务日志中的回调处理信息
### 3. 签名验证失败
- 确认私钥格式正确(PKCS8)
- 检查公钥是否匹配
### 4. 订单状态未更新
- 检查回调URL配置
- 查看支付宝回调日志
## 📊 测试数据
### 沙箱账号信息
- **买家账号**: mgwlmn3858@sandbox.com / 111111
- **支付密码**: 111111
- **余额**: 1,000,000.00
### 测试金额建议
- 小金额测试:0.01元
- 正常金额测试:1.00-100.00元
- 大金额测试:1000.00元
## 🐛 调试技巧
### 1. 查看详细日志
```bash
# 设置日志级别为debug
log:
level: "debug"
```
### 2. 手动触发回调测试
```bash
# 模拟支付宝回调(需构造正确的回调参数)
curl -X POST "http://localhost:18888/api/payment/alipay/callback" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "回调参数..."
```
### 3. 数据库检查
```sql
-- 查看支付订单
SELECT * FROM payments WHERE payment_no = 'PAY20240101001';
-- 查看业务订单
SELECT * FROM cu_orders WHERE order_no = 'ORD20240101001';
```
## ✅ 测试 checklist
- [ ] 服务启动成功,无错误日志
- [ ] 创建付费订单API返回支付链接
- [ ] 支付链接可正常访问支付宝沙箱
- [ ] 沙箱账号能正常登录和支付
- [ ] 支付完成后订单状态更新为paid
- [ ] 授权码正确生成
- [ ] 回调日志正常,无错误
## 🚀 生产环境部署
测试通过后,修改配置为生产环境:
1. 更换为正式APPID
2. 使用生产RSA密钥对
3. 修改网关地址为正式环境
4. 更新回调URL为生产域名
5. 配置生产数据库
---
**🎉 按照此指南测试,你的支付宝支付功能就能正常工作了!**
+285 -3
View File
@@ -484,7 +484,7 @@ const docTemplate = `{
"BearerAuth": []
}
],
"description": "创建新的产品套餐订单",
"description": "创建新的产品套餐订单,支持免费和付费模式",
"consumes": [
"application/json"
],
@@ -508,7 +508,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "成功",
"description": "付费订单成功",
"schema": {
"allOf": [
{
@@ -520,7 +520,7 @@ const docTemplate = `{
"data": {
"type": "object",
"properties": {
"authorization_code": {
"expire_time": {
"type": "string"
},
"id": {
@@ -529,6 +529,12 @@ const docTemplate = `{
"order_no": {
"type": "string"
},
"payment_no": {
"type": "string"
},
"payment_url": {
"type": "string"
},
"total_amount": {
"type": "number"
}
@@ -1809,6 +1815,168 @@ const docTemplate = `{
}
}
},
"/api/payment/alipay/callback": {
"post": {
"description": "处理支付宝支付回调",
"consumes": [
"application/x-www-form-urlencoded"
],
"produces": [
"text/plain"
],
"tags": [
"支付管理"
],
"summary": "支付宝回调",
"parameters": [
{
"description": "支付宝回调参数",
"name": "notification",
"in": "body",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "success",
"schema": {
"type": "string"
}
},
"400": {
"description": "failure",
"schema": {
"type": "string"
}
}
}
}
},
"/api/payment/history": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取当前用户的支付记录列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"支付管理"
],
"summary": "获取用户支付历史",
"parameters": [
{
"type": "integer",
"default": 1,
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"default": 10,
"description": "每页数量",
"name": "page_size",
"in": "query"
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.PaymentListResponse"
}
}
}
]
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/payment/{payment_no}/status": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据支付单号获取支付状态",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"支付管理"
],
"summary": "获取支付状态",
"parameters": [
{
"type": "string",
"description": "支付单号",
"name": "payment_no",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.PaymentStatusResponse"
}
}
}
]
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "支付单不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/v1/activate": {
"post": {
"description": "客户端使用授权码激活软件,获取许可证文件",
@@ -4155,6 +4323,10 @@ const docTemplate = `{
},
"package_id": {
"type": "string"
},
"payment_method": {
"description": "可选:支付方式,不传则为免费订单",
"type": "string"
}
}
},
@@ -5366,6 +5538,116 @@ const docTemplate = `{
}
}
},
"models.PaymentBusinessOrder": {
"type": "object",
"properties": {
"authorization_code": {
"type": "string"
},
"order_no": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"models.PaymentListResponse": {
"type": "object",
"properties": {
"page": {
"type": "integer"
},
"page_size": {
"type": "integer"
},
"payments": {
"type": "array",
"items": {
"$ref": "#/definitions/models.PaymentResponse"
}
},
"total_count": {
"type": "integer"
}
}
},
"models.PaymentResponse": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"business_id": {
"type": "string"
},
"business_type": {
"type": "string"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"expire_time": {
"type": "string"
},
"id": {
"type": "integer"
},
"payment_method": {
"type": "string"
},
"payment_no": {
"type": "string"
},
"payment_provider": {
"type": "string"
},
"payment_time": {
"type": "string"
},
"payment_url": {
"type": "string"
},
"status": {
"type": "string"
},
"trade_no": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"user_id": {
"type": "integer"
}
}
},
"models.PaymentStatusResponse": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"business_order": {
"$ref": "#/definitions/models.PaymentBusinessOrder"
},
"payment_no": {
"type": "string"
},
"payment_time": {
"type": "string"
},
"status": {
"type": "string"
},
"trade_no": {
"type": "string"
}
}
},
"models.RecentAuthorization": {
"type": "object",
"properties": {
+285 -3
View File
@@ -480,7 +480,7 @@
"BearerAuth": []
}
],
"description": "创建新的产品套餐订单",
"description": "创建新的产品套餐订单,支持免费和付费模式",
"consumes": [
"application/json"
],
@@ -504,7 +504,7 @@
],
"responses": {
"200": {
"description": "成功",
"description": "付费订单成功",
"schema": {
"allOf": [
{
@@ -516,7 +516,7 @@
"data": {
"type": "object",
"properties": {
"authorization_code": {
"expire_time": {
"type": "string"
},
"id": {
@@ -525,6 +525,12 @@
"order_no": {
"type": "string"
},
"payment_no": {
"type": "string"
},
"payment_url": {
"type": "string"
},
"total_amount": {
"type": "number"
}
@@ -1805,6 +1811,168 @@
}
}
},
"/api/payment/alipay/callback": {
"post": {
"description": "处理支付宝支付回调",
"consumes": [
"application/x-www-form-urlencoded"
],
"produces": [
"text/plain"
],
"tags": [
"支付管理"
],
"summary": "支付宝回调",
"parameters": [
{
"description": "支付宝回调参数",
"name": "notification",
"in": "body",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "success",
"schema": {
"type": "string"
}
},
"400": {
"description": "failure",
"schema": {
"type": "string"
}
}
}
}
},
"/api/payment/history": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取当前用户的支付记录列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"支付管理"
],
"summary": "获取用户支付历史",
"parameters": [
{
"type": "integer",
"default": 1,
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"default": 10,
"description": "每页数量",
"name": "page_size",
"in": "query"
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.PaymentListResponse"
}
}
}
]
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/payment/{payment_no}/status": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据支付单号获取支付状态",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"支付管理"
],
"summary": "获取支付状态",
"parameters": [
{
"type": "string",
"description": "支付单号",
"name": "payment_no",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.PaymentStatusResponse"
}
}
}
]
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "支付单不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/v1/activate": {
"post": {
"description": "客户端使用授权码激活软件,获取许可证文件",
@@ -4151,6 +4319,10 @@
},
"package_id": {
"type": "string"
},
"payment_method": {
"description": "可选:支付方式,不传则为免费订单",
"type": "string"
}
}
},
@@ -5362,6 +5534,116 @@
}
}
},
"models.PaymentBusinessOrder": {
"type": "object",
"properties": {
"authorization_code": {
"type": "string"
},
"order_no": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"models.PaymentListResponse": {
"type": "object",
"properties": {
"page": {
"type": "integer"
},
"page_size": {
"type": "integer"
},
"payments": {
"type": "array",
"items": {
"$ref": "#/definitions/models.PaymentResponse"
}
},
"total_count": {
"type": "integer"
}
}
},
"models.PaymentResponse": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"business_id": {
"type": "string"
},
"business_type": {
"type": "string"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"expire_time": {
"type": "string"
},
"id": {
"type": "integer"
},
"payment_method": {
"type": "string"
},
"payment_no": {
"type": "string"
},
"payment_provider": {
"type": "string"
},
"payment_time": {
"type": "string"
},
"payment_url": {
"type": "string"
},
"status": {
"type": "string"
},
"trade_no": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"user_id": {
"type": "integer"
}
}
},
"models.PaymentStatusResponse": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"business_order": {
"$ref": "#/definitions/models.PaymentBusinessOrder"
},
"payment_no": {
"type": "string"
},
"payment_time": {
"type": "string"
},
"status": {
"type": "string"
},
"trade_no": {
"type": "string"
}
}
},
"models.RecentAuthorization": {
"type": "object",
"properties": {
+180 -3
View File
@@ -469,6 +469,9 @@ definitions:
type: integer
package_id:
type: string
payment_method:
description: 可选:支付方式,不传则为免费订单
type: string
required:
- license_count
- package_id
@@ -1319,6 +1322,78 @@ definitions:
description: 响应消息
type: string
type: object
models.PaymentBusinessOrder:
properties:
authorization_code:
type: string
order_no:
type: string
status:
type: string
type: object
models.PaymentListResponse:
properties:
page:
type: integer
page_size:
type: integer
payments:
items:
$ref: '#/definitions/models.PaymentResponse'
type: array
total_count:
type: integer
type: object
models.PaymentResponse:
properties:
amount:
type: number
business_id:
type: string
business_type:
type: string
created_at:
type: string
currency:
type: string
expire_time:
type: string
id:
type: integer
payment_method:
type: string
payment_no:
type: string
payment_provider:
type: string
payment_time:
type: string
payment_url:
type: string
status:
type: string
trade_no:
type: string
updated_at:
type: string
user_id:
type: integer
type: object
models.PaymentStatusResponse:
properties:
amount:
type: number
business_order:
$ref: '#/definitions/models.PaymentBusinessOrder'
payment_no:
type: string
payment_time:
type: string
status:
type: string
trade_no:
type: string
type: object
models.RecentAuthorization:
properties:
code:
@@ -1736,7 +1811,7 @@ paths:
post:
consumes:
- application/json
description: 创建新的产品套餐订单
description: 创建新的产品套餐订单,支持免费和付费模式
parameters:
- description: 订单创建信息
in: body
@@ -1748,19 +1823,23 @@ paths:
- application/json
responses:
"200":
description: 成功
description: 付费订单成功
schema:
allOf:
- $ref: '#/definitions/models.APIResponse'
- properties:
data:
properties:
authorization_code:
expire_time:
type: string
id:
type: string
order_no:
type: string
payment_no:
type: string
payment_url:
type: string
total_amount:
type: number
type: object
@@ -2557,6 +2636,104 @@ paths:
summary: 获取指定类型的枚举值
tags:
- 枚举管理
/api/payment/{payment_no}/status:
get:
consumes:
- application/json
description: 根据支付单号获取支付状态
parameters:
- description: 支付单号
in: path
name: payment_no
required: true
type: string
produces:
- application/json
responses:
"200":
description: 成功
schema:
allOf:
- $ref: '#/definitions/models.APIResponse'
- properties:
data:
$ref: '#/definitions/models.PaymentStatusResponse'
type: object
"401":
description: 未认证
schema:
$ref: '#/definitions/models.ErrorResponse'
"404":
description: 支付单不存在
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: 获取支付状态
tags:
- 支付管理
/api/payment/alipay/callback:
post:
consumes:
- application/x-www-form-urlencoded
description: 处理支付宝支付回调
parameters:
- description: 支付宝回调参数
in: body
name: notification
schema:
type: string
produces:
- text/plain
responses:
"200":
description: success
schema:
type: string
"400":
description: failure
schema:
type: string
summary: 支付宝回调
tags:
- 支付管理
/api/payment/history:
get:
consumes:
- application/json
description: 获取当前用户的支付记录列表
parameters:
- default: 1
description: 页码
in: query
name: page
type: integer
- default: 10
description: 每页数量
in: query
name: page_size
type: integer
produces:
- application/json
responses:
"200":
description: 成功
schema:
allOf:
- $ref: '#/definitions/models.APIResponse'
- properties:
data:
$ref: '#/definitions/models.PaymentListResponse'
type: object
"401":
description: 未认证
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: 获取用户支付历史
tags:
- 支付管理
/api/v1/activate:
post:
consumes:
+4
View File
@@ -62,6 +62,10 @@ require (
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/smartwalle/alipay/v3 v3.2.28 // indirect
github.com/smartwalle/ncrypto v1.0.4 // indirect
github.com/smartwalle/ngx v1.0.12 // indirect
github.com/smartwalle/nsign v1.0.9 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
+8
View File
@@ -126,6 +126,14 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartwalle/alipay/v3 v3.2.28 h1:pANvguTjmFtfTw3le44qjQHYDrRcjCy8r8YUonFeblE=
github.com/smartwalle/alipay/v3 v3.2.28/go.mod h1:dwPyjY5y17qUsDrsVYCQqmDBtQ/qpvY8JmQMvuJYGLQ=
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk=
github.com/smartwalle/ngx v1.0.12 h1:jcoCyu/0HtQ1y/gbiSLzqOUZcHnVLlKOmm0awRF7Mcg=
github.com/smartwalle/ngx v1.0.12/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@@ -14,11 +14,13 @@ import (
type CuOrderHandler struct {
cuOrderService service.CuOrderService
paymentService service.PaymentService
}
func NewCuOrderHandler(cuOrderService service.CuOrderService) *CuOrderHandler {
func NewCuOrderHandler(cuOrderService service.CuOrderService, paymentService service.PaymentService) *CuOrderHandler {
return &CuOrderHandler{
cuOrderService: cuOrderService,
paymentService: paymentService,
}
}
@@ -53,7 +55,8 @@ func (h *CuOrderHandler) getPackageList() []map[string]interface{} {
"type": "trial",
"price": 0,
"max_devices": 1,
"description": "免费试用,有效期至本月25日",
"description": "免费体验全部功能",
"details": "包含全部功能|试用期限制|1个许可等",
},
{
"id": "basic",
@@ -61,7 +64,8 @@ func (h *CuOrderHandler) getPackageList() []map[string]interface{} {
"type": "basic",
"price": 300,
"max_devices": 1000,
"description": "基础功能,支持批量许可购买",
"description": "小型企业最佳选择",
"details": "包含基础功能|批量许可购买|折扣优惠等",
},
{
"id": "professional",
@@ -69,7 +73,8 @@ func (h *CuOrderHandler) getPackageList() []map[string]interface{} {
"type": "professional",
"price": 2000,
"max_devices": 1000,
"description": "全部功能,支持批量许可购买",
"description": "企业级完整解决方案",
"details": "包含高级功能|技术支持|数据分析等",
},
}
}
@@ -138,15 +143,16 @@ func (h *CuOrderHandler) CalculatePrice(c *gin.Context) {
})
}
// CreateOrder 创建订单
// CreateOrder 创建订单(支持支付)
// @Summary 创建订单
// @Description 创建新的产品套餐订单
// @Description 创建新的产品套餐订单,支持免费和付费模式
// @Tags 客户订单管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param order body models.CuOrderCreateRequest true "订单创建信息"
// @Success 200 {object} models.APIResponse{data=object{id=string,order_no=string,total_amount=number,authorization_code=string}} "成功"
// @Success 200 {object} models.APIResponse{data=object{id=string,order_no=string,total_amount=number,authorization_code=string}} "免费订单成功"
// @Success 200 {object} models.APIResponse{data=object{id=string,order_no=string,payment_no=string,total_amount=number,payment_url=string,expire_time=string}} "付费订单成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
// @Failure 404 {object} models.ErrorResponse "套餐不存在"
@@ -190,12 +196,23 @@ func (h *CuOrderHandler) CreateOrder(c *gin.Context) {
return
}
// 创建订单
order, err := h.cuOrderService.CreateOrder(c.Request.Context(), cuUserID.(string), customerID.(string), &req)
// 根据是否有支付方式决定处理逻辑
if req.PaymentMethod == "" {
// 免费订单:直接创建订单
h.createFreeOrder(c, cuUserID.(string), customerID.(string), &req)
} else {
// 付费订单:创建订单和支付单
h.createPaidOrder(c, cuUserID.(string), customerID.(string), &req)
}
}
// createFreeOrder 创建免费订单
func (h *CuOrderHandler) createFreeOrder(c *gin.Context, cuUserID, customerID string, req *models.CuOrderCreateRequest) {
order, err := h.cuOrderService.CreateOrder(c.Request.Context(), cuUserID, customerID, req)
if err != nil {
c.JSON(err.(*i18n.I18nError).HttpCode, models.ErrorResponse{
Code: err.(*i18n.I18nError).Code,
Message: err.(*i18n.I18nError).Message, // 直接使用错误对象中的消息
Message: err.(*i18n.I18nError).Message,
Timestamp: getCurrentTimestamp(),
})
return
@@ -216,6 +233,73 @@ func (h *CuOrderHandler) CreateOrder(c *gin.Context) {
})
}
// createPaidOrder 创建付费订单
func (h *CuOrderHandler) createPaidOrder(c *gin.Context, cuUserID, customerID string, req *models.CuOrderCreateRequest) {
// 客户用户创建订单,不需要额外的管理员权限检查
// cuUserID 和 customerID 已经在上层方法中验证过了
// 计算价格
priceResult, err := h.cuOrderService.CalculatePrice(c.Request.Context(), req.PackageID, req.LicenseCount)
if err != nil {
c.JSON(err.(*i18n.I18nError).HttpCode, models.ErrorResponse{
Code: err.(*i18n.I18nError).Code,
Message: err.(*i18n.I18nError).Message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 创建业务订单(pending状态)
order, err := h.cuOrderService.CreatePendingOrder(c.Request.Context(), cuUserID, customerID, req)
if err != nil {
c.JSON(err.(*i18n.I18nError).HttpCode, models.ErrorResponse{
Code: err.(*i18n.I18nError).Code,
Message: err.(*i18n.I18nError).Message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 创建支付单
paymentReq := &models.PaymentCreateRequest{
BusinessType: models.BusinessTypePackageOrder,
BusinessID: &order.ID,
CustomerID: customerID,
CuUserID: cuUserID,
Amount: priceResult.TotalAmount,
Currency: "CNY",
PaymentMethod: req.PaymentMethod,
}
payment, err := h.paymentService.CreatePayment(c.Request.Context(), paymentReq)
if err != nil {
// 如果支付单创建失败,需要回滚业务订单
// 这里应该有事务处理,暂时先返回错误
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Code: "602001",
Message: "支付单创建失败",
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMsg := i18n.GetI18nErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMsg,
Data: gin.H{
"id": order.ID,
"order_no": order.OrderNo,
"payment_no": payment.PaymentNo,
"total_amount": priceResult.TotalAmount,
"payment_url": payment.PaymentURL,
"expire_time": payment.ExpireTime.Format("2006-01-02T15:04:05Z"),
},
Timestamp: getCurrentTimestamp(),
})
}
// GetOrder 获取订单详情
// @Summary 获取订单详情
// @Description 根据订单ID获取订单详细信息
@@ -0,0 +1,158 @@
package handlers
import (
"net/http"
"strconv"
"license-manager/internal/api/middleware"
"license-manager/internal/models"
"license-manager/internal/service"
"license-manager/pkg/i18n"
"license-manager/pkg/utils"
"github.com/gin-gonic/gin"
)
type PaymentHandler struct {
paymentService service.PaymentService
}
func NewPaymentHandler(paymentService service.PaymentService) *PaymentHandler {
return &PaymentHandler{
paymentService: paymentService,
}
}
// GetPaymentStatus 获取支付状态
// @Summary 获取支付状态
// @Description 根据支付单号获取支付状态
// @Tags 支付管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param payment_no path string true "支付单号"
// @Success 200 {object} models.APIResponse{data=models.PaymentStatusResponse} "成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 404 {object} models.ErrorResponse "支付单不存在"
// @Router /api/payment/{payment_no}/status [get]
func (h *PaymentHandler) GetPaymentStatus(c *gin.Context) {
paymentNo := c.Param("payment_no")
if paymentNo == "" {
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message,
Timestamp: getCurrentTimestamp(),
})
return
}
status, err := h.paymentService.GetPaymentStatus(c.Request.Context(), paymentNo)
if err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Code: "602001",
Message: "获取支付状态失败",
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMsg := i18n.GetI18nErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMsg,
Data: status,
Timestamp: getCurrentTimestamp(),
})
}
// GetUserPayments 获取用户支付历史
// @Summary 获取用户支付历史
// @Description 获取当前用户的支付记录列表
// @Tags 支付管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query integer false "页码" default(1)
// @Param page_size query integer false "每页数量" default(10)
// @Success 200 {object} models.APIResponse{data=models.PaymentListResponse} "成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Router /api/payment/history [get]
func (h *PaymentHandler) GetUserPayments(c *gin.Context) {
// 解析分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
// 获取客户用户ID和客户ID
cuUserID, exists := c.Get("cu_user_id")
if !exists {
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("100004", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message,
Timestamp: getCurrentTimestamp(),
})
return
}
customerID, exists := c.Get("cu_customer_id")
if !exists {
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("100004", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message,
Timestamp: getCurrentTimestamp(),
})
return
}
payments, err := h.paymentService.GetUserPayments(c.Request.Context(), customerID.(string), cuUserID.(string), page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Code: "602001",
Message: "获取支付历史失败",
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMsg := i18n.GetI18nErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMsg,
Data: payments,
Timestamp: getCurrentTimestamp(),
})
}
// AlipayCallback 支付宝回调处理
// @Summary 支付宝回调
// @Description 处理支付宝支付回调
// @Tags 支付管理
// @Accept application/x-www-form-urlencoded
// @Produce text/plain
// @Param notification body string false "支付宝回调参数"
// @Success 200 {string} string "success"
// @Failure 400 {string} string "failure"
// @Router /api/payment/alipay/callback [post]
func (h *PaymentHandler) AlipayCallback(c *gin.Context) {
// 这里我们需要一个alipay client实例来解析回调
// 由于我们在service层已经有了client,这里暂时简化处理
// 实际应该从service中获取client
// 暂时使用简化的处理方式
// 实际项目中应该使用service中的client来验证和处理
utils.SendNotificationResponse(c.Writer, true)
}
+55 -1
View File
@@ -1,6 +1,7 @@
package routes
import (
"fmt"
_ "license-manager/docs/swagger" // swagger docs
"license-manager/internal/api/handlers"
"license-manager/internal/api/middleware"
@@ -43,6 +44,7 @@ func SetupRouter() *gin.Engine {
authCodeRepo := repository.NewAuthorizationCodeRepository(db)
licenseRepo := repository.NewLicenseRepository(db)
dashboardRepo := repository.NewDashboardRepository(db)
paymentRepo := repository.NewPaymentRepository(db)
// 获取logger实例
log := logger.GetLogger()
@@ -55,6 +57,7 @@ func SetupRouter() *gin.Engine {
authCodeService := service.NewAuthorizationCodeService(authCodeRepo, customerRepo, cuUserRepo, licenseRepo)
cuOrderService := service.NewCuOrderService(cuOrderRepo, cuUserRepo, authCodeRepo, db)
cuDeviceService := service.NewCuDeviceService(licenseRepo)
paymentService := service.NewPaymentService(paymentRepo, cuOrderRepo, convertPaymentConfig(cfg.Payment))
enumService := service.NewEnumService()
licenseService := service.NewLicenseService(licenseRepo, db, log)
dashboardService := service.NewDashboardService(dashboardRepo)
@@ -65,8 +68,9 @@ func SetupRouter() *gin.Engine {
customerHandler := handlers.NewCustomerHandler(customerService)
cuAuthHandler := handlers.NewCuAuthHandler(cuUserService)
cuProfileHandler := handlers.NewCuProfileHandler(cuUserService)
cuOrderHandler := handlers.NewCuOrderHandler(cuOrderService)
cuOrderHandler := handlers.NewCuOrderHandler(cuOrderService, paymentService)
cuDeviceHandler := handlers.NewCuDeviceHandler(cuDeviceService)
paymentHandler := handlers.NewPaymentHandler(paymentService)
cuAuthorizationHandler := handlers.NewCuAuthorizationHandler(authCodeService)
enumHandler := handlers.NewEnumHandler(enumService)
authCodeHandler := handlers.NewAuthorizationCodeHandler(authCodeService)
@@ -90,6 +94,9 @@ func SetupRouter() *gin.Engine {
// 许可证激活接口(客户端软件使用)
public.POST("/v1/activate", licenseHandler.ActivateLicense)
public.POST("/v1/heartbeat", licenseHandler.Heartbeat)
// 支付回调接口
public.POST("/payment/alipay/callback", paymentHandler.AlipayCallback)
}
// 需要认证的接口
@@ -174,6 +181,10 @@ func SetupRouter() *gin.Engine {
cuAuth.GET("/orders/:order_id", cuOrderHandler.GetOrder)
cuAuth.GET("/orders", cuOrderHandler.GetUserOrders)
// 支付管理
cuAuth.GET("/payment/:payment_no/status", paymentHandler.GetPaymentStatus)
cuAuth.GET("/payment/history", paymentHandler.GetUserPayments)
// 授权码管理
cuAuth.POST("/authorization-codes/:codeId/share", cuAuthorizationHandler.ShareAuthorizationCode)
@@ -185,3 +196,46 @@ func SetupRouter() *gin.Engine {
return router
}
// convertPaymentConfig 转换支付配置
func convertPaymentConfig(cfg config.PaymentConfig) *service.PaymentConfig {
fmt.Printf("convertPaymentConfig: cfg.DefaultMethod=%s, cfg.Providers is nil: %v\n", cfg.DefaultMethod, cfg.Providers == nil)
if cfg.Providers != nil {
fmt.Printf("convertPaymentConfig: Found %d providers\n", len(cfg.Providers))
for name, provider := range cfg.Providers {
fmt.Printf("convertPaymentConfig: Provider %s, Enabled=%v\n", name, provider.Enabled)
}
}
config := &service.PaymentConfig{
DefaultMethod: cfg.DefaultMethod,
ExpireMinutes: cfg.ExpireMinutes,
}
if cfg.Providers != nil {
config.Providers = make(map[string]*service.AlipayProviderConfig)
for name, provider := range cfg.Providers {
if provider.Enabled {
config.Providers[name] = &service.AlipayProviderConfig{
Enabled: provider.Enabled,
AppID: provider.AppID,
PrivateKey: provider.PrivateKey,
PublicKey: provider.PublicKey,
GatewayURL: provider.GatewayURL,
NotifyURL: provider.NotifyURL,
ReturnURL: provider.ReturnURL,
SignType: provider.SignType,
Charset: provider.Charset,
Format: provider.Format,
}
}
}
}
fmt.Printf("convertPaymentConfig: Final config.Providers is nil: %v\n", config.Providers == nil)
if config.Providers != nil {
fmt.Printf("convertPaymentConfig: Final config has %d providers\n", len(config.Providers))
}
return config
}
+28
View File
@@ -14,6 +14,7 @@ type Config struct {
Log LogConfig `mapstructure:"log"`
I18n I18nConfig `mapstructure:"i18n"`
License LicenseConfig `mapstructure:"license"`
Payment PaymentConfig `mapstructure:"payment"`
}
type ServerConfig struct {
@@ -88,6 +89,27 @@ type RSAConfig struct {
KeySize int `mapstructure:"key_size"` // 密钥大小(2048或4096),默认2048
}
type PaymentConfig struct {
DefaultMethod string `mapstructure:"default_method"` // 默认支付方式
Providers map[string]*PaymentProvider `mapstructure:"providers"` // 支付提供商配置
ExpireMinutes int `mapstructure:"expire_minutes"` // 支付过期时间(分钟)
RetryTimes int `mapstructure:"retry_times"` // 支付重试次数
EnableLog bool `mapstructure:"enable_log"` // 启用支付日志
}
type PaymentProvider struct {
AppID string `mapstructure:"app_id"`
PrivateKey string `mapstructure:"private_key"`
PublicKey string `mapstructure:"public_key"`
GatewayURL string `mapstructure:"gateway_url"`
NotifyURL string `mapstructure:"notify_url"`
ReturnURL string `mapstructure:"return_url"`
SignType string `mapstructure:"sign_type"`
Charset string `mapstructure:"charset"`
Format string `mapstructure:"format"`
Enabled bool `mapstructure:"enabled"`
}
var AppConfig *Config
func Load(configPath string) error {
@@ -172,6 +194,12 @@ func setDefaults() {
viper.SetDefault("license.heartbeat_timeout", 300)
viper.SetDefault("license.offline_timeout", 1440)
viper.SetDefault("license.expiring_days", 30)
// Payment defaults
viper.SetDefault("payment.default_method", "alipay")
viper.SetDefault("payment.expire_minutes", 30)
viper.SetDefault("payment.retry_times", 3)
viper.SetDefault("payment.enable_log", true)
}
func GetConfig() *Config {
+1
View File
@@ -25,6 +25,7 @@ func AutoMigrate() error {
&models.User{},
&models.CuUser{},
&models.CuOrder{},
&models.Payment{},
&models.AuthorizationCode{},
&models.License{},
&models.AuthorizationChange{},
+3 -2
View File
@@ -59,8 +59,9 @@ func (o *CuOrder) BeforeUpdate(tx *gorm.DB) error {
// CuOrderCreateRequest 创建订单请求结构
type CuOrderCreateRequest struct {
PackageID string `json:"package_id" binding:"required"`
LicenseCount int `json:"license_count" binding:"required,min=1,max=1000"`
PackageID string `json:"package_id" binding:"required"`
LicenseCount int `json:"license_count" binding:"required,min=1,max=1000"`
PaymentMethod string `json:"payment_method,omitempty"` // 可选:支付方式,不传则为免费订单
}
// CuOrderResponse 订单响应结构
+157
View File
@@ -0,0 +1,157 @@
package models
import (
"time"
"gorm.io/gorm"
)
// Payment 支付订单模型
type Payment struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
PaymentNo string `gorm:"type:varchar(64);uniqueIndex;not null" json:"payment_no"`
BusinessType string `gorm:"type:varchar(50);not null;index:business_type_id" json:"business_type"`
BusinessID *string `gorm:"type:varchar(36);index:business_type_id" json:"business_id"`
CustomerID string `gorm:"type:varchar(36);not null;index" json:"customer_id"`
CuUserID string `gorm:"type:varchar(36);not null;index" json:"cu_user_id"`
Amount float64 `gorm:"type:decimal(10,2);not null" json:"amount"`
Currency string `gorm:"type:varchar(3);default:'CNY'" json:"currency"`
PaymentMethod string `gorm:"type:varchar(20);default:'alipay'" json:"payment_method"`
PaymentProvider string `gorm:"type:varchar(20);default:'alipay'" json:"payment_provider"`
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"`
TradeNo *string `gorm:"type:varchar(64)" json:"trade_no"`
PaymentTime *time.Time `gorm:"type:datetime(3)" json:"payment_time"`
ExpireTime time.Time `gorm:"type:datetime(3);not null" json:"expire_time"`
PaymentURL *string `gorm:"type:text" json:"payment_url"`
NotifyData *string `gorm:"type:json" json:"notify_data"`
ExtraData *string `gorm:"type:json" json:"extra_data"`
CreatedAt time.Time `gorm:"type:datetime(3);not null;index" json:"created_at"`
UpdatedAt time.Time `gorm:"type:datetime(3);not null" json:"updated_at"`
}
// TableName 指定表名
func (Payment) TableName() string {
return "payments"
}
// BeforeCreate 创建前自动设置时间戳
func (p *Payment) BeforeCreate(tx *gorm.DB) error {
now := time.Now()
if p.CreatedAt.IsZero() {
p.CreatedAt = now
}
if p.UpdatedAt.IsZero() {
p.UpdatedAt = now
}
return nil
}
// BeforeUpdate 更新前自动设置时间戳
func (p *Payment) BeforeUpdate(tx *gorm.DB) error {
p.UpdatedAt = time.Now()
return nil
}
// PaymentCreateRequest 创建支付请求结构
type PaymentCreateRequest struct {
BusinessType string `json:"business_type" binding:"required"`
BusinessID *string `json:"business_id"`
CustomerID string `json:"customer_id" binding:"required"`
CuUserID string `json:"cu_user_id" binding:"required"`
Amount float64 `json:"amount" binding:"required,min=0.01"`
Currency string `json:"currency"`
PaymentMethod string `json:"payment_method"`
}
// PaymentResponse 支付响应结构
type PaymentResponse struct {
ID int `json:"id"`
PaymentNo string `json:"payment_no"`
BusinessType string `json:"business_type"`
BusinessID *string `json:"business_id"`
CustomerID string `json:"customer_id"`
CuUserID string `json:"cu_user_id"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
PaymentMethod string `json:"payment_method"`
PaymentProvider string `json:"payment_provider"`
Status string `json:"status"`
TradeNo *string `json:"trade_no"`
PaymentTime *time.Time `json:"payment_time"`
ExpireTime time.Time `json:"expire_time"`
PaymentURL *string `json:"payment_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ToResponse 转换为响应结构
func (p *Payment) ToResponse() *PaymentResponse {
return &PaymentResponse{
ID: p.ID,
PaymentNo: p.PaymentNo,
BusinessType: p.BusinessType,
BusinessID: p.BusinessID,
CustomerID: p.CustomerID,
CuUserID: p.CuUserID,
Amount: p.Amount,
Currency: p.Currency,
PaymentMethod: p.PaymentMethod,
PaymentProvider: p.PaymentProvider,
Status: p.Status,
TradeNo: p.TradeNo,
PaymentTime: p.PaymentTime,
ExpireTime: p.ExpireTime,
PaymentURL: p.PaymentURL,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
}
// PaymentStatusResponse 支付状态响应结构
type PaymentStatusResponse struct {
PaymentNo string `json:"payment_no"`
Status string `json:"status"`
Amount float64 `json:"amount"`
PaymentTime *time.Time `json:"payment_time"`
TradeNo *string `json:"trade_no"`
BusinessOrder *PaymentBusinessOrder `json:"business_order,omitempty"`
}
// PaymentBusinessOrder 支付关联的业务订单信息
type PaymentBusinessOrder struct {
OrderNo string `json:"order_no"`
AuthorizationCode *string `json:"authorization_code"`
Status string `json:"status"`
}
// PaymentListResponse 支付列表响应结构
type PaymentListResponse struct {
Payments []*PaymentResponse `json:"payments"`
TotalCount int64 `json:"total_count"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// 业务类型常量
const (
BusinessTypePackageOrder = "package_order" // 套餐购买订单
BusinessTypeLicenseExtend = "license_extend" // 许可延期
BusinessTypeServiceUpgrade = "service_upgrade" // 服务升级
)
// 支付状态常量
const (
PaymentStatusPending = "pending" // 待支付
PaymentStatusPaid = "paid" // 已支付
PaymentStatusCancelled = "cancelled" // 已取消
PaymentStatusExpired = "expired" // 已过期
PaymentStatusFailed = "failed" // 支付失败
PaymentStatusRefunded = "refunded" // 已退款
)
// 支付方式常量
const (
PaymentMethodAlipay = "alipay" // 支付宝
PaymentMethodWechat = "wechat" // 微信支付
PaymentMethodBank = "bank" // 银行卡
)
+30
View File
@@ -141,3 +141,33 @@ type DashboardRepository interface {
// GetRecentAuthorizations 获取最近授权列表
GetRecentAuthorizations(ctx context.Context, req *models.DashboardRecentAuthorizationsRequest) (*models.DashboardRecentAuthorizationsResponse, error)
}
// PaymentRepository 支付数据访问接口
type PaymentRepository interface {
// Create 创建支付订单
Create(ctx context.Context, payment *models.Payment) error
// GetByID 根据ID获取支付订单
GetByID(ctx context.Context, id int) (*models.Payment, error)
// GetByPaymentNo 根据支付单号获取支付订单
GetByPaymentNo(ctx context.Context, paymentNo string) (*models.Payment, error)
// GetByBusinessID 根据业务ID获取支付订单
GetByBusinessID(ctx context.Context, businessType, businessID string) (*models.Payment, error)
// GetByCustomerAndCuUserID 根据客户ID和客户用户ID获取支付订单列表
GetByCustomerAndCuUserID(ctx context.Context, customerID, cuUserID string, offset, limit int) ([]*models.Payment, int64, error)
// Update 更新支付订单
Update(ctx context.Context, payment *models.Payment) error
// UpdateStatus 更新支付状态
UpdateStatus(ctx context.Context, paymentNo, status string, tradeNo *string, paymentTime *string) error
// Delete 删除支付订单
Delete(ctx context.Context, id int) error
// GetExpiredPayments 获取过期的支付订单
GetExpiredPayments(ctx context.Context) ([]*models.Payment, error)
}
@@ -0,0 +1,102 @@
package repository
import (
"context"
"license-manager/internal/models"
"gorm.io/gorm"
)
type paymentRepository struct {
db *gorm.DB
}
func NewPaymentRepository(db *gorm.DB) PaymentRepository {
return &paymentRepository{db: db}
}
func (r *paymentRepository) Create(ctx context.Context, payment *models.Payment) error {
return r.db.WithContext(ctx).Create(payment).Error
}
func (r *paymentRepository) GetByID(ctx context.Context, id int) (*models.Payment, error) {
var payment models.Payment
err := r.db.WithContext(ctx).Where("id = ?", id).First(&payment).Error
if err != nil {
return nil, err
}
return &payment, nil
}
func (r *paymentRepository) GetByPaymentNo(ctx context.Context, paymentNo string) (*models.Payment, error) {
var payment models.Payment
err := r.db.WithContext(ctx).Where("payment_no = ?", paymentNo).First(&payment).Error
if err != nil {
return nil, err
}
return &payment, nil
}
func (r *paymentRepository) GetByBusinessID(ctx context.Context, businessType, businessID string) (*models.Payment, error) {
var payment models.Payment
err := r.db.WithContext(ctx).Where("business_type = ? AND business_id = ?", businessType, businessID).First(&payment).Error
if err != nil {
return nil, err
}
return &payment, nil
}
func (r *paymentRepository) GetByCustomerAndCuUserID(ctx context.Context, customerID, cuUserID string, offset, limit int) ([]*models.Payment, int64, error) {
var payments []*models.Payment
var total int64
query := r.db.WithContext(ctx).Model(&models.Payment{}).Where("customer_id = ? AND cu_user_id = ?", customerID, cuUserID)
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取分页数据
err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&payments).Error
if err != nil {
return nil, 0, err
}
return payments, total, nil
}
func (r *paymentRepository) Update(ctx context.Context, payment *models.Payment) error {
return r.db.WithContext(ctx).Save(payment).Error
}
func (r *paymentRepository) UpdateStatus(ctx context.Context, paymentNo, status string, tradeNo *string, paymentTime *string) error {
updateData := map[string]interface{}{
"status": status,
}
if tradeNo != nil {
updateData["trade_no"] = *tradeNo
}
if paymentTime != nil {
updateData["payment_time"] = *paymentTime
}
return r.db.WithContext(ctx).Model(&models.Payment{}).Where("payment_no = ?", paymentNo).Updates(updateData).Error
}
func (r *paymentRepository) Delete(ctx context.Context, id int) error {
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.Payment{}).Error
}
func (r *paymentRepository) GetExpiredPayments(ctx context.Context) ([]*models.Payment, error) {
var payments []*models.Payment
err := r.db.WithContext(ctx).Where("status = ? AND expire_time < NOW()", models.PaymentStatusPending).Find(&payments).Error
if err != nil {
return nil, err
}
return payments, nil
}
@@ -17,9 +17,11 @@ import (
type CuOrderService interface {
CreateOrder(ctx context.Context, cuUserID, customerID string, req *models.CuOrderCreateRequest) (*models.CuOrder, error)
CreatePendingOrder(ctx context.Context, cuUserID, customerID string, req *models.CuOrderCreateRequest) (*models.CuOrder, error)
GetOrder(ctx context.Context, orderID, cuUserID string) (*models.CuOrder, error)
GetUserOrders(ctx context.Context, cuUserID string, offset, limit int) ([]*models.CuOrder, int64, error)
CalculatePrice(ctx context.Context, packageID string, licenseCount int) (*PriceCalculationResult, error)
UpdateOrderStatus(ctx context.Context, orderID string, status string) error
}
type PriceCalculationResult struct {
@@ -85,6 +87,62 @@ func NewCuOrderService(repo repository.CuOrderRepository, cuUserRepo repository.
}
}
func (s *cuOrderService) CreatePendingOrder(ctx context.Context, cuUserID, customerID string, req *models.CuOrderCreateRequest) (*models.CuOrder, error) {
// 计算价格
priceResult, err := s.CalculatePrice(ctx, req.PackageID, req.LicenseCount)
if err != nil {
return nil, err
}
// 生成订单号
orderNo := s.generateOrderNo()
// 获取套餐配置
packageConfig, exists := packageConfigs[req.PackageID]
if !exists {
return nil, i18n.NewI18nError("600001", "套餐不存在")
}
// 创建订单(pending状态)
order := &models.CuOrder{
OrderNo: orderNo,
CustomerID: customerID,
CuUserID: cuUserID,
PackageID: req.PackageID,
PackageName: packageConfig.Name,
LicenseCount: req.LicenseCount,
UnitPrice: priceResult.UnitPrice,
DiscountRate: priceResult.DiscountRate,
TotalAmount: priceResult.TotalAmount,
Status: "pending", // 待支付状态
}
// 对于试用版,设置过期时间
if req.PackageID == "trial" {
expiredAt := s.getTrialExpiryTime()
order.ExpiredAt = &expiredAt
}
// 保存订单
if err := s.repo.Create(order); err != nil {
return nil, i18n.NewI18nError("601001", "订单创建失败: "+err.Error())
}
return order, nil
}
func (s *cuOrderService) UpdateOrderStatus(ctx context.Context, orderID string, status string) error {
// 这里需要实现更新订单状态的逻辑
// 暂时先返回nil,后面再实现
return nil
}
// getTrialExpiryTime 获取试用版过期时间(当月25日23:59:59)
func (s *cuOrderService) getTrialExpiryTime() time.Time {
now := time.Now()
return time.Date(now.Year(), now.Month(), 25, 23, 59, 59, 0, now.Location())
}
func (s *cuOrderService) CreateOrder(ctx context.Context, cuUserID, customerID string, req *models.CuOrderCreateRequest) (*models.CuOrder, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
+1
View File
@@ -82,3 +82,4 @@ type CuDeviceService interface {
// 解绑设备
UnbindDevice(ctx context.Context, customerID, licenseID string) error
}
+245
View File
@@ -0,0 +1,245 @@
package service
import (
"context"
"fmt"
"license-manager/internal/models"
"license-manager/internal/repository"
"license-manager/pkg/utils"
"time"
"github.com/smartwalle/alipay/v3"
)
type PaymentService interface {
CreatePayment(ctx context.Context, req *models.PaymentCreateRequest) (*models.Payment, error)
GetPayment(ctx context.Context, paymentNo string) (*models.Payment, error)
GetPaymentStatus(ctx context.Context, paymentNo string) (*models.PaymentStatusResponse, error)
ProcessPaymentCallback(ctx context.Context, notification interface{}) error
GetUserPayments(ctx context.Context, customerID, cuUserID string, page, pageSize int) (*models.PaymentListResponse, error)
CancelExpiredPayments(ctx context.Context) error
}
type paymentService struct {
paymentRepo repository.PaymentRepository
cuOrderRepo repository.CuOrderRepository
alipayClient *alipay.Client
config *PaymentConfig
}
type PaymentConfig struct {
DefaultMethod string `json:"default_method"`
Providers map[string]*AlipayProviderConfig `json:"providers"`
ExpireMinutes int `json:"expire_minutes"`
}
type AlipayProviderConfig struct {
Enabled bool `json:"enabled"`
AppID string `json:"app_id"`
PrivateKey string `json:"private_key"`
PublicKey string `json:"public_key"`
GatewayURL string `json:"gateway_url"`
NotifyURL string `json:"notify_url"`
ReturnURL string `json:"return_url"`
SignType string `json:"sign_type"`
Charset string `json:"charset"`
Format string `json:"format"`
}
func NewPaymentService(
paymentRepo repository.PaymentRepository,
cuOrderRepo repository.CuOrderRepository,
config *PaymentConfig,
) PaymentService {
var alipayClient *alipay.Client
fmt.Printf("PaymentService: Checking providers, config.Providers is nil: %v\n", config.Providers == nil)
if config.Providers != nil {
fmt.Printf("PaymentService: Found %d providers\n", len(config.Providers))
for providerName := range config.Providers {
fmt.Printf("PaymentService: Provider: %s\n", providerName)
}
if alipayConfig, exists := config.Providers["alipay"]; exists && alipayConfig.Enabled {
fmt.Printf("Initializing Alipay client with APPID: %s\n", alipayConfig.AppID)
fmt.Printf("Private key length: %d\n", len(alipayConfig.PrivateKey))
fmt.Printf("Public key length: %d\n", len(alipayConfig.PublicKey))
// 使用官方SDK创建客户端
fmt.Printf("Creating Alipay client with APPID: %s\n", alipayConfig.AppID)
client, err := alipay.New(alipayConfig.AppID, alipayConfig.PrivateKey, false)
if err != nil {
fmt.Printf("Failed to create Alipay client: %v\n", err)
fmt.Printf("Private key length: %d\n", len(alipayConfig.PrivateKey))
} else {
fmt.Printf("Alipay client created successfully\n")
// 先不加载公钥,测试基本功能
alipayClient = client
fmt.Println("Alipay client initialized successfully")
}
} else {
fmt.Println("Alipay provider not found or disabled")
}
} else {
fmt.Println("No payment providers configured")
}
return &paymentService{
paymentRepo: paymentRepo,
cuOrderRepo: cuOrderRepo,
alipayClient: alipayClient,
config: config,
}
}
func (s *paymentService) CreatePayment(ctx context.Context, req *models.PaymentCreateRequest) (*models.Payment, error) {
// 设置默认值
if req.Currency == "" {
req.Currency = "CNY"
}
if req.PaymentMethod == "" {
req.PaymentMethod = s.config.DefaultMethod
}
// 生成支付单号
paymentNo := utils.GeneratePaymentNo()
// 计算过期时间
expireTime := time.Now().Add(time.Duration(s.config.ExpireMinutes) * time.Minute)
// 创建支付单
payment := &models.Payment{
PaymentNo: paymentNo,
BusinessType: req.BusinessType,
BusinessID: req.BusinessID,
CustomerID: req.CustomerID,
CuUserID: req.CuUserID,
Amount: req.Amount,
Currency: req.Currency,
PaymentMethod: req.PaymentMethod,
PaymentProvider: req.PaymentMethod,
Status: models.PaymentStatusPending,
ExpireTime: expireTime,
}
// 如果是支付宝,生成支付URL
if req.PaymentMethod == "alipay" {
fmt.Printf("Payment method is alipay, checking client...\n")
if s.alipayClient == nil {
fmt.Printf("Alipay client is nil!\n")
return nil, fmt.Errorf("alipay client not initialized")
}
fmt.Printf("Alipay client is available, creating payment request...\n")
// 使用官方SDK创建支付请求
// TradePagePay继承了Trade,需要通过Trade字段设置
payRequest := alipay.TradePagePay{
Trade: alipay.Trade{
OutTradeNo: paymentNo,
TotalAmount: fmt.Sprintf("%.2f", req.Amount),
Subject: "产品套餐购买",
ProductCode: "FAST_INSTANT_TRADE_PAY",
},
}
fmt.Printf("Creating Alipay payment request: OutTradeNo=%s, TotalAmount=%s\n", paymentNo, fmt.Sprintf("%.2f", req.Amount))
// 生成支付URL
payURL, err := s.alipayClient.TradePagePay(payRequest)
if err != nil {
fmt.Printf("TradePagePay failed: %v\n", err)
return nil, fmt.Errorf("generate payment URL failed: %w", err)
}
payURLStr := payURL.String()
fmt.Printf("Generated payment URL: %s\n", payURLStr[:100]+"...")
payment.PaymentURL = &payURLStr
} else {
fmt.Printf("Payment method is: %s\n", req.PaymentMethod)
}
// 保存到数据库
if err := s.paymentRepo.Create(ctx, payment); err != nil {
return nil, fmt.Errorf("create payment failed: %w", err)
}
return payment, nil
}
func (s *paymentService) GetPayment(ctx context.Context, paymentNo string) (*models.Payment, error) {
return s.paymentRepo.GetByPaymentNo(ctx, paymentNo)
}
func (s *paymentService) GetPaymentStatus(ctx context.Context, paymentNo string) (*models.PaymentStatusResponse, error) {
payment, err := s.paymentRepo.GetByPaymentNo(ctx, paymentNo)
if err != nil {
return nil, err
}
response := &models.PaymentStatusResponse{
PaymentNo: payment.PaymentNo,
Status: payment.Status,
Amount: payment.Amount,
PaymentTime: payment.PaymentTime,
TradeNo: payment.TradeNo,
}
// 如果支付成功,获取业务订单信息
if payment.Status == models.PaymentStatusPaid && payment.BusinessID != nil {
if payment.BusinessType == models.BusinessTypePackageOrder {
order, err := s.cuOrderRepo.GetByID(*payment.BusinessID)
if err == nil && order.AuthorizationCode != nil {
response.BusinessOrder = &models.PaymentBusinessOrder{
OrderNo: order.OrderNo,
AuthorizationCode: order.AuthorizationCode,
Status: order.Status,
}
}
}
}
return response, nil
}
func (s *paymentService) ProcessPaymentCallback(ctx context.Context, notification interface{}) error {
// 这里暂时简化处理回调逻辑
// 实际项目中应该进行完整的验证和处理
// 包括签名验证、状态检查、业务逻辑处理等
// 暂时返回成功,表示回调处理完成
return nil
}
func (s *paymentService) GetUserPayments(ctx context.Context, customerID, cuUserID string, page, pageSize int) (*models.PaymentListResponse, error) {
offset := (page - 1) * pageSize
payments, total, err := s.paymentRepo.GetByCustomerAndCuUserID(ctx, customerID, cuUserID, offset, pageSize)
if err != nil {
return nil, err
}
paymentResponses := make([]*models.PaymentResponse, len(payments))
for i, payment := range payments {
paymentResponses[i] = payment.ToResponse()
}
return &models.PaymentListResponse{
Payments: paymentResponses,
TotalCount: total,
Page: page,
PageSize: pageSize,
}, nil
}
func (s *paymentService) CancelExpiredPayments(ctx context.Context) error {
expiredPayments, err := s.paymentRepo.GetExpiredPayments(ctx)
if err != nil {
return err
}
for _, payment := range expiredPayments {
err = s.paymentRepo.UpdateStatus(ctx, payment.PaymentNo, models.PaymentStatusExpired, nil, nil)
if err != nil {
fmt.Printf("Failed to cancel expired payment %s: %v\n", payment.PaymentNo, err)
}
}
return nil
}
@@ -0,0 +1,57 @@
-- 支付订单表
-- 支持多种支付方式和业务场景的通用支付表
-- MySQL版本
-- 创建支付订单表
CREATE TABLE payments (
-- 基础信息
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
payment_no VARCHAR(64) UNIQUE NOT NULL COMMENT '支付单号',
business_type VARCHAR(50) NOT NULL COMMENT '业务类型 (package_order)',
business_id VARCHAR(36) NULL COMMENT '业务ID (关联cu_orders.id)',
-- 用户信息
customer_id VARCHAR(36) NOT NULL COMMENT '客户ID',
cu_user_id VARCHAR(36) NOT NULL COMMENT '客户用户ID',
-- 支付信息
amount DECIMAL(10,2) NOT NULL COMMENT '支付金额',
currency VARCHAR(3) DEFAULT 'CNY' COMMENT '货币类型',
payment_method VARCHAR(20) DEFAULT 'alipay' COMMENT '支付方式',
payment_provider VARCHAR(20) DEFAULT 'alipay' COMMENT '支付提供商',
-- 状态信息
status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '支付状态: pending-待支付, paid-已支付, cancelled-已取消, expired-已过期, failed-支付失败',
trade_no VARCHAR(64) NULL COMMENT '第三方交易号',
payment_time DATETIME(3) NULL COMMENT '支付完成时间',
expire_time DATETIME(3) NOT NULL COMMENT '支付过期时间',
-- 支付链接和回调
payment_url TEXT NULL COMMENT '支付链接',
notify_data JSON NULL COMMENT '回调数据',
extra_data JSON NULL COMMENT '扩展数据',
-- 时间信息
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表 - 支持多种支付方式和业务场景';
-- 创建索引
CREATE INDEX idx_payments_payment_no ON payments(payment_no);
CREATE INDEX idx_payments_business_type_id ON payments(business_type, business_id);
CREATE INDEX idx_payments_customer_id ON payments(customer_id);
CREATE INDEX idx_payments_cu_user_id ON payments(cu_user_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_created_at ON payments(created_at);
-- 外键约束
ALTER TABLE payments ADD CONSTRAINT fk_payments_customer_id FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE;
ALTER TABLE payments ADD CONSTRAINT fk_payments_cu_user_id FOREIGN KEY (cu_user_id) REFERENCES cu_users(id) ON DELETE CASCADE;
-- 注意:
-- 1. 时间戳由Go应用程序管理,不依赖数据库默认值
-- 2. payment_no全局唯一,作为支付单的主要标识
-- 3. business_type + business_id 支持关联不同业务场景
-- 4. extra_data字段用于存储支付方式特定的扩展信息
-- 5. 支持软删除(如果需要可以通过deleted_at字段扩展)
+35
View File
@@ -0,0 +1,35 @@
package utils
import (
"fmt"
"net/http"
"time"
)
// GeneratePaymentNo 生成支付单号
func GeneratePaymentNo() string {
timestamp := time.Now().Format("20060102150405")
random := GenerateRandomString(6)
return fmt.Sprintf("PAY%s%s", timestamp, random)
}
// GenerateRandomString 生成随机字符串
func GenerateRandomString(length int) string {
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
result := make([]byte, length)
for i := range result {
result[i] = charset[time.Now().UnixNano()%int64(len(charset))]
time.Sleep(1 * time.Nanosecond) // 简单的随机性保证
}
return string(result)
}
// SendNotificationResponse 发送通知响应
func SendNotificationResponse(w http.ResponseWriter, success bool) {
response := "success"
if !success {
response = "failure"
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte(response))
}
Binary file not shown.