回调优化
This commit is contained in:
@@ -17,7 +17,8 @@ use ReflectionMethod;
|
||||
* 小程序在线支付(旺铺网关行业版):下单 → 调起支付 → 后台通知/主动查询结账
|
||||
*
|
||||
* 通讯协议与官方示例 demo/IndexController.php 一致:
|
||||
* 业务报文 AES-128-ECB 加密(随机 16 位密钥)+ RSA 公钥加密 AES 密钥,JSON 信封传输。
|
||||
* 下单/查询的业务报文 AES-128-ECB 加密(随机 16 位密钥)+ RSA 公钥加密 AES 密钥,JSON 信封传输;
|
||||
* 支付结果后台通知为明文表单报文,仅 MD5 验签(ASCII 升序拼接 + 专用加签Key)。
|
||||
*
|
||||
* - Http::fake 模拟微信 code2session 与旺铺网关,不触网
|
||||
* - 结账幂等:重复通知/查询不重复累加门店总采购金额
|
||||
@@ -44,6 +45,7 @@ class MiniOnlinePaymentTest extends ProcurementTestCase
|
||||
'services.wangpu.public_key' => self::TEST_PUBLIC_KEY,
|
||||
'services.wangpu.private_key' => self::TEST_PRIVATE_KEY,
|
||||
'services.wangpu.payway_code' => 'WECHAT_MINI',
|
||||
'services.wangpu.sign_key' => 'notify-test-key',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -106,6 +108,14 @@ class MiniOnlinePaymentTest extends ProcurementTestCase
|
||||
'order_id' => 'WP202608270001',
|
||||
'tradeNo' => 'T20260827001',
|
||||
'user_openid' => $openid,
|
||||
'wxjsapistr' => (string) json_encode([
|
||||
'appId' => 'wx-mini-test',
|
||||
'timeStamp' => '1724745600',
|
||||
'nonceStr' => 'nonce001',
|
||||
'package' => 'prepay_id=wp-prepay-001',
|
||||
'signType' => 'RSA',
|
||||
'paySign' => 'sign001',
|
||||
]),
|
||||
]),
|
||||
200
|
||||
),
|
||||
@@ -151,10 +161,10 @@ class MiniOnlinePaymentTest extends ProcurementTestCase
|
||||
return $payment;
|
||||
}
|
||||
|
||||
/** 构造加密的支付成功通知信封 */
|
||||
private function encryptedNotifyParams(PaymentModel $payment, array $overrides = []): array
|
||||
/** 构造带 MD5 签名的支付成功通知表单报文(加签算法与官方文档一致,测试侧独立实现) */
|
||||
private function signedNotifyParams(PaymentModel $payment, array $overrides = []): array
|
||||
{
|
||||
return $this->gatewayEnvelope(array_merge([
|
||||
$params = array_merge([
|
||||
'mer_order_id' => $payment->payment_no,
|
||||
'order_status' => '1',
|
||||
'order_amt' => (string) $payment->amount,
|
||||
@@ -166,7 +176,13 @@ class MiniOnlinePaymentTest extends ProcurementTestCase
|
||||
'mer_no' => 'mer001',
|
||||
'device_no' => 'dev001',
|
||||
'order_title' => '账单合并付款',
|
||||
], $overrides));
|
||||
], $overrides);
|
||||
|
||||
$signParams = array_filter($params, static fn ($value): bool => $value !== null && $value !== '');
|
||||
ksort($signParams, SORT_STRING);
|
||||
$str = implode('&', array_map(static fn ($k, $v) => $k . '=' . $v, array_keys($signParams), $signParams));
|
||||
$params['sign'] = strtoupper(md5($str . '&key=' . config('services.wangpu.sign_key')));
|
||||
return $params;
|
||||
}
|
||||
|
||||
/** AES-128-ECB 加密 golden test:与官方示例 demo/functions.php encryption 算法输出一致 */
|
||||
@@ -186,19 +202,41 @@ class MiniOnlinePaymentTest extends ProcurementTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/** 信封加解密回环:网关侧加密(demo 算法)→ 服务解密还原业务报文 */
|
||||
public function test_notify_envelope_round_trip(): void
|
||||
/** 通知 MD5 验签 golden test:官方文档示例报文 + SignKey 直算签名一致(含中文、空值剔除) */
|
||||
public function test_notify_sign_matches_doc_golden(): void
|
||||
{
|
||||
config(['services.wangpu.sign_key' => '07714583f82b4db8b675b32cd5e0969743']);
|
||||
|
||||
$params = [
|
||||
'mer_order_id' => 'ZF202608270001',
|
||||
'mer_order_id' => 'CBC92E5GTL000083202004121010143',
|
||||
'trade_no' => '11420200410120144102483',
|
||||
'mer_no' => '2001071119360E5Riu',
|
||||
'order_amt' => '0.01',
|
||||
'payway_code' => 'QR_WECHAT_BARPAY',
|
||||
'order_id' => '202004101201444525348059',
|
||||
'order_status' => '1',
|
||||
'order_amt' => '150.50',
|
||||
'order_title' => '账单合并付款', // 中文报文
|
||||
'order_title' => '住宿酒店',
|
||||
'mer_code' => 'W00000000001381',
|
||||
'device_no' => 'CBC92E5GTL000083',
|
||||
'order_time' => '2020-04-10 12:01:44',
|
||||
'trade_time' => '2020-04-10 12:01:47',
|
||||
'gateway_mer_order_id' => '2020041012014445269',
|
||||
'fee' => '', // 空值不参与签名(平台不下发空值数据元)
|
||||
'sign' => 'A31998F2E0549E0A80B2A4B3A0473784', // 文档示例签名值
|
||||
];
|
||||
|
||||
$decrypted = app(WangpuPayService::class)->decryptNotify($this->gatewayEnvelope($params));
|
||||
$verified = app(WangpuPayService::class)->verifyNotify($params);
|
||||
|
||||
$this->assertSame($params, $decrypted);
|
||||
$this->assertSame($params, $verified);
|
||||
}
|
||||
|
||||
/** 通知验签失败(签名缺失/错误)抛异常 */
|
||||
public function test_notify_verify_rejects_bad_sign(): void
|
||||
{
|
||||
$service = app(WangpuPayService::class);
|
||||
|
||||
$this->expectException(RepositoryException::class);
|
||||
$service->verifyNotify(['mer_order_id' => 'ZF202608270001', 'sign' => 'INVALIDSIGN']);
|
||||
}
|
||||
|
||||
/** 密钥配置错误:公钥栏误填私钥时给出明确中文报错(而非 openssl 警告) */
|
||||
@@ -228,7 +266,9 @@ class MiniOnlinePaymentTest extends ProcurementTestCase
|
||||
|
||||
$paymentNo = $response->json('data.payment_no');
|
||||
$this->assertSame('150.50', $response->json('data.amount'));
|
||||
$this->assertSame('WP202608270001', $response->json('data.pay_params.order_id'));
|
||||
// 调起支付参数取自网关应答 wxjsapistr(小程序 wx.requestPayment 直接透传)
|
||||
$this->assertSame('wx-mini-test', $response->json('data.pay_params.appId'));
|
||||
$this->assertSame('prepay_id=wp-prepay-001', $response->json('data.pay_params.package'));
|
||||
|
||||
$payment = PaymentModel::where('payment_no', $paymentNo)->first();
|
||||
$this->assertSame(PaymentModel::TYPE_ONLINE, $payment->pay_type);
|
||||
@@ -336,7 +376,7 @@ class MiniOnlinePaymentTest extends ProcurementTestCase
|
||||
$this->assertSame(0, $bill->fresh()->payment_id, '账单释放可重新付款');
|
||||
}
|
||||
|
||||
/** 支付成功通知:解密信封 → 幂等结账(账单置已支付 + 累加门店总采购金额 + 通知门店) */
|
||||
/** 支付成功通知:验签 → 幂等结账(账单置已支付 + 累加门店总采购金额 + 通知门店) */
|
||||
public function test_notify_settles_payment(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
@@ -344,7 +384,7 @@ class MiniOnlinePaymentTest extends ProcurementTestCase
|
||||
$bill2 = $this->makeBill($store, '50.00');
|
||||
$payment = $this->makeOnlinePayment($store, '150.00', $bill1, $bill2);
|
||||
|
||||
$this->postJson('/mini/payment/notify', $this->encryptedNotifyParams($payment))
|
||||
$this->postJson('/mini/payment/notify', $this->signedNotifyParams($payment))
|
||||
->assertJsonPath('code', '00');
|
||||
|
||||
$payment->refresh();
|
||||
@@ -366,34 +406,39 @@ class MiniOnlinePaymentTest extends ProcurementTestCase
|
||||
);
|
||||
|
||||
// 重复通知幂等:仍应答成功,金额不重复累加
|
||||
$this->postJson('/mini/payment/notify', $this->encryptedNotifyParams($payment))
|
||||
$this->postJson('/mini/payment/notify', $this->signedNotifyParams($payment))
|
||||
->assertJsonPath('code', '00');
|
||||
$this->assertSame('150.00', (string) $store->fresh()->total_purchase_amount);
|
||||
$this->assertSame(1, NoticeModel::where('store_id', $store->id)->count());
|
||||
}
|
||||
|
||||
/** 通知解密失败 / 金额不一致 / 订单号不存在 / 非支付成功状态:应答失败且不结账 */
|
||||
/** 通知验签失败 / 金额不一致 / 订单号不存在 / 非支付成功状态:应答失败且不结账 */
|
||||
public function test_notify_rejects_invalid_messages(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
$bill = $this->makeBill($store, '100.00');
|
||||
$payment = $this->makeOnlinePayment($store, '100.00', $bill);
|
||||
|
||||
// 信封 signature 非法 → 解密失败
|
||||
$badEnvelope = $this->encryptedNotifyParams($payment);
|
||||
$badEnvelope['signature'] = 'INVALIDSIGN';
|
||||
$this->postJson('/mini/payment/notify', $badEnvelope)->assertJsonPath('code', '01');
|
||||
// 签名非法 → 验签失败
|
||||
$badSign = $this->signedNotifyParams($payment);
|
||||
$badSign['sign'] = 'INVALIDSIGN';
|
||||
$this->postJson('/mini/payment/notify', $badSign)->assertJsonPath('code', '01');
|
||||
|
||||
// 金额不一致(防篡改)
|
||||
$this->postJson('/mini/payment/notify', $this->encryptedNotifyParams($payment, ['order_amt' => '99.99']))
|
||||
// 篡改金额后未重签 → 验签失败
|
||||
$tampered = $this->signedNotifyParams($payment);
|
||||
$tampered['order_amt'] = '99.99';
|
||||
$this->postJson('/mini/payment/notify', $tampered)->assertJsonPath('code', '01');
|
||||
|
||||
// 金额不一致(防篡改,签名正确但金额与支付单不符)
|
||||
$this->postJson('/mini/payment/notify', $this->signedNotifyParams($payment, ['order_amt' => '99.99']))
|
||||
->assertJsonPath('code', '01');
|
||||
|
||||
// 订单号不存在
|
||||
$this->postJson('/mini/payment/notify', $this->encryptedNotifyParams($payment, ['mer_order_id' => 'ZF000000000000']))
|
||||
$this->postJson('/mini/payment/notify', $this->signedNotifyParams($payment, ['mer_order_id' => 'ZF000000000000']))
|
||||
->assertJsonPath('code', '01');
|
||||
|
||||
// 非支付成功状态
|
||||
$this->postJson('/mini/payment/notify', $this->encryptedNotifyParams($payment, ['order_status' => '0']))
|
||||
$this->postJson('/mini/payment/notify', $this->signedNotifyParams($payment, ['order_status' => '0']))
|
||||
->assertJsonPath('code', '01');
|
||||
|
||||
// 均未结账
|
||||
|
||||
Reference in New Issue
Block a user