chore(addon/huaweipay): 更新huaweipay
This commit is contained in:
318
src/addon/huaweipay/MANUAL_TEST_GUIDE.md
Normal file
318
src/addon/huaweipay/MANUAL_TEST_GUIDE.md
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
# 华为支付手动测试指南
|
||||||
|
|
||||||
|
## 1. 测试环境准备
|
||||||
|
|
||||||
|
### 1.1 配置文件准备
|
||||||
|
|
||||||
|
#### 1.1.1 证书准备
|
||||||
|
|
||||||
|
确保在 `mock/cert/` 目录下有以下证书文件:
|
||||||
|
- `merchant_private_key.pem` - 商户应用私钥证书
|
||||||
|
- `huawei_public_key.pem` - 华为平台公钥证书
|
||||||
|
|
||||||
|
#### 1.1.2 配置文件设置
|
||||||
|
|
||||||
|
编辑 `tests/mock/data.yml` 文件,确保以下配置项正确设置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 华为支付测试配置
|
||||||
|
HuaweiPay:
|
||||||
|
# 沙盒模式
|
||||||
|
sandbox: true
|
||||||
|
# 应用ID
|
||||||
|
app_id: 'test_app_id'
|
||||||
|
# 商户号
|
||||||
|
merc_no: 'test_merc_no'
|
||||||
|
# 授权ID
|
||||||
|
mch_auth_id: 'test_mch_auth_id'
|
||||||
|
# 证书文件路径(相对于项目根目录)
|
||||||
|
private_key: 'mock/cert/merchant_private_key.pem'
|
||||||
|
# 私钥文本内容
|
||||||
|
private_key_text: |
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLW1j8a1KQw7X2
|
||||||
|
...
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
# 华为公钥文件路径(相对于项目根目录)
|
||||||
|
huawei_public_key: 'mock/cert/huawei_public_key.pem'
|
||||||
|
# 华为公钥文本内容
|
||||||
|
huawei_public_key_text: |
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0T1JZ7X2Y5
|
||||||
|
...
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
# 异步通知URL
|
||||||
|
notify_url: 'https://your-domain.com/huaweipay/notify'
|
||||||
|
# 同步返回URL
|
||||||
|
return_url: 'https://your-domain.com/huaweipay/return'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 系统配置
|
||||||
|
|
||||||
|
1. 登录系统后台
|
||||||
|
2. 进入「支付配置」页面
|
||||||
|
3. 找到「华为支付」配置项
|
||||||
|
4. 填写以下信息:
|
||||||
|
- 应用ID
|
||||||
|
- 商户号
|
||||||
|
- 授权ID
|
||||||
|
- 私钥(可以选择文件上传或文本输入)
|
||||||
|
- 华为公钥(可以选择文件上传或文本输入)
|
||||||
|
- 异步通知URL
|
||||||
|
- 同步返回URL
|
||||||
|
5. 开启华为支付
|
||||||
|
|
||||||
|
## 2. 单元测试
|
||||||
|
|
||||||
|
### 2.1 运行证书格式化测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./vendor/bin/phpunit addon/huaweipay/tests/HuaweiPayConfigTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 运行支付模型测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./vendor/bin/phpunit addon/huaweipay/tests/PayModelTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 功能测试
|
||||||
|
|
||||||
|
### 3.1 H5支付测试
|
||||||
|
|
||||||
|
1. 进入系统前台
|
||||||
|
2. 选择一个商品加入购物车
|
||||||
|
3. 提交订单
|
||||||
|
4. 选择「华为支付」作为支付方式
|
||||||
|
5. 点击「立即支付」
|
||||||
|
6. 系统将跳转到华为H5支付页面
|
||||||
|
7. 测试不同支付场景:
|
||||||
|
- 成功支付
|
||||||
|
- 取消支付
|
||||||
|
- 超时未支付
|
||||||
|
|
||||||
|
### 3.2 微信小程序支付测试
|
||||||
|
|
||||||
|
1. 打开微信开发者工具
|
||||||
|
2. 导入小程序项目
|
||||||
|
3. 登录小程序账号
|
||||||
|
4. 选择一个商品加入购物车
|
||||||
|
5. 提交订单
|
||||||
|
6. 选择「华为支付」作为支付方式
|
||||||
|
7. 点击「立即支付」
|
||||||
|
8. 调用华为微信小程序支付接口
|
||||||
|
9. 测试不同支付场景:
|
||||||
|
- 成功支付
|
||||||
|
- 取消支付
|
||||||
|
- 超时未支付
|
||||||
|
|
||||||
|
### 3.3 APP支付测试
|
||||||
|
|
||||||
|
1. 编译APP项目
|
||||||
|
2. 在测试设备上安装APP
|
||||||
|
3. 登录APP账号
|
||||||
|
4. 选择一个商品加入购物车
|
||||||
|
5. 提交订单
|
||||||
|
6. 选择「华为支付」作为支付方式
|
||||||
|
7. 点击「立即支付」
|
||||||
|
8. 调用华为APP支付接口
|
||||||
|
9. 测试不同支付场景:
|
||||||
|
- 成功支付
|
||||||
|
- 取消支付
|
||||||
|
- 超时未支付
|
||||||
|
|
||||||
|
## 4. 回调测试
|
||||||
|
|
||||||
|
### 4. 回调测试
|
||||||
|
|
||||||
|
**测试目标**:验证华为支付异步通知处理流程是否正常工作,包括签名验证和订单状态更新。
|
||||||
|
|
||||||
|
#### 4.1 测试准备
|
||||||
|
1. **配置回调地址**:
|
||||||
|
- 在华为支付模型的配置中设置回调URL(通常为`http://your-domain.com/addon/huaweipay/shop/controller/pay/notify`)
|
||||||
|
- 开发环境可使用ngrok等工具进行本地端口映射:`ngrok http 80`
|
||||||
|
|
||||||
|
2. **了解回调处理流程**:
|
||||||
|
- 华为支付异步回调通过`PayNotify`事件触发
|
||||||
|
- 事件监听器:`addon\huaweipay\event\PayNotify`
|
||||||
|
- 实际处理逻辑:`addon\huaweipay\model\Pay::notify()`方法
|
||||||
|
|
||||||
|
#### 4.2 模拟回调测试
|
||||||
|
|
||||||
|
**方法一:使用Postman模拟回调请求**
|
||||||
|
|
||||||
|
1. **构造回调参数**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pay_type": "huaweipay",
|
||||||
|
"out_trade_no": "TEST_ORDER_202310100001",
|
||||||
|
"trade_no": "HWPAY2023101000000001",
|
||||||
|
"total_amount": "1.00",
|
||||||
|
"trade_status": "SUCCESS",
|
||||||
|
"timestamp": "1696944000000",
|
||||||
|
"sign": "xxxxxxxxx",
|
||||||
|
"app_type": "h5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **发送POST请求**:
|
||||||
|
- URL: `http://your-domain.com/addon/huaweipay/shop/controller/pay/notify`
|
||||||
|
- Method: POST
|
||||||
|
- Body: 选择raw JSON格式,粘贴上面的测试数据
|
||||||
|
|
||||||
|
3. **验证回调结果**:
|
||||||
|
- 检查响应是否为"success"字符串
|
||||||
|
- 查看系统日志(`runtime/log`目录)
|
||||||
|
- 确认订单状态是否已更新为已支付
|
||||||
|
|
||||||
|
**方法二:使用命令行工具**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
http://your-domain.com/addon/huaweipay/shop/controller/pay/notify \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"pay_type":"huaweipay","out_trade_no":"TEST_ORDER_202310100001","trade_no":"HWPAY2023101000000001","total_amount":"1.00","trade_status":"SUCCESS","timestamp":"1696944000000","sign":"xxxxxxxxx"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3 回调签名验证测试
|
||||||
|
|
||||||
|
**测试目标**:验证回调签名验证逻辑是否正常工作
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 构造正确的回调参数和签名
|
||||||
|
2. 发送回调请求,验证处理成功
|
||||||
|
3. 修改签名为错误值,重新发送请求
|
||||||
|
4. 验证签名错误时返回"fail"
|
||||||
|
|
||||||
|
**测试要点**:
|
||||||
|
- 检查`HuaweiPayClient::verifyNotify()`方法的实现
|
||||||
|
- 确保使用正确的公钥进行签名验证
|
||||||
|
- 验证失败时记录详细日志
|
||||||
|
|
||||||
|
**日志检查**:
|
||||||
|
```
|
||||||
|
# 签名验证成功日志
|
||||||
|
[info] 华为支付回调参数: {"out_trade_no":"TEST_ORDER_202310100001",...}
|
||||||
|
[info] 华为支付回调处理成功,订单号: TEST_ORDER_202310100001
|
||||||
|
|
||||||
|
# 签名验证失败日志
|
||||||
|
[error] 华为支付回调签名验证失败: {"out_trade_no":"TEST_ORDER_202310100001",...}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- 回调请求需要包含正确的签名,使用华为支付公钥进行验证
|
||||||
|
- 回调处理完成后,必须向华为服务器返回"success"字符串,否则华为会持续发送通知(默认重试间隔为15s,30s,1m,2m,5m,10m,30m,1h,2h,6h,15h)
|
||||||
|
- 回调处理逻辑需要支持幂等性,防止重复处理同一笔订单
|
||||||
|
|
||||||
|
## 5. 常见问题排查
|
||||||
|
|
||||||
|
### 5.1 证书加载失败
|
||||||
|
|
||||||
|
**问题现象:** 系统提示「加载商户应用私有证书失败」或「加载华为平台支付证书失败」
|
||||||
|
|
||||||
|
**解决方法:**
|
||||||
|
- 检查证书文件路径是否正确
|
||||||
|
- 验证证书格式是否符合要求(PEM格式)
|
||||||
|
- 确保证书内容没有多余的空格或换行
|
||||||
|
- 检查文件权限是否正确
|
||||||
|
|
||||||
|
### 5.2 签名验证失败
|
||||||
|
|
||||||
|
**问题现象:** 系统提示「签名验证失败」或华为支付API返回签名错误
|
||||||
|
|
||||||
|
**解决方法:**
|
||||||
|
- 检查私钥和公钥是否匹配
|
||||||
|
- 验证签名算法是否正确(RSA2)
|
||||||
|
- 确保请求参数没有被篡改
|
||||||
|
- 检查时间戳是否在有效范围内
|
||||||
|
|
||||||
|
### 5.3 支付接口调用失败
|
||||||
|
|
||||||
|
**问题现象:** 调用华为支付API返回错误码
|
||||||
|
|
||||||
|
**解决方法:**
|
||||||
|
- 检查API请求参数是否完整
|
||||||
|
- 验证参数格式是否符合要求
|
||||||
|
- 检查网络连接是否正常
|
||||||
|
- 查看华为支付API文档,了解错误码含义
|
||||||
|
|
||||||
|
### 5.4 订单状态更新失败
|
||||||
|
|
||||||
|
**问题现象:** 支付成功后,系统订单状态未更新
|
||||||
|
|
||||||
|
**解决方法:**
|
||||||
|
- 检查异步通知URL是否可访问
|
||||||
|
- 验证通知签名是否正确
|
||||||
|
- 查看系统日志,了解处理过程
|
||||||
|
- 检查订单号是否正确匹配
|
||||||
|
|
||||||
|
## 6. 测试注意事项
|
||||||
|
|
||||||
|
1. **沙盒环境:** 测试时建议使用华为支付沙盒环境,避免产生真实交易
|
||||||
|
2. **日志记录:** 开启详细日志记录,便于排查问题
|
||||||
|
3. **安全防护:** 测试完成后,及时清理测试数据和敏感信息
|
||||||
|
4. **兼容性测试:** 确保在不同浏览器、设备上都能正常支付
|
||||||
|
5. **性能测试:** 测试高并发场景下的支付处理能力
|
||||||
|
|
||||||
|
## 7. 测试报告模板
|
||||||
|
|
||||||
|
```
|
||||||
|
# 华为支付测试报告
|
||||||
|
|
||||||
|
## 测试基本信息
|
||||||
|
- 测试时间:YYYY-MM-DD
|
||||||
|
- 测试环境:开发环境/测试环境/生产环境
|
||||||
|
- 测试人员:XXX
|
||||||
|
|
||||||
|
## 测试内容
|
||||||
|
|
||||||
|
### 7.1 配置测试
|
||||||
|
- [ ] 证书文件存在性检查
|
||||||
|
- [ ] 配置文件格式检查
|
||||||
|
- [ ] 证书加载测试
|
||||||
|
|
||||||
|
### 7.2 功能测试
|
||||||
|
- [ ] H5支付测试
|
||||||
|
- [ ] 成功支付流程
|
||||||
|
- [ ] 取消支付流程
|
||||||
|
- [ ] 超时未支付流程
|
||||||
|
- [ ] 微信小程序支付测试
|
||||||
|
- [ ] 成功支付流程
|
||||||
|
- [ ] 取消支付流程
|
||||||
|
- [ ] 超时未支付流程
|
||||||
|
- [ ] APP支付测试
|
||||||
|
- [ ] 成功支付流程
|
||||||
|
- [ ] 取消支付流程
|
||||||
|
- [ ] 超时未支付流程
|
||||||
|
|
||||||
|
### 7.3 回调测试
|
||||||
|
- [ ] 异步通知处理
|
||||||
|
- [ ] 同步返回处理
|
||||||
|
- [ ] 订单状态更新
|
||||||
|
|
||||||
|
## 测试结果
|
||||||
|
|
||||||
|
### 7.1 通过的测试
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### 7.2 失败的测试
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### 7.3 问题总结
|
||||||
|
|
||||||
|
| 问题描述 | 严重程度 | 状态 | 解决方案 |
|
||||||
|
|---------|---------|------|---------|
|
||||||
|
| ... | 高/中/低 | 已解决/待解决 | ... |
|
||||||
|
|
||||||
|
## 测试结论
|
||||||
|
|
||||||
|
综合测试结果,华为支付模块是否满足上线要求:
|
||||||
|
- [ ] 是
|
||||||
|
- [ ] 否(需要进一步修复)
|
||||||
|
|
||||||
|
## 建议
|
||||||
|
|
||||||
|
1. ...
|
||||||
|
2. ...
|
||||||
|
3. ...
|
||||||
|
```
|
||||||
@@ -9,6 +9,9 @@ namespace addon\huaweipay\data\sdk;
|
|||||||
use app\exception\ApiException;
|
use app\exception\ApiException;
|
||||||
use think\facade\Log;
|
use think\facade\Log;
|
||||||
|
|
||||||
|
// 引入工具类
|
||||||
|
use addon\huaweipay\data\sdk\Utils;
|
||||||
|
|
||||||
class HuaweiPayClient
|
class HuaweiPayClient
|
||||||
{
|
{
|
||||||
// 华为支付网关地址
|
// 华为支付网关地址
|
||||||
@@ -32,53 +35,14 @@ class HuaweiPayClient
|
|||||||
// 是否加载了华为平台支付服务加密证书
|
// 是否加载了华为平台支付服务加密证书
|
||||||
private $has_huawei_public_key_certificate_instance_encrypt = false;
|
private $has_huawei_public_key_certificate_instance_encrypt = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化证书内容,添加适当的格式头尾
|
|
||||||
* @param string $content 证书内容
|
|
||||||
* @param string $type 证书类型:private_key, public_key
|
|
||||||
* @return string 格式化后的证书内容
|
|
||||||
*/
|
|
||||||
private function formatCertificateContent($content, $type)
|
|
||||||
{
|
|
||||||
// 移除空白字符和换行
|
|
||||||
$content = preg_replace('/\s+/', '', $content);
|
|
||||||
|
|
||||||
// 检查是否已经包含格式头尾
|
|
||||||
if (preg_match('/-----BEGIN.*-----/', $content) && preg_match('/-----END.*-----/', $content)) {
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加适当的格式头尾
|
|
||||||
$header = '';
|
|
||||||
$footer = '';
|
|
||||||
|
|
||||||
if ($type == 'private_key') {
|
|
||||||
$header = "-----BEGIN PRIVATE KEY-----\n";
|
|
||||||
$footer = "-----END PRIVATE KEY-----";
|
|
||||||
} elseif ($type == 'public_key') {
|
|
||||||
$header = "-----BEGIN PUBLIC KEY-----\n";
|
|
||||||
$footer = "-----END PUBLIC KEY-----";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每64个字符添加一个换行
|
|
||||||
$formattedContent = $header;
|
|
||||||
$length = strlen($content);
|
|
||||||
$lines = [];
|
|
||||||
|
|
||||||
for ($i = 0; $i < $length; $i += 64) {
|
|
||||||
$lines[] = substr($content, $i, 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
$formattedContent .= implode("\n", $lines) . $footer;
|
|
||||||
|
|
||||||
return $formattedContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造函数
|
* 构造函数
|
||||||
* @param array $config 华为支付配置
|
* @param array $config 华为支付配置
|
||||||
|
* @param string $cert_root_path 证书根路径
|
||||||
*/
|
*/
|
||||||
public function __construct($config)
|
public function __construct($config, $cert_root_path = '')
|
||||||
{
|
{
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
|
|
||||||
@@ -89,16 +53,36 @@ class HuaweiPayClient
|
|||||||
if (empty($this->config['merc_no'])) {
|
if (empty($this->config['merc_no'])) {
|
||||||
throw new \Exception('缺少必要配置:merc_no');
|
throw new \Exception('缺少必要配置:merc_no');
|
||||||
}
|
}
|
||||||
|
if (empty($this->config['site_id'])) {
|
||||||
|
throw new \Exception('缺少必要配置:site_id');
|
||||||
|
}
|
||||||
|
if (empty($this->config['mch_auth_id'])) {
|
||||||
|
throw new \Exception('缺少必要配置:mch_auth_id');
|
||||||
|
}
|
||||||
|
if (empty($this->config['private_key_text']) && empty($this->config['private_key'])) {
|
||||||
|
throw new \Exception('缺少必要配置:private_key_text或private_key');
|
||||||
|
}
|
||||||
|
if (empty($this->config['huawei_public_key_text']) && empty($this->config['huawei_public_key'])) {
|
||||||
|
throw new \Exception('缺少必要配置:huawei_public_key_text或huawei_public_key');
|
||||||
|
}
|
||||||
|
|
||||||
// 证书基础路径
|
// 证书基础路径
|
||||||
$cert_base_path = app()->getRootPath();
|
$cert_base_path = $cert_root_path;
|
||||||
|
if (empty($cert_base_path)) {
|
||||||
|
// 没有指定证书根路径时,使用默认路径
|
||||||
|
try {
|
||||||
|
$cert_base_path = app()->getRootPath();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 捕获异常,使用默认路径
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 加载商户应用私有证书
|
// 加载商户应用私有证书
|
||||||
$private_key_content = '';
|
$private_key_content = '';
|
||||||
if (!empty($this->config['private_key_text'])) {
|
if (!empty($this->config['private_key_text'])) {
|
||||||
// 文本模式,需要格式化
|
// 文本模式,需要格式化
|
||||||
$private_key_content = $this->formatCertificateContent($this->config['private_key_text'], 'private_key');
|
$private_key_content = Utils::formatCertificateContent($this->config['private_key_text'], 'private_key');
|
||||||
} elseif (!empty($this->config['private_key'])) {
|
} elseif (!empty($this->config['private_key'])) {
|
||||||
// 文件模式
|
// 文件模式
|
||||||
$private_key_path = realpath($cert_base_path . $this->config['private_key']);
|
$private_key_path = realpath($cert_base_path . $this->config['private_key']);
|
||||||
@@ -112,14 +96,19 @@ class HuaweiPayClient
|
|||||||
|
|
||||||
$this->private_key_certificate_instance = openssl_pkey_get_private($private_key_content);
|
$this->private_key_certificate_instance = openssl_pkey_get_private($private_key_content);
|
||||||
if (!$this->private_key_certificate_instance) {
|
if (!$this->private_key_certificate_instance) {
|
||||||
throw new \Exception('加载商户应用私有证书失败,请检查证书格式是否正确');
|
// 输出详细的openssl错误信息
|
||||||
|
$error = '';
|
||||||
|
while ($err = openssl_error_string()) {
|
||||||
|
$error .= $err . ' ';
|
||||||
|
}
|
||||||
|
throw new \Exception('加载商户应用私有证书失败,请检查证书格式是否正确。OpenSSL错误: ' . $error . ' 证书内容开头: ' . substr($private_key_content, 0, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载华为平台支付证书
|
// 加载华为平台支付证书
|
||||||
$huawei_public_key_content = '';
|
$huawei_public_key_content = '';
|
||||||
if (!empty($this->config['huawei_public_key_text'])) {
|
if (!empty($this->config['huawei_public_key_text'])) {
|
||||||
// 文本模式,需要格式化
|
// 文本模式,需要格式化
|
||||||
$huawei_public_key_content = $this->formatCertificateContent($this->config['huawei_public_key_text'], 'public_key');
|
$huawei_public_key_content = Utils::formatCertificateContent($this->config['huawei_public_key_text'], 'public_key');
|
||||||
} elseif (!empty($this->config['huawei_public_key'])) {
|
} elseif (!empty($this->config['huawei_public_key'])) {
|
||||||
// 文件模式
|
// 文件模式
|
||||||
$huawei_public_key_path = realpath($cert_base_path . $this->config['huawei_public_key']);
|
$huawei_public_key_path = realpath($cert_base_path . $this->config['huawei_public_key']);
|
||||||
@@ -139,7 +128,7 @@ class HuaweiPayClient
|
|||||||
// 加载华为平台支付服务加密证书(可选)
|
// 加载华为平台支付服务加密证书(可选)
|
||||||
if (!empty($this->config['huawei_public_key_for_sessionkey_text'])) {
|
if (!empty($this->config['huawei_public_key_for_sessionkey_text'])) {
|
||||||
// 文本模式,需要格式化
|
// 文本模式,需要格式化
|
||||||
$huawei_public_key_encrypt_content = $this->formatCertificateContent($this->config['huawei_public_key_for_sessionkey_text'], 'public_key');
|
$huawei_public_key_encrypt_content = Utils::formatCertificateContent($this->config['huawei_public_key_for_sessionkey_text'], 'public_key');
|
||||||
$this->huawei_public_key_certificate_instance_encrypt = openssl_pkey_get_public($huawei_public_key_encrypt_content);
|
$this->huawei_public_key_certificate_instance_encrypt = openssl_pkey_get_public($huawei_public_key_encrypt_content);
|
||||||
if (!$this->huawei_public_key_certificate_instance_encrypt) {
|
if (!$this->huawei_public_key_certificate_instance_encrypt) {
|
||||||
throw new \Exception('加载华为平台支付服务加密证书失败');
|
throw new \Exception('加载华为平台支付服务加密证书失败');
|
||||||
@@ -166,7 +155,7 @@ class HuaweiPayClient
|
|||||||
|
|
||||||
// 根据配置设置网关地址
|
// 根据配置设置网关地址
|
||||||
if (isset($config['sandbox']) && $config['sandbox']) {
|
if (isset($config['sandbox']) && $config['sandbox']) {
|
||||||
$this->gatewayUrl = 'https://pay-drcn.cloud.huawei.com/gateway/api/pay';
|
$this->gatewayUrl = 'https://petalpay-developer-sandbox.cloud.huawei.com.cn/gateway/api/pay';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
61
src/addon/huaweipay/data/sdk/Utils.php
Normal file
61
src/addon/huaweipay/data/sdk/Utils.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 华为支付工具类
|
||||||
|
* 提供华为支付相关的工具方法
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace addon\huaweipay\data\sdk;
|
||||||
|
|
||||||
|
class Utils
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 格式化证书内容,添加适当的格式头尾
|
||||||
|
* @param string $content 证书内容
|
||||||
|
* @param string $type 证书类型:private_key, public_key
|
||||||
|
* @return string 格式化后的证书内容
|
||||||
|
*/
|
||||||
|
public static function formatCertificateContent($content, $type)
|
||||||
|
{
|
||||||
|
// 移除空白字符和换行
|
||||||
|
$content = preg_replace('/\s+/', '', $content);
|
||||||
|
|
||||||
|
// 检查是否已经包含格式头尾
|
||||||
|
if (preg_match('/-----BEGIN\s+.*?-----/', $content) && preg_match('/-----END\s+.*?-----/', $content)) {
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除可能存在的旧格式
|
||||||
|
$content = preg_replace('/-----BEGIN.*?-----/', '', $content);
|
||||||
|
$content = preg_replace('/-----END.*?-----/', '', $content);
|
||||||
|
|
||||||
|
// 添加适当的格式头尾
|
||||||
|
$header = '';
|
||||||
|
$footer = '';
|
||||||
|
|
||||||
|
if ($type == 'private_key') {
|
||||||
|
$header = "-----BEGIN PRIVATE KEY-----\n";
|
||||||
|
$footer = "-----END PRIVATE KEY-----";
|
||||||
|
} elseif ($type == 'public_key') {
|
||||||
|
$header = "-----BEGIN PUBLIC KEY-----\n";
|
||||||
|
$footer = "-----END PUBLIC KEY-----";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每64个字符添加一个换行
|
||||||
|
$formattedContent = $header;
|
||||||
|
$length = strlen($content);
|
||||||
|
$lines = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $length; $i += 64) {
|
||||||
|
$lines[] = substr($content, $i, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保在添加footer之前有一个换行符
|
||||||
|
if (!empty($lines)) {
|
||||||
|
$formattedContent .= implode("\n", $lines) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattedContent .= $footer;
|
||||||
|
|
||||||
|
return $formattedContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,15 +39,22 @@ class Pay extends BaseModel
|
|||||||
/**
|
/**
|
||||||
* 构造函数
|
* 构造函数
|
||||||
* @param $site_id 站点ID
|
* @param $site_id 站点ID
|
||||||
|
* @param $config_data 配置数据, 为空时从数据库获取
|
||||||
*/
|
*/
|
||||||
function __construct($site_id)
|
function __construct($site_id, $config_data = [])
|
||||||
{
|
{
|
||||||
$this->site_id = $site_id;
|
$this->site_id = $site_id;
|
||||||
|
|
||||||
// 获取华为支付参数
|
// 获取华为支付参数
|
||||||
$config_info = (new Config())->getPayConfig($this->site_id)['data']['value'];
|
if (empty($config_data)) {
|
||||||
$this->config = $config_info;
|
$config_info = (new Config())->getPayConfig($this->site_id)['data']['value'];
|
||||||
if (empty($this->config)) throw new ApiException(-1, "平台未配置华为支付");
|
$this->config = $config_info;
|
||||||
|
if (empty($this->config)) throw new ApiException(-1, "平台未配置华为支付");
|
||||||
|
} else {
|
||||||
|
$this->config = $config_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加站点ID
|
||||||
$this->config['site_id'] = $this->site_id;
|
$this->config['site_id'] = $this->site_id;
|
||||||
|
|
||||||
// 初始化华为支付客户端
|
// 初始化华为支付客户端
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="layui-form form-wrap">
|
<div class="layui-form form-wrap">
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<label class="layui-form-label">证书配置方式:</label>
|
<label class="layui-form-label"><span class="required">*</span>证书配置方式:</label>
|
||||||
<div class="layui-input-block cert-type-switch">
|
<div class="layui-input-block cert-type-switch">
|
||||||
<input type="radio" name="cert_type" value="text" title="文本填写" lay-filter="cert_type" {if empty($info.cert_type) || $info.cert_type == 'text'}checked{/if}>
|
<input type="radio" name="cert_type" value="text" title="文本填写" lay-filter="cert_type" {if empty($info.cert_type) || $info.cert_type == 'text'}checked{/if}>
|
||||||
<input type="radio" name="cert_type" value="file" title="文件上传" lay-filter="cert_type" {if !empty($info.cert_type) && $info.cert_type == 'file'}checked{/if}>
|
<input type="radio" name="cert_type" value="file" title="文件上传" lay-filter="cert_type" {if !empty($info.cert_type) && $info.cert_type == 'file'}checked{/if}>
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* 华为支付证书内容格式化测试
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 直接测试证书内容格式化逻辑
|
|
||||||
*/
|
|
||||||
class CertificateFormatTest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 测试配置数据
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $configData;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数,加载配置文件
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
// 加载配置文件
|
|
||||||
$yamlPath = __DIR__ . '/mock/data.yml';
|
|
||||||
if (file_exists($yamlPath)) {
|
|
||||||
if (function_exists('yaml_parse_file')) {
|
|
||||||
$this->configData = yaml_parse_file($yamlPath);
|
|
||||||
} else {
|
|
||||||
// 如果没有yaml_parse_file函数,手动解析
|
|
||||||
$this->configData = $this->parseYamlFile($yamlPath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Exception("配置文件不存在: {$yamlPath}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 手动解析YAML文件
|
|
||||||
* @param string $filePath
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
private function parseYamlFile($filePath)
|
|
||||||
{
|
|
||||||
$content = file_get_contents($filePath);
|
|
||||||
$lines = explode("\n", $content);
|
|
||||||
$result = [];
|
|
||||||
$currentKey = '';
|
|
||||||
$isMultiline = false;
|
|
||||||
$multilineValue = '';
|
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
$line = trim($line);
|
|
||||||
|
|
||||||
// 跳过注释和空行
|
|
||||||
if (empty($line) || strpos($line, '#') === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理多行值
|
|
||||||
if ($isMultiline) {
|
|
||||||
if (strpos($line, '-----END') === 0) {
|
|
||||||
$multilineValue .= $line . "\n";
|
|
||||||
$result[$currentKey] = rtrim($multilineValue);
|
|
||||||
$isMultiline = false;
|
|
||||||
$multilineValue = '';
|
|
||||||
} else {
|
|
||||||
$multilineValue .= $line . "\n";
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理单行键值对
|
|
||||||
if (strpos($line, ':') !== false) {
|
|
||||||
list($key, $value) = explode(':', $line, 2);
|
|
||||||
$key = trim($key);
|
|
||||||
$value = trim($value);
|
|
||||||
|
|
||||||
// 处理多行值开始
|
|
||||||
if ($value === '|') {
|
|
||||||
$isMultiline = true;
|
|
||||||
$currentKey = $key;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理普通值
|
|
||||||
if (!empty($value)) {
|
|
||||||
$result[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟证书内容格式化方法
|
|
||||||
* 与HuaweiPayClient::formatCertificateContent方法实现一致
|
|
||||||
*/
|
|
||||||
private function formatCertificateContent($content, $type)
|
|
||||||
{
|
|
||||||
// 移除空白字符和换行
|
|
||||||
$content = preg_replace('/\s+/', '', $content);
|
|
||||||
|
|
||||||
// 检查是否已经包含格式头尾
|
|
||||||
if (preg_match('/-----BEGIN\s+.*?-----/', $content) && preg_match('/-----END\s+.*?-----/', $content)) {
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加适当的格式头尾
|
|
||||||
$header = '';
|
|
||||||
$footer = '';
|
|
||||||
|
|
||||||
if ($type == 'private_key') {
|
|
||||||
$header = "-----BEGIN PRIVATE KEY-----";
|
|
||||||
$footer = "-----END PRIVATE KEY-----";
|
|
||||||
} elseif ($type == 'public_key') {
|
|
||||||
$header = "-----BEGIN PUBLIC KEY-----";
|
|
||||||
$footer = "-----END PUBLIC KEY-----";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除可能存在的旧格式
|
|
||||||
$content = preg_replace('/-----BEGIN.*?-----/', '', $content);
|
|
||||||
$content = preg_replace('/-----END.*?-----/', '', $content);
|
|
||||||
|
|
||||||
// 组合最终格式
|
|
||||||
return $header . $content . $footer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试证书内容格式化功能
|
|
||||||
*/
|
|
||||||
public function testFormatCertificateContent()
|
|
||||||
{
|
|
||||||
echo "开始测试证书内容格式化功能...\n\n";
|
|
||||||
|
|
||||||
// 从配置文件获取实际的密钥内容
|
|
||||||
$privateKey = $this->configData['privateKey'];
|
|
||||||
$publicKey = $this->configData['huawei_public_key'];
|
|
||||||
|
|
||||||
// 测试1:私钥格式化
|
|
||||||
echo "测试1:私钥格式化...";
|
|
||||||
$formattedPrivateKey = $this->formatCertificateContent($privateKey, 'private_key');
|
|
||||||
|
|
||||||
// 移除预期结果和实际结果中的所有换行符,统一比较
|
|
||||||
$expectedClean = str_replace("\n", '', $privateKey);
|
|
||||||
$actualClean = str_replace("\n", '', $formattedPrivateKey);
|
|
||||||
|
|
||||||
if (strpos($actualClean, '-----BEGIN PRIVATE KEY-----') !== false && strpos($actualClean, '-----END PRIVATE KEY-----') !== false) {
|
|
||||||
echo " ✅ 通过\n";
|
|
||||||
} else {
|
|
||||||
echo " ❌ 失败\n";
|
|
||||||
echo " 实际:$formattedPrivateKey\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试2:公钥格式化
|
|
||||||
echo "测试2:公钥格式化...";
|
|
||||||
$formattedPublicKey = $this->formatCertificateContent($publicKey, 'public_key');
|
|
||||||
|
|
||||||
// 移除预期结果和实际结果中的所有换行符,统一比较
|
|
||||||
$expectedClean = str_replace("\n", '', $publicKey);
|
|
||||||
$actualClean = str_replace("\n", '', $formattedPublicKey);
|
|
||||||
|
|
||||||
if (strpos($actualClean, '-----BEGIN PUBLIC KEY-----') !== false && strpos($actualClean, '-----END PUBLIC KEY-----') !== false) {
|
|
||||||
echo " ✅ 通过\n";
|
|
||||||
} else {
|
|
||||||
echo " ❌ 失败\n";
|
|
||||||
echo " 实际:$formattedPublicKey\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试3:带空格和换行的私钥格式化
|
|
||||||
echo "测试3:带空格和换行的私钥格式化...";
|
|
||||||
$privateKeyWithSpaces = ' ' . $privateKey . ' ';
|
|
||||||
$formattedPrivateKeyWithSpaces = $this->formatCertificateContent($privateKeyWithSpaces, 'private_key');
|
|
||||||
|
|
||||||
if (strpos($formattedPrivateKeyWithSpaces, '-----BEGIN PRIVATE KEY-----') !== false && strpos($formattedPrivateKeyWithSpaces, '-----END PRIVATE KEY-----') !== false) {
|
|
||||||
echo " ✅ 通过\n";
|
|
||||||
} else {
|
|
||||||
echo " ❌ 失败\n";
|
|
||||||
echo " 实际:$formattedPrivateKeyWithSpaces\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n✅ 所有证书内容格式化测试通过!\n";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试文本模式配置结构
|
|
||||||
*/
|
|
||||||
public function testTextModeConfigStructure()
|
|
||||||
{
|
|
||||||
echo "\n开始测试文本模式配置结构...\n";
|
|
||||||
|
|
||||||
// 测试1:基本文本模式配置
|
|
||||||
echo "测试1:基本文本模式配置...";
|
|
||||||
$config1 = [
|
|
||||||
'app_id' => $this->configData['app_id'],
|
|
||||||
'mch_id' => $this->configData['mch_id'],
|
|
||||||
'private_key_text' => $this->configData['privateKey'],
|
|
||||||
'huawei_public_key_text' => $this->configData['huawei_public_key']
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isset($config1['private_key_text']) && isset($config1['huawei_public_key_text'])) {
|
|
||||||
echo " ✅ 通过\n";
|
|
||||||
} else {
|
|
||||||
echo " ❌ 失败\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试2:验证配置数据完整性
|
|
||||||
echo "测试2:验证配置数据完整性...";
|
|
||||||
$requiredFields = ['app_id', 'mch_id', 'mch_auth_id', 'privateKey', 'huawei_public_key'];
|
|
||||||
$missingFields = [];
|
|
||||||
|
|
||||||
foreach ($requiredFields as $field) {
|
|
||||||
if (!isset($this->configData[$field])) {
|
|
||||||
$missingFields[] = $field;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($missingFields)) {
|
|
||||||
echo " ✅ 通过\n";
|
|
||||||
} else {
|
|
||||||
echo " ❌ 失败,缺少字段:" . implode(', ', $missingFields) . "\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n✅ 所有文本模式配置结构测试通过!\n";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 运行所有测试
|
|
||||||
*/
|
|
||||||
public function runTests()
|
|
||||||
{
|
|
||||||
echo "开始运行华为支付文本模式测试...\n\n";
|
|
||||||
|
|
||||||
try {
|
|
||||||
$formatResult = $this->testFormatCertificateContent();
|
|
||||||
$configResult = $this->testTextModeConfigStructure();
|
|
||||||
|
|
||||||
if ($formatResult && $configResult) {
|
|
||||||
echo "\n🎉 所有测试通过!华为支付文本模式功能正常工作!\n";
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
echo "\n💥 部分测试失败!\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "\n💥 测试失败:" . $e->getMessage() . "\n";
|
|
||||||
echo "错误位置:" . $e->getFile() . " 第" . $e->getLine() . "行\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果直接运行此文件,则执行测试
|
|
||||||
if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
|
|
||||||
$test = new CertificateFormatTest();
|
|
||||||
$test->runTests();
|
|
||||||
}
|
|
||||||
191
src/addon/huaweipay/tests/HuaweiPayConfigTest.php
Normal file
191
src/addon/huaweipay/tests/HuaweiPayConfigTest.php
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 华为支付证书内容格式化测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace addon\huaweipay\tests;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 证书内容格式化测试类
|
||||||
|
*/
|
||||||
|
class HuaweiPayConfigTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试证书内容格式化功能
|
||||||
|
*/
|
||||||
|
public function testCertificateContentFormatting()
|
||||||
|
{
|
||||||
|
// 从配置文件获取实际的密钥内容
|
||||||
|
$privateKey = $this->configData['private_key_text'];
|
||||||
|
$publicKey = $this->configData['huawei_public_key_text'];
|
||||||
|
|
||||||
|
// 测试1:私钥格式化(文本内容模式)
|
||||||
|
$formattedPrivateKey = $this->formatCertificateContent($privateKey, 'private_key');
|
||||||
|
$this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $formattedPrivateKey, '私钥格式化失败:缺少BEGIN标记');
|
||||||
|
$this->assertStringContainsString('-----END PRIVATE KEY-----', $formattedPrivateKey, '私钥格式化失败:缺少END标记');
|
||||||
|
|
||||||
|
// 测试2:公钥格式化(文本内容模式)
|
||||||
|
$formattedPublicKey = $this->formatCertificateContent($publicKey, 'public_key');
|
||||||
|
$this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $formattedPublicKey, '公钥格式化失败:缺少BEGIN标记');
|
||||||
|
$this->assertStringContainsString('-----END PUBLIC KEY-----', $formattedPublicKey, '公钥格式化失败:缺少END标记');
|
||||||
|
|
||||||
|
// 测试3:带空格和换行的私钥格式化
|
||||||
|
$privateKeyWithSpaces = ' ' . $privateKey . ' ';
|
||||||
|
$formattedPrivateKeyWithSpaces = $this->formatCertificateContent($privateKeyWithSpaces, 'private_key');
|
||||||
|
$this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $formattedPrivateKeyWithSpaces, '带空格私钥格式化失败:缺少BEGIN标记');
|
||||||
|
$this->assertStringContainsString('-----END PRIVATE KEY-----', $formattedPrivateKeyWithSpaces, '带空格私钥格式化失败:缺少END标记');
|
||||||
|
|
||||||
|
// 测试4:没有格式的私钥格式化
|
||||||
|
$rawPrivateKey = str_replace(["\n", '-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----'], '', $privateKey);
|
||||||
|
$formattedRawPrivateKey = $this->formatCertificateContent($rawPrivateKey, 'private_key');
|
||||||
|
$this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $formattedRawPrivateKey, '无格式私钥格式化失败:缺少BEGIN标记');
|
||||||
|
$this->assertStringContainsString('-----END PRIVATE KEY-----', $formattedRawPrivateKey, '无格式私钥格式化失败:缺少END标记');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试文本模式配置结构
|
||||||
|
*/
|
||||||
|
public function testTextModeConfigStructure()
|
||||||
|
{
|
||||||
|
// 测试1:基本文本模式配置
|
||||||
|
$config = [
|
||||||
|
'app_id' => $this->configData['app_id'],
|
||||||
|
'merc_no' => $this->configData['merc_no'],
|
||||||
|
'private_key_text' => $this->configData['private_key_text'],
|
||||||
|
'huawei_public_key_text' => $this->configData['huawei_public_key_text']
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('private_key_text', $config, '配置缺少private_key_text字段');
|
||||||
|
$this->assertArrayHasKey('huawei_public_key_text', $config, '配置缺少huawei_public_key_text字段');
|
||||||
|
|
||||||
|
// 测试2:验证配置数据完整性
|
||||||
|
$requiredFields = ['app_id', 'merc_no', 'mch_auth_id', 'private_key_text', 'huawei_public_key_text'];
|
||||||
|
foreach ($requiredFields as $field) {
|
||||||
|
$this->assertArrayHasKey($field, $this->configData, "配置文件缺少必填字段:{$field}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试文件路径模式配置
|
||||||
|
*/
|
||||||
|
public function testFilePathModeConfig()
|
||||||
|
{
|
||||||
|
// 测试1:检查文件路径配置是否存在
|
||||||
|
$this->assertArrayHasKey('private_key', $this->configData, '配置缺少private_key字段(文件路径模式)');
|
||||||
|
$this->assertArrayHasKey('huawei_public_key', $this->configData, '配置缺少huawei_public_key字段(文件路径模式)');
|
||||||
|
|
||||||
|
// 测试2:验证文件路径格式
|
||||||
|
$privateKeyPath = $this->configData['private_key'];
|
||||||
|
$publicKeyPath = $this->configData['huawei_public_key'];
|
||||||
|
|
||||||
|
$this->assertStringEndsWith('.pem', $privateKeyPath, '私钥文件路径必须以.pem结尾');
|
||||||
|
$this->assertStringEndsWith('.pem', $publicKeyPath, '公钥文件路径必须以.pem结尾');
|
||||||
|
|
||||||
|
// 测试3:验证文件是否存在(如果在测试环境中可用)
|
||||||
|
$fullPrivateKeyPath = __DIR__ . '/' . $privateKeyPath;
|
||||||
|
$fullPublicKeyPath = __DIR__ . '/' . $publicKeyPath;
|
||||||
|
|
||||||
|
if (file_exists($fullPrivateKeyPath)) {
|
||||||
|
$this->assertFileExists($fullPrivateKeyPath, '私钥文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($fullPublicKeyPath)) {
|
||||||
|
$this->assertFileExists($fullPublicKeyPath, '公钥文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试4:验证文件内容(如果文件存在)
|
||||||
|
if (file_exists($fullPrivateKeyPath)) {
|
||||||
|
$privateKeyContent = file_get_contents($fullPrivateKeyPath);
|
||||||
|
$this->assertNotEmpty($privateKeyContent, '私钥文件内容为空');
|
||||||
|
$this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $privateKeyContent, '私钥文件内容格式不正确');
|
||||||
|
$this->assertStringContainsString('-----END PRIVATE KEY-----', $privateKeyContent, '私钥文件内容格式不正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($fullPublicKeyPath)) {
|
||||||
|
$publicKeyContent = file_get_contents($fullPublicKeyPath);
|
||||||
|
$this->assertNotEmpty($publicKeyContent, '公钥文件内容为空');
|
||||||
|
$this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $publicKeyContent, '公钥文件内容格式不正确');
|
||||||
|
$this->assertStringContainsString('-----END PUBLIC KEY-----', $publicKeyContent, '公钥文件内容格式不正确');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试两种配置模式的相互转换
|
||||||
|
*/
|
||||||
|
public function testConfigModeConversion()
|
||||||
|
{
|
||||||
|
// 测试1:文本内容转换为文件路径模式(模拟)
|
||||||
|
$privateKeyText = $this->configData['private_key_text'];
|
||||||
|
$publicKeyText = $this->configData['huawei_public_key_text'];
|
||||||
|
|
||||||
|
// 模拟将文本内容保存到临时文件
|
||||||
|
$tempPrivateKeyPath = sys_get_temp_dir() . '/temp_private_key.pem';
|
||||||
|
$tempPublicKeyPath = sys_get_temp_dir() . '/temp_public_key.pem';
|
||||||
|
|
||||||
|
file_put_contents($tempPrivateKeyPath, $privateKeyText);
|
||||||
|
file_put_contents($tempPublicKeyPath, $publicKeyText);
|
||||||
|
|
||||||
|
// 验证临时文件内容
|
||||||
|
$this->assertFileExists($tempPrivateKeyPath, '临时私钥文件创建失败');
|
||||||
|
$this->assertFileExists($tempPublicKeyPath, '临时公钥文件创建失败');
|
||||||
|
|
||||||
|
$this->assertStringEqualsFile($tempPrivateKeyPath, $privateKeyText, '临时私钥文件内容与原文本不一致');
|
||||||
|
$this->assertStringEqualsFile($tempPublicKeyPath, $publicKeyText, '临时公钥文件内容与原文本不一致');
|
||||||
|
|
||||||
|
// 清理临时文件
|
||||||
|
unlink($tempPrivateKeyPath);
|
||||||
|
unlink($tempPublicKeyPath);
|
||||||
|
|
||||||
|
// 测试2:文件路径转换为文本内容模式(如果文件存在)
|
||||||
|
$privateKeyPath = $this->configData['private_key'];
|
||||||
|
$publicKeyPath = $this->configData['huawei_public_key'];
|
||||||
|
|
||||||
|
$fullPrivateKeyPath = __DIR__ . '/' . $privateKeyPath;
|
||||||
|
$fullPublicKeyPath = __DIR__ . '/' . $publicKeyPath;
|
||||||
|
|
||||||
|
if (file_exists($fullPrivateKeyPath) && file_exists($fullPublicKeyPath)) {
|
||||||
|
$privateKeyContent = file_get_contents($fullPrivateKeyPath);
|
||||||
|
$publicKeyContent = file_get_contents($fullPublicKeyPath);
|
||||||
|
|
||||||
|
// 验证从文件读取的内容可以被正确格式化
|
||||||
|
$formattedPrivateKey = $this->formatCertificateContent($privateKeyContent, 'private_key');
|
||||||
|
$formattedPublicKey = $this->formatCertificateContent($publicKeyContent, 'public_key');
|
||||||
|
|
||||||
|
$this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $formattedPrivateKey, '从文件读取的私钥格式化失败');
|
||||||
|
$this->assertStringContainsString('-----END PRIVATE KEY-----', $formattedPrivateKey, '从文件读取的私钥格式化失败');
|
||||||
|
$this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $formattedPublicKey, '从文件读取的公钥格式化失败');
|
||||||
|
$this->assertStringContainsString('-----END PUBLIC KEY-----', $formattedPublicKey, '从文件读取的公钥格式化失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试配置模式优先级
|
||||||
|
*/
|
||||||
|
public function testConfigModePriority()
|
||||||
|
{
|
||||||
|
// 测试1:文本内容模式优先于文件路径模式
|
||||||
|
// 模拟配置同时包含两种模式
|
||||||
|
$config = [
|
||||||
|
'private_key' => 'mock/cert/merchant_private_key.pem',
|
||||||
|
'private_key_text' => $this->configData['private_key_text'],
|
||||||
|
'huawei_public_key' => 'mock/cert/huawei_public_key.pem',
|
||||||
|
'huawei_public_key_text' => $this->configData['huawei_public_key_text']
|
||||||
|
];
|
||||||
|
|
||||||
|
// 验证两种模式的配置都存在
|
||||||
|
$this->assertArrayHasKey('private_key', $config, '配置缺少private_key字段');
|
||||||
|
$this->assertArrayHasKey('private_key_text', $config, '配置缺少private_key_text字段');
|
||||||
|
$this->assertArrayHasKey('huawei_public_key', $config, '配置缺少huawei_public_key字段');
|
||||||
|
$this->assertArrayHasKey('huawei_public_key_text', $config, '配置缺少huawei_public_key_text字段');
|
||||||
|
|
||||||
|
// 模拟应用文本内容优先的逻辑
|
||||||
|
$finalPrivateKey = isset($config['private_key_text']) ? $config['private_key_text'] : $config['private_key'];
|
||||||
|
$finalPublicKey = isset($config['huawei_public_key_text']) ? $config['huawei_public_key_text'] : $config['huawei_public_key'];
|
||||||
|
|
||||||
|
// 验证文本内容模式被优先使用
|
||||||
|
$this->assertSame($config['private_key_text'], $finalPrivateKey, '文本内容模式应该优先于文件路径模式');
|
||||||
|
$this->assertSame($config['huawei_public_key_text'], $finalPublicKey, '文本内容模式应该优先于文件路径模式');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,263 +3,417 @@
|
|||||||
* 华为支付模型测试类
|
* 华为支付模型测试类
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace addon\huaweipay\tests;
|
// 设置错误报告
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
use addon\huaweipay\model\Pay;
|
// 添加自动加载路径
|
||||||
use addon\huaweipay\model\Config;
|
$root_path = dirname(__DIR__, 3);
|
||||||
use think\facade\Log;
|
require_once $root_path . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// 定义常量
|
||||||
|
if (!defined('ROOT_PATH')) {
|
||||||
|
define('ROOT_PATH', $root_path . '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟app()函数,解决框架依赖问题
|
||||||
|
function app() {
|
||||||
|
return new class {
|
||||||
|
public function getRootPath() {
|
||||||
|
return dirname(__DIR__, 3) . '\\';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class PayModelTest
|
class PayModelTest
|
||||||
{
|
{
|
||||||
/**
|
protected $config = [];
|
||||||
* 测试站点ID
|
protected $pay_model = null;
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $siteId = 1;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试配置数据
|
* 构造函数
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $configData;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数,加载配置文件
|
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
// 加载配置文件
|
// 加载测试配置
|
||||||
$yamlPath = __DIR__ . '/mock/data.yml';
|
$this->config = $this->parseYamlFile(__DIR__ . '/mock/data.yml');
|
||||||
if (file_exists($yamlPath)) {
|
echo "配置加载完成\n";
|
||||||
if (function_exists('yaml_parse_file')) {
|
// print_r($this->config);
|
||||||
$this->configData = yaml_parse_file($yamlPath);
|
|
||||||
} else {
|
|
||||||
// 如果没有yaml_parse_file函数,手动解析
|
|
||||||
$this->configData = $this->parseYamlFile($yamlPath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Exception("配置文件不存在: {$yamlPath}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手动解析YAML文件
|
* 解析YAML文件
|
||||||
* @param string $filePath
|
* @param string $file_path YAML文件路径
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function parseYamlFile($filePath)
|
protected function parseYamlFile($file_path)
|
||||||
{
|
{
|
||||||
$content = file_get_contents($filePath);
|
if (!file_exists($file_path)) {
|
||||||
$lines = explode("\n", $content);
|
throw new Exception("YAML文件不存在: {$file_path}");
|
||||||
$result = [];
|
}
|
||||||
$currentKey = '';
|
|
||||||
$isMultiline = false;
|
$yaml_content = file_get_contents($file_path);
|
||||||
$multilineValue = '';
|
if ($yaml_content === false) {
|
||||||
|
throw new Exception("读取YAML文件失败: {$file_path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用正则替换YAML中的\n为实际换行符
|
||||||
|
$yaml_content = preg_replace_callback('/\\\\n/', function($matches) {
|
||||||
|
return "\n";
|
||||||
|
}, $yaml_content);
|
||||||
|
|
||||||
|
// 简单的YAML解析(仅支持当前测试文件的格式)
|
||||||
|
$config = [];
|
||||||
|
$lines = explode("\n", $yaml_content);
|
||||||
|
$current_key = '';
|
||||||
|
$in_block = false;
|
||||||
|
$block_content = '';
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$line = trim($line);
|
$line = trim($line);
|
||||||
|
|
||||||
// 跳过注释和空行
|
|
||||||
if (empty($line) || strpos($line, '#') === 0) {
|
if (empty($line) || strpos($line, '#') === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理多行值
|
if ($in_block) {
|
||||||
if ($isMultiline) {
|
if (strpos($line, '---') === 0) {
|
||||||
if (strpos($line, '-----END') === 0) {
|
continue;
|
||||||
$multilineValue .= $line . "\n";
|
}
|
||||||
$result[$currentKey] = rtrim($multilineValue);
|
if (preg_match('/^\w+:/', $line)) {
|
||||||
$isMultiline = false;
|
$config[$current_key] = $block_content;
|
||||||
$multilineValue = '';
|
$in_block = false;
|
||||||
} else {
|
$block_content = '';
|
||||||
$multilineValue .= $line . "\n";
|
} else {
|
||||||
|
$block_content .= $line . "\n";
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理单行键值对
|
|
||||||
if (strpos($line, ':') !== false) {
|
if (strpos($line, ':') !== false) {
|
||||||
list($key, $value) = explode(':', $line, 2);
|
list($key, $value) = explode(':', $line, 2);
|
||||||
$key = trim($key);
|
$key = trim($key);
|
||||||
$value = trim($value);
|
$value = trim($value);
|
||||||
|
|
||||||
// 处理多行值开始
|
if (strpos($value, '|') !== false) {
|
||||||
if ($value === '|') {
|
$in_block = true;
|
||||||
$isMultiline = true;
|
$current_key = $key;
|
||||||
$currentKey = $key;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理普通值
|
|
||||||
if (!empty($value)) {
|
if (!empty($value)) {
|
||||||
$result[$key] = $value;
|
$config[$key] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
if ($in_block && !empty($current_key)) {
|
||||||
|
$config[$current_key] = $block_content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运行所有测试
|
* 运行测试
|
||||||
*/
|
*/
|
||||||
public function runTests()
|
public function runTests()
|
||||||
{
|
{
|
||||||
echo "开始测试华为支付模型...\n";
|
echo "开始运行华为支付模型测试...\n";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1. 测试支付方法
|
||||||
|
echo "1. 测试支付方法...\n";
|
||||||
$this->testPayMethod();
|
$this->testPayMethod();
|
||||||
$this->testNotifyMethod();
|
echo "1. 支付方法测试通过\n";
|
||||||
$this->testCloseOrderMethod();
|
|
||||||
$this->testRefundMethod();
|
|
||||||
$this->testQueryOrderMethod();
|
|
||||||
|
|
||||||
echo "\n✅ 所有测试通过!\n";
|
// 2. 测试关闭订单方法
|
||||||
return true;
|
echo "2. 测试关闭订单方法...\n";
|
||||||
} catch (\Exception $e) {
|
$this->testCloseOrderMethod();
|
||||||
echo "\n❌ 测试失败:" . $e->getMessage() . "\n";
|
echo "2. 关闭订单方法测试通过\n";
|
||||||
echo "错误位置:" . $e->getFile() . " 第" . $e->getLine() . "行\n";
|
|
||||||
|
// 3. 测试退款方法
|
||||||
|
echo "3. 测试退款方法...\n";
|
||||||
|
$this->testRefundMethod();
|
||||||
|
echo "3. 退款方法测试通过\n";
|
||||||
|
|
||||||
|
// 4. 测试查询订单方法
|
||||||
|
echo "4. 测试查询订单方法...\n";
|
||||||
|
$this->testQueryOrderMethod();
|
||||||
|
echo "4. 查询订单方法测试通过\n";
|
||||||
|
|
||||||
|
// 5. 测试回调验证方法
|
||||||
|
echo "5. 测试回调验证方法...\n";
|
||||||
|
$this->testCallbackVerification();
|
||||||
|
echo "5. 回调验证方法测试通过\n";
|
||||||
|
|
||||||
|
echo "\n所有测试通过!\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "\n测试失败:" . $e->getMessage() . "\n";
|
||||||
|
echo "错误堆栈:" . $e->getTraceAsString() . "\n";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试支付方法
|
* 测试支付方法
|
||||||
*/
|
*/
|
||||||
public function testPayMethod()
|
protected function testPayMethod()
|
||||||
{
|
{
|
||||||
echo "测试支付方法...";
|
// 创建支付模型实例
|
||||||
|
require_once dirname(__DIR__) . '/model/Pay.php';
|
||||||
|
require_once dirname(__DIR__) . '/data/sdk/utils.php';
|
||||||
|
require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php';
|
||||||
|
|
||||||
|
// 添加必要的配置
|
||||||
|
$site_id = '1';
|
||||||
|
$config_data = $this->config;
|
||||||
|
$config_data['site_id'] = $site_id;
|
||||||
|
// 使用文本模式的密钥,而不是证书文件
|
||||||
|
unset($config_data['private_key']);
|
||||||
|
unset($config_data['huawei_public_key']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 模拟配置
|
// 创建实际的华为支付客户端实例
|
||||||
$this->mockConfig();
|
$project_root = dirname(__DIR__, 4); // 获取项目根目录
|
||||||
|
$huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root);
|
||||||
|
echo " - 华为支付客户端实例创建成功\n";
|
||||||
|
|
||||||
// 创建测试订单数据
|
// 测试H5支付
|
||||||
$orderInfo = [
|
$h5_params = [
|
||||||
'out_trade_no' => 'test_order_' . time(),
|
'out_trade_no' => 'TEST_H5_' . time(),
|
||||||
'total_amount' => 100,
|
'subject' => '测试H5支付商品',
|
||||||
'subject' => '测试商品',
|
'body' => '测试H5支付商品描述',
|
||||||
'notify_url' => $this->configData['notifyUrl'],
|
'total_amount' => 0.01,
|
||||||
'return_url' => $this->configData['returnUrl'],
|
'client_ip' => '127.0.0.1'
|
||||||
'buyer_id' => '1',
|
|
||||||
'type' => 'h5'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 尝试创建Pay实例
|
// 调用真实的H5支付方法
|
||||||
$payModel = new Pay($this->siteId);
|
$h5_result = $huawei_pay_client->h5Pay($h5_params, $config_data['returnUrl'], $config_data['notifyUrl']);
|
||||||
|
echo " - H5支付方法调用成功\n";
|
||||||
|
echo " 结果: " . json_encode($h5_result, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
|
||||||
// 由于需要实际的支付客户端,这里只测试模型初始化
|
// 测试APP支付
|
||||||
echo " ⚠️ 跳过实际支付测试,仅测试模型初始化\n";
|
$app_params = [
|
||||||
} catch (\Exception $e) {
|
'out_trade_no' => 'TEST_APP_' . time(),
|
||||||
echo " ⚠️ 模型初始化测试跳过: " . $e->getMessage() . "\n";
|
'subject' => '测试APP支付商品',
|
||||||
}
|
'body' => '测试APP支付商品描述',
|
||||||
}
|
'total_amount' => 0.01
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试通知方法
|
|
||||||
*/
|
|
||||||
public function testNotifyMethod()
|
|
||||||
{
|
|
||||||
echo "测试通知方法...";
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 模拟配置
|
|
||||||
$this->mockConfig();
|
|
||||||
|
|
||||||
// 创建模拟通知数据
|
|
||||||
$notifyData = [
|
|
||||||
'app_id' => $this->configData['app_id'],
|
|
||||||
'merc_no' => $this->configData['mch_id'],
|
|
||||||
'order_id' => 'test_order_001',
|
|
||||||
'trade_status' => 'SUCCESS',
|
|
||||||
'sign' => 'mock_signature'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 尝试创建Pay实例
|
// 调用真实的APP支付方法
|
||||||
$payModel = new Pay($this->siteId);
|
$app_result = $huawei_pay_client->appPay($app_params, $config_data['notifyUrl']);
|
||||||
|
echo " - APP支付方法调用成功\n";
|
||||||
|
echo " 结果: " . json_encode($app_result, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
|
||||||
echo " ⚠️ 跳过实际通知处理测试\n";
|
} catch (Exception $e) {
|
||||||
} catch (\Exception $e) {
|
echo " - 支付测试失败: " . $e->getMessage() . "\n";
|
||||||
echo " ⚠️ 通知测试跳过: " . $e->getMessage() . "\n";
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试关闭订单方法
|
* 测试关闭订单方法
|
||||||
*/
|
*/
|
||||||
public function testCloseOrderMethod()
|
protected function testCloseOrderMethod()
|
||||||
{
|
{
|
||||||
echo "测试关闭订单方法...";
|
require_once dirname(__DIR__) . '/data/sdk/utils.php';
|
||||||
|
require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php';
|
||||||
|
|
||||||
|
// 添加必要的配置
|
||||||
|
$site_id = '1';
|
||||||
|
$config_data = $this->config;
|
||||||
|
$config_data['site_id'] = $site_id;
|
||||||
|
// 使用文本模式的密钥,而不是证书文件
|
||||||
|
unset($config_data['private_key']);
|
||||||
|
unset($config_data['huawei_public_key']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 模拟配置
|
// 创建实际的华为支付客户端实例
|
||||||
$this->mockConfig();
|
$project_root = dirname(__DIR__, 4); // 获取项目根目录
|
||||||
|
$huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root);
|
||||||
|
echo " - 华为支付客户端实例创建成功\n";
|
||||||
|
|
||||||
// 尝试创建Pay实例
|
// 测试关闭订单
|
||||||
$payModel = new Pay($this->siteId);
|
$params = [
|
||||||
|
'out_trade_no' => 'TEST_CLOSE_' . time()
|
||||||
|
];
|
||||||
|
|
||||||
echo " ⚠️ 跳过实际关闭订单测试\n";
|
// 调用真实的关闭订单方法
|
||||||
} catch (\Exception $e) {
|
$close_result = $huawei_pay_client->closeOrder($params);
|
||||||
echo " ⚠️ 关闭订单测试跳过: " . $e->getMessage() . "\n";
|
echo " - 关闭订单方法调用成功\n";
|
||||||
|
echo " 结果: " . json_encode($close_result, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " - 关闭订单测试失败: " . $e->getMessage() . "\n";
|
||||||
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试退款方法
|
* 测试退款方法
|
||||||
*/
|
*/
|
||||||
public function testRefundMethod()
|
protected function testRefundMethod()
|
||||||
{
|
{
|
||||||
echo "测试退款方法...";
|
require_once dirname(__DIR__) . '/data/sdk/utils.php';
|
||||||
|
require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php';
|
||||||
|
|
||||||
|
// 添加必要的配置
|
||||||
|
$site_id = '1';
|
||||||
|
$config_data = $this->config;
|
||||||
|
$config_data['site_id'] = $site_id;
|
||||||
|
// 使用文本模式的密钥,而不是证书文件
|
||||||
|
unset($config_data['private_key']);
|
||||||
|
unset($config_data['huawei_public_key']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 模拟配置
|
// 创建实际的华为支付客户端实例
|
||||||
$this->mockConfig();
|
$project_root = dirname(__DIR__, 4); // 获取项目根目录
|
||||||
|
$huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root);
|
||||||
|
echo " - 华为支付客户端实例创建成功\n";
|
||||||
|
|
||||||
// 尝试创建Pay实例
|
// 测试退款
|
||||||
$payModel = new Pay($this->siteId);
|
$params = [
|
||||||
|
'out_trade_no' => 'TEST_REFUND_' . time(),
|
||||||
|
'out_request_no' => 'REFUND_' . time(),
|
||||||
|
'refund_amount' => 0.01,
|
||||||
|
'refund_reason' => '测试退款'
|
||||||
|
];
|
||||||
|
|
||||||
echo " ⚠️ 跳过实际退款测试\n";
|
// 调用真实的退款方法
|
||||||
} catch (\Exception $e) {
|
$refund_result = $huawei_pay_client->refund($params);
|
||||||
echo " ⚠️ 退款测试跳过: " . $e->getMessage() . "\n";
|
echo " - 退款方法调用成功\n";
|
||||||
|
echo " 结果: " . json_encode($refund_result, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " - 退款测试失败: " . $e->getMessage() . "\n";
|
||||||
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试查询订单方法
|
* 测试查询订单方法
|
||||||
*/
|
*/
|
||||||
public function testQueryOrderMethod()
|
protected function testQueryOrderMethod()
|
||||||
{
|
{
|
||||||
echo "测试查询订单方法...";
|
require_once dirname(__DIR__) . '/data/sdk/utils.php';
|
||||||
|
require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php';
|
||||||
|
|
||||||
|
// 添加必要的配置
|
||||||
|
$site_id = '1';
|
||||||
|
$config_data = $this->config;
|
||||||
|
$config_data['site_id'] = $site_id;
|
||||||
|
// 使用文本模式的密钥,而不是证书文件
|
||||||
|
unset($config_data['private_key']);
|
||||||
|
unset($config_data['huawei_public_key']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 模拟配置
|
// 创建实际的华为支付客户端实例
|
||||||
$this->mockConfig();
|
$project_root = dirname(__DIR__, 4); // 获取项目根目录
|
||||||
|
$huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root);
|
||||||
|
echo " - 华为支付客户端实例创建成功\n";
|
||||||
|
|
||||||
// 尝试创建Pay实例
|
// 测试查询订单
|
||||||
$payModel = new Pay($this->siteId);
|
$params = [
|
||||||
|
'out_trade_no' => 'TEST_QUERY_' . time()
|
||||||
|
];
|
||||||
|
|
||||||
echo " ⚠️ 跳过实际查询订单测试\n";
|
// 调用真实的查询订单方法
|
||||||
} catch (\Exception $e) {
|
$query_result = $huawei_pay_client->queryOrder($params);
|
||||||
echo " ⚠️ 查询订单测试跳过: " . $e->getMessage() . "\n";
|
echo " - 查询订单方法调用成功\n";
|
||||||
|
echo " 结果: " . json_encode($query_result, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " - 查询订单测试失败: " . $e->getMessage() . "\n";
|
||||||
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模拟配置
|
* 测试回调验证方法
|
||||||
*/
|
*/
|
||||||
private function mockConfig()
|
protected function testCallbackVerification()
|
||||||
{
|
{
|
||||||
// 这里可以添加更高级的模拟技术,例如使用PHPUnit的mock对象
|
require_once dirname(__DIR__) . '/data/sdk/utils.php';
|
||||||
// 目前我们只需要确保配置数据能正确加载
|
require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php';
|
||||||
echo "\n使用配置数据:\n";
|
|
||||||
echo "App ID: {$this->configData['app_id']}\n";
|
// 添加必要的配置
|
||||||
echo "商户号: {$this->configData['mch_id']}\n";
|
$site_id = '1';
|
||||||
echo "证书ID: {$this->configData['mch_auth_id']}\n";
|
$config_data = $this->config;
|
||||||
|
$config_data['site_id'] = $site_id;
|
||||||
|
// 使用文本模式的密钥,而不是证书文件
|
||||||
|
unset($config_data['private_key']);
|
||||||
|
unset($config_data['huawei_public_key']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建实际的华为支付客户端实例
|
||||||
|
$project_root = dirname(__DIR__, 4); // 获取项目根目录
|
||||||
|
$huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root);
|
||||||
|
echo " - 华为支付客户端实例创建成功\n";
|
||||||
|
|
||||||
|
// 生成模拟回调参数并签名
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$callback_params = [
|
||||||
|
'out_trade_no' => 'TEST_CALLBACK_' . time(),
|
||||||
|
'trade_no' => 'HWPAY_' . time(),
|
||||||
|
'total_amount' => '0.01',
|
||||||
|
'trade_status' => 'SUCCESS',
|
||||||
|
'timestamp' => $timestamp,
|
||||||
|
'app_id' => $config_data['app_id'],
|
||||||
|
'merc_no' => $config_data['merc_no'],
|
||||||
|
'sign_type' => 'RSA2'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
$sign = '';
|
||||||
|
$signParams = $callback_params;
|
||||||
|
unset($signParams['sign']);
|
||||||
|
unset($signParams['sign_type']);
|
||||||
|
ksort($signParams);
|
||||||
|
|
||||||
|
$stringToSign = '';
|
||||||
|
foreach ($signParams as $key => $value) {
|
||||||
|
$stringToSign .= $key . '=' . $value . '&';
|
||||||
|
}
|
||||||
|
$stringToSign = rtrim($stringToSign, '&');
|
||||||
|
|
||||||
|
// 使用文本模式的私钥生成签名
|
||||||
|
$private_key_content = $config_data['private_key_text'];
|
||||||
|
|
||||||
|
// 对私钥进行格式化,确保openssl_sign能够正确处理
|
||||||
|
$cleanContent = preg_replace('/\s+/', '', $private_key_content);
|
||||||
|
$cleanContent = preg_replace('/-----BEGIN.*?-----/', '', $cleanContent);
|
||||||
|
$cleanContent = preg_replace('/-----END.*?-----/', '', $cleanContent);
|
||||||
|
$formattedKey = "-----BEGIN PRIVATE KEY-----\n";
|
||||||
|
$length = strlen($cleanContent);
|
||||||
|
for ($i = 0; $i < $length; $i += 64) {
|
||||||
|
$formattedKey .= substr($cleanContent, $i, 64) . "\n";
|
||||||
|
}
|
||||||
|
$formattedKey .= "-----END PRIVATE KEY-----";
|
||||||
|
|
||||||
|
openssl_sign($stringToSign, $sign, $formattedKey, OPENSSL_ALGO_SHA256);
|
||||||
|
$callback_params['sign'] = base64_encode($sign);
|
||||||
|
|
||||||
|
// 使用华为支付客户端验证签名
|
||||||
|
$verify_result = $huawei_pay_client->verifySign($callback_params);
|
||||||
|
|
||||||
|
if ($verify_result) {
|
||||||
|
echo " - 回调签名验证通过\n";
|
||||||
|
echo " - 回调参数:" . json_encode($callback_params, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
echo " - 交易状态:{$callback_params['trade_status']}\n";
|
||||||
|
} else {
|
||||||
|
throw new Exception("回调签名验证失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " - 回调验证测试失败: " . $e->getMessage() . "\n";
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果直接运行此文件,则执行测试
|
// 运行测试
|
||||||
if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
|
$test = new PayModelTest();
|
||||||
$test = new PayModelTest();
|
$test->runTests();
|
||||||
$test->runTests();
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# 测试
|
|
||||||
|
|
||||||
由于不引入PHPUnit,所以测试用例只能手动执行。
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd src/
|
|
||||||
php addon/huaweipay/tests/HuaweiPayClientTest.php
|
|
||||||
php addon/huaweipay/tests/PayModelTest.php
|
|
||||||
```
|
|
||||||
117
src/addon/huaweipay/tests/TestCase.php
Normal file
117
src/addon/huaweipay/tests/TestCase.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 华为支付测试基类
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace addon\huaweipay\tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||||||
|
// 引入工具类
|
||||||
|
use addon\huaweipay\data\sdk\Utils;
|
||||||
|
|
||||||
|
class TestCase extends BaseTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 测试配置数据
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $configData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试站点ID
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $siteId = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setUp()方法在每个测试方法执行前调用
|
||||||
|
*/
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// 加载配置文件
|
||||||
|
$this->loadConfigData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载配置数据
|
||||||
|
*/
|
||||||
|
protected function loadConfigData()
|
||||||
|
{
|
||||||
|
$yamlPath = __DIR__ . '/mock/data.yml';
|
||||||
|
if (file_exists($yamlPath)) {
|
||||||
|
if (function_exists('yaml_parse_file')) {
|
||||||
|
$this->configData = yaml_parse_file($yamlPath);
|
||||||
|
} else {
|
||||||
|
// 如果没有yaml_parse_file函数,手动解析
|
||||||
|
$this->configData = $this->parseYamlFile($yamlPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->fail("配置文件不存在: {$yamlPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formatCertificateContent($content, $type = 'public_key')
|
||||||
|
{
|
||||||
|
return Utils::formatCertificateContent($content, $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动解析YAML文件
|
||||||
|
* @param string $filePath
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function parseYamlFile($filePath)
|
||||||
|
{
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
$lines = explode("\n", $content);
|
||||||
|
$result = [];
|
||||||
|
$currentKey = '';
|
||||||
|
$isMultiline = false;
|
||||||
|
$multilineValue = '';
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
|
||||||
|
// 跳过注释和空行
|
||||||
|
if (empty($line) || strpos($line, '#') === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理多行值
|
||||||
|
if ($isMultiline) {
|
||||||
|
if (strpos($line, '-----END') === 0) {
|
||||||
|
$multilineValue .= $line . "\n";
|
||||||
|
$result[$currentKey] = rtrim($multilineValue);
|
||||||
|
$isMultiline = false;
|
||||||
|
$multilineValue = '';
|
||||||
|
} else {
|
||||||
|
$multilineValue .= $line . "\n";
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理单行键值对
|
||||||
|
if (strpos($line, ':') !== false) {
|
||||||
|
list($key, $value) = explode(':', $line, 2);
|
||||||
|
$key = trim($key);
|
||||||
|
$value = trim($value);
|
||||||
|
|
||||||
|
// 处理多行值开始
|
||||||
|
if ($value === '|') {
|
||||||
|
$isMultiline = true;
|
||||||
|
$currentKey = $key;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理普通值
|
||||||
|
if (!empty($value)) {
|
||||||
|
$result[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
-----BEGIN PUBLIC KEY-----\nMOCK_PUBLIC_KEY_CONTENT\n-----END PUBLIC KEY-----
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA9g1+QcqvC4f1pUiwJ1um1iBUlNn6hRDJrNdv5zB77l5DNo6S6hE4w7VyhkMnkIk89i8kTej1m1ByjRpo7B5OPqafNqI9JBQyQ26A1Zp71zSfe/UicAFiMtF4lWNnAHBYH06sUTvybwYllDVybpi6lL2i8VAGIN8YgoK36lPaYsxWZ911lPCegy7B3kDj1xhBe41cNHgu8wYmjqLU7njleY5Pseherx+Kb58aQvB5xQr8w7KgAyMrsfRH30Btpg/ZWRn8qOXd/DW6eEla3djah4ug8jKdi0qUkA24FLDdOZST4vb5qhgQDVXpqJhYmBIU14YOHsCX9Olu6b7DDjQo/dvOaY3vzWROfV+sV60fUVIps8Vy1EpS/UXeHUxg6r37U8WAxUbSV8d6e4VylLuiIgbX5JpSC1s7jq/cwUwXfSJmKzaCj+C+LJ958IM17FYxIz5xWJtZEzWsPAH7WVCP3b1m4MHU/UwGuMu/Gfdzusnr+Qtan6Wqn9AqUyJP/JfrAgMBAAE=
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2m2vKv9jH0QZc7K
|
MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQD2DX5Byq8Lh/WlSLAnW6bWIFSU2fqFEMms12/nMHvuXkM2jpLqETjDtXKGQyeQiTz2LyRN6PWbUHKNGmjsHk4+pp82oj0kFDJDboDVmnvXNJ979SJwAWIy0XiVY2cAcFgfTqxRO/JvBiWUNXJumLqUvaLxUAYg3xiCgrfqU9pizFZn3XWU8J6DLsHeQOPXGEF7jVw0eC7zBiaOotTueOV5jk+x6F6vH4pvnxpC8HnFCvzDsqADIyux9EffQG2mD9lZGfyo5d38Nbp4SVrd2NqHi6DyMp2LSpSQDbgUsN05lJPi9vmqGBANVemomFiYEhTXhg4ewJf06W7pvsMONCj9285pje/NZE59X6xXrR9RUimzxXLUSlL9Rd4dTGDqvftTxYDFRtJXx3p7hXKUu6IiBtfkmlILWzuOr9zBTBd9ImYrNoKP4L4sn3nwgzXsVjEjPnFYm1kTNaw8AftZUI/dvWbgwdT9TAa4y78Z93O6yev5C1qfpaqf0CpTIk/8l+sCAwEAAQKCAYAVlJFiS9iWdlJBMOLiUNONLEC+3W9vhE1r72lNKZ91BKd4fYC9Ls1/vMZSqEksEB1cqj3Q54HDIYcqgQp6yx2puQt1yzz5kRvndiWulmIOOftS7+kZUcW/F0gwMguyqifQdyH97fgRbMSW/ykOMi8LJKbJ627eKzMHH1fqIXih+bIKYg4SBhihANTYHXDeSK5Vm8xefbwAbKWtFPMAB3J4+tZakDrduTJ3H8k53cWQVqpcr6oBHHCUpww2tHvpeLI3a/FXyHYBqrx8ErnXCjkVBHBtwQf+43H+buyDjrYUwJUi3RSJgeefcyyJoO0I9GwGb6nY8kK5kZ0aeIIkfiVOexMYS9w11FVl96LjC7HjJQrNY4jOLI/X76xyEwBFy9LNogRTafjZVZmVRj/9Kembx6+/eDxvyEv5FnelsHqfFbv1KPkR6e7FvwpDbYgJBpfKTE6SICqo52bHxewpSL7KHRlTpp0IfMM/IOOs8CCwI37ixKQun6W4en457j/tP4ECgcEA+0pa3omCKJfiVR4PpyXY+t9FEqdcZkdYEDtCSX9vqu6yE4sdt7rNnZ6cIiD6ViC2xvQ5VLry1y57JvP57svt5OJYhmILuj3jJ7pdcfKRAVaV4W7C8vGMit6ssckEOnOSj/bvEazfHorjXKU9cn9uCymczRQC9azaRkN+1LLk2mBbczghxd7rSJyXtbt9NVNUi9Gr11AzJsrWN/Bqqna5BizBLIaGYthJ/04LYTl1AjWPEJXjXI72/WvMdQ/w6itxAoHBAPqqAi5mbvjRDitmLQchc/R3Yl329OaB3GSz3YtFqfk9lLJIyl91+MzxBg1ChHnRFC8MB1s1Z05ZkFmcy8AVqvJ6MSZhUCLNjq8HyVgp24CqQTr8ciomF03ncXnKKSsH7tidInkQ8R3e7qRRGQMnDvV+Hs3FjlOk8vJyZCcGkUCVA0kkwkk/95qIfrZsthtewAxJR/rY63FnG+MUayEBB1lwyJZ7xDi/GyX6SAnPBl6bLRA1BdBeTV8K0MEwwIqzGwKBwBqo+9UKT7XQz2FqbAy2tjt/fouJGAN95DjsoI69p3JCGsB6DPAWMIRddIEmcIi8tceL151GrEbqFoS+c7DDD/0timjPdCEROc1YN1vEeV/j+MjPAH3X5KpDD51ZD0rIQi9l6l08svtBjvegTFGedWVXx9v2GI5KBWpY9NbKF/+XI3yo4uRkTyAIBQxx1MnYimq/FvUj/BlMgceziQ2GxQCDtQbtSsqn2cntVMW+28wdNI106YdDX67pRerRgyTE8QKBwHAwLxG9XuWWC5V5AaYzXsaHuEr+ANY6QP4BUqLG5zBaU3cIBSt8jYKMTX0ZzFkJLtNvussjt7zlcSnqd3bdO8mSzvSykT9CaR4FiiQfd9K6YL+ZxS8AJWYEtFEiHhLYVho1Gfy9jG0mHgEFGwDCNnvBmt/WD8F4DhRdBl5BHjmdd/8AqMRIEPXlKXFUbp0JZ0MYeVLYS2hSEbUsqlX3M+bgB6bydfw/7FKvFhbtxZgKM70RPizoSBDFsnEE9OgfCQKBwQD4M4f2f678YScvrwQnV2J8SSnJhQbZqht1Rlb34zU0JIcfpwOKpoNvRyZz1BxvzPSm5S3TLDytK16uvAsMFCHVByyCajPozlWnLzeEqvE4DZXZcWeY+/+MZ4nsajLTTeJWos6UrazZcd8/2c301x4OUKfgZWW1tS65MA48X0+Y4uPyOuH365wQ0obBfg4aB85sZl71v7Rveq+COHBstp/BdDVAsdufWCr7Nkbeu/3qh886ZumpYe+89So1oDIuoys=
|
||||||
+1q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
|
|
||||||
3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
|
|
||||||
3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
|
|
||||||
3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
|
|
||||||
3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
|
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
@@ -1,26 +1,34 @@
|
|||||||
# 华为支付测试用简单配置
|
# ========华为支付测试用简单配置==========
|
||||||
|
|
||||||
|
# 沙盒模式
|
||||||
|
sandbox: true
|
||||||
|
|
||||||
# 商户应用ID,(例如:快应用)
|
# 商户应用ID,(例如:快应用)
|
||||||
app_id: 115644647
|
app_id: 115644647
|
||||||
|
|
||||||
# 商户号
|
# 商户号
|
||||||
mch_id: 102751500028
|
merc_no: 102751500028
|
||||||
|
|
||||||
# 商户证书ID
|
# 商户证书ID
|
||||||
mch_auth_id: 10086000901972225
|
mch_auth_id: 10086000901972225
|
||||||
|
|
||||||
|
# 商户应用私有密钥(支付私钥) - 文件路径
|
||||||
|
private_key: mock/cert/merchant_private_key.pem
|
||||||
|
|
||||||
# 商户应用私有密钥(支付私钥)
|
# 商户应用私有密钥(支付私钥) - 文本内容
|
||||||
privateKey: |
|
private_key_text: |
|
||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
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
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
|
|
||||||
# 华为支付公钥
|
# 华为支付公钥 - 文件路径
|
||||||
huawei_public_key: |
|
huawei_public_key: mock/cert/huawei_public_key.pem
|
||||||
|
|
||||||
|
# 华为支付公钥 - 文本内容
|
||||||
|
huawei_public_key_text: |
|
||||||
-----BEGIN PUBLIC KEY-----
|
-----BEGIN PUBLIC KEY-----
|
||||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA9g1+QcqvC4f1pUiwJ1um1iBUlNn6hRDJrNdv5zB77l5DNo6S6hE4w7VyhkMnkIk89i8kTej1m1ByjRpo7B5OPqafNqI9JBQyQ26A1Zp71zSfe/UicAFiMtF4lWNnAHBYH06sUTvybwYllDVybpi6lL2i8VAGIN8YgoK36lPaYsxWZ911lPCegy7B3kDj1xhBe41cNHgu8wYmjqLU7njleY5Pseherx+Kb58aQvB5xQr8w7KgAyMrsfRH30Btpg/ZWRn8qOXd/DW6eEla3djah4ug8jKdi0qUkA24FLDdOZST4vb5qhgQDVXpqJhYmBIU14YOHsCX9Olu6b7DDjQo/dvOaY3vzWROfV+sV60fUVIps8Vy1EpS/UXeHUxg6r37U8WAxUbSV8d6e4VylLuiIgbX5JpSC1s7jq/cwUwXfSJmKzaCj+C+LJ958IM17FYxIz5xWJtZEzWsPAH7WVCP3b1m4MHU/UwGuMu/Gfdzusnr+Qtan6Wqn9AqUyJP/JfrAgMBAAE=
|
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA9g1+QcqvC4f1pUiwJ1um1iBUlNn6hRDJrNdv5zB77l5DNo6S6hE4w7VyhkMnkIk89i8kTej1m1ByjRpo7B5OPqafNqI9JBQyQ26A1Zp71zSfe/UicAFiMtF4lWNnAHBYH06sUTvybwYllDVybpi6lL2i8VAGIN8YgoK36lPaYsxWZ911lPCegy7B3kDj1xhBe41cNHgu8wYmjqLU7njleY5Pseherx+Kb58aQvB5xQr8w7KgAyMrsfRH30Btpg/ZWRn8qOXd/DW6eEla3djah4ug8jKdi0qUkA24FLDdOZST4vb5qhgQDVXpqJhYmBIU14YOHsCX9Olu6b7DDjQo/dvOaY3vzWROfV+sV60fUVIps8Vy1EpS/UXeHUxg6r37U8WAxUbSV8d6e4VylLuiIgbX5JpSC1s7jq/cwUwXfSJmKzaCj+C+LJ958IM17FYxIz5xWJtZEzWsPAH7WVCP3b1m4MHU/UwGuMu/Gfdzusnr+Qtan6Wqn9AqUyJP/JfrAgMBAAE=
|
||||||
-----END PUBLIC KEY-----
|
-----END PUBLIC KEY-----
|
||||||
|
|
||||||
notifyUrl: https://test.example.com/huawei-pay/notify
|
notifyUrl: https://dev.aigc-quickapp.com/pay/pay/notify.html
|
||||||
returnUrl: https://test.example.com/huawei-pay/return
|
returnUrl: https://dev.aigc-quickapp.com/pay/pay/payreturn.html
|
||||||
|
|||||||
26
src/addon/huaweipay/tests/phpunit.xml.dist
Normal file
26
src/addon/huaweipay/tests/phpunit.xml.dist
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
|
||||||
|
bootstrap="../vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
verbose="true"
|
||||||
|
stopOnFailure="false">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Huawei Pay Tests">
|
||||||
|
<directory>./</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<filter>
|
||||||
|
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||||
|
<directory suffix=".php">../</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory suffix=".php">../vendor/</directory>
|
||||||
|
<directory suffix=".php">../tests/</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_DEBUG" value="true"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
Reference in New Issue
Block a user