321
This commit is contained in:
parent
434fe8f833
commit
ad3bd91ec3
|
|
@ -46,7 +46,7 @@
|
|||
"mysql": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"D:/CodeManage/HaniBlindBox/server/scripts/mysql-mcp-server/index.js"
|
||||
"D:/outsource/HaniBlindBox/server/scripts/mysql-mcp-server/index.js"
|
||||
],
|
||||
"env": {
|
||||
"MYSQL_HOST": "192.168.195.16",
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
"sqlserver": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"D:/CodeManage/HaniBlindBox/server/scripts/mssql-mcp-server/index.js"
|
||||
"D:/outsource/HaniBlindBox/server/scripts/mssql-mcp-server/index.js"
|
||||
],
|
||||
"env": {
|
||||
"MSSQL_SERVER": "192.168.195.15",
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
"admin-sqlserver": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"D:/CodeManage/HaniBlindBox/server/scripts/mssql-mcp-server/index.js"
|
||||
"D:/outsource/HaniBlindBox/server/scripts/mssql-mcp-server/index.js"
|
||||
],
|
||||
"env": {
|
||||
"MSSQL_SERVER": "192.168.195.15",
|
||||
|
|
|
|||
630
.kiro/specs/wechat-pay-v3-upgrade/design.md
Normal file
630
.kiro/specs/wechat-pay-v3-upgrade/design.md
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
# Design Document: 微信支付 V3 升级
|
||||
|
||||
## Overview
|
||||
|
||||
本设计文档描述将微信支付从 V2 版本升级到 V3 版本的技术方案。V3 版本使用更安全的 RSA-SHA256 签名算法和 AES-256-GCM 加密,替代 V2 的 MD5 签名和 XML 格式。
|
||||
|
||||
### 核心变更
|
||||
|
||||
| 特性 | V2 版本 | V3 版本 |
|
||||
|------|---------|---------|
|
||||
| 数据格式 | XML | JSON |
|
||||
| 签名算法 | MD5 | RSA-SHA256 |
|
||||
| 认证方式 | API密钥 | 商户API证书 + 微信支付公钥 |
|
||||
| 回调解密 | 无加密 | AES-256-GCM |
|
||||
|
||||
## Architecture
|
||||
|
||||
### 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 小程序前端 │
|
||||
│ (honey_box/common/server/pay.js) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ API 层 │
|
||||
│ (HoneyBox.Api) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ PayController │ │
|
||||
│ │ - CreatePayment() → 根据配置选择 V2/V3 │ │
|
||||
│ │ - NotifyCallback() → 自动识别 V2/V3 格式 │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Core 层 │
|
||||
│ (HoneyBox.Core) │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ IWechatPayService │ │ IWechatPayV3Service │ (新增) │
|
||||
│ │ (V2 实现) │ │ (V3 实现) │ │
|
||||
│ └──────────────────────┘ └──────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ └────────────┬───────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ IWechatPayConfigService │ │
|
||||
│ │ - GetMerchantByOrderNo() → 返回包含 PayVersion 的配置 │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 微信支付平台 │
|
||||
│ V2: https://api.mch.weixin.qq.com/pay/unifiedorder │
|
||||
│ V3: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 版本路由策略
|
||||
|
||||
```
|
||||
支付请求 → 获取商户配置 → 检查 PayVersion
|
||||
│
|
||||
┌───────────────┴───────────────┐
|
||||
▼ ▼
|
||||
PayVersion = "V2" PayVersion = "V3"
|
||||
│ │
|
||||
▼ ▼
|
||||
WechatPayService.CreatePaymentAsync() WechatPayV3Service.CreateJsapiOrderAsync()
|
||||
│ │
|
||||
▼ ▼
|
||||
XML + MD5签名 JSON + RSA签名
|
||||
```
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### 1. 配置模型扩展
|
||||
|
||||
#### WeixinPayMerchant (扩展)
|
||||
|
||||
```csharp
|
||||
// 文件: HoneyBox.Admin.Business/Models/Config/ConfigModels.cs
|
||||
|
||||
public class WeixinPayMerchant
|
||||
{
|
||||
// 现有字段保持不变
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("mch_id")]
|
||||
public string MchId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("order_prefix")]
|
||||
public string OrderPrefix { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("api_key")]
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
[JsonPropertyName("is_enabled")]
|
||||
public string? IsEnabled { get; set; }
|
||||
|
||||
// ===== V3 新增字段 =====
|
||||
|
||||
/// <summary>
|
||||
/// 支付版本: "V2" 或 "V3",默认 "V2"
|
||||
/// </summary>
|
||||
[JsonPropertyName("pay_version")]
|
||||
public string PayVersion { get; set; } = "V2";
|
||||
|
||||
/// <summary>
|
||||
/// APIv3 密钥(32位字符串)
|
||||
/// </summary>
|
||||
[JsonPropertyName("api_v3_key")]
|
||||
public string? ApiV3Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户API证书序列号
|
||||
/// </summary>
|
||||
[JsonPropertyName("cert_serial_no")]
|
||||
public string? CertSerialNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户私钥文件路径
|
||||
/// </summary>
|
||||
[JsonPropertyName("private_key_path")]
|
||||
public string? PrivateKeyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付公钥ID
|
||||
/// </summary>
|
||||
[JsonPropertyName("wechat_public_key_id")]
|
||||
public string? WechatPublicKeyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付公钥文件路径
|
||||
/// </summary>
|
||||
[JsonPropertyName("wechat_public_key_path")]
|
||||
public string? WechatPublicKeyPath { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### WechatPayMerchantConfig (扩展)
|
||||
|
||||
```csharp
|
||||
// 文件: HoneyBox.Model/Models/Payment/PaymentModels.cs
|
||||
|
||||
public class WechatPayMerchantConfig
|
||||
{
|
||||
// 现有字段保持不变
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string MchId { get; set; } = string.Empty;
|
||||
public string AppId { get; set; } = string.Empty;
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string OrderPrefix { get; set; } = string.Empty;
|
||||
public int Weight { get; set; } = 1;
|
||||
public string NotifyUrl { get; set; } = string.Empty;
|
||||
|
||||
// ===== V3 新增字段 =====
|
||||
public string PayVersion { get; set; } = "V2";
|
||||
public string? ApiV3Key { get; set; }
|
||||
public string? CertSerialNo { get; set; }
|
||||
public string? PrivateKeyPath { get; set; }
|
||||
public string? WechatPublicKeyId { get; set; }
|
||||
public string? WechatPublicKeyPath { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. V3 支付服务接口
|
||||
|
||||
```csharp
|
||||
// 文件: HoneyBox.Core/Interfaces/IWechatPayV3Service.cs (新建)
|
||||
|
||||
public interface IWechatPayV3Service
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建 JSAPI 下单(小程序支付)
|
||||
/// </summary>
|
||||
Task<WechatPayResult> CreateJsapiOrderAsync(WechatPayRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 查询订单状态
|
||||
/// </summary>
|
||||
Task<WechatPayV3QueryResult> QueryOrderAsync(string orderNo);
|
||||
|
||||
/// <summary>
|
||||
/// 关闭订单
|
||||
/// </summary>
|
||||
Task<WechatPayV3CloseResult> CloseOrderAsync(string orderNo);
|
||||
|
||||
/// <summary>
|
||||
/// 申请退款
|
||||
/// </summary>
|
||||
Task<WechatPayV3RefundResult> RefundAsync(WechatPayV3RefundRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 验证回调签名
|
||||
/// </summary>
|
||||
bool VerifyNotifySignature(string timestamp, string nonce, string body, string signature, string serialNo);
|
||||
|
||||
/// <summary>
|
||||
/// 解密回调数据
|
||||
/// </summary>
|
||||
string DecryptNotifyResource(string ciphertext, string nonce, string associatedData, string apiV3Key);
|
||||
|
||||
/// <summary>
|
||||
/// 生成 V3 请求签名
|
||||
/// </summary>
|
||||
string GenerateSignature(string method, string url, string timestamp, string nonce, string body, string privateKey);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. V3 请求/响应模型
|
||||
|
||||
```csharp
|
||||
// 文件: HoneyBox.Model/Models/Payment/WechatPayV3Models.cs (新建)
|
||||
|
||||
/// <summary>
|
||||
/// V3 JSAPI 下单请求
|
||||
/// </summary>
|
||||
public class WechatPayV3JsapiRequest
|
||||
{
|
||||
[JsonPropertyName("appid")]
|
||||
public string AppId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("mchid")]
|
||||
public string MchId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("out_trade_no")]
|
||||
public string OutTradeNo { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("notify_url")]
|
||||
public string NotifyUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public WechatPayV3Amount Amount { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("payer")]
|
||||
public WechatPayV3Payer Payer { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("attach")]
|
||||
public string? Attach { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 金额信息
|
||||
/// </summary>
|
||||
public class WechatPayV3Amount
|
||||
{
|
||||
[JsonPropertyName("total")]
|
||||
public int Total { get; set; }
|
||||
|
||||
[JsonPropertyName("currency")]
|
||||
public string Currency { get; set; } = "CNY";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 支付者信息
|
||||
/// </summary>
|
||||
public class WechatPayV3Payer
|
||||
{
|
||||
[JsonPropertyName("openid")]
|
||||
public string OpenId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 下单响应
|
||||
/// </summary>
|
||||
public class WechatPayV3JsapiResponse
|
||||
{
|
||||
[JsonPropertyName("prepay_id")]
|
||||
public string PrepayId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 回调通知
|
||||
/// </summary>
|
||||
public class WechatPayV3Notification
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("create_time")]
|
||||
public string CreateTime { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("event_type")]
|
||||
public string EventType { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("resource_type")]
|
||||
public string ResourceType { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("resource")]
|
||||
public WechatPayV3Resource Resource { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 回调资源(加密数据)
|
||||
/// </summary>
|
||||
public class WechatPayV3Resource
|
||||
{
|
||||
[JsonPropertyName("algorithm")]
|
||||
public string Algorithm { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ciphertext")]
|
||||
public string Ciphertext { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("nonce")]
|
||||
public string Nonce { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("associated_data")]
|
||||
public string AssociatedData { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 解密后的支付结果
|
||||
/// </summary>
|
||||
public class WechatPayV3PaymentResult
|
||||
{
|
||||
[JsonPropertyName("appid")]
|
||||
public string AppId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("mchid")]
|
||||
public string MchId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("out_trade_no")]
|
||||
public string OutTradeNo { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("transaction_id")]
|
||||
public string TransactionId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("trade_state")]
|
||||
public string TradeState { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("trade_state_desc")]
|
||||
public string TradeStateDesc { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("success_time")]
|
||||
public string SuccessTime { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("payer")]
|
||||
public WechatPayV3Payer Payer { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public WechatPayV3PaymentAmount Amount { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 支付金额(回调)
|
||||
/// </summary>
|
||||
public class WechatPayV3PaymentAmount
|
||||
{
|
||||
[JsonPropertyName("total")]
|
||||
public int Total { get; set; }
|
||||
|
||||
[JsonPropertyName("payer_total")]
|
||||
public int PayerTotal { get; set; }
|
||||
|
||||
[JsonPropertyName("currency")]
|
||||
public string Currency { get; set; } = "CNY";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 退款请求
|
||||
/// </summary>
|
||||
public class WechatPayV3RefundRequest
|
||||
{
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
public string RefundNo { get; set; } = string.Empty;
|
||||
public string? Reason { get; set; }
|
||||
public int TotalAmount { get; set; }
|
||||
public int RefundAmount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 查询结果
|
||||
/// </summary>
|
||||
public class WechatPayV3QueryResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string TradeState { get; set; } = string.Empty;
|
||||
public string TradeStateDesc { get; set; } = string.Empty;
|
||||
public string? TransactionId { get; set; }
|
||||
public string? ErrorCode { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 关闭结果
|
||||
/// </summary>
|
||||
public class WechatPayV3CloseResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorCode { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V3 退款结果
|
||||
/// </summary>
|
||||
public class WechatPayV3RefundResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? RefundId { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? ErrorCode { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### 数据库配置存储
|
||||
|
||||
配置存储在 `config` 表中,key 为 `weixinpay_setting`,value 为 JSON 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"merchants": [
|
||||
{
|
||||
"name": "商户名称",
|
||||
"mch_id": "1738725801",
|
||||
"order_prefix": "MYH",
|
||||
"is_enabled": "1",
|
||||
"api_key": "V2密钥(兼容)",
|
||||
|
||||
"pay_version": "V3",
|
||||
"api_v3_key": "d1cxc0vXCUH2984901DxddPJMYqcwcnd",
|
||||
"cert_serial_no": "证书序列号",
|
||||
"private_key_path": "certs/1738725801/apiclient_key.pem",
|
||||
"wechat_public_key_id": "PUB_KEY_ID_0117387258012026012500291641000801",
|
||||
"wechat_public_key_path": "certs/1738725801/pub_key.pem"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 证书文件存储
|
||||
|
||||
证书文件存储在服务器的 `certs` 目录下:
|
||||
|
||||
```
|
||||
server/HoneyBox/certs/
|
||||
└── 1738725801/ # 商户号目录
|
||||
├── apiclient_key.pem # 商户私钥
|
||||
└── pub_key.pem # 微信支付公钥
|
||||
```
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||||
|
||||
### Property 1: 配置序列化 Round-Trip
|
||||
|
||||
*For any* 有效的 `WeixinPayMerchant` 配置对象(包含 V2 或 V3 字段),序列化为 JSON 后再反序列化,应该得到与原始对象等价的配置。
|
||||
|
||||
**Validates: Requirements 1.4, 1.5**
|
||||
|
||||
### Property 2: V3 配置字段完整性
|
||||
|
||||
*For any* PayVersion 为 "V3" 的商户配置,当配置加载成功时,ApiV3Key、CertSerialNo、PrivateKeyPath、WechatPublicKeyId、WechatPublicKeyPath 字段应该都能正确读取(非空或有默认值)。
|
||||
|
||||
**Validates: Requirements 1.1, 1.2**
|
||||
|
||||
### Property 3: V2 向后兼容性
|
||||
|
||||
*For any* PayVersion 为 "V2" 的商户配置,ApiKey 字段应该能正确读取,且 V3 字段不影响 V2 功能。
|
||||
|
||||
**Validates: Requirements 1.3**
|
||||
|
||||
### Property 4: 版本路由正确性
|
||||
|
||||
*For any* 支付请求,当商户配置的 PayVersion 为 "V3" 时,应该调用 V3 接口;当 PayVersion 为 "V2" 时,应该调用 V2 接口。
|
||||
|
||||
**Validates: Requirements 3.1, 3.5**
|
||||
|
||||
### Property 5: V3 请求签名正确性
|
||||
|
||||
*For any* V3 请求数据,使用相同的私钥和参数生成的签名应该是确定性的(相同输入产生相同输出),且签名格式符合 RSA-SHA256 规范。
|
||||
|
||||
**Validates: Requirements 3.3**
|
||||
|
||||
### Property 6: V3 请求字段完整性
|
||||
|
||||
*For any* V3 JSAPI 下单请求,构建的请求体应该包含所有必要字段:appid、mchid、description、out_trade_no、notify_url、amount、payer。
|
||||
|
||||
**Validates: Requirements 3.2**
|
||||
|
||||
### Property 7: V3 支付参数完整性
|
||||
|
||||
*For any* 成功的 V3 下单响应,返回给前端的支付参数应该包含:timeStamp、nonceStr、package、signType(RSA)、paySign。
|
||||
|
||||
**Validates: Requirements 3.4**
|
||||
|
||||
### Property 8: 回调格式识别正确性
|
||||
|
||||
*For any* 支付回调请求,当请求体为 JSON 格式且包含 resource 字段时,应该使用 V3 解密流程;当请求体为 XML 格式时,应该使用 V2 解密流程。
|
||||
|
||||
**Validates: Requirements 4.1, 4.5**
|
||||
|
||||
### Property 9: V3 回调解密 Round-Trip
|
||||
|
||||
*For any* 有效的支付结果数据,使用 AES-256-GCM 加密后再解密,应该得到与原始数据等价的结果。
|
||||
|
||||
**Validates: Requirements 4.3**
|
||||
|
||||
### Property 10: V3 回调签名验证
|
||||
|
||||
*For any* V3 回调请求,使用正确的微信支付公钥验证签名应该返回 true;使用错误的公钥或篡改的数据应该返回 false。
|
||||
|
||||
**Validates: Requirements 4.2**
|
||||
|
||||
### Property 11: 回调响应格式正确性
|
||||
|
||||
*For any* 回调处理结果,V3 回调应该返回 JSON 格式响应,V2 回调应该返回 XML 格式响应。
|
||||
|
||||
**Validates: Requirements 4.6**
|
||||
|
||||
### Property 12: 部分退款金额正确性
|
||||
|
||||
*For any* 部分退款请求,退款金额应该小于等于订单总金额,且退款请求中的金额字段应该正确设置。
|
||||
|
||||
**Validates: Requirements 7.4**
|
||||
|
||||
## Error Handling
|
||||
|
||||
### 配置错误处理
|
||||
|
||||
| 错误场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| V3 配置缺少必要字段 | 记录警告日志,回退到 V2 |
|
||||
| 私钥文件不存在 | 抛出配置异常,阻止启动 |
|
||||
| 公钥文件不存在 | 抛出配置异常,阻止启动 |
|
||||
| APIv3 密钥格式错误 | 记录错误日志,返回配置错误 |
|
||||
|
||||
### 支付错误处理
|
||||
|
||||
| 错误场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| V3 签名生成失败 | 记录错误日志,返回系统错误 |
|
||||
| V3 下单请求失败 | 解析错误码,返回友好提示 |
|
||||
| V3 回调验签失败 | 记录警告日志,返回失败响应 |
|
||||
| V3 回调解密失败 | 记录错误日志,返回失败响应 |
|
||||
|
||||
### 错误码映射
|
||||
|
||||
```csharp
|
||||
private static readonly Dictionary<string, string> V3ErrorMessages = new()
|
||||
{
|
||||
{ "PARAM_ERROR", "参数错误" },
|
||||
{ "OUT_TRADE_NO_USED", "订单号已使用" },
|
||||
{ "ORDER_NOT_EXIST", "订单不存在" },
|
||||
{ "ORDER_CLOSED", "订单已关闭" },
|
||||
{ "SIGN_ERROR", "签名错误" },
|
||||
{ "MCH_NOT_EXISTS", "商户号不存在" },
|
||||
{ "APPID_MCHID_NOT_MATCH", "AppID和商户号不匹配" },
|
||||
{ "FREQUENCY_LIMITED", "请求频率超限" },
|
||||
{ "SYSTEM_ERROR", "系统错误" }
|
||||
};
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### 单元测试
|
||||
|
||||
1. **配置模型测试**
|
||||
- 测试 V3 字段序列化/反序列化
|
||||
- 测试 V2 向后兼容性
|
||||
- 测试配置验证逻辑
|
||||
|
||||
2. **签名算法测试**
|
||||
- 测试 RSA-SHA256 签名生成
|
||||
- 测试签名验证
|
||||
- 测试签名字符串构建
|
||||
|
||||
3. **加解密测试**
|
||||
- 测试 AES-256-GCM 加密
|
||||
- 测试 AES-256-GCM 解密
|
||||
- 测试解密失败场景
|
||||
|
||||
### 属性测试
|
||||
|
||||
使用 FsCheck 进行属性测试:
|
||||
|
||||
```csharp
|
||||
// 配置 round-trip 测试
|
||||
[Property]
|
||||
public Property ConfigRoundTrip()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Arb.From<WeixinPayMerchant>(),
|
||||
config =>
|
||||
{
|
||||
var json = JsonSerializer.Serialize(config);
|
||||
var deserialized = JsonSerializer.Deserialize<WeixinPayMerchant>(json);
|
||||
return config.Equals(deserialized);
|
||||
});
|
||||
}
|
||||
|
||||
// 签名确定性测试
|
||||
[Property]
|
||||
public Property SignatureDeterministic()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Arb.From<string>(), // method
|
||||
Arb.From<string>(), // url
|
||||
Arb.From<string>(), // body
|
||||
(method, url, body) =>
|
||||
{
|
||||
var sign1 = service.GenerateSignature(method, url, timestamp, nonce, body, privateKey);
|
||||
var sign2 = service.GenerateSignature(method, url, timestamp, nonce, body, privateKey);
|
||||
return sign1 == sign2;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
1. **支付流程测试**
|
||||
- 测试 V3 下单流程(使用 Mock)
|
||||
- 测试 V3 回调处理
|
||||
- 测试版本路由逻辑
|
||||
|
||||
2. **后台配置测试**
|
||||
- 测试配置保存和加载
|
||||
- 测试 V2/V3 切换
|
||||
101
.kiro/specs/wechat-pay-v3-upgrade/requirements.md
Normal file
101
.kiro/specs/wechat-pay-v3-upgrade/requirements.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
将现有的微信支付从 V2 版本升级到 V3 版本,以支持更安全的 RSA-SHA256 签名和 AES-GCM 加密。当前系统使用 V2 版本(XML格式、MD5签名),需要升级到 V3 版本(JSON格式、RSA-SHA256签名)以满足微信支付的最新安全要求。
|
||||
|
||||
## Glossary
|
||||
|
||||
- **WechatPay_V2**: 微信支付 V2 版本,使用 XML 数据格式和 MD5 签名算法
|
||||
- **WechatPay_V3**: 微信支付 V3 版本,使用 JSON 数据格式和 RSA-SHA256 签名算法
|
||||
- **APIv3_Key**: 微信支付 V3 版本的 API 密钥,用于 AES-GCM 解密回调通知
|
||||
- **Merchant_Private_Key**: 商户 API 私钥,用于请求签名
|
||||
- **Wechat_Public_Key**: 微信支付平台公钥,用于验证回调签名
|
||||
- **JSAPI_Payment**: 小程序/公众号内支付方式
|
||||
- **Payment_Callback**: 微信支付结果异步通知
|
||||
- **Config_System**: 后台配置管理系统,存储支付参数到数据库
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: V3 配置模型扩展
|
||||
|
||||
**User Story:** As a 开发者, I want 后端配置模型支持 V3 字段, so that 系统能够存储和读取 V3 支付配置。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 系统加载微信支付配置 THEN THE Config_System SHALL 支持读取 PayVersion 字段(值为 "V2" 或 "V3")
|
||||
2. WHEN PayVersion 为 "V3" THEN THE Config_System SHALL 读取以下字段:ApiV3Key、CertSerialNo、PrivateKeyPath、WechatPublicKeyId、WechatPublicKeyPath
|
||||
3. WHEN PayVersion 为 "V2" THEN THE Config_System SHALL 保持原有字段兼容(ApiKey)
|
||||
4. WHEN 配置保存时 THEN THE Config_System SHALL 正确序列化 V3 字段到 JSON 格式
|
||||
5. WHEN 配置加载时 THEN THE Config_System SHALL 正确反序列化 V3 字段
|
||||
|
||||
### Requirement 2: 后台管理页面配置
|
||||
|
||||
**User Story:** As a 系统管理员, I want 在后台管理页面配置微信支付 V3 参数, so that 系统能够使用 V3 接口进行支付。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 管理员打开微信支付配置页面 THEN THE Admin_System SHALL 显示「支付版本」选择项(V2/V3)
|
||||
2. WHEN 管理员选择 V3 版本 THEN THE Admin_System SHALL 显示 V3 专属配置项:APIv3密钥、证书序列号、商户私钥路径、微信支付公钥ID、微信支付公钥路径
|
||||
3. WHEN 管理员选择 V2 版本 THEN THE Admin_System SHALL 隐藏 V3 配置项并显示 V2 配置项
|
||||
4. WHEN 管理员保存配置 THEN THE Admin_System SHALL 验证必填字段并保存到数据库
|
||||
5. WHEN 页面加载时 THEN THE Admin_System SHALL 正确回显已保存的配置值
|
||||
|
||||
### Requirement 3: V3 JSAPI 下单接口
|
||||
|
||||
**User Story:** As a 小程序用户, I want 使用 V3 接口发起支付, so that 完成商品购买。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 用户发起支付请求且商户配置为 V3 THEN THE WechatPay_V3 SHALL 调用 V3 JSAPI 下单接口
|
||||
2. WHEN 构建 V3 请求 THEN THE WechatPay_V3 SHALL 使用 JSON 格式并包含:appid、mchid、description、out_trade_no、notify_url、amount、payer
|
||||
3. WHEN 签名 V3 请求 THEN THE WechatPay_V3 SHALL 使用 RSA-SHA256 算法对请求进行签名
|
||||
4. WHEN 下单成功 THEN THE WechatPay_V3 SHALL 返回小程序调起支付所需参数:timeStamp、nonceStr、package、signType(RSA)、paySign
|
||||
5. WHEN 商户配置为 V2 THEN THE WechatPay_V3 SHALL 回退到 V2 接口处理
|
||||
|
||||
### Requirement 4: V3 支付回调处理
|
||||
|
||||
**User Story:** As a 系统, I want 正确解密 V3 支付回调通知, so that 更新订单支付状态。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 收到 V3 格式回调(JSON格式且包含 resource 字段)THEN THE Payment_Callback SHALL 使用 V3 解密流程
|
||||
2. WHEN 验证 V3 回调签名 THEN THE Payment_Callback SHALL 使用微信支付公钥验证 Wechatpay-Signature 头
|
||||
3. WHEN 解密 V3 回调数据 THEN THE Payment_Callback SHALL 使用 AES-256-GCM 算法和 APIv3 密钥解密 resource.ciphertext
|
||||
4. WHEN 解密成功 THEN THE Payment_Callback SHALL 提取订单号和支付状态并更新订单
|
||||
5. WHEN 收到 V2 格式回调(XML格式)THEN THE Payment_Callback SHALL 使用 V2 解密流程
|
||||
6. WHEN 回调处理成功 THEN THE Payment_Callback SHALL 返回正确的响应格式(V3: JSON, V2: XML)
|
||||
|
||||
### Requirement 5: V3 订单查询
|
||||
|
||||
**User Story:** As a 系统, I want 使用 V3 接口查询订单状态, so that 处理支付超时和异常情况。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 查询订单且商户配置为 V3 THEN THE WechatPay_V3 SHALL 调用 V3 订单查询接口
|
||||
2. WHEN 构建查询请求 THEN THE WechatPay_V3 SHALL 使用商户订单号作为路径参数
|
||||
3. WHEN 查询成功 THEN THE WechatPay_V3 SHALL 解析返回的订单状态(SUCCESS、NOTPAY、CLOSED 等)
|
||||
4. IF 查询失败 THEN THE WechatPay_V3 SHALL 返回错误信息并记录日志
|
||||
|
||||
### Requirement 6: V3 订单关闭
|
||||
|
||||
**User Story:** As a 系统, I want 使用 V3 接口关闭未支付订单, so that 释放库存和资源。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 关闭订单且商户配置为 V3 THEN THE WechatPay_V3 SHALL 调用 V3 订单关闭接口
|
||||
2. WHEN 构建关闭请求 THEN THE WechatPay_V3 SHALL 使用商户订单号作为路径参数并包含 mchid
|
||||
3. WHEN 关闭成功 THEN THE WechatPay_V3 SHALL 返回成功状态(HTTP 204)
|
||||
4. IF 关闭失败 THEN THE WechatPay_V3 SHALL 返回错误信息并记录日志
|
||||
|
||||
### Requirement 7: V3 退款接口
|
||||
|
||||
**User Story:** As a 系统管理员, I want 使用 V3 接口发起退款, so that 处理用户退款请求。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 发起退款且商户配置为 V3 THEN THE WechatPay_V3 SHALL 调用 V3 退款接口
|
||||
2. WHEN 构建退款请求 THEN THE WechatPay_V3 SHALL 包含:out_trade_no、out_refund_no、reason、notify_url、amount
|
||||
3. WHEN 退款成功 THEN THE WechatPay_V3 SHALL 返回退款单号和状态
|
||||
4. THE WechatPay_V3 SHALL 支持部分退款(退款金额小于订单金额)
|
||||
5. IF 退款失败 THEN THE WechatPay_V3 SHALL 返回错误信息并记录日志
|
||||
154
.kiro/specs/wechat-pay-v3-upgrade/tasks.md
Normal file
154
.kiro/specs/wechat-pay-v3-upgrade/tasks.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# Implementation Plan: 微信支付 V3 升级
|
||||
|
||||
## Overview
|
||||
|
||||
本实现计划将微信支付从 V2 升级到 V3,采用增量开发方式,确保 V2 功能不受影响。实现顺序:配置模型 → V3 服务 → 回调处理 → 后台管理页面。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] 1. 扩展配置模型支持 V3 字段
|
||||
- [ ] 1.1 扩展 WeixinPayMerchant 模型添加 V3 字段
|
||||
- 在 `HoneyBox.Admin.Business/Models/Config/ConfigModels.cs` 中添加 V3 字段
|
||||
- 字段:PayVersion、ApiV3Key、CertSerialNo、PrivateKeyPath、WechatPublicKeyId、WechatPublicKeyPath
|
||||
- _Requirements: 1.1, 1.2, 1.3_
|
||||
- [ ] 1.2 扩展 WechatPayMerchantConfig 模型添加 V3 字段
|
||||
- 在 `HoneyBox.Model/Models/Payment/PaymentModels.cs` 中添加对应字段
|
||||
- _Requirements: 1.1, 1.2_
|
||||
- [ ] 1.3 更新 WechatPayConfigService 支持 V3 配置映射
|
||||
- 在配置加载时映射 V3 字段
|
||||
- _Requirements: 1.4, 1.5_
|
||||
- [ ] 1.4 编写配置序列化 round-trip 属性测试
|
||||
- **Property 1: 配置序列化 Round-Trip**
|
||||
- **Validates: Requirements 1.4, 1.5**
|
||||
|
||||
- [ ] 2. 创建 V3 支付数据模型
|
||||
- [ ] 2.1 创建 WechatPayV3Models.cs 文件
|
||||
- 在 `HoneyBox.Model/Models/Payment/` 目录下创建
|
||||
- 包含:WechatPayV3JsapiRequest、WechatPayV3Amount、WechatPayV3Payer、WechatPayV3JsapiResponse
|
||||
- _Requirements: 3.2_
|
||||
- [ ] 2.2 创建 V3 回调通知模型
|
||||
- 包含:WechatPayV3Notification、WechatPayV3Resource、WechatPayV3PaymentResult
|
||||
- _Requirements: 4.3, 4.4_
|
||||
- [ ] 2.3 创建 V3 查询、关闭、退款结果模型
|
||||
- 包含:WechatPayV3QueryResult、WechatPayV3CloseResult、WechatPayV3RefundResult、WechatPayV3RefundRequest
|
||||
- _Requirements: 5.3, 6.3, 7.3_
|
||||
|
||||
- [ ] 3. 实现 V3 支付服务核心功能
|
||||
- [ ] 3.1 创建 IWechatPayV3Service 接口
|
||||
- 在 `HoneyBox.Core/Interfaces/` 目录下创建
|
||||
- 定义:CreateJsapiOrderAsync、QueryOrderAsync、CloseOrderAsync、RefundAsync
|
||||
- _Requirements: 3.1, 5.1, 6.1, 7.1_
|
||||
- [ ] 3.2 实现 V3 签名生成方法
|
||||
- 实现 RSA-SHA256 签名算法
|
||||
- 签名字符串格式:HTTP方法\nURL\n时间戳\n随机串\n请求体\n
|
||||
- _Requirements: 3.3_
|
||||
- [ ] 3.3 编写签名确定性属性测试
|
||||
- **Property 5: V3 请求签名正确性**
|
||||
- **Validates: Requirements 3.3**
|
||||
- [ ] 3.4 实现 CreateJsapiOrderAsync 方法
|
||||
- 构建 V3 JSAPI 下单请求
|
||||
- 调用微信 V3 API
|
||||
- 返回小程序支付参数
|
||||
- _Requirements: 3.2, 3.4_
|
||||
- [ ] 3.5 编写请求字段完整性属性测试
|
||||
- **Property 6: V3 请求字段完整性**
|
||||
- **Validates: Requirements 3.2**
|
||||
|
||||
- [ ] 4. Checkpoint - 确保 V3 下单功能测试通过
|
||||
- 运行所有测试,确保通过
|
||||
- 如有问题请询问用户
|
||||
|
||||
- [ ] 5. 实现 V3 回调处理
|
||||
- [ ] 5.1 实现回调签名验证方法
|
||||
- 使用微信支付公钥验证 Wechatpay-Signature 头
|
||||
- _Requirements: 4.2_
|
||||
- [ ] 5.2 实现 AES-256-GCM 解密方法
|
||||
- 解密 resource.ciphertext 字段
|
||||
- 使用 APIv3 密钥作为解密密钥
|
||||
- _Requirements: 4.3_
|
||||
- [ ] 5.3 编写解密 round-trip 属性测试
|
||||
- **Property 9: V3 回调解密 Round-Trip**
|
||||
- **Validates: Requirements 4.3**
|
||||
- [ ] 5.4 实现回调格式自动识别
|
||||
- JSON 格式且包含 resource 字段 → V3 流程
|
||||
- XML 格式 → V2 流程
|
||||
- _Requirements: 4.1, 4.5_
|
||||
- [ ] 5.5 编写回调格式识别属性测试
|
||||
- **Property 8: 回调格式识别正确性**
|
||||
- **Validates: Requirements 4.1, 4.5**
|
||||
- [ ] 5.6 更新 PaymentNotifyService 支持 V3 回调
|
||||
- 在现有回调处理中添加 V3 分支
|
||||
- _Requirements: 4.4, 4.6_
|
||||
|
||||
- [ ] 6. 实现 V3 订单查询和关闭
|
||||
- [ ] 6.1 实现 QueryOrderAsync 方法
|
||||
- 调用 V3 订单查询接口
|
||||
- 解析订单状态
|
||||
- _Requirements: 5.1, 5.2, 5.3_
|
||||
- [ ] 6.2 实现 CloseOrderAsync 方法
|
||||
- 调用 V3 订单关闭接口
|
||||
- 处理 HTTP 204 响应
|
||||
- _Requirements: 6.1, 6.2, 6.3_
|
||||
|
||||
- [ ] 7. 实现 V3 退款接口
|
||||
- [ ] 7.1 实现 RefundAsync 方法
|
||||
- 调用 V3 退款接口
|
||||
- 支持部分退款
|
||||
- _Requirements: 7.1, 7.2, 7.3, 7.4_
|
||||
- [ ] 7.2 编写部分退款金额属性测试
|
||||
- **Property 12: 部分退款金额正确性**
|
||||
- **Validates: Requirements 7.4**
|
||||
|
||||
- [ ] 8. 实现版本路由逻辑
|
||||
- [ ] 8.1 更新 WechatPayService 支持版本路由
|
||||
- 根据商户配置的 PayVersion 选择 V2 或 V3 服务
|
||||
- _Requirements: 3.1, 3.5_
|
||||
- [ ] 8.2 编写版本路由属性测试
|
||||
- **Property 4: 版本路由正确性**
|
||||
- **Validates: Requirements 3.1, 3.5**
|
||||
- [ ] 8.3 注册 V3 服务到依赖注入容器
|
||||
- 在 ServiceModule.cs 中注册 IWechatPayV3Service
|
||||
- _Requirements: 3.1_
|
||||
|
||||
- [ ] 9. Checkpoint - 确保后端 V3 功能完整
|
||||
- 运行所有测试,确保通过
|
||||
- 如有问题请询问用户
|
||||
|
||||
- [ ] 10. 更新后台管理页面
|
||||
- [ ] 10.1 更新前端配置接口类型定义
|
||||
- 在 `admin-web/src/api/business/config.ts` 中添加 V3 字段
|
||||
- _Requirements: 2.1_
|
||||
- [ ] 10.2 更新微信商户配置表单组件
|
||||
- 在 `admin-web/src/views/business/config/components/WeixinMerchantForm.vue` 中添加 V3 配置项
|
||||
- 添加支付版本选择(V2/V3)
|
||||
- 根据版本显示/隐藏对应配置项
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
- [ ] 10.3 实现配置验证逻辑
|
||||
- V3 版本时验证必填字段
|
||||
- _Requirements: 2.4_
|
||||
- [ ] 10.4 测试配置保存和回显
|
||||
- 确保配置正确保存到数据库
|
||||
- 确保页面加载时正确回显
|
||||
- _Requirements: 2.4, 2.5_
|
||||
|
||||
- [ ] 11. 准备证书文件
|
||||
- [ ] 11.1 创建证书目录结构
|
||||
- 创建 `server/HoneyBox/certs/1738725801/` 目录
|
||||
- _Requirements: 1.2_
|
||||
- [ ] 11.2 解压并放置证书文件
|
||||
- 从 `微信支付商户号/商户API证书/` 解压获取 apiclient_key.pem
|
||||
- 从 `微信支付商户号/微信支付公钥/` 复制 pub_key.pem
|
||||
- _Requirements: 1.2_
|
||||
|
||||
- [ ] 12. Final Checkpoint - 完整功能验证
|
||||
- 运行所有测试,确保通过
|
||||
- 验证 V2 功能不受影响
|
||||
- 如有问题请询问用户
|
||||
|
||||
## Notes
|
||||
|
||||
- 所有任务均为必需,包括属性测试任务
|
||||
- 每个任务都引用了具体的需求条款以便追溯
|
||||
- Checkpoint 任务用于阶段性验证
|
||||
- 属性测试验证通用正确性属性
|
||||
- 单元测试验证具体示例和边界情况
|
||||
|
|
@ -11,8 +11,8 @@
|
|||
|
||||
// 测试环境配置 - .NET 10 后端
|
||||
const testing = {
|
||||
// baseUrl: 'https://app.zpc-xy.com/honey/api',
|
||||
baseUrl: 'http://192.168.1.24:5238',
|
||||
baseUrl: 'https://app.zpc-xy.com/honey/api',
|
||||
// baseUrl: 'http://192.168.1.24:5238',
|
||||
imageUrl: 'https://youdas-1308826010.cos.ap-shanghai.myqcloud.com',
|
||||
loginPage: '',
|
||||
wxAppId: ''
|
||||
|
|
|
|||
|
|
@ -212,6 +212,32 @@ class BasePlatform {
|
|||
* @param {Object} item 菜单项
|
||||
*/
|
||||
navigateToPath(item) {
|
||||
// 需要登录才能访问的页面路径
|
||||
const needLoginPaths = [
|
||||
'/pages/other/order_list', // 消费记录
|
||||
'/package/mine/collect', // 我的收藏
|
||||
'/pages/user/coupon', // 优惠券
|
||||
'/pages/user/tui-guang', // 邀请好友
|
||||
'/pages/user/cancel-account-page', // 注销账号
|
||||
];
|
||||
|
||||
// 检查是否需要登录
|
||||
const needLogin = needLoginPaths.some(path => item.path.startsWith(path));
|
||||
if (needLogin) {
|
||||
const token = uni.getStorageSync('token');
|
||||
if (!token) {
|
||||
uni.setStorageSync('redirect', item.path);
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
navigateTo('/pages/user/login');
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
navigateTo(item.path);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -400,19 +400,32 @@ class RequestManager {
|
|||
|
||||
// 处理 HTTP 401 未授权(Token 过期或无效)
|
||||
if (res.statusCode === 401) {
|
||||
console.log('Token过期或无效,尝试自动刷新');
|
||||
console.log('Token过期或无效');
|
||||
|
||||
// 白名单接口不进行自动刷新,直接拒绝
|
||||
// 白名单接口直接返回错误,不处理
|
||||
if (RequestManager.isUrlInWhitelist(requestUrl)) {
|
||||
reject({ status: -1, msg: '登录已过期' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有 token(用户是否曾经登录过)
|
||||
const currentToken = uni.getStorageSync('token');
|
||||
if (!currentToken) {
|
||||
// 用户从未登录过,直接返回错误,不跳转
|
||||
console.log('用户未登录,返回错误');
|
||||
reject({ status: -1, msg: '请先登录' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有 refreshToken
|
||||
const refreshToken = uni.getStorageSync('refreshToken');
|
||||
if (!refreshToken) {
|
||||
console.log('没有 refreshToken,直接跳转登录页');
|
||||
RequestManager.clearTokensAndRedirect();
|
||||
console.log('没有 refreshToken,清除token并返回错误');
|
||||
// 清除过期的token,但不跳转登录页
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('accessToken');
|
||||
uni.removeStorageSync('tokenExpireTime');
|
||||
uni.removeStorageSync('userinfo');
|
||||
reject({ status: -1, msg: '登录已过期' });
|
||||
return;
|
||||
}
|
||||
|
|
@ -443,9 +456,13 @@ class RequestManager {
|
|||
// 处理队列中的其他请求
|
||||
RequestManager.processRefreshQueue(true);
|
||||
} else {
|
||||
console.log('Token 刷新失败,跳转登录页');
|
||||
// 刷新失败,清除 Token 并跳转登录页
|
||||
RequestManager.clearTokensAndRedirect();
|
||||
console.log('Token 刷新失败');
|
||||
// 刷新失败,清除 Token,但不跳转登录页
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('accessToken');
|
||||
uni.removeStorageSync('refreshToken');
|
||||
uni.removeStorageSync('tokenExpireTime');
|
||||
uni.removeStorageSync('userinfo');
|
||||
reject({ status: -1, msg: '登录已过期' });
|
||||
|
||||
// 拒绝队列中的所有请求
|
||||
|
|
@ -455,7 +472,11 @@ class RequestManager {
|
|||
.catch(error => {
|
||||
console.error('Token 刷新异常:', error);
|
||||
RequestManager.isRefreshing = false;
|
||||
RequestManager.clearTokensAndRedirect();
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('accessToken');
|
||||
uni.removeStorageSync('refreshToken');
|
||||
uni.removeStorageSync('tokenExpireTime');
|
||||
uni.removeStorageSync('userinfo');
|
||||
reject({ status: -1, msg: '登录已过期' });
|
||||
RequestManager.processRefreshQueue(false);
|
||||
});
|
||||
|
|
@ -516,55 +537,22 @@ class RequestManager {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取当前页面路径和参数
|
||||
var currentPage = pages[pages.length - 1];
|
||||
if (currentPage) {
|
||||
var currentRoute = currentPage.route;
|
||||
var currentParams = currentPage.options || {};
|
||||
|
||||
// 只有非登录页面才保存重定向信息
|
||||
if (currentRoute && currentRoute !== 'pages/user/login') {
|
||||
// 构建完整的重定向URL
|
||||
var redirectPath = '/' + currentRoute;
|
||||
|
||||
// 如果有参数,拼接参数
|
||||
if (Object.keys(currentParams).length > 0) {
|
||||
var paramString = Object.keys(currentParams)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(currentParams[key])}`)
|
||||
.join('&');
|
||||
redirectPath += '?' + paramString;
|
||||
}
|
||||
|
||||
// 保存重定向URL到缓存
|
||||
console.log('保存重定向URL:', redirectPath);
|
||||
uni.setStorageSync('redirect', redirectPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 白名单接口直接返回错误,不跳转登录页
|
||||
console.log(requestUrl);
|
||||
if (RequestManager.isUrlInWhitelist(requestUrl)) {
|
||||
reject(res.data)
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
}, 100)
|
||||
|
||||
// 清除所有Token相关存储
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('accessToken');
|
||||
uni.removeStorageSync('refreshToken');
|
||||
uni.removeStorageSync('tokenExpireTime');
|
||||
uni.removeStorageSync('userinfo');
|
||||
// 使用新的路由守卫方法进行跳转
|
||||
RouterManager.navigateTo('/pages/user/login', {}, 'navigateTo')
|
||||
.catch(err => {
|
||||
console.error('登录页面跳转失败:', err);
|
||||
});
|
||||
|
||||
// 不再自动跳转登录页,只返回错误让调用方处理
|
||||
// 调用方可以根据业务需要决定是否跳转登录页
|
||||
reject(res.data)
|
||||
} else {
|
||||
reject(res.data)
|
||||
|
|
|
|||
|
|
@ -98,29 +98,9 @@ export function routerTo(options) {
|
|||
url += (url.indexOf('?') === -1 ? '?' : '&') + queryString;
|
||||
}
|
||||
|
||||
// 检查是否需要登录
|
||||
const needLogin = !isInWhiteList(url);
|
||||
|
||||
if (needLogin && !isLogin()) {
|
||||
// 需要登录但未登录,跳转到登录页面
|
||||
// 先保存当前URL用于登录后重定向
|
||||
uni.setStorageSync('redirect', url);
|
||||
|
||||
// 使用navigateTo而非redirectTo,保留页面栈
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login',
|
||||
success: () => {
|
||||
console.log('跳转到登录页面成功,保存的重定向地址:', url);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('跳转到登录页面失败:', err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
// 拒绝当前的导航请求
|
||||
return reject(new Error('需要登录'));
|
||||
}
|
||||
// 移除路由层面的登录拦截
|
||||
// 允许用户浏览所有页面,只在需要执行特定操作时才要求登录
|
||||
// 登录检查应该在具体的业务操作中进行(如抽奖、下单等)
|
||||
|
||||
// 根据type选择跳转方式
|
||||
if (type) {
|
||||
|
|
@ -251,5 +231,53 @@ export default {
|
|||
navigateBack,
|
||||
collectWhitePath,
|
||||
getCollectedWhitePaths,
|
||||
clearCollectedWhitePaths
|
||||
};
|
||||
clearCollectedWhitePaths,
|
||||
requireLogin,
|
||||
isLogin
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查登录状态,未登录则跳转登录页
|
||||
* 用于需要登录才能执行的操作(如抽奖、下单等)
|
||||
* @param {String} message 提示消息,可选
|
||||
* @returns {Boolean} true表示已登录,false表示未登录(会跳转登录页)
|
||||
*/
|
||||
export function requireLogin(message = '请先登录') {
|
||||
if (isLogin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 保存当前页面用于登录后跳转
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
if (currentPage) {
|
||||
const currentRoute = currentPage.route;
|
||||
const currentParams = currentPage.options || {};
|
||||
|
||||
if (currentRoute && currentRoute !== 'pages/user/login') {
|
||||
let redirectPath = '/' + currentRoute;
|
||||
if (Object.keys(currentParams).length > 0) {
|
||||
const paramString = Object.keys(currentParams)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(currentParams[key])}`)
|
||||
.join('&');
|
||||
redirectPath += '?' + paramString;
|
||||
}
|
||||
uni.setStorageSync('redirect', redirectPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none'
|
||||
});
|
||||
|
||||
// 跳转登录页
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
});
|
||||
}, 100);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -51,25 +51,25 @@
|
|||
|
||||
<view v-if="getIsCheck('user_money')" class="money-card">
|
||||
<view class="other-num">
|
||||
<view class="other-item" @click="$c.to({ url: '/pages/user/bi_jl' })">
|
||||
<view class="other-item" @click="toFinancePage('/pages/user/bi_jl')">
|
||||
<view class="item-content">
|
||||
<view class="title">{{ $config.getAppSetting('currency1_name') }}</view>
|
||||
<view class="num">{{ formatNumber(userinfo.integral) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="other-item" @click="$c.to({ url: '/pages/user/jf_jl' })">
|
||||
<view class="other-item" @click="toFinancePage('/pages/user/jf_jl')">
|
||||
<view class="item-content">
|
||||
<view class="title">{{ $config.getAppSetting('currency2_name') }}</view>
|
||||
<view class="num">{{ formatNumber(userinfo.money2) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="zuanshi" class="divider"></view>
|
||||
<view v-if="zuanshi" class="other-item" @click="$c.to({ url: '/pages/user/yetx' })">
|
||||
<view v-if="zuanshi" class="other-item" @click="toFinancePage('/pages/user/yetx')">
|
||||
<view class="item-content">
|
||||
<view class="title-wrapper">
|
||||
<view class="title">{{ $config.getAppSetting('balance_name') }}</view>
|
||||
<view class="recharge-tag" @click.stop="$c.to({ url: '/pages/user/recharge-page' })">充值
|
||||
<view class="recharge-tag" @click.stop="toFinancePage('/pages/user/recharge-page')">充值
|
||||
</view>
|
||||
</view>
|
||||
<view class="num">{{ formatNumber(userinfo.money) }}</view>
|
||||
|
|
@ -367,6 +367,25 @@
|
|||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 跳转到财务相关页面(需要登录)
|
||||
* @param {String} url - 页面路径
|
||||
*/
|
||||
toFinancePage(url) {
|
||||
const token = uni.getStorageSync('token');
|
||||
if (!token) {
|
||||
uni.setStorageSync('redirect', '/pages/user/index');
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.$c.nav('/pages/user/login');
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
this.$c.to({ url });
|
||||
},
|
||||
/**
|
||||
* 格式化数字,如果小数部分是.00则只显示整数部分
|
||||
* @param {Number|String} num - 要格式化的数字
|
||||
|
|
|
|||
1
server/HoneyBox/微信支付商户号/1738725801.txt
Normal file
1
server/HoneyBox/微信支付商户号/1738725801.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
商户号:1738725801
|
||||
9
server/HoneyBox/微信支付商户号/微信支付公钥/pub_key.pem
Normal file
9
server/HoneyBox/微信支付商户号/微信支付公钥/pub_key.pem
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KeKMd6Yxovf4kPI0c1Q
|
||||
Islyq9fi/Wg60dodzPNkRRoraqmqbbW7uQcKHkHvIZi5Z9fK8SGkezyhcjiR3o8z
|
||||
uwnH5QiFuMw6P+1XB1koFfbxxCc6Eh0iuRI5BqNfyRwXwn9wIEUNwfF/SAPJGTkk
|
||||
hCzViil3tOmnJDMxQUJitt4RsnL6BvQ3afWcm7oqt7MLlcIhIW8jAsSFeWPuZcW5
|
||||
Hj+o2udrTUaTRkw7AEsHr9xyePhsqYjGxbi9fTlghkUYnRUNikSydtQoHbGHP70Q
|
||||
tz4HbPqH4gpsCqabPVuANFGH5a8uidOH3XKq2iPLggbPci1nFI8xMmHMaT88u/o5
|
||||
GQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
1
server/HoneyBox/微信支付商户号/微信支付公钥/公钥ID.txt
Normal file
1
server/HoneyBox/微信支付商户号/微信支付公钥/公钥ID.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
PUB_KEY_ID_0117387258012026012500291641000801
|
||||
1
server/HoneyBox/微信支付商户号/解密回调APIv3密钥/解密回调APIV3密钥.txt
Normal file
1
server/HoneyBox/微信支付商户号/解密回调APIv3密钥/解密回调APIV3密钥.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
d1cxc0vXCUH2984901DxddPJMYqcwcnd
|
||||
Loading…
Reference in New Issue
Block a user