chore(addon/huaweipay): 更新huaweipay

This commit is contained in:
2025-12-04 10:32:00 +08:00
parent 09ed1bd427
commit a793ed541b
14 changed files with 1093 additions and 498 deletions

View 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. ...
```

View File

@@ -9,6 +9,9 @@ namespace addon\huaweipay\data\sdk;
use app\exception\ApiException;
use think\facade\Log;
// 引入工具类
use addon\huaweipay\data\sdk\Utils;
class HuaweiPayClient
{
// 华为支付网关地址
@@ -32,53 +35,14 @@ class HuaweiPayClient
// 是否加载了华为平台支付服务加密证书
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 string $cert_root_path 证书根路径
*/
public function __construct($config)
public function __construct($config, $cert_root_path = '')
{
$this->config = $config;
@@ -89,16 +53,36 @@ class HuaweiPayClient
if (empty($this->config['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 = $cert_root_path;
if (empty($cert_base_path)) {
// 没有指定证书根路径时,使用默认路径
try {
$cert_base_path = app()->getRootPath();
} catch (\Exception $e) {
// 捕获异常,使用默认路径
}
}
try {
// 加载商户应用私有证书
$private_key_content = '';
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'])) {
// 文件模式
$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);
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 = '';
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'])) {
// 文件模式
$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'])) {
// 文本模式,需要格式化
$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);
if (!$this->huawei_public_key_certificate_instance_encrypt) {
throw new \Exception('加载华为平台支付服务加密证书失败');
@@ -166,7 +155,7 @@ class HuaweiPayClient
// 根据配置设置网关地址
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';
}
}

View 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;
}
}

View File

@@ -39,15 +39,22 @@ class Pay extends BaseModel
/**
* 构造函数
* @param $site_id 站点ID
* @param $config_data 配置数据, 为空时从数据库获取
*/
function __construct($site_id)
function __construct($site_id, $config_data = [])
{
$this->site_id = $site_id;
// 获取华为支付参数
if (empty($config_data)) {
$config_info = (new Config())->getPayConfig($this->site_id)['data']['value'];
$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;
// 初始化华为支付客户端

View File

@@ -7,7 +7,7 @@
<div class="layui-form form-wrap">
<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">
<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}>

View File

@@ -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();
}

View 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, '文本内容模式应该优先于文件路径模式');
}
}

View File

@@ -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;
use think\facade\Log;
// 添加自动加载路径
$root_path = dirname(__DIR__, 3);
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
{
/**
* 测试站点ID
* @var int
*/
protected $siteId = 1;
protected $config = [];
protected $pay_model = null;
/**
* 测试配置数据
* @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}");
}
// 加载测试配置
$this->config = $this->parseYamlFile(__DIR__ . '/mock/data.yml');
echo "配置加载完成\n";
// print_r($this->config);
}
/**
* 手动解析YAML文件
* @param string $filePath
* 解析YAML文件
* @param string $file_path YAML文件路径
* @return array
*/
private function parseYamlFile($filePath)
protected function parseYamlFile($file_path)
{
$content = file_get_contents($filePath);
$lines = explode("\n", $content);
$result = [];
$currentKey = '';
$isMultiline = false;
$multilineValue = '';
if (!file_exists($file_path)) {
throw new Exception("YAML文件不存在: {$file_path}");
}
$yaml_content = file_get_contents($file_path);
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) {
$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";
}
if ($in_block) {
if (strpos($line, '---') === 0) {
continue;
}
if (preg_match('/^\w+:/', $line)) {
$config[$current_key] = $block_content;
$in_block = false;
$block_content = '';
} else {
$block_content .= $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;
if (strpos($value, '|') !== false) {
$in_block = true;
$current_key = $key;
continue;
}
// 处理普通值
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()
{
echo "开始测试华为支付模型...\n";
echo "开始运行华为支付模型测试...\n";
try {
// 1. 测试支付方法
echo "1. 测试支付方法...\n";
$this->testPayMethod();
$this->testNotifyMethod();
$this->testCloseOrderMethod();
$this->testRefundMethod();
$this->testQueryOrderMethod();
echo "1. 支付方法测试通过\n";
echo "\n✅ 所有测试通过!\n";
return true;
} catch (\Exception $e) {
echo "\n❌ 测试失败:" . $e->getMessage() . "\n";
echo "错误位置:" . $e->getFile() . "" . $e->getLine() . "\n";
// 2. 测试关闭订单方法
echo "2. 测试关闭订单方法...\n";
$this->testCloseOrderMethod();
echo "2. 关闭订单方法测试通过\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 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 {
// 模拟配置
$this->mockConfig();
// 创建实际的华为支付客户端实例
$project_root = dirname(__DIR__, 4); // 获取项目根目录
$huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root);
echo " - 华为支付客户端实例创建成功\n";
// 创建测试订单数据
$orderInfo = [
'out_trade_no' => 'test_order_' . time(),
'total_amount' => 100,
'subject' => '测试商品',
'notify_url' => $this->configData['notifyUrl'],
'return_url' => $this->configData['returnUrl'],
'buyer_id' => '1',
'type' => 'h5'
// 测试H5支付
$h5_params = [
'out_trade_no' => 'TEST_H5_' . time(),
'subject' => '测试H5支付商品',
'body' => '测试H5支付商品描述',
'total_amount' => 0.01,
'client_ip' => '127.0.0.1'
];
// 尝试创建Pay实例
$payModel = new Pay($this->siteId);
// 调用真实的H5支付方法
$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";
// 由于需要实际的支付客户端,这里只测试模型初始化
echo " ⚠️ 跳过实际支付测试,仅测试模型初始化\n";
} catch (\Exception $e) {
echo " ⚠️ 模型初始化测试跳过: " . $e->getMessage() . "\n";
}
}
/**
* 测试通知方法
*/
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'
// 测试APP支付
$app_params = [
'out_trade_no' => 'TEST_APP_' . time(),
'subject' => '测试APP支付商品',
'body' => '测试APP支付商品描述',
'total_amount' => 0.01
];
// 尝试创建Pay实例
$payModel = new Pay($this->siteId);
// 调用真实的APP支付方法
$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) {
echo " ⚠️ 通知测试跳过: " . $e->getMessage() . "\n";
} catch (Exception $e) {
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 {
// 模拟配置
$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) {
echo " ⚠️ 关闭订单测试跳过: " . $e->getMessage() . "\n";
// 调用真实的关闭订单方法
$close_result = $huawei_pay_client->closeOrder($params);
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 {
// 模拟配置
$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) {
echo " ⚠️ 退款测试跳过: " . $e->getMessage() . "\n";
// 调用真实的退款方法
$refund_result = $huawei_pay_client->refund($params);
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 {
// 模拟配置
$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) {
echo " ⚠️ 查询订单测试跳过: " . $e->getMessage() . "\n";
// 调用真实的查询订单方法
$query_result = $huawei_pay_client->queryOrder($params);
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对象
// 目前我们只需要确保配置数据能正确加载
echo "\n使用配置数据:\n";
echo "App ID: {$this->configData['app_id']}\n";
echo "商户号: {$this->configData['mch_id']}\n";
echo "证书ID: {$this->configData['mch_auth_id']}\n";
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 {
// 创建实际的华为支付客户端实例
$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->runTests();
}
// 行测试
$test = new PayModelTest();
$test->runTests();

View File

@@ -1,10 +0,0 @@
# 测试
由于不引入PHPUnit所以测试用例只能手动执行。
```bash
cd src/
php addon/huaweipay/tests/HuaweiPayClientTest.php
php addon/huaweipay/tests/PayModelTest.php
```

View 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;
}
}

View File

@@ -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-----

View File

@@ -1,8 +1,3 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2m2vKv9jH0QZc7K
+1q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q
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=
-----END PRIVATE KEY-----

View File

@@ -1,26 +1,34 @@
# 华为支付测试用简单配置
# ========华为支付测试用简单配置==========
# 沙盒模式
sandbox: true
# 商户应用ID例如快应用
app_id: 115644647
# 商户号
mch_id: 102751500028
merc_no: 102751500028
# 商户证书ID
mch_auth_id: 10086000901972225
# 商户应用私有密钥(支付私钥) - 文件路径
private_key: mock/cert/merchant_private_key.pem
# 商户应用私有密钥(支付私钥)
privateKey: |
# 商户应用私有密钥(支付私钥) - 文本内容
private_key_text: |
-----BEGIN PRIVATE KEY-----
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
-----END PRIVATE KEY-----
# 华为支付公钥
huawei_public_key: |
# 华为支付公钥 - 文件路径
huawei_public_key: mock/cert/huawei_public_key.pem
# 华为支付公钥 - 文本内容
huawei_public_key_text: |
-----BEGIN PUBLIC KEY-----
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA9g1+QcqvC4f1pUiwJ1um1iBUlNn6hRDJrNdv5zB77l5DNo6S6hE4w7VyhkMnkIk89i8kTej1m1ByjRpo7B5OPqafNqI9JBQyQ26A1Zp71zSfe/UicAFiMtF4lWNnAHBYH06sUTvybwYllDVybpi6lL2i8VAGIN8YgoK36lPaYsxWZ911lPCegy7B3kDj1xhBe41cNHgu8wYmjqLU7njleY5Pseherx+Kb58aQvB5xQr8w7KgAyMrsfRH30Btpg/ZWRn8qOXd/DW6eEla3djah4ug8jKdi0qUkA24FLDdOZST4vb5qhgQDVXpqJhYmBIU14YOHsCX9Olu6b7DDjQo/dvOaY3vzWROfV+sV60fUVIps8Vy1EpS/UXeHUxg6r37U8WAxUbSV8d6e4VylLuiIgbX5JpSC1s7jq/cwUwXfSJmKzaCj+C+LJ958IM17FYxIz5xWJtZEzWsPAH7WVCP3b1m4MHU/UwGuMu/Gfdzusnr+Qtan6Wqn9AqUyJP/JfrAgMBAAE=
-----END PUBLIC KEY-----
notifyUrl: https://test.example.com/huawei-pay/notify
returnUrl: https://test.example.com/huawei-pay/return
notifyUrl: https://dev.aigc-quickapp.com/pay/pay/notify.html
returnUrl: https://dev.aigc-quickapp.com/pay/pay/payreturn.html

View 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>