微信公众号支付
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\BillModel;
|
||||
use App\Models\PaymentModel;
|
||||
use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Services\WechatPayService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
/**
|
||||
* 公众号 H5 JSAPI 在线支付(scene=mp):网页授权 code 换 openid → 公众号 appid 下单 → 调起参数
|
||||
*
|
||||
* - 微信公众号维度 openid 与小程序 openid 不同:分场景绑定(store.mp_openid / store.openid 互不覆盖)
|
||||
* - 微信官方渠道:JSAPI 下单 appid=公众号 appid,调起参数喂 WeixinJSBridge getBrandWCPayRequest
|
||||
* - 旺铺渠道:sub_appid=公众号 appid、payway_code 取 mp_payway_code(留空回退 payway_code)
|
||||
*/
|
||||
class MiniMpPaymentTest extends ProcurementTestCase
|
||||
{
|
||||
/** 测试专用微信商户 API 私钥(自签名,仅测试使用) */
|
||||
private const string WXPAY_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQChGoUMvCaiaf97\nXcUExEA3VP4FVwUCPQf/GPvm2cjHZACcPj65rjKNs5nJ0xaKELrJkUCZ3+N5YJJ4\nVGBBhh9GXekrzFckeqUn1sRS9FFbWfNeqAvz3qgps+ohnriPMkTRTw/SONTVRuNV\nesKz8FTYbT5Vl6jSHv71CMMcm6qrhvZ9av1nB03MrUN06v2OMhfXrZTBV5KI5zvI\nOwxetOz0MTAHcfRyrW8cejiJo3baRsRRHXsnDRB7p4H1KIPx+N7yTJKv/L2+tgXY\nmhIjKJRC5hDmrLrLOzievqKlBTsBcG41UWzcLEDG96BcR6Le+dzlJIXXi2taoFXG\nFuDUVt/bAgMBAAECggEAEagPv57kt8AuQ/MO+MAZhExIWtrNWGV1Tua0v6RMIQOQ\nyDmk3kIn6ufYNF/HhJzjcTFThzRxKzMDr98aKD7u2+NxqV8yiHOjTi53mIU2YBtc\n/5l4qzhSmIojYEk+U6i1aiCOzKzLMWTHiELKvAsxrAFXvOxrkDvdbYrm92ey2Nbm\nnvFTzTlPNEVf8HwKIFpbbV/r5RlrqDOqj8np6t5X+PlpL4Kx+oO8XVaaWjjb+2NB\nPX7laZNLa2oJ+sY4qWnwPyCeZQu2glPtBjJQRWX8+eP5p7xMMWylosL056LHasb2\nNC+KDjXSA/4O19NhBwsX8Q7QDbuJrVt/2WMboJxfYQKBgQDU7U64gzdTFezTgJ18\nb4hXqvVyzksireABV2BfKiah25yMH5s4YtgFneusn0flh5yKOh1phbn9EmMGAnob\nyHYfcyiFFR1i+4j3ELblKTkbXeaMIthTqL9QRULc3HwVjKINs3tUAlpSHqdFrzAT\nQb8JcgHfmnDrqSNCNXLJY1cgLwKBgQDBsXj7PDX+URT4m7kMWD4GzTlAbAotLBO5\nGxugKAxczGbAk3G+xmYbwgbuKUHTzBAXX0atfCMjbK9DdowRlyVbI948o8drXqz2\nR2l6u3VyV0ERP+9BV0xfLNbiQGWiPZiYR9hYg+ewbzCmsQEyAUJGisxPZZtNz598\nmqhhQLeEFQKBgQCqTCJp8IiPKzn+7x8GJy4k79bfu57cXbSLXhb2BgBf9AWBDMZY\nkrWzyFp19e7K5WuOImzjuNDIV6xbYh/HmMzg2nnN9tVKFWO2NugQ4KeL+84oxrW+\nM3jP+pU/kBiuI4x46NP6nOcgRuQCF9ubizn/k+9rp1opIV3R1m24JHvKgQKBgFUd\nZtuIHvXtrXh9/bg2ArO8dR8hGuu0xcn+5onfb2dMDw8q+73oszAZeDAqAxpOPvf2\nTzmnJk3H0dCkhHKqZ6kAPwWItvYwuXLT+L8NJ1QikZ5B6SJeeVoNezQbNk4wISEx\n2Rk0hZibk9Z8S9ksgnI7RgLR1IhB54S69ake5kXtAoGANB/pN7MBP9ZeUBKq6eoB\nTHcHZjcD8ZRC/4rsgDcwpHd67xWloALc7FXA1gQfX2VcwFbtf4pl6umQd8kmsmEN\n0AJJ+riyMMrPcHZV0PCo9Kq4ex+6bLFENdRbjUBNk2nrQ/le8BxZLNi0QZPZTOLW\n+XXHpYd3dKJ/BJA/RfiP7sg=\n-----END PRIVATE KEY-----";
|
||||
|
||||
/** 测试专用微信商户 API 证书(自签名 X509,请求签名序列号取自证书) */
|
||||
private const string WXPAY_CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIC4TCCAcmgAwIBAgIUIDIEwVtecMJKfY0NCx1l8Fwv7LowDQYJKoZIhvcNAQEL\nBQAwADAeFw0yNjA5MDExMzM5NThaFw0zNjA4MjkxMzM5NThaMAAwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChGoUMvCaiaf97XcUExEA3VP4FVwUCPQf/\nGPvm2cjHZACcPj65rjKNs5nJ0xaKELrJkUCZ3+N5YJJ4VGBBhh9GXekrzFckeqUn\n1sRS9FFbWfNeqAvz3qgps+ohnriPMkTRTw/SONTVRuNVesKz8FTYbT5Vl6jSHv71\nCMMcm6qrhvZ9av1nB03MrUN06v2OMhfXrZTBV5KI5zvIOwxetOz0MTAHcfRyrW8c\nejiJo3baRsRRHXsnDRB7p4H1KIPx+N7yTJKv/L2+tgXYmhIjKJRC5hDmrLrLOzie\nvqKlBTsBcG41UWzcLEDG96BcR6Le+dzlJIXXi2taoFXGFuDUVt/bAgMBAAGjUzBR\nMB0GA1UdDgQWBBQ2SD9beEd/0EU6ouWHHSW6Pdh8OjAfBgNVHSMEGDAWgBQ2SD9b\neEd/0EU6ouWHHSW6Pdh8OjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA\nA4IBAQByZckpIfdi5BeWwX4FZK9D5+78hZpOpDi+a8rxyd22BYYbkft5WfWnTjWh\nuXZgwOBPoYwmQ9FtC8rIeXWCadcp/ohGCkVVm9Y2CtkutrTC+uxdl4LnDPRQjXPL\nuRubAzvv3leRrphd1dOmAku32TfE0bK6Xul6pCnQaEgIlFp8zp8UkCrYcm9SPNye\nRt/1vt5iBPB6UiZ9oQL1IDUdqJV2Hl9snzHi0AzuZHy+ZMZY6WEY2lBohN8wvfak\nqrejyeA3nY7+a+QRPCg9fAOhxJlIj/eJXJXUUgJTiwTDHN16JQYcqgX5/AdrqnNj\ndkhBOR8mPPmE9hCoXwcDt017y7YV\n-----END CERTIFICATE-----";
|
||||
|
||||
/** 测试专用微信 APIv3 密钥(32 字节) */
|
||||
private const string WXPAY_SECRET_KEY = 'testSecr3tKey0123456789abcdef01';
|
||||
|
||||
/** 测试专用旺铺 RSA 密钥对(仅测试使用,公私钥同源便于加解密回环) */
|
||||
private const string WANGPU_PUBLIC_KEY = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqZ4z7UatOnbBolcpFhW2q585II7atHK9w/opQ/k8QUqjwkwIVNYzC9qf/SFDoOnMBufZwS2tM8UUPcgNoy0hbmR5QqBsp9ugBaSL1ZHb5jStgpikzWEjFkyVR5WLtxl4nxKv0R5pV/mfq0RXsIpbutZqugnXRvHPYaqbKomXzf0FKlxmbpvPOXsH9L9rYrnOSKQ1t16vAa6UrqUdkgdQBH7+hI4kiDwY0MIZVOhDhB4r0ODrpWMd0U3yrbhwpHZdtF2B3dtn3JSS92F16Yo7Hec8+TUzNKX0Mu1rn07F6yGZeUf+5/egXsBc2Mx1TgZswFlvkmHS96kbH9gqJyGOVwIDAQAB';
|
||||
|
||||
private const string WANGPU_PRIVATE_KEY = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpnjPtRq06dsGiVykWFbarnzkgjtq0cr3D+ilD+TxBSqPCTAhU1jML2p/9IUOg6cwG59nBLa0zxRQ9yA2jLSFuZHlCoGyn26AFpIvVkdvmNK2CmKTNYSMWTJVHlYu3GXifEq/RHmlX+Z+rRFewilu61mq6CddG8c9hqpsqiZfN/QUqXGZum885ewf0v2tiuc5IpDW3Xq8BrpSupR2SB1AEfv6EjiSIPBjQwhlU6EOEHivQ4OulYx3RTfKtuHCkdl20XYHd22fclJL3YXXpijsd5zz5NTM0pfQy7WufTsXrIZl5R/7n96BewFzYzHVOBmzAWW+SYdL3qRsf2ConIY5XAgMBAAECggEAANg+h97uaNFYF9c5XjNeLYU0ULqkeD6SKAApIpgTY+6w6FkBoWIuT1y+q0jNYqKpwN+Ds2+vfizT7GX5Bz1iLoN8etHawBeLCmIus83A75VeAr20gOyy6pWv3Qlty1yvMsczEydqsL3vA0+gdQoB2RW+ib4uKmxl66WNR+xQE0DaGp3GGe117l4PrhqZWsMMugQ2sLJy6ZZ/L6+ILnTPYwz+6NdOor8owoSEYHpKJ03wTcDGdMtRkzanEtfBNklHolymof+aDwXeugw5teGm+/AtaTf8dINeUIGp6AFrUa+g78wyO/piUf+inLNa+e6vgGqck+ErsbRPb6VFQFBsQQKBgQDqkgfXGETdBtE5g9QiV2aY+DDHzcmQZBLgRGDoHFhn2SgRdG2E66Xq6uGEnwIpqLuKsCYe4t2JvNjSVFZ14Ul8DOow6NQfWLPxU6YT3fShAh/gTMgccKb7/Pe3fQqwl+6aEV3oMOKNFErKSGrkSptLh9Nf/ii9khpMFhUARErgjQKBgQC5HRwRb45CIkEeS2Q1Gz2xS4ezXACw9P0h4Qp5bMQmqgn/fjN+t4HJRlHCuDfLTnHZ48Vpihwu7BU1RmK1ozXJswDzQXCYE4ejEXTGVoUNxCHht4/VlAbBVbcnOsIREgvtZRIOHvPaMqUQqeSX9e3HcEAHfIaJRkVAhalQnKMrcwKBgDUtH7viU5Irvnikaw3R9H9PHHffLgeeuCzBM5rK+juong2+8CkG5tknoDJZfbsF9mYNYsbztTdJaXndBrC4ftkxcFHgJl5o1Hor9WVhlth9S86keWUBIMnVYi7lmOvJtZyVvU0q7+D9rarH2fug8i2gQAnt6zx2h6GiC+bAlJztAoGBAI/YxgnqhUJw+ec/sKPwAjW2usGu2b6o8deU153Z3mcpNVG70OpEUW+F3F0S6BBtad1muO41a4cu36AhjO0W4eJV3oQpMwSKEJmwI+1IKGa1JZsQGI5gVAuCvyuV5l57hpc4NhqRBO9m8YwMaV2ItviHCsqGgslDuObVtue0gLtvAoGAWSPjf+POrVy2VJ0yY9zJ6Ks1oGLYzejUTx6Ue/DmjZcOcn6IBGqvgj10D5Gu9vX847GiC8D632A3US4FMLJZki0IxEYWME3d3pg0vhqS24YxZwIo97JvxnrHOqMvd486K+Cyvlft8xgxlZdL79Ez5eec4Lvw7OoQaaoxcdDvf08=';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'services.wechat.mp.appid' => 'wx-mp-test',
|
||||
'services.wechat.mp.secret' => 'wx-mp-secret',
|
||||
]);
|
||||
}
|
||||
|
||||
/** 模拟公众号网页授权 code 换 openid 成功 */
|
||||
private function fakeMpOauth(string $openid = 'oMpOpenid001'): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.weixin.qq.com/*' => Http::response([
|
||||
'access_token' => 'mp-access-token',
|
||||
'expires_in' => 7200,
|
||||
'refresh_token' => 'mp-refresh-token',
|
||||
'openid' => $openid,
|
||||
'scope' => 'snsapi_base',
|
||||
], 200),
|
||||
]);
|
||||
}
|
||||
|
||||
/** 以 MockHttpClient 替换微信支付接口应答,并注入容器(控制器经由容器取同一实例) */
|
||||
private function fakeWechatApi(callable $callback): void
|
||||
{
|
||||
$service = app(WechatPayService::class);
|
||||
$service->app()->setHttpClient(new MockHttpClient($callback, 'https://api.mch.weixin.qq.com'));
|
||||
$this->app->instance(WechatPayService::class, $service);
|
||||
}
|
||||
|
||||
/** 密钥 PEM 包装(与服务内实现一致) */
|
||||
private function pem(string $body, string $kind): string
|
||||
{
|
||||
return "-----BEGIN {$kind} KEY-----\n" . wordwrap($body, 64, "\n", true) . "\n-----END {$kind} KEY-----";
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟网关加密信封(demo 协议:AES-128-ECB 加密报文 + RSA 公钥加密 AES 密钥)
|
||||
*
|
||||
* @param array<string, mixed> $data 业务报文
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function gatewayEnvelope(array $data): array
|
||||
{
|
||||
$key = Str::random(16);
|
||||
openssl_public_encrypt($key, $encryptedKey, $this->pem(self::WANGPU_PUBLIC_KEY, 'PUBLIC'));
|
||||
return [
|
||||
'serialNo' => Str::random(32),
|
||||
'version' => '1.0',
|
||||
'timestamp' => now()->format('YmdHis'),
|
||||
'data' => base64_encode((string) openssl_encrypt((string) json_encode($data), 'AES-128-ECB', $key, OPENSSL_RAW_DATA)),
|
||||
'signature' => base64_encode($encryptedKey),
|
||||
'extras' => '',
|
||||
'organizNo' => 'org001',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密我方发往网关的请求信封(断言上送报文用;服务侧 urlencode 需先解码)
|
||||
*
|
||||
* @param array<string, mixed> $body 请求信封
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function decryptRequest(array $body): array
|
||||
{
|
||||
openssl_private_decrypt(
|
||||
(string) base64_decode(urldecode((string) $body['signature'])),
|
||||
$key,
|
||||
$this->pem(self::WANGPU_PRIVATE_KEY, 'PRIVATE')
|
||||
);
|
||||
$plain = openssl_decrypt(
|
||||
(string) base64_decode(urldecode((string) $body['data'])),
|
||||
'AES-128-ECB',
|
||||
(string) $key,
|
||||
OPENSSL_RAW_DATA
|
||||
);
|
||||
return (array) json_decode((string) $plain, true);
|
||||
}
|
||||
|
||||
/** 旺铺渠道测试配置 */
|
||||
private function configWangpu(): void
|
||||
{
|
||||
config([
|
||||
'services.wangpu.base_url' => 'https://wangpu.test',
|
||||
'services.wangpu.organiz_no' => 'org001',
|
||||
'services.wangpu.mer_no' => 'mer001',
|
||||
'services.wangpu.mer_code' => 'code001',
|
||||
'services.wangpu.term_code' => 'term001',
|
||||
'services.wangpu.public_key' => self::WANGPU_PUBLIC_KEY,
|
||||
'services.wangpu.private_key' => self::WANGPU_PRIVATE_KEY,
|
||||
'services.wangpu.payway_code' => 'WECHAT_MINI',
|
||||
'services.wangpu.mp_payway_code' => 'WECHAT_JSPAY',
|
||||
]);
|
||||
}
|
||||
|
||||
/** 造一张指定金额的未支付账单(总额=商品金额) */
|
||||
private function makeBill(StoreModel $store, string $amount, array $attributes = []): BillModel
|
||||
{
|
||||
return BillModel::create(array_merge([
|
||||
'bill_no' => 'ZD' . random_int(100000000000, 999999999999),
|
||||
'purchase_id' => PurchaseOrderModel::factory()->create()->id,
|
||||
'store_id' => $store->id,
|
||||
'bill_date' => '2026-09-04',
|
||||
'product_amount' => $amount,
|
||||
'delivery_fee' => '0.00',
|
||||
'box_num' => 0,
|
||||
'tray_num' => 0,
|
||||
'box_price' => '0.00',
|
||||
'tray_price' => '0.00',
|
||||
'added_amount' => '0.00',
|
||||
'total_amount' => $amount,
|
||||
'status' => BillModel::STATUS_UNPAID,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
/** 微信官方渠道公众号下单:JSAPI appid=公众号、付款人=公众号 openid、调起参数同构、mp_openid 绑定且不覆盖小程序 openid */
|
||||
public function test_mp_scene_wechat_channel(): void
|
||||
{
|
||||
config([
|
||||
'services.pay.online_channel' => 'wechat',
|
||||
'services.wxpay.mch_id' => '1630000001',
|
||||
'services.wxpay.private_key' => self::WXPAY_PRIVATE_KEY,
|
||||
'services.wxpay.certificate' => self::WXPAY_CERTIFICATE,
|
||||
'services.wxpay.secret_key' => self::WXPAY_SECRET_KEY,
|
||||
]);
|
||||
|
||||
$this->fakeMpOauth();
|
||||
$captured = [];
|
||||
$this->fakeWechatApi(function (string $method, string $url, array $options) use (&$captured) {
|
||||
$captured = compact('method', 'url', 'options');
|
||||
return new MockResponse((string) json_encode(['prepay_id' => 'wx-mp-prepay-001']), ['http_code' => 200]);
|
||||
});
|
||||
|
||||
$store = StoreModel::factory()->create(['openid' => 'oMiniOpenidKeep']);
|
||||
$bill = $this->makeBill($store, '66.00');
|
||||
|
||||
$this->actingAsMiniStore($store);
|
||||
$response = $this->postJson('/mini/payment/online', [
|
||||
'bill_ids' => [$bill->id],
|
||||
'code' => 'mp-oauth-code',
|
||||
'scene' => 'mp',
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
// 调起参数:appId=公众号 appid,package 含 prepay_id(H5 喂 WeixinJSBridge getBrandWCPayRequest)
|
||||
$payParams = $response->json('data.pay_params');
|
||||
$this->assertSame('wx-mp-test', $payParams['appId']);
|
||||
$this->assertSame('prepay_id=wx-mp-prepay-001', $payParams['package']);
|
||||
$this->assertNotEmpty($payParams['timeStamp']);
|
||||
$this->assertNotEmpty($payParams['nonceStr']);
|
||||
$this->assertSame('RSA', $payParams['signType']);
|
||||
$this->assertNotEmpty($payParams['paySign']);
|
||||
|
||||
// 上送微信报文:appid=公众号 appid、payer=公众号 openid、金额元转分
|
||||
$body = (array) json_decode((string) ($captured['options']['body'] ?? ''), true);
|
||||
$this->assertSame('wx-mp-test', $body['appid'] ?? null);
|
||||
$this->assertSame('1630000001', $body['mchid'] ?? null);
|
||||
$this->assertSame('oMpOpenid001', $body['payer']['openid'] ?? null);
|
||||
$this->assertSame(6600, $body['amount']['total'] ?? null);
|
||||
|
||||
// 支付单与门店绑定:公众号 openid 写 mp_openid,不覆盖小程序 openid
|
||||
$payment = PaymentModel::where('payment_no', $response->json('data.payment_no'))->first();
|
||||
$this->assertSame('oMpOpenid001', $payment->openid);
|
||||
$store->refresh();
|
||||
$this->assertSame('oMpOpenid001', $store->mp_openid);
|
||||
$this->assertSame('oMiniOpenidKeep', $store->openid, '公众号场景不得覆盖小程序 openid');
|
||||
}
|
||||
|
||||
/** 旺铺渠道公众号下单:sub_appid=公众号 appid、payway_code=公众号支付方式代码、open_id=公众号 openid */
|
||||
public function test_mp_scene_wangpu_channel(): void
|
||||
{
|
||||
$this->configWangpu();
|
||||
$this->fakeMpOauth();
|
||||
Http::fake([
|
||||
'https://api.weixin.qq.com/*' => Http::response(['openid' => 'oMpOpenid001', 'access_token' => 'at'], 200),
|
||||
'https://wangpu.test/industrial/payment/order' => Http::response(
|
||||
['code' => '0000', 'msg' => '调用成功'] + $this->gatewayEnvelope([
|
||||
'order_id' => 'WP202609040001',
|
||||
'tradeNo' => 'T20260904001',
|
||||
'user_openid' => 'oMpOpenid001',
|
||||
]),
|
||||
200
|
||||
),
|
||||
]);
|
||||
|
||||
$store = StoreModel::factory()->create();
|
||||
$bill = $this->makeBill($store, '88.00');
|
||||
|
||||
$this->actingAsMiniStore($store);
|
||||
$response = $this->postJson('/mini/payment/online', [
|
||||
'bill_ids' => [$bill->id],
|
||||
'code' => 'mp-oauth-code',
|
||||
'scene' => 'mp',
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
$paymentNo = $response->json('data.payment_no');
|
||||
$this->assertSame('oMpOpenid001', $store->fresh()->mp_openid);
|
||||
|
||||
// 上送网关的加密信封:解出业务报文校验公众号 sub_appid / 公众号 payway_code / 公众号 openid
|
||||
Http::assertSent(function ($request) use ($paymentNo) {
|
||||
if (! str_contains($request->url(), '/industrial/payment/order')) {
|
||||
return false;
|
||||
}
|
||||
$plain = $this->decryptRequest($request->data());
|
||||
return ($plain['mer_order_id'] ?? '') === $paymentNo
|
||||
&& ($plain['order_amt'] ?? '') === '88.00'
|
||||
&& ($plain['open_id'] ?? '') === 'oMpOpenid001'
|
||||
&& ($plain['sub_appid'] ?? '') === 'wx-mp-test'
|
||||
&& ($plain['payway_code'] ?? '') === 'WECHAT_JSPAY'
|
||||
&& ! empty($plain['notifyurl']);
|
||||
});
|
||||
}
|
||||
|
||||
/** 公众号网页授权失败:errcode 应答 → 下单拒绝并提示授权失败 */
|
||||
public function test_mp_scene_oauth_failure(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.weixin.qq.com/*' => Http::response(['errcode' => 40029, 'errmsg' => 'invalid code'], 200),
|
||||
]);
|
||||
|
||||
$store = StoreModel::factory()->create();
|
||||
$bill = $this->makeBill($store, '10.00');
|
||||
|
||||
$this->actingAsMiniStore($store);
|
||||
$response = $this->postJson('/mini/payment/online', [
|
||||
'bill_ids' => [$bill->id],
|
||||
'code' => 'bad-code',
|
||||
'scene' => 'mp',
|
||||
])->assertJsonPath('success', false);
|
||||
|
||||
$this->assertStringContainsString('微信网页授权失败', (string) $response->json('msg'));
|
||||
// 授权失败不产生支付单、账单不被锁定
|
||||
$this->assertSame(0, PaymentModel::count());
|
||||
$this->assertSame(0, $bill->fresh()->payment_id);
|
||||
}
|
||||
|
||||
/** 公众号未配置 AppID/Secret:明确报错 */
|
||||
public function test_mp_scene_requires_mp_config(): void
|
||||
{
|
||||
config([
|
||||
'services.wechat.mp.appid' => '',
|
||||
'services.wechat.mp.secret' => '',
|
||||
]);
|
||||
|
||||
$store = StoreModel::factory()->create();
|
||||
$bill = $this->makeBill($store, '10.00');
|
||||
|
||||
$this->actingAsMiniStore($store);
|
||||
$response = $this->postJson('/mini/payment/online', [
|
||||
'bill_ids' => [$bill->id],
|
||||
'code' => 'mp-oauth-code',
|
||||
'scene' => 'mp',
|
||||
])->assertJsonPath('success', false);
|
||||
|
||||
$this->assertStringContainsString('微信公众号未配置', (string) $response->json('msg'));
|
||||
$this->assertSame(0, PaymentModel::count());
|
||||
}
|
||||
|
||||
/** 不传 scene 默认走小程序链路(code2session,小程序 appid 下单) */
|
||||
public function test_default_scene_remains_mini(): void
|
||||
{
|
||||
config([
|
||||
'services.wechat.mini.appid' => 'wx-mini-test',
|
||||
'services.wechat.mini.secret' => 'wx-secret-test',
|
||||
'services.pay.online_channel' => 'wechat',
|
||||
'services.wxpay.mch_id' => '1630000001',
|
||||
'services.wxpay.private_key' => self::WXPAY_PRIVATE_KEY,
|
||||
'services.wxpay.certificate' => self::WXPAY_CERTIFICATE,
|
||||
'services.wxpay.secret_key' => self::WXPAY_SECRET_KEY,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://api.weixin.qq.com/*' => Http::response(['openid' => 'oMiniOpenid001', 'session_key' => 'sk'], 200),
|
||||
]);
|
||||
$captured = [];
|
||||
$this->fakeWechatApi(function (string $method, string $url, array $options) use (&$captured) {
|
||||
$captured = compact('method', 'url', 'options');
|
||||
return new MockResponse((string) json_encode(['prepay_id' => 'wx-mini-prepay-001']), ['http_code' => 200]);
|
||||
});
|
||||
|
||||
$store = StoreModel::factory()->create();
|
||||
$bill = $this->makeBill($store, '20.00');
|
||||
|
||||
$this->actingAsMiniStore($store);
|
||||
$response = $this->postJson('/mini/payment/online', [
|
||||
'bill_ids' => [$bill->id],
|
||||
'code' => 'wx-login-code',
|
||||
])->assertJsonPath('success', true);
|
||||
|
||||
// 小程序链路:appId=小程序 appid、openid 绑定到 store.openid,mp_openid 保持空
|
||||
$this->assertSame('wx-mini-test', $response->json('data.pay_params.appId'));
|
||||
$body = (array) json_decode((string) ($captured['options']['body'] ?? ''), true);
|
||||
$this->assertSame('wx-mini-test', $body['appid'] ?? null);
|
||||
$store->refresh();
|
||||
$this->assertSame('oMiniOpenid001', $store->openid);
|
||||
$this->assertSame('', (string) $store->mp_openid);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user