mirror of
https://gitee.com/liuxioabin/fengketrade.git
synced 2026-04-17 12:57:32 +08:00
up
This commit is contained in:
parent
adda69de16
commit
16e3078104
95
DEPLOY_README.md
Normal file
95
DEPLOY_README.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# 快速部署说明
|
||||||
|
|
||||||
|
## 方式1: 一键自动部署 (推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy_fix.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
这个脚本会自动完成:
|
||||||
|
1. ✅ 检查本地代码
|
||||||
|
2. ✅ 上传诊断脚本
|
||||||
|
3. ✅ 运行服务器诊断
|
||||||
|
4. ✅ 上传修复文件
|
||||||
|
5. ✅ 重启PHP-FPM
|
||||||
|
6. ✅ 清除OPcache
|
||||||
|
7. ✅ 验证部署
|
||||||
|
|
||||||
|
**注意:** 需要输入服务器密码 `atBSGSu0$!T`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方式2: 手动步骤
|
||||||
|
|
||||||
|
### 1. 上传诊断脚本
|
||||||
|
```bash
|
||||||
|
scp server_diagnose.sh root@47.96.77.165:/www/wwwroot/fengketrade/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. SSH登录服务器
|
||||||
|
```bash
|
||||||
|
ssh root@47.96.77.165
|
||||||
|
# 密码: atBSGSu0$!T
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 运行诊断
|
||||||
|
```bash
|
||||||
|
cd /www/wwwroot/fengketrade
|
||||||
|
bash server_diagnose.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 上传修复文件(在本地新终端执行)
|
||||||
|
```bash
|
||||||
|
scp addons/shopro/library/ccblife/CcbPaymentService.php \
|
||||||
|
root@47.96.77.165:/www/wwwroot/fengketrade/addons/shopro/library/ccblife/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 重启PHP(在服务器上)
|
||||||
|
```bash
|
||||||
|
systemctl restart php-fpm
|
||||||
|
php -r "opcache_reset();"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验证部署
|
||||||
|
|
||||||
|
### 方法1: 监控日志
|
||||||
|
```bash
|
||||||
|
ssh root@47.96.77.165 \
|
||||||
|
'tail -f /www/wwwroot/fengketrade/runtime/log/$(date +%Y%m)/$(date +%d).log' \
|
||||||
|
| grep '最终支付串'
|
||||||
|
```
|
||||||
|
|
||||||
|
然后触发一次支付,观察输出:
|
||||||
|
- ✅ **正确**: `PROINFO=%u6F2B` (% 后直接是 u)
|
||||||
|
- ❌ **错误**: `PROINFO=%25u6F2B` (% 被编码成 %25)
|
||||||
|
|
||||||
|
### 方法2: 检查关键代码行
|
||||||
|
```bash
|
||||||
|
ssh root@47.96.77.165 \
|
||||||
|
'sed -n "279p" /www/wwwroot/fengketrade/addons/shopro/library/ccblife/CcbPaymentService.php'
|
||||||
|
```
|
||||||
|
|
||||||
|
应该输出: `$finalParts[] = $key . '=' . $value;`
|
||||||
|
不应包含: `urlencode`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 详细文档
|
||||||
|
|
||||||
|
查看 `SERVER_FIX_GUIDE.md` 获取完整的操作指南和故障排查。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已修复的问题
|
||||||
|
|
||||||
|
1. ✅ **URL编码问题**: 移除了最终支付串的URL编码
|
||||||
|
2. ✅ **ENCPUB格式**: 转换为URL-safe BASE64 (+ → -, / → _)
|
||||||
|
3. ✅ **十六进制商户公钥**: 支持.env中的十六进制格式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 预期结果
|
||||||
|
|
||||||
|
修复后,建行支付应该能成功拉起收银台,不再报"支付签名生成不成功"错误。
|
||||||
195
SERVER_FIX_GUIDE.md
Normal file
195
SERVER_FIX_GUIDE.md
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
# 服务器修复指南
|
||||||
|
|
||||||
|
## 服务器信息
|
||||||
|
- **地址**: 47.96.77.165
|
||||||
|
- **用户**: root
|
||||||
|
- **密码**: atBSGSu0$!T
|
||||||
|
- **项目路径**: /www/wwwroot/fengketrade
|
||||||
|
|
||||||
|
## 步骤1: 上传诊断脚本到服务器
|
||||||
|
|
||||||
|
在本地执行:
|
||||||
|
```bash
|
||||||
|
scp server_diagnose.sh root@47.96.77.165:/www/wwwroot/fengketrade/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 步骤2: 登录服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@47.96.77.165
|
||||||
|
# 密码: atBSGSu0$!T
|
||||||
|
```
|
||||||
|
|
||||||
|
## 步骤3: 运行诊断脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /www/wwwroot/fengketrade
|
||||||
|
bash server_diagnose.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 步骤4: 根据诊断结果修复
|
||||||
|
|
||||||
|
### 如果使用Git管理代码:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /www/wwwroot/fengketrade
|
||||||
|
|
||||||
|
# 查看当前状态
|
||||||
|
git status
|
||||||
|
|
||||||
|
# 拉取最新代码
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# 或者如果有冲突,强制更新
|
||||||
|
git fetch --all
|
||||||
|
git reset --hard origin/main # 或 origin/master
|
||||||
|
```
|
||||||
|
|
||||||
|
### 如果需要手动替换文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在本地执行(上传修复后的文件)
|
||||||
|
scp addons/shopro/library/ccblife/CcbPaymentService.php \
|
||||||
|
root@47.96.77.165:/www/wwwroot/fengketrade/addons/shopro/library/ccblife/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 步骤5: 重启PHP服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 方法1: systemctl
|
||||||
|
systemctl restart php-fpm
|
||||||
|
|
||||||
|
# 方法2: service
|
||||||
|
service php-fpm restart
|
||||||
|
|
||||||
|
# 方法3: 如果是宝塔面板
|
||||||
|
bt restart php-fpm
|
||||||
|
|
||||||
|
# 查看PHP进程
|
||||||
|
ps aux | grep php-fpm
|
||||||
|
```
|
||||||
|
|
||||||
|
## 步骤6: 清除OPcache缓存
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 直接清除
|
||||||
|
php -r "opcache_reset(); echo 'OPcache cleared\n';"
|
||||||
|
|
||||||
|
# 或者创建清除脚本
|
||||||
|
cat > /www/wwwroot/fengketrade/public/clear_cache.php << 'EOF'
|
||||||
|
<?php
|
||||||
|
if (function_exists('opcache_reset')) {
|
||||||
|
opcache_reset();
|
||||||
|
echo 'OPcache cleared successfully!';
|
||||||
|
} else {
|
||||||
|
echo 'OPcache is not enabled';
|
||||||
|
}
|
||||||
|
phpinfo(INFO_GENERAL);
|
||||||
|
?>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 通过浏览器访问: http://fengketrade.com/clear_cache.php
|
||||||
|
# 清除后记得删除这个文件:
|
||||||
|
# rm /www/wwwroot/fengketrade/public/clear_cache.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 步骤7: 实时监控日志验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /www/wwwroot/fengketrade
|
||||||
|
|
||||||
|
# 监控支付日志
|
||||||
|
tail -f runtime/log/$(date +%Y%m)/$(date +%d).log | grep -E "建行支付.*最终支付串|MAC签名字符串"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 步骤8: 触发测试支付
|
||||||
|
|
||||||
|
保持步骤7的日志监控,然后在手机或浏览器发起一次支付。
|
||||||
|
|
||||||
|
观察日志输出:
|
||||||
|
|
||||||
|
**✅ 正确的日志:**
|
||||||
|
```
|
||||||
|
[建行支付] MAC签名字符串: ...PROINFO=%u6F2B...
|
||||||
|
[建行支付] 最终支付串: ...PROINFO=%u6F2B...
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ 错误的日志:**
|
||||||
|
```
|
||||||
|
[建行支付] MAC签名字符串: ...PROINFO=%u6F2B...
|
||||||
|
[建行支付] 最终支付串: ...PROINFO=%25u6F2B... ← %被编码成了%25
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键检查点
|
||||||
|
|
||||||
|
修复后的代码第279行应该是:
|
||||||
|
```php
|
||||||
|
$finalParts[] = $key . '=' . $value;
|
||||||
|
```
|
||||||
|
|
||||||
|
**不应该**是:
|
||||||
|
```php
|
||||||
|
$finalParts[] = $key . '=' . urlencode($value); // ❌ 错误
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速验证命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查第279行
|
||||||
|
sed -n '279p' /www/wwwroot/fengketrade/addons/shopro/library/ccblife/CcbPaymentService.php
|
||||||
|
|
||||||
|
# 应该输出: $finalParts[] = $key . '=' . $value;
|
||||||
|
# 如果包含 urlencode,说明代码还未更新
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q1: git pull失败
|
||||||
|
```bash
|
||||||
|
# 查看冲突
|
||||||
|
git status
|
||||||
|
|
||||||
|
# 放弃本地修改,强制更新
|
||||||
|
git reset --hard
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q2: PHP-FPM重启失败
|
||||||
|
```bash
|
||||||
|
# 查看错误日志
|
||||||
|
tail -50 /var/log/php-fpm/error.log
|
||||||
|
|
||||||
|
# 检查PHP-FPM状态
|
||||||
|
systemctl status php-fpm
|
||||||
|
|
||||||
|
# 检查配置语法
|
||||||
|
php-fpm -t
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q3: 修复后仍然报签名错误
|
||||||
|
```bash
|
||||||
|
# 1. 确认代码已更新
|
||||||
|
md5sum /www/wwwroot/fengketrade/addons/shopro/library/ccblife/CcbPaymentService.php
|
||||||
|
|
||||||
|
# 2. 确认PHP进程已重启
|
||||||
|
ps aux | grep php-fpm | grep -v grep
|
||||||
|
|
||||||
|
# 3. 检查文件权限
|
||||||
|
ls -lh /www/wwwroot/fengketrade/addons/shopro/library/ccblife/CcbPaymentService.php
|
||||||
|
|
||||||
|
# 4. 查看完整日志
|
||||||
|
tail -100 /www/wwwroot/fengketrade/runtime/log/$(date +%Y%m)/$(date +%d).log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 退出服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **备份**: 修改前建议备份当前文件
|
||||||
|
2. **权限**: 确保PHP有权限读取修改后的文件
|
||||||
|
3. **缓存**: 务必重启PHP-FPM和清除OPcache
|
||||||
|
4. **验证**: 修改后一定要查看日志验证是否生效
|
||||||
@ -1,447 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace addons\shopro\library\ccblife;
|
|
||||||
|
|
||||||
use think\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 建行生活加密解密核心类
|
|
||||||
*
|
|
||||||
* ⚠️ 已废弃:请使用 CcbRSA、CcbMD5、CcbHttpClient 类替代
|
|
||||||
*
|
|
||||||
* 废弃原因:
|
|
||||||
* 1. formatKey() 方法存在密钥格式化错误(PKCS#1 vs PKCS#8 混淆)
|
|
||||||
* 2. chunk_split() 使用不当导致 OpenSSL ASN1 解析错误
|
|
||||||
* 3. 与 CcbRSA 类功能重复,维护成本高
|
|
||||||
*
|
|
||||||
* 迁移指南:
|
|
||||||
* - RSA加密/解密 → 使用 CcbRSA::encrypt() / CcbRSA::decrypt()
|
|
||||||
* - MD5签名 → 使用 CcbMD5::signApiMessage() / CcbMD5::verifyApiSignature()
|
|
||||||
* - 加密商户公钥 → 在 CcbPaymentService 中使用 encryptPublicKeyLast30()
|
|
||||||
*
|
|
||||||
* @deprecated 2025-01-21 统一使用 CcbRSA、CcbMD5 类
|
|
||||||
* @author Billy
|
|
||||||
* @date 2025-01-16
|
|
||||||
*/
|
|
||||||
class CcbEncryption
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 配置信息
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $config;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务方私钥
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $privateKey;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务方公钥
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $publicKey;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
*
|
|
||||||
* @param array $config 配置数组
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function __construct($config = [])
|
|
||||||
{
|
|
||||||
// 如果没有传入配置,从配置文件读取
|
|
||||||
if (empty($config)) {
|
|
||||||
$config = config('ccblife');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->config = $config;
|
|
||||||
|
|
||||||
// 加载密钥
|
|
||||||
$this->loadKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载密钥
|
|
||||||
*
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
private function loadKeys()
|
|
||||||
{
|
|
||||||
$this->privateKey = $this->config['private_key'] ?? '';
|
|
||||||
$this->publicKey = $this->config['public_key'] ?? '';
|
|
||||||
|
|
||||||
if (empty($this->privateKey)) {
|
|
||||||
throw new Exception('服务方私钥未配置');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($this->publicKey)) {
|
|
||||||
throw new Exception('服务方公钥未配置');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RSA加密 (使用建行平台公钥加密)
|
|
||||||
*
|
|
||||||
* @param string $data 原始数据
|
|
||||||
* @return string BASE64编码的加密数据
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function rsaEncrypt($data)
|
|
||||||
{
|
|
||||||
// 格式化公钥
|
|
||||||
$publicKey = $this->formatKey($this->publicKey);
|
|
||||||
|
|
||||||
// 获取公钥资源
|
|
||||||
$pubKeyId = openssl_pkey_get_public($publicKey);
|
|
||||||
if (!$pubKeyId) {
|
|
||||||
throw new Exception('建行平台公钥格式错误: ' . openssl_error_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⚠️ 动态获取RSA密钥大小,而非写死117字节
|
|
||||||
$keyDetails = openssl_pkey_get_details($pubKeyId);
|
|
||||||
if (!$keyDetails || !isset($keyDetails['bits'])) {
|
|
||||||
throw new Exception('无法获取RSA密钥详情');
|
|
||||||
}
|
|
||||||
|
|
||||||
$keySize = $keyDetails['bits'] / 8; // 密钥字节数: 1024位=128字节, 2048位=256字节
|
|
||||||
$chunkSize = $keySize - 11; // PKCS1填充需要预留11字节
|
|
||||||
|
|
||||||
// RSA加密 (分段加密)
|
|
||||||
$encrypted = '';
|
|
||||||
$dataLen = strlen($data);
|
|
||||||
|
|
||||||
for ($i = 0; $i < $dataLen; $i += $chunkSize) {
|
|
||||||
$chunk = substr($data, $i, $chunkSize);
|
|
||||||
$encryptedChunk = '';
|
|
||||||
|
|
||||||
if (!openssl_public_encrypt($chunk, $encryptedChunk, $pubKeyId, OPENSSL_PKCS1_PADDING)) {
|
|
||||||
throw new Exception('RSA加密失败: ' . openssl_error_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
$encrypted .= $encryptedChunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PHP 8+ 资源自动释放
|
|
||||||
if (PHP_VERSION_ID < 80000) {
|
|
||||||
openssl_free_key($pubKeyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// BASE64编码并去除换行符
|
|
||||||
return str_replace(["\r", "\n"], '', base64_encode($encrypted));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RSA解密 (使用服务方私钥解密)
|
|
||||||
*
|
|
||||||
* @param string $data BASE64编码的加密数据
|
|
||||||
* @return string 解密后的原始数据
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function rsaDecrypt($data)
|
|
||||||
{
|
|
||||||
// 格式化私钥
|
|
||||||
$privateKey = $this->formatKey($this->privateKey, 'PRIVATE');
|
|
||||||
|
|
||||||
// 获取私钥资源
|
|
||||||
$privKeyId = openssl_pkey_get_private($privateKey);
|
|
||||||
if (!$privKeyId) {
|
|
||||||
throw new Exception('服务方私钥格式错误: ' . openssl_error_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⚠️ 动态获取RSA密钥大小
|
|
||||||
$keyDetails = openssl_pkey_get_details($privKeyId);
|
|
||||||
if (!$keyDetails || !isset($keyDetails['bits'])) {
|
|
||||||
throw new Exception('无法获取RSA密钥详情');
|
|
||||||
}
|
|
||||||
|
|
||||||
$keySize = $keyDetails['bits'] / 8; // 密钥字节数: 1024位=128字节, 2048位=256字节
|
|
||||||
|
|
||||||
// BASE64解码
|
|
||||||
$encrypted = base64_decode($data);
|
|
||||||
|
|
||||||
// RSA解密 (分段解密,每段密文长度等于密钥字节数)
|
|
||||||
$decrypted = '';
|
|
||||||
$encryptedLen = strlen($encrypted);
|
|
||||||
|
|
||||||
for ($i = 0; $i < $encryptedLen; $i += $keySize) {
|
|
||||||
$chunk = substr($encrypted, $i, $keySize);
|
|
||||||
$decryptedChunk = '';
|
|
||||||
|
|
||||||
if (!openssl_private_decrypt($chunk, $decryptedChunk, $privKeyId, OPENSSL_PKCS1_PADDING)) {
|
|
||||||
throw new Exception('RSA解密失败: ' . openssl_error_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
$decrypted .= $decryptedChunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PHP 8+ 资源自动释放
|
|
||||||
if (PHP_VERSION_ID < 80000) {
|
|
||||||
openssl_free_key($privKeyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $decrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成MD5签名
|
|
||||||
*
|
|
||||||
* @param string $data 原始数据(JSON字符串)
|
|
||||||
* @return string 32位大写MD5签名 (与Java保持一致)
|
|
||||||
*/
|
|
||||||
public function generateSign($data)
|
|
||||||
{
|
|
||||||
// 签名规则: MD5(原始数据 + 服务方私钥)
|
|
||||||
$signString = $data . $this->privateKey;
|
|
||||||
return strtoupper(md5($signString)); // 转为大写,与Java一致
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证MD5签名
|
|
||||||
*
|
|
||||||
* @param string $data 原始数据
|
|
||||||
* @param string $sign 签名
|
|
||||||
* @return bool 验证结果
|
|
||||||
*/
|
|
||||||
public function verifySign($data, $sign)
|
|
||||||
{
|
|
||||||
$expectedSign = $this->generateSign($data);
|
|
||||||
return $expectedSign === $sign;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造完整加密报文
|
|
||||||
*
|
|
||||||
* @param string $txCode 交易代码 (如: A3341TP01)
|
|
||||||
* @param array $bodyData 业务数据
|
|
||||||
* @return array 加密后的完整报文 ['cnt' => '...', 'mac' => '...', 'svcid' => '...']
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function buildEncryptedMessage($txCode, $bodyData)
|
|
||||||
{
|
|
||||||
// 1. 构造原始报文
|
|
||||||
$txSeq = $this->generateTransSeq();
|
|
||||||
$txTime = date('YmdHis');
|
|
||||||
|
|
||||||
$message = [
|
|
||||||
'CLD_HEADER' => [
|
|
||||||
'CLD_TX_CHNL' => $this->config['service_id'],
|
|
||||||
'CLD_TX_TIME' => $txTime,
|
|
||||||
'CLD_TX_CODE' => $txCode,
|
|
||||||
'CLD_TX_SEQ' => $txSeq,
|
|
||||||
],
|
|
||||||
'CLD_BODY' => $bodyData,
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2. 转换为JSON (不转义中文和斜杠)
|
|
||||||
$jsonData = json_encode($message, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
||||||
if ($jsonData === false) {
|
|
||||||
throw new Exception('JSON编码失败: ' . json_last_error_msg());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. RSA加密
|
|
||||||
$encryptedData = $this->rsaEncrypt($jsonData);
|
|
||||||
|
|
||||||
// 4. 生成MD5签名
|
|
||||||
$sign = $this->generateSign($jsonData);
|
|
||||||
|
|
||||||
// 5. 组装最终报文
|
|
||||||
return [
|
|
||||||
'cnt' => $encryptedData,
|
|
||||||
'mac' => $sign,
|
|
||||||
'svcid' => $this->config['service_id'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析响应报文
|
|
||||||
*
|
|
||||||
* @param string $response 响应JSON字符串
|
|
||||||
* @return array 解析后的业务数据
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function parseResponse($response)
|
|
||||||
{
|
|
||||||
// 1. 解析JSON
|
|
||||||
$result = json_decode($response, true);
|
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
||||||
throw new Exception('响应不是有效的JSON: ' . json_last_error_msg());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 检查是否包含加密字段
|
|
||||||
if (!isset($result['cnt']) || !isset($result['mac'])) {
|
|
||||||
// 可能是错误响应,直接返回
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 验证签名 (如果启用)
|
|
||||||
if ($this->config['security']['verify_sign'] ?? true) {
|
|
||||||
// 解密后验证签名
|
|
||||||
$decryptedData = $this->rsaDecrypt($result['cnt']);
|
|
||||||
|
|
||||||
if (!$this->verifySign($decryptedData, $result['mac'])) {
|
|
||||||
throw new Exception('响应签名验证失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析业务数据
|
|
||||||
$businessData = json_decode($decryptedData, true);
|
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
||||||
throw new Exception('业务数据解析失败: ' . json_last_error_msg());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $businessData;
|
|
||||||
} else {
|
|
||||||
// 不验证签名,直接解密
|
|
||||||
$decryptedData = $this->rsaDecrypt($result['cnt']);
|
|
||||||
return json_decode($decryptedData, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成唯一交易流水号
|
|
||||||
*
|
|
||||||
* 格式: YmdHis + 微秒 + 4位随机数
|
|
||||||
* 示例: 202501161200001234567890
|
|
||||||
*
|
|
||||||
* @return string 24位交易流水号
|
|
||||||
*/
|
|
||||||
public function generateTransSeq()
|
|
||||||
{
|
|
||||||
$date = date('YmdHis'); // 14位
|
|
||||||
$microtime = substr(microtime(), 2, 6); // 6位微秒
|
|
||||||
$random = str_pad(mt_rand(0, 9999), 4, '0', STR_PAD_LEFT); // 4位随机数
|
|
||||||
|
|
||||||
return $date . $microtime . $random; // 24位
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成支付流水号
|
|
||||||
*
|
|
||||||
* 格式: PAY + YmdHis + 8位随机数
|
|
||||||
* 示例: PAY2025011612000012345678
|
|
||||||
*
|
|
||||||
* @return string 支付流水号
|
|
||||||
*/
|
|
||||||
public function generatePayFlowId()
|
|
||||||
{
|
|
||||||
$prefix = 'PAY';
|
|
||||||
$date = date('YmdHis'); // 14位
|
|
||||||
$random = str_pad(mt_rand(0, 99999999), 8, '0', STR_PAD_LEFT); // 8位随机数
|
|
||||||
|
|
||||||
return $prefix . $date . $random; // 3 + 14 + 8 = 25位
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化密钥 (添加PEM头尾)
|
|
||||||
*
|
|
||||||
* 密钥格式说明:
|
|
||||||
* - 公钥: X.509格式 (BEGIN PUBLIC KEY)
|
|
||||||
* - 私钥: PKCS#8格式 (BEGIN PRIVATE KEY) - 与Java保持一致
|
|
||||||
*
|
|
||||||
* @param string $key 密钥内容 (BASE64字符串,不含头尾)
|
|
||||||
* @param string $type 类型: PUBLIC 或 PRIVATE
|
|
||||||
* @return string 格式化后的PEM密钥
|
|
||||||
*/
|
|
||||||
private function formatKey($key, $type = 'PUBLIC')
|
|
||||||
{
|
|
||||||
// 如果已经包含头尾,直接返回
|
|
||||||
if (strpos($key, '-----BEGIN') !== false) {
|
|
||||||
return $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加头尾 (注意: 私钥使用PKCS#8格式,与Java的PKCS8EncodedKeySpec一致)
|
|
||||||
if ($type === 'PUBLIC') {
|
|
||||||
$header = "-----BEGIN PUBLIC KEY-----\n";
|
|
||||||
$footer = "\n-----END PUBLIC KEY-----";
|
|
||||||
} else {
|
|
||||||
// 使用PKCS#8格式 (不是RSA PRIVATE KEY)
|
|
||||||
$header = "-----BEGIN PRIVATE KEY-----\n";
|
|
||||||
$footer = "\n-----END PRIVATE KEY-----";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每64个字符换行
|
|
||||||
$key = chunk_split($key, 64, "\n");
|
|
||||||
|
|
||||||
return $header . $key . $footer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成支付串签名
|
|
||||||
*
|
|
||||||
* 用于生成建行支付串的MAC签名
|
|
||||||
*
|
|
||||||
* @param array $params 支付参数数组
|
|
||||||
* @return string 32位大写MD5签名 (与Java保持一致)
|
|
||||||
*/
|
|
||||||
public function generatePaymentSign($params)
|
|
||||||
{
|
|
||||||
// 1. 按参数名ASCII排序
|
|
||||||
ksort($params);
|
|
||||||
|
|
||||||
// 2. 拼接成字符串: key1=value1&key2=value2&...
|
|
||||||
$signString = http_build_query($params);
|
|
||||||
|
|
||||||
// 3. 追加平台公钥
|
|
||||||
$signString .= '&PLATFORMPUB=' . $this->publicKey;
|
|
||||||
|
|
||||||
// 4. 生成MD5签名 (转为大写,与Java一致)
|
|
||||||
return strtoupper(md5($signString . $this->privateKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加密商户公钥 (用于支付串的ENCPUB字段)
|
|
||||||
*
|
|
||||||
* ⚠️ 已废弃:建行要求只加密公钥后30位,请使用 encryptMerchantPublicKeyLast30()
|
|
||||||
*
|
|
||||||
* @return string BASE64编码的加密公钥
|
|
||||||
* @throws Exception
|
|
||||||
* @deprecated 使用 encryptMerchantPublicKeyLast30() 替代
|
|
||||||
*/
|
|
||||||
public function encryptMerchantPublicKey()
|
|
||||||
{
|
|
||||||
if (empty($this->publicKey)) {
|
|
||||||
throw new Exception('服务方公钥未配置');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用建行平台公钥加密商户公钥
|
|
||||||
return $this->rsaEncrypt($this->publicKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加密商户公钥后30位 (用于支付串的ENCPUB字段)
|
|
||||||
*
|
|
||||||
* 根据建行文档v2.2规范:
|
|
||||||
* "使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文"
|
|
||||||
*
|
|
||||||
* @return string BASE64编码的加密密文
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function encryptMerchantPublicKeyLast30()
|
|
||||||
{
|
|
||||||
if (empty($this->publicKey)) {
|
|
||||||
throw new Exception('服务方公钥未配置');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取商户公钥的后30位
|
|
||||||
$publicKeyContent = $this->publicKey;
|
|
||||||
|
|
||||||
// 如果是PEM格式,去除头尾和换行符
|
|
||||||
$publicKeyContent = str_replace([
|
|
||||||
'-----BEGIN PUBLIC KEY-----',
|
|
||||||
'-----END PUBLIC KEY-----',
|
|
||||||
"\r", "\n", " "
|
|
||||||
], '', $publicKeyContent);
|
|
||||||
|
|
||||||
// 取后30位
|
|
||||||
$last30Chars = substr($publicKeyContent, -30);
|
|
||||||
|
|
||||||
if (strlen($last30Chars) < 30) {
|
|
||||||
throw new Exception('商户公钥长度不足30位');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用建行平台公钥加密这30位字符
|
|
||||||
return $this->rsaEncrypt($last30Chars);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
88
deploy_fix.sh
Executable file
88
deploy_fix.sh
Executable file
@ -0,0 +1,88 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 一键部署修复脚本
|
||||||
|
# 用法: bash deploy_fix.sh
|
||||||
|
|
||||||
|
set -e # 遇到错误立即退出
|
||||||
|
|
||||||
|
SERVER="root@47.96.77.165"
|
||||||
|
SERVER_DIR="/www/wwwroot/fengketrade"
|
||||||
|
FILE_TO_UPLOAD="addons/shopro/library/ccblife/CcbPaymentService.php"
|
||||||
|
|
||||||
|
echo "===================================="
|
||||||
|
echo "建行支付修复 - 一键部署"
|
||||||
|
echo "===================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查本地文件是否存在
|
||||||
|
if [ ! -f "$FILE_TO_UPLOAD" ]; then
|
||||||
|
echo "❌ 本地文件不存在: $FILE_TO_UPLOAD"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "【步骤1】检查本地代码..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
if grep -q "urlencode" <(sed -n '279p' "$FILE_TO_UPLOAD"); then
|
||||||
|
echo "❌ 本地代码第279行仍包含urlencode,请先修复本地代码"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ 本地代码已修复"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【步骤2】上传诊断脚本到服务器..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
scp server_diagnose.sh $SERVER:$SERVER_DIR/ || {
|
||||||
|
echo "❌ 上传诊断脚本失败"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "✅ 诊断脚本上传成功"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【步骤3】运行服务器端诊断..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
ssh $SERVER "cd $SERVER_DIR && bash server_diagnose.sh" || {
|
||||||
|
echo "❌ 服务器诊断失败"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【步骤4】上传修复后的文件..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
echo "正在上传: $FILE_TO_UPLOAD"
|
||||||
|
scp $FILE_TO_UPLOAD $SERVER:$SERVER_DIR/$FILE_TO_UPLOAD || {
|
||||||
|
echo "❌ 文件上传失败"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "✅ 文件上传成功"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【步骤5】重启PHP-FPM..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
ssh $SERVER "systemctl restart php-fpm || service php-fpm restart" || {
|
||||||
|
echo "⚠️ PHP-FPM重启可能失败,请手动检查"
|
||||||
|
}
|
||||||
|
echo "✅ PHP-FPM重启完成"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【步骤6】清除OPcache..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
ssh $SERVER "php -r 'if(function_exists(\"opcache_reset\")){opcache_reset();echo \"OPcache cleared\n\";}'" || {
|
||||||
|
echo "⚠️ OPcache清除可能失败"
|
||||||
|
}
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【步骤7】验证部署..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
echo "检查服务器上的代码..."
|
||||||
|
ssh $SERVER "sed -n '279p' $SERVER_DIR/$FILE_TO_UPLOAD"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "===================================="
|
||||||
|
echo "✅ 部署完成!"
|
||||||
|
echo "===================================="
|
||||||
|
echo ""
|
||||||
|
echo "下一步:"
|
||||||
|
echo "1. 触发一次支付测试"
|
||||||
|
echo "2. 监控日志: ssh $SERVER 'tail -f $SERVER_DIR/runtime/log/\$(date +%Y%m)/\$(date +%d).log | grep 最终支付串'"
|
||||||
|
echo "3. 检查是否还有 %25u (错误) 还是 %u (正确)"
|
||||||
|
echo ""
|
||||||
112
server_diagnose.sh
Executable file
112
server_diagnose.sh
Executable file
@ -0,0 +1,112 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 服务器端建行支付诊断脚本
|
||||||
|
# 在服务器上运行: bash server_diagnose.sh
|
||||||
|
|
||||||
|
echo "===================================="
|
||||||
|
echo "建行支付服务器端诊断"
|
||||||
|
echo "===================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
PROJECT_DIR="/www/wwwroot/fengketrade"
|
||||||
|
FILE="$PROJECT_DIR/addons/shopro/library/ccblife/CcbPaymentService.php"
|
||||||
|
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "❌ 文件不存在: $FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "【1. 文件信息】"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
ls -lh "$FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【2. 最后修改时间】"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
stat "$FILE" | grep "Modify"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【3. 检查第279行(最终支付串)】"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
echo "应该是: \$finalParts[] = \$key . '=' . \$value;"
|
||||||
|
echo "实际是:"
|
||||||
|
sed -n '279p' "$FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if grep -q "urlencode" <(sed -n '279p' "$FILE"); then
|
||||||
|
echo "❌ 错误: 第279行包含urlencode,需要修复!"
|
||||||
|
else
|
||||||
|
echo "✅ 正确: 第279行无URL编码"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【4. 检查第286行(非MAC参数)】"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
sed -n '286p' "$FILE"
|
||||||
|
if grep -q "urlencode" <(sed -n '286p' "$FILE"); then
|
||||||
|
echo "❌ 错误: 第286行包含urlencode"
|
||||||
|
else
|
||||||
|
echo "✅ 正确: 第286行无URL编码"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【5. 检查ENCPUB的URL-safe处理】"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
if grep -q "str_replace(\['+', '/'\], \['-', '_'\]" "$FILE"; then
|
||||||
|
echo "✅ 找到URL-safe BASE64转换"
|
||||||
|
grep -n "str_replace(\['+', '/'\]" "$FILE" | head -1
|
||||||
|
else
|
||||||
|
echo "❌ 未找到URL-safe BASE64转换"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【6. PHP版本和OPcache状态】"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
php -v | head -1
|
||||||
|
php -r "echo 'OPcache enabled: ' . (function_exists('opcache_reset') ? 'Yes' : 'No') . \"\n\";"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【7. 最近的支付日志】"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
LOGFILE="$PROJECT_DIR/runtime/log/$(date +%Y%m)/$(date +%d).log"
|
||||||
|
if [ -f "$LOGFILE" ]; then
|
||||||
|
echo "日志文件: $LOGFILE"
|
||||||
|
echo "最近的支付串:"
|
||||||
|
tail -50 "$LOGFILE" | grep "最终支付串" | tail -1
|
||||||
|
echo ""
|
||||||
|
echo "检查是否有URL编码问题:"
|
||||||
|
if tail -50 "$LOGFILE" | grep "最终支付串" | grep -q "%25u"; then
|
||||||
|
echo "❌ 发现URL编码问题: %被编码成%25"
|
||||||
|
else
|
||||||
|
echo "✅ 未发现%25,可能已修复或未测试"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "日志文件不存在: $LOGFILE"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "【8. 建议操作】"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
echo "如果发现代码有问题,执行以下步骤:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 检查git状态:"
|
||||||
|
echo " cd $PROJECT_DIR"
|
||||||
|
echo " git status"
|
||||||
|
echo ""
|
||||||
|
echo "2. 拉取最新代码:"
|
||||||
|
echo " git pull"
|
||||||
|
echo ""
|
||||||
|
echo "3. 重启PHP-FPM:"
|
||||||
|
echo " systemctl restart php-fpm"
|
||||||
|
echo " # 或者"
|
||||||
|
echo " service php-fpm restart"
|
||||||
|
echo ""
|
||||||
|
echo "4. 清除OPcache:"
|
||||||
|
echo " php -r 'opcache_reset();'"
|
||||||
|
echo ""
|
||||||
|
echo "5. 再次测试支付,查看日志:"
|
||||||
|
echo " tail -f $PROJECT_DIR/runtime/log/$(date +%Y%m)/$(date +%d).log | grep '最终支付串'"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "===================================="
|
||||||
|
echo "诊断完成"
|
||||||
|
echo "===================================="
|
||||||
Loading…
x
Reference in New Issue
Block a user