微信公众号支付
This commit is contained in:
@@ -16,10 +16,10 @@ use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 小程序在线支付(渠道二选一:旺铺网关 JSAPI / 微信官方支付 APIv3)
|
||||
* 小程序在线支付(渠道二选一:旺铺网关 JSAPI / 微信官方支付 APIv3;场景二选一:小程序 / 公众号 H5)
|
||||
*
|
||||
* 链路:POST /mini/payment/online 下单(返回调起支付参数)
|
||||
* → 小程序 wx.requestPayment 完成支付
|
||||
* 链路:POST /mini/payment/online 下单(scene=mini 小程序 / scene=mp 公众号,返回调起支付参数)
|
||||
* → 小程序 wx.requestPayment / 公众号 H5 WeixinJSBridge 完成支付
|
||||
* → 渠道后台通知(旺铺 /mini/payment/notify;微信 /mini/payment/wechat-notify)验签 + 幂等结账
|
||||
* → 小程序 GET /mini/payment/online/{paymentNo}/query 主动同步支付结果(回调兜底)
|
||||
*/
|
||||
@@ -33,7 +33,10 @@ class OnlinePaymentController extends BaseMiniController
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 发起在线支付:合并选择本店未支付账单 → 旺铺下单 → 返回调起支付参数
|
||||
* 发起在线支付:合并选择本店未支付账单 → 渠道下单 → 返回调起支付参数
|
||||
*
|
||||
* scene=mini(默认):小程序 wx.login code 换 openid,wx.requestPayment 调起;
|
||||
* scene=mp:公众号 H5 网页授权 code 换 openid,WeixinJSBridge getBrandWCPayRequest 调起。
|
||||
* @throws Throwable
|
||||
*/
|
||||
#[PostRoute('/payment/online', authorize: true)]
|
||||
@@ -43,11 +46,13 @@ class OnlinePaymentController extends BaseMiniController
|
||||
'bill_ids' => 'required|array|min:1',
|
||||
'bill_ids.*' => 'integer|distinct',
|
||||
'code' => 'required|string|max:64',
|
||||
'scene' => 'nullable|string|in:mini,mp',
|
||||
'remark' => 'nullable|string|max:255',
|
||||
], [
|
||||
'bill_ids.required' => '请选择要付款的账单',
|
||||
'bill_ids.min' => '请选择要付款的账单',
|
||||
'code.required' => '微信登录凭证缺失,请重新进入小程序',
|
||||
'code.required' => '微信登录/授权凭证缺失,请重新进入',
|
||||
'scene.in' => '支付场景不正确',
|
||||
'remark.max' => '备注超过最大长度',
|
||||
]);
|
||||
|
||||
@@ -58,6 +63,7 @@ class OnlinePaymentController extends BaseMiniController
|
||||
array_map('intval', $data['bill_ids']),
|
||||
(string) $data['code'],
|
||||
(string) ($data['remark'] ?? ''),
|
||||
(string) ($data['scene'] ?? OnlinePaymentService::SCENE_MINI),
|
||||
);
|
||||
|
||||
return $this->success([
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Exceptions\RepositoryException;
|
||||
use App\Models\BillModel;
|
||||
use App\Models\PaymentModel;
|
||||
use App\Services\BillNumberService;
|
||||
use App\Services\WechatMpService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -48,6 +49,8 @@ class PaymentController extends BaseMiniController
|
||||
'wechat_qrcode' => $resolve(site_config('pay.wechat_qrcode', '')),
|
||||
'alipay_qrcode' => $resolve(site_config('pay.alipay_qrcode', '')),
|
||||
'bank_info' => (string) site_config('pay.bank_info', ''),
|
||||
// 公众号 AppID(H5 在微信内置浏览器拼网页授权链接使用,授权回调 code 传给 /mini/payment/online scene=mp)
|
||||
'mp_appid' => app(WechatMpService::class)->appid(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class StoreModel extends Authenticatable
|
||||
'address',
|
||||
'payment_cycle_days',
|
||||
'openid',
|
||||
'mp_openid',
|
||||
'status',
|
||||
'remark',
|
||||
];
|
||||
|
||||
@@ -32,11 +32,17 @@ class OnlinePaymentService
|
||||
/** 在线支付渠道:微信官方支付 */
|
||||
public const string CHANNEL_WECHAT = 'wechat';
|
||||
|
||||
/** 支付场景:微信小程序(wx.login 换 openid,wx.requestPayment 调起) */
|
||||
public const string SCENE_MINI = 'mini';
|
||||
/** 支付场景:公众号 H5(网页授权换 openid,WeixinJSBridge 调起) */
|
||||
public const string SCENE_MP = 'mp';
|
||||
|
||||
public function __construct(
|
||||
protected BillNumberService $billNumber,
|
||||
protected WangpuPayService $wangpu,
|
||||
protected WechatPayService $wxpay,
|
||||
protected WechatMiniService $wechat,
|
||||
protected WechatMpService $wechatMp,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -55,20 +61,29 @@ class OnlinePaymentService
|
||||
* 发起在线支付
|
||||
*
|
||||
* @param array<int, int> $billIds 合并付款的账单ID
|
||||
* @param string $code 小程序 wx.login() 返回的登录凭证
|
||||
* @param string $code 小程序 wx.login() 登录凭证 / 公众号网页授权 code(按 scene 区分)
|
||||
* @param string $scene 支付场景:mini=小程序(默认) / mp=公众号 H5
|
||||
* @return array{0: PaymentModel, 1: array<string, mixed>} 支付单与渠道返回的调起支付参数
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function create(StoreModel $store, array $billIds, string $code, string $remark = ''): array
|
||||
public function create(StoreModel $store, array $billIds, string $code, string $remark = '', string $scene = self::SCENE_MINI): array
|
||||
{
|
||||
$channel = $this->channel();
|
||||
$payMethod = $channel === self::CHANNEL_WECHAT ? PaymentModel::METHOD_WECHAT : PaymentModel::METHOD_WANGPU;
|
||||
|
||||
// 换取付款人 openid 并绑定到门店(下次可直接复用)
|
||||
$openid = $this->wechat->code2session($code);
|
||||
if ((string) $store->openid !== $openid) {
|
||||
$store->openid = $openid;
|
||||
$store->save();
|
||||
// 换取付款人 openid 并绑定到门店(小程序/公众号分场景绑定,两者 openid 维度不同不可混用)
|
||||
if ($scene === self::SCENE_MP) {
|
||||
$openid = $this->wechatMp->code2openid($code);
|
||||
if ((string) $store->mp_openid !== $openid) {
|
||||
$store->mp_openid = $openid;
|
||||
$store->save();
|
||||
}
|
||||
} else {
|
||||
$openid = $this->wechat->code2session($code);
|
||||
if ((string) $store->openid !== $openid) {
|
||||
$store->openid = $openid;
|
||||
$store->save();
|
||||
}
|
||||
}
|
||||
|
||||
[$payment, $bills] = DB::transaction(function () use ($store, $billIds, $openid, $remark, $payMethod) {
|
||||
@@ -122,12 +137,14 @@ class OnlinePaymentService
|
||||
(string) $payment->amount,
|
||||
$openid,
|
||||
'账单合并付款-' . $payment->payment_no,
|
||||
$scene === self::SCENE_MP ? $this->wechatMp->appid() : '',
|
||||
)
|
||||
: $this->wangpu->createOrder([
|
||||
'mer_order_id' => $payment->payment_no,
|
||||
'order_amt' => (string) $payment->amount,
|
||||
'open_id' => $openid,
|
||||
'sub_appid' => $this->subAppid(),
|
||||
'sub_appid' => $this->subAppid($scene),
|
||||
'payway_code' => $this->wangpu->paywayCode($scene),
|
||||
'order_title' => '账单合并付款-' . $payment->payment_no,
|
||||
'notifyurl' => $this->wangpu->notifyUrl(),
|
||||
]);
|
||||
@@ -391,10 +408,13 @@ class OnlinePaymentService
|
||||
}
|
||||
|
||||
/**
|
||||
* 下单微信子 appid(默认取小程序 appid)
|
||||
* 下单微信子 appid(默认取小程序 appid;公众号 H5 场景取公众号 appid)
|
||||
*/
|
||||
protected function subAppid(): string
|
||||
protected function subAppid(string $scene = self::SCENE_MINI): string
|
||||
{
|
||||
if ($scene === self::SCENE_MP) {
|
||||
return $this->wechatMp->appid();
|
||||
}
|
||||
$subAppid = (string) site_config('wangpu.sub_appid', '');
|
||||
if ($subAppid === '') {
|
||||
$subAppid = (string) config('services.wangpu.sub_appid', '');
|
||||
|
||||
@@ -90,6 +90,20 @@ class WangpuPayService
|
||||
return rtrim((string) config('app.url'), '/') . '/mini/payment/notify';
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付方式代码(公众号 H5 场景优先取 mp_payway_code,留空回退 payway_code)
|
||||
*/
|
||||
public function paywayCode(string $scene = 'mini'): string
|
||||
{
|
||||
if ($scene === 'mp') {
|
||||
$mpPayway = $this->config('mp_payway_code');
|
||||
if ($mpPayway !== '') {
|
||||
return $mpPayway;
|
||||
}
|
||||
}
|
||||
return $this->config('payway_code');
|
||||
}
|
||||
|
||||
/**
|
||||
* 网关 POST 请求:业务报文加密装信封 → JSON 发送 → 校验应答码 → 解密应答
|
||||
*
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 微信公众号服务(网页授权 code 换 openid,公众号 JSAPI 支付前置)
|
||||
*
|
||||
* H5 页面在微信内置浏览器打开 → 网页授权(snsapi_base)回调带回 code
|
||||
* → 本服务以 code 换付款人 openid(公众号维度,与小程序 openid 不同,不可混用)
|
||||
*/
|
||||
class WechatMpService
|
||||
{
|
||||
/**
|
||||
* 网页授权 code 换 openid(sns/oauth2/access_token)
|
||||
*
|
||||
* @return string 用户公众号维度 openid
|
||||
* @throws RepositoryException 未配置公众号或授权失败
|
||||
*/
|
||||
public function code2openid(string $code): string
|
||||
{
|
||||
$appid = $this->appid();
|
||||
$secret = $this->config('mp_secret');
|
||||
if ($appid === '' || $secret === '') {
|
||||
throw new RepositoryException('微信公众号未配置 AppID/Secret,请联系管理员');
|
||||
}
|
||||
|
||||
$result = Http::timeout(10)->withOptions([
|
||||
'verify' => false, // 禁用 SSL 证书验证
|
||||
])->get('https://api.weixin.qq.com/sns/oauth2/access_token', [
|
||||
'appid' => $appid,
|
||||
'secret' => $secret,
|
||||
'code' => $code,
|
||||
'grant_type' => 'authorization_code',
|
||||
])->json();
|
||||
|
||||
$openid = is_array($result) ? (string) ($result['openid'] ?? '') : '';
|
||||
if ($openid === '') {
|
||||
Log::warning('微信网页授权失败', ['response' => $result]);
|
||||
throw new RepositoryException('微信网页授权失败:' . ($result['errmsg'] ?? '请重新授权'));
|
||||
}
|
||||
|
||||
return $openid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公众号 AppID(公众号 JSAPI 下单 appid、H5 拼网页授权链接使用)
|
||||
*/
|
||||
public function appid(): string
|
||||
{
|
||||
return $this->config('mp_appid');
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取应用配置:后台站点配置(微信应用配置分组)优先,为空回退 config/services.php(env)
|
||||
*/
|
||||
protected function config(string $key): string
|
||||
{
|
||||
$value = site_config('wechat.' . $key, '');
|
||||
if ($value === null || $value === '') {
|
||||
$value = match ($key) {
|
||||
'mp_appid' => config('services.wechat.mp.appid', ''),
|
||||
'mp_secret' => config('services.wechat.mp.secret', ''),
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
return trim((string) $value);
|
||||
}
|
||||
}
|
||||
@@ -29,19 +29,22 @@ class WechatPayService
|
||||
protected ?Application $application = null;
|
||||
|
||||
/**
|
||||
* 统一下单(小程序 JSAPI)
|
||||
* 统一下单(JSAPI:小程序 / 公众号 H5 通用)
|
||||
*
|
||||
* @param string $paymentNo 商户订单号(本系统支付单号)
|
||||
* @param string $amountYuan 金额(元,两位小数)
|
||||
* @param string $openid 付款人 openid
|
||||
* @param string $openid 付款人 openid(按下单 appid 维度:小程序 openid 或公众号 openid)
|
||||
* @param string $description 订单标题
|
||||
* @return array<string, mixed> wx.requestPayment 调起参数(timeStamp/nonceStr/package/signType/paySign)
|
||||
* @param string $appid 下单应用 appid(公众号 JSAPI 传公众号 appid;留空取配置的小程序 appid)
|
||||
* @return array<string, mixed> 调起支付参数(appId/timeStamp/nonceStr/package/signType/paySign,
|
||||
* 小程序喂 wx.requestPayment,公众号 H5 喂 WeixinJSBridge getBrandWCPayRequest,结构同构)
|
||||
* @throws RepositoryException 未配置或下单失败
|
||||
*/
|
||||
public function createOrder(string $paymentNo, string $amountYuan, string $openid, string $description): array
|
||||
public function createOrder(string $paymentNo, string $amountYuan, string $openid, string $description, string $appid = ''): array
|
||||
{
|
||||
$appid = trim($appid) !== '' ? trim($appid) : $this->appid();
|
||||
$result = $this->call('POST', '/v3/pay/transactions/jsapi', [
|
||||
'appid' => $this->appid(),
|
||||
'appid' => $appid,
|
||||
'mchid' => $this->config('mch_id'),
|
||||
'description' => mb_substr($description, 0, 127),
|
||||
'out_trade_no' => $paymentNo,
|
||||
@@ -59,8 +62,9 @@ class WechatPayService
|
||||
throw new RepositoryException('微信支付下单失败:应答缺少 prepay_id');
|
||||
}
|
||||
|
||||
$params = $this->app()->getUtils()->buildMiniAppConfig($prepayId, $this->appid());
|
||||
Log::driver('pay')->info('微信支付下单成功', ['out_trade_no' => $paymentNo, 'prepay_id' => $prepayId]);
|
||||
// buildMiniAppConfig 即 buildBridgeConfig(小程序 wx.requestPayment 与公众号 WeixinJSBridge 参数同构)
|
||||
$params = $this->app()->getUtils()->buildMiniAppConfig($prepayId, $appid);
|
||||
Log::driver('pay')->info('微信支付下单成功', ['out_trade_no' => $paymentNo, 'prepay_id' => $prepayId, 'appid' => $appid]);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user