From af76eaf1b1c959bf0d471946a9cd94c8c80192f7 Mon Sep 17 00:00:00 2001 From: wood911010 Date: Fri, 9 Jan 2026 19:21:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=EF=BC=8C=E6=94=AF=E6=8C=81=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E5=AE=9D=E6=94=AF=E4=BB=98=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E6=8B=AC=E8=AE=A2=E5=8D=95=E5=88=9B=E5=BB=BA=E3=80=81=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E7=8A=B6=E6=80=81=E6=9F=A5=E8=AF=A2=E5=8F=8A=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/configs/config.dev.yaml | 25 +- backend/configs/config.example.yaml | 36 +- backend/configs/config.yaml | 25 +- .../modules_design/2026.1.9-支付宝对接.md | 492 ++++++++++++++++++ .../modules_design/2026.1.9-支付宝沙箱.md | 38 ++ .../modules_design/2026.1.9-支付宝测试指南.md | 182 +++++++ backend/docs/swagger/docs.go | 288 +++++++++- backend/docs/swagger/swagger.json | 288 +++++++++- backend/docs/swagger/swagger.yaml | 183 ++++++- backend/go.mod | 4 + backend/go.sum | 8 + .../internal/api/handlers/cu_order_handler.go | 104 +++- .../internal/api/handlers/payment_handler.go | 158 ++++++ backend/internal/api/routes/router.go | 56 +- backend/internal/config/config.go | 28 + backend/internal/database/migration.go | 1 + backend/internal/models/cu_order.go | 5 +- backend/internal/models/payment.go | 157 ++++++ backend/internal/repository/interfaces.go | 30 ++ .../internal/repository/payment_repository.go | 102 ++++ backend/internal/service/cu_order_service.go | 58 +++ backend/internal/service/interfaces.go | 1 + backend/internal/service/payment_service.go | 245 +++++++++ .../migrations/011_create_payments_table.sql | 57 ++ backend/pkg/utils/alipay.go | 35 ++ backend/temp_line.txt | Bin 0 -> 152 bytes 26 files changed, 2580 insertions(+), 26 deletions(-) create mode 100644 backend/docs/modules_design/2026.1.9-支付宝对接.md create mode 100644 backend/docs/modules_design/2026.1.9-支付宝沙箱.md create mode 100644 backend/docs/modules_design/2026.1.9-支付宝测试指南.md create mode 100644 backend/internal/api/handlers/payment_handler.go create mode 100644 backend/internal/models/payment.go create mode 100644 backend/internal/repository/payment_repository.go create mode 100644 backend/internal/service/payment_service.go create mode 100644 backend/migrations/011_create_payments_table.sql create mode 100644 backend/pkg/utils/alipay.go create mode 100644 backend/temp_line.txt diff --git a/backend/configs/config.dev.yaml b/backend/configs/config.dev.yaml index 8dae10f..e8d5fa5 100644 --- a/backend/configs/config.dev.yaml +++ b/backend/configs/config.dev.yaml @@ -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 # 启用支付日志 + diff --git a/backend/configs/config.example.yaml b/backend/configs/config.example.yaml index 25b16f0..e918749 100644 --- a/backend/configs/config.example.yaml +++ b/backend/configs/config.example.yaml @@ -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天内到期算即将过期 \ No newline at end of file + 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 # 启用支付日志 \ No newline at end of file diff --git a/backend/configs/config.yaml b/backend/configs/config.yaml index 60046e7..bfb571a 100644 --- a/backend/configs/config.yaml +++ b/backend/configs/config.yaml @@ -50,4 +50,27 @@ license: heartbeat_timeout: 300 # 心跳超时时间(秒) - 5分钟内未上报心跳视为离线 offline_timeout: 1440 # 离线超时时间(分钟) - 24小时后为异常 - expiring_days: 30 # 即将过期天数 - 30天内到期算即将过期 \ No newline at end of file + 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 # 启用支付日志 \ No newline at end of file diff --git a/backend/docs/modules_design/2026.1.9-支付宝对接.md b/backend/docs/modules_design/2026.1.9-支付宝对接.md new file mode 100644 index 0000000..cbc84f8 --- /dev/null +++ b/backend/docs/modules_design/2026.1.9-支付宝对接.md @@ -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) - 获取测试环境账号 diff --git a/backend/docs/modules_design/2026.1.9-支付宝沙箱.md b/backend/docs/modules_design/2026.1.9-支付宝沙箱.md new file mode 100644 index 0000000..5415b01 --- /dev/null +++ b/backend/docs/modules_design/2026.1.9-支付宝沙箱.md @@ -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 \ No newline at end of file diff --git a/backend/docs/modules_design/2026.1.9-支付宝测试指南.md b/backend/docs/modules_design/2026.1.9-支付宝测试指南.md new file mode 100644 index 0000000..f5723f3 --- /dev/null +++ b/backend/docs/modules_design/2026.1.9-支付宝测试指南.md @@ -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. 配置生产数据库 + +--- + +**🎉 按照此指南测试,你的支付宝支付功能就能正常工作了!** diff --git a/backend/docs/swagger/docs.go b/backend/docs/swagger/docs.go index 346a26a..98e94de 100644 --- a/backend/docs/swagger/docs.go +++ b/backend/docs/swagger/docs.go @@ -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": { diff --git a/backend/docs/swagger/swagger.json b/backend/docs/swagger/swagger.json index 89d06de..60625aa 100644 --- a/backend/docs/swagger/swagger.json +++ b/backend/docs/swagger/swagger.json @@ -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": { diff --git a/backend/docs/swagger/swagger.yaml b/backend/docs/swagger/swagger.yaml index 5fe23d5..eb6755b 100644 --- a/backend/docs/swagger/swagger.yaml +++ b/backend/docs/swagger/swagger.yaml @@ -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: diff --git a/backend/go.mod b/backend/go.mod index d1532a6..b58e2bd 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -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 diff --git a/backend/go.sum b/backend/go.sum index dd61558..ae65ef6 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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= diff --git a/backend/internal/api/handlers/cu_order_handler.go b/backend/internal/api/handlers/cu_order_handler.go index 2403126..4f8b5c0 100644 --- a/backend/internal/api/handlers/cu_order_handler.go +++ b/backend/internal/api/handlers/cu_order_handler.go @@ -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获取订单详细信息 diff --git a/backend/internal/api/handlers/payment_handler.go b/backend/internal/api/handlers/payment_handler.go new file mode 100644 index 0000000..dba52ce --- /dev/null +++ b/backend/internal/api/handlers/payment_handler.go @@ -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) +} diff --git a/backend/internal/api/routes/router.go b/backend/internal/api/routes/router.go index cb82090..8961667 100644 --- a/backend/internal/api/routes/router.go +++ b/backend/internal/api/routes/router.go @@ -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 +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 0578177..aaa3bf6 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -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 { diff --git a/backend/internal/database/migration.go b/backend/internal/database/migration.go index f7dce6e..249f416 100644 --- a/backend/internal/database/migration.go +++ b/backend/internal/database/migration.go @@ -25,6 +25,7 @@ func AutoMigrate() error { &models.User{}, &models.CuUser{}, &models.CuOrder{}, + &models.Payment{}, &models.AuthorizationCode{}, &models.License{}, &models.AuthorizationChange{}, diff --git a/backend/internal/models/cu_order.go b/backend/internal/models/cu_order.go index dd45d8d..cde7512 100644 --- a/backend/internal/models/cu_order.go +++ b/backend/internal/models/cu_order.go @@ -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 订单响应结构 diff --git a/backend/internal/models/payment.go b/backend/internal/models/payment.go new file mode 100644 index 0000000..52c9869 --- /dev/null +++ b/backend/internal/models/payment.go @@ -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" // 银行卡 +) diff --git a/backend/internal/repository/interfaces.go b/backend/internal/repository/interfaces.go index 8e439d7..aed3dca 100644 --- a/backend/internal/repository/interfaces.go +++ b/backend/internal/repository/interfaces.go @@ -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) +} \ No newline at end of file diff --git a/backend/internal/repository/payment_repository.go b/backend/internal/repository/payment_repository.go new file mode 100644 index 0000000..e46e27c --- /dev/null +++ b/backend/internal/repository/payment_repository.go @@ -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 +} diff --git a/backend/internal/service/cu_order_service.go b/backend/internal/service/cu_order_service.go index 1e63b0a..548ea1c 100644 --- a/backend/internal/service/cu_order_service.go +++ b/backend/internal/service/cu_order_service.go @@ -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) diff --git a/backend/internal/service/interfaces.go b/backend/internal/service/interfaces.go index e364d0e..bbfac76 100644 --- a/backend/internal/service/interfaces.go +++ b/backend/internal/service/interfaces.go @@ -82,3 +82,4 @@ type CuDeviceService interface { // 解绑设备 UnbindDevice(ctx context.Context, customerID, licenseID string) error } + diff --git a/backend/internal/service/payment_service.go b/backend/internal/service/payment_service.go new file mode 100644 index 0000000..154a598 --- /dev/null +++ b/backend/internal/service/payment_service.go @@ -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 +} diff --git a/backend/migrations/011_create_payments_table.sql b/backend/migrations/011_create_payments_table.sql new file mode 100644 index 0000000..899f158 --- /dev/null +++ b/backend/migrations/011_create_payments_table.sql @@ -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字段扩展) diff --git a/backend/pkg/utils/alipay.go b/backend/pkg/utils/alipay.go new file mode 100644 index 0000000..f8a5dd7 --- /dev/null +++ b/backend/pkg/utils/alipay.go @@ -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)) +} diff --git a/backend/temp_line.txt b/backend/temp_line.txt new file mode 100644 index 0000000000000000000000000000000000000000..e03a4f01fbcecb737529394819afea03afe0aff3 GIT binary patch literal 152 zcmZ9EISxQj6h%)ahjXvf;<<&LnvtG{h=QDw y3LlSHnc-lfV`h^!Z|`$s^&dToHu?t