添加虚拟支付
This commit is contained in:
@@ -9,3 +9,14 @@ username = root
|
||||
password = root
|
||||
hostport = 3306
|
||||
prefix = fa_
|
||||
|
||||
[wechat]
|
||||
; 虚拟支付:MP 后台「虚拟支付-基本配置」
|
||||
vp_offer_id =
|
||||
vp_app_key =
|
||||
vp_sandbox_app_key =
|
||||
vp_env = 0
|
||||
vp_coin_per_yuan = 1
|
||||
; 消息推送 URL:{app.url}/api/order/virtualPayNotify ,加解密方式选明文时可只填 Token
|
||||
vp_msg_token =
|
||||
vp_aes_key =
|
||||
|
||||
@@ -5,13 +5,14 @@ namespace app\api\controller;
|
||||
use app\common\controller\Api;
|
||||
use app\common\service\OrderService;
|
||||
use app\common\service\ToolService;
|
||||
use app\common\service\VirtualPayService;
|
||||
use think\Exception;
|
||||
use think\exception\HttpResponseException;
|
||||
use think\Log;
|
||||
|
||||
class Order extends Api
|
||||
{
|
||||
protected $noNeedLogin = ['payNotify', 'detail'];
|
||||
protected $noNeedLogin = ['payNotify', 'virtualPayNotify', 'detail'];
|
||||
protected $noNeedRight = ['*'];
|
||||
|
||||
/**
|
||||
@@ -296,6 +297,143 @@ class Order extends Api
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟支付收银台成功后确认入账
|
||||
* @ApiMethod (POST)
|
||||
*/
|
||||
public function virtualPayConfirm()
|
||||
{
|
||||
try {
|
||||
$orderNo = $this->request->post('orderNo', '');
|
||||
if (!$orderNo) {
|
||||
$this->error('订单号不能为空');
|
||||
}
|
||||
$res = OrderService::confirmVirtualPay($orderNo, $this->auth->id);
|
||||
$this->success('支付成功', $res);
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof HttpResponseException) {
|
||||
throw $e;
|
||||
}
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟支付消息推送。MP 后台消息推送 URL 填本接口,明文模式即可
|
||||
*/
|
||||
public function virtualPayNotify()
|
||||
{
|
||||
$echostr = (string)$this->request->get('echostr', '');
|
||||
if ($echostr !== '') {
|
||||
try {
|
||||
$this->assertVirtualPaySignature();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('虚拟支付消息验签失败 ' . $e->getMessage());
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
echo $echostr;
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw = trim((string)$this->request->getInput());
|
||||
$asJson = strpos($raw, '{') === 0;
|
||||
Log::info('虚拟支付推送[进入] ' . $raw);
|
||||
try {
|
||||
$payload = $this->parseVirtualPayBody($raw);
|
||||
if (!empty($payload['Encrypt'])) {
|
||||
$plain = VirtualPayService::decryptMessage(
|
||||
(string)$payload['Encrypt'],
|
||||
(string)$this->request->get('msg_signature', ''),
|
||||
(string)$this->request->get('nonce', ''),
|
||||
(string)$this->request->get('timestamp', '')
|
||||
);
|
||||
$payload = $this->parseVirtualPayBody($plain);
|
||||
} else {
|
||||
$this->assertVirtualPaySignature();
|
||||
}
|
||||
$reply = VirtualPayService::handleMessage($payload);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('虚拟支付推送处理失败 ' . $e->getMessage());
|
||||
$message = $e->getMessage();
|
||||
if (strpos($message, '订单不存在') !== false || strpos($message, '获取订单数据失败') !== false) {
|
||||
$reply = ['ErrCode' => 0, 'ErrMsg' => 'success'];
|
||||
} else {
|
||||
$reply = ['ErrCode' => 1, 'ErrMsg' => $message];
|
||||
}
|
||||
}
|
||||
$this->replyVirtualPay(isset($reply) ? $reply : ['ErrCode' => 1, 'ErrMsg' => 'fail'], $asJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验消息推送签名
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function assertVirtualPaySignature()
|
||||
{
|
||||
$token = (string)config('common.virtual_pay.msg_token');
|
||||
if ($token === '') {
|
||||
throw new \Exception('未配置虚拟支付消息令牌');
|
||||
}
|
||||
$parts = [
|
||||
$token,
|
||||
(string)$this->request->get('timestamp', ''),
|
||||
(string)$this->request->get('nonce', ''),
|
||||
];
|
||||
sort($parts, SORT_STRING);
|
||||
if (sha1(implode($parts)) !== (string)$this->request->get('signature', '')) {
|
||||
throw new \Exception('消息签名校验失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $raw
|
||||
* @return array
|
||||
*/
|
||||
private function parseVirtualPayBody($raw)
|
||||
{
|
||||
$raw = trim((string)$raw);
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
if ($raw[0] === '{') {
|
||||
$data = json_decode($raw, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
$xml = simplexml_load_string($raw, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NONET);
|
||||
if (!$xml) {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode(json_encode($xml), true);
|
||||
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param bool $asJson
|
||||
*/
|
||||
private function replyVirtualPay(array $data, $asJson)
|
||||
{
|
||||
if ($asJson) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
header('Content-Type: text/xml; charset=utf-8');
|
||||
$xml = '<xml>';
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_int($value) || is_float($value)) {
|
||||
$xml .= '<' . $key . '>' . $value . '</' . $key . '>';
|
||||
} else {
|
||||
$xml .= '<' . $key . '><![CDATA[' . $value . ']]></' . $key . '>';
|
||||
}
|
||||
}
|
||||
$xml .= '</xml>';
|
||||
echo $xml;
|
||||
exit;
|
||||
}
|
||||
|
||||
public function businessTime()
|
||||
{
|
||||
$res = [
|
||||
|
||||
@@ -168,8 +168,11 @@ class OrderService
|
||||
'tradeNo' => $orderData['order_no'],
|
||||
'interactId' => 0,
|
||||
'transactionId' => 0,
|
||||
'payInfo' => self::pay($orderData['order_no'], $total_amount, $openid), //统一下单接口
|
||||
'payInfo' => self::pay($orderData['order_no'], $total_amount, $openid),
|
||||
];
|
||||
if (!empty($orderData['payInfo']['paid'])) {
|
||||
$orderData['order_status'] = self::ORDER_STATUS_WAIT_SHIP;
|
||||
}
|
||||
//存入缓存
|
||||
Cache::set($lockOrderKey, json_encode($orderData), 30);
|
||||
|
||||
@@ -300,11 +303,15 @@ class OrderService
|
||||
'tradeNo' => $orderData['order_no'],
|
||||
'interactId' => 1,
|
||||
'transactionId' => 0,
|
||||
'payInfo' => self::pay($orderInfo->order_no, $orderInfo->total_amount, $openid), //统一下单接口
|
||||
'payInfo' => self::pay($orderInfo->order_no, $orderInfo->payment_amount, $openid),
|
||||
];
|
||||
Log::info('微信下单[下单成功] ' . json_encode([
|
||||
if (!empty($orderData['payInfo']['paid'])) {
|
||||
$orderData['order_status'] = self::ORDER_STATUS_WAIT_SHIP;
|
||||
}
|
||||
Log::info('虚拟支付下单[下单成功] ' . json_encode([
|
||||
'order_no' => $orderNo,
|
||||
'payInfo' => $orderData['payInfo'],
|
||||
'paid' => $orderData['payInfo']['paid'] ?? 0,
|
||||
'mode' => $orderData['payInfo']['mode'] ?? '',
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return $orderData;
|
||||
@@ -471,84 +478,18 @@ class OrderService
|
||||
'payment_amount'=> $orderInfo->payment_amount,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
//金额校验(v3 通知金额单位为分)
|
||||
$checkMoney = sprintf('%.1f', $total / 100) == sprintf('%.1f', $orderInfo->payment_amount);
|
||||
if (!$checkMoney) {
|
||||
Log::error($prefix . '金额校验失败: ' . json_encode([
|
||||
'notify_amount' => sprintf('%.2f', $total / 100),
|
||||
'order_amount' => sprintf('%.2f', $orderInfo->payment_amount),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
throw new \Exception('金额错误:' . sprintf('%.2f', $total / 100) . '----' . sprintf('%.2f', $orderInfo->payment_amount));
|
||||
}
|
||||
if (($message->trade_state ?? '') != 'SUCCESS') {
|
||||
Log::error($prefix . '支付状态异常: trade_state=' . ($message->trade_state ?? ''));
|
||||
throw new \Exception('支付失败:' . ($message->trade_state ?? ''));
|
||||
}
|
||||
|
||||
//如果为待支付,才可更新为已支付。防止重复更新状态,导致重复发货。
|
||||
if ($orderInfo->order_status != self::ORDER_STATUS_WAIT_PAY) {
|
||||
Log::info($prefix . '订单已处理过,跳过状态更新(幂等): ' . json_encode([
|
||||
'order_no' => $order_no,
|
||||
'order_status' => $orderInfo->order_status,
|
||||
'status_text' => self::ORDER_STATUS_MAP[$orderInfo->order_status] ?? '未知',
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$paymentData = [
|
||||
'order_no' => $order_no,
|
||||
'pay_status' => 'SUCCESS',
|
||||
'amount' => $total / 100,
|
||||
'currency' => $message->amount['currency'] ?? 'CNY',
|
||||
'pay_type' => '1', // 1微信 2支付宝
|
||||
'pay_way' => $message->trade_type ?? 'JSAPI',
|
||||
'trace_no' => $message->transaction_id ?? '',
|
||||
'pay_time' => time(),
|
||||
'pay_success_time' => strtotime((string)($message->success_time ?? '')) ?: time(),
|
||||
];
|
||||
Log::info($prefix . '开始更新订单状态: ' . json_encode($paymentData, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
try {
|
||||
Db::transaction(function () use ($order_no, $paymentData, $orderInfo, $prefix) {
|
||||
$updateRows = self::where('order_no', $order_no)->update(['order_status' => self::ORDER_STATUS_WAIT_SHIP]);
|
||||
Log::info($prefix . '订单状态更新: order_no=' . $order_no
|
||||
. ' 10(待支付) -> ' . self::ORDER_STATUS_WAIT_SHIP . '(待发货) 影响行数=' . $updateRows);
|
||||
|
||||
$payment = OrderPaymentService::create($paymentData);
|
||||
Log::info($prefix . '支付流水写入: ' . json_encode([
|
||||
'payment_id' => $payment->id ?? 0,
|
||||
'order_no' => $payment->order_no ?? '',
|
||||
'pay_status' => $payment->pay_status ?? '',
|
||||
'amount' => $payment->amount ?? 0,
|
||||
'trace_no' => $payment->trace_no ?? '',
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
if ($orderInfo->order_type == self::ORDER_TYPE_FIRST_PAYMENT) {
|
||||
Log::info($prefix . '需求首付款订单,处理合同成交: parent_order_no=' . $orderInfo->parent_order_no);
|
||||
DemandService::customerContractFinishedDeal($orderInfo->parent_order_no);
|
||||
} elseif ($orderInfo->order_type == self::ORDER_TYPE_FINAL_PAYMENT) {
|
||||
Log::info($prefix . '需求尾款订单,处理验收结算: parent_order_no=' . $orderInfo->parent_order_no);
|
||||
DemandService::customerWorkAcceptFinishedDeal($orderInfo->parent_order_no);
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Log::error($prefix . '事务执行失败已回滚: msg=' . $e->getMessage()
|
||||
. ' file=' . $e->getFile() . ':' . $e->getLine());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// 复核:确认状态与支付流水真的落库了
|
||||
$newOrder = OrderService::getOne(['order_no' => $order_no]);
|
||||
$payment = OrderPaymentService::getOne(['order_no' => $order_no]);
|
||||
Log::info($prefix . '回调处理成功: ' . json_encode([
|
||||
'order_no' => $order_no,
|
||||
'new_order_status'=> $newOrder->order_status ?? null,
|
||||
'new_status_text' => self::ORDER_STATUS_MAP[$newOrder->order_status ?? -1] ?? '未知',
|
||||
'payment_id' => $payment->id ?? 0,
|
||||
'payment_status' => $payment->pay_status ?? '',
|
||||
'trace_no' => $payment->trace_no ?? '',
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
self::finishPaid(
|
||||
$order_no,
|
||||
(int)$total,
|
||||
(string)($message->transaction_id ?? ''),
|
||||
(string)($message->trade_type ?? 'JSAPI'),
|
||||
strtotime((string)($message->success_time ?? '')) ?: time(),
|
||||
$traceId
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -568,35 +509,131 @@ class OrderService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $order_sn
|
||||
* @param $total_fee
|
||||
* @param $openid
|
||||
* 支付成功入账。虚拟支付确认和微信支付回调共用,重复通知直接返回
|
||||
* @param string $orderNo
|
||||
* @param int $amountFen 实付金额,单位分
|
||||
* @param string $transactionId
|
||||
* @param string $payWay
|
||||
* @param int $paidTime
|
||||
* @param string $traceId
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function finishPaid($orderNo, $amountFen, $transactionId, $payWay, $paidTime, $traceId = '')
|
||||
{
|
||||
$prefix = '订单支付入账 trace=' . $traceId . ' ';
|
||||
try {
|
||||
Db::transaction(function () use ($orderNo, $amountFen, $transactionId, $payWay, $paidTime, $prefix) {
|
||||
$orderInfo = self::where('order_no', $orderNo)->lock(true)->find();
|
||||
if (!$orderInfo) {
|
||||
Log::error($prefix . '订单不存在: ' . $orderNo);
|
||||
throw new \Exception('获取订单数据失败');
|
||||
}
|
||||
if ((int)$orderInfo->order_status !== self::ORDER_STATUS_WAIT_PAY) {
|
||||
Log::info($prefix . '订单已处理过,跳过状态更新: ' . json_encode([
|
||||
'order_no' => $orderNo,
|
||||
'order_status' => $orderInfo->order_status,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
return;
|
||||
}
|
||||
$checkMoney = sprintf('%.1f', $amountFen / 100) == sprintf('%.1f', $orderInfo->payment_amount);
|
||||
if (!$checkMoney) {
|
||||
Log::error($prefix . '金额校验失败: ' . json_encode([
|
||||
'notify_amount' => sprintf('%.2f', $amountFen / 100),
|
||||
'order_amount' => sprintf('%.2f', $orderInfo->payment_amount),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
throw new \Exception('金额错误:' . sprintf('%.2f', $amountFen / 100) . '----' . sprintf('%.2f', $orderInfo->payment_amount));
|
||||
}
|
||||
$paymentData = [
|
||||
'order_no' => $orderNo,
|
||||
'pay_status' => 'SUCCESS',
|
||||
'amount' => $amountFen / 100,
|
||||
'currency' => 'CNY',
|
||||
'pay_type' => '1',
|
||||
'pay_way' => $payWay ?: 'XPAY',
|
||||
'trace_no' => $transactionId,
|
||||
'pay_time' => time(),
|
||||
'pay_success_time' => $paidTime > 0 ? $paidTime : time(),
|
||||
];
|
||||
self::where('order_no', $orderNo)->update(['order_status' => self::ORDER_STATUS_WAIT_SHIP]);
|
||||
OrderPaymentService::create($paymentData);
|
||||
if ($orderInfo->order_type == self::ORDER_TYPE_FIRST_PAYMENT) {
|
||||
DemandService::customerContractFinishedDeal($orderInfo->parent_order_no);
|
||||
} elseif ($orderInfo->order_type == self::ORDER_TYPE_FINAL_PAYMENT) {
|
||||
DemandService::customerWorkAcceptFinishedDeal($orderInfo->parent_order_no);
|
||||
}
|
||||
Log::info($prefix . '入账成功 ' . json_encode($paymentData, JSON_UNESCAPED_UNICODE));
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
Log::error($prefix . '入账失败: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟支付下单。上一笔现金单已经支付成功时直接入账,不再拉起收银台
|
||||
* @param string $order_sn
|
||||
* @param float|string $total_fee
|
||||
* @param string $openid
|
||||
* @param string $body
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function pay($order_sn, $total_fee, $openid, string $body = '软件产品服务')
|
||||
{
|
||||
if (!($body && $order_sn && $total_fee && $openid)) {
|
||||
Log::error('微信下单[参数错误] ' . json_encode([
|
||||
'body' => $body,
|
||||
'order_sn' => $order_sn,
|
||||
Log::error('虚拟支付下单[参数错误] ' . json_encode([
|
||||
'body' => $body,
|
||||
'order_sn' => $order_sn,
|
||||
'total_fee' => $total_fee,
|
||||
'openid' => $openid,
|
||||
'openid' => $openid,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
throw new \Exception('支付参数错误2');
|
||||
}
|
||||
|
||||
$EasyWeChatService = new EasyWeChatService();
|
||||
$payRes = $EasyWeChatService->unify($body, $order_sn, $total_fee, $openid);
|
||||
if (!($payRes && !empty($payRes['prepay_id']))) {
|
||||
Log::error('微信下单[统一下单失败] order_sn=' . $order_sn . ' res=' . json_encode($payRes, JSON_UNESCAPED_UNICODE));
|
||||
throw new \Exception('统一下单接口调用失败:' . ($payRes['message'] ?? ($payRes['errmsg'] ?? '未知错误')));
|
||||
$current = self::getOne(['order_no' => $order_sn]);
|
||||
if ($current && !in_array((int)$current->order_status, [
|
||||
self::ORDER_STATUS_WAIT_PAY,
|
||||
self::ORDER_STATUS_WAIT_SHIP,
|
||||
self::ORDER_STATUS_SHIPPED,
|
||||
self::ORDER_STATUS_FINISHED,
|
||||
], true)) {
|
||||
throw new \Exception('订单状态不可支付');
|
||||
}
|
||||
if (VirtualPayService::settleIfPaid($order_sn, $openid)) {
|
||||
return ['paid' => 1];
|
||||
}
|
||||
Log::info('微信下单[prepay_id] order_sn=' . $order_sn . ' prepay_id=' . $payRes['prepay_id']);
|
||||
$payData = $EasyWeChatService->sdkConfig($payRes['prepay_id']);
|
||||
|
||||
return $payData;
|
||||
return VirtualPayService::createClientPay($order_sn, $total_fee, $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端支付成功后确认:查现金单、扣代币、更新订单
|
||||
* @param string $orderNo
|
||||
* @param int $userId
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function confirmVirtualPay($orderNo, $userId)
|
||||
{
|
||||
$order = self::getOne(['order_no' => $orderNo, 'user_id' => $userId]);
|
||||
if (!$order) {
|
||||
throw new \Exception('订单不存在');
|
||||
}
|
||||
if ((int)$order->order_status !== self::ORDER_STATUS_WAIT_PAY) {
|
||||
return ['order_status' => (int)$order->order_status];
|
||||
}
|
||||
$openid = \app\common\model\User::where('id', $userId)->value('openid');
|
||||
if (!$openid) {
|
||||
throw new \Exception('用户未绑定微信');
|
||||
}
|
||||
if (!VirtualPayService::settleIfPaid($orderNo, $openid)) {
|
||||
throw new \Exception('支付结果确认中,请稍后在订单里再次点击支付,不会重复扣款');
|
||||
}
|
||||
$fresh = self::getOne(['order_no' => $orderNo]);
|
||||
|
||||
return ['order_status' => (int)($fresh->order_status ?? self::ORDER_STATUS_WAIT_SHIP)];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,6 +42,10 @@ class UserService
|
||||
if (!$account) {
|
||||
$acRes = $user->account()->save([]);
|
||||
}
|
||||
// 虚拟支付用户态签名依赖 session_key,只放缓存,不回给前端
|
||||
if (!empty($result['session_key']) && !empty($user->id)) {
|
||||
VirtualPayService::saveSessionKey($user->id, $result['session_key']);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\User;
|
||||
use EasyWeChat\Kernel\Encryptor;
|
||||
use think\Cache;
|
||||
use think\Log;
|
||||
|
||||
/**
|
||||
* 小程序虚拟支付(代币充值)
|
||||
* 文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html
|
||||
* 客户端:wx.requestVirtualPayment,mode=short_series_coin
|
||||
* 服务器:/xpay/query_order 确认现金单,/xpay/currency_pay 扣减代币
|
||||
*/
|
||||
class VirtualPayService
|
||||
{
|
||||
const SESSION_PREFIX = 'wx_session_key:';
|
||||
const TRADE_PREFIX = 'vp_trade:';
|
||||
const LATEST_PREFIX = 'vp_order_latest:';
|
||||
const CACHE_TTL = 1296000; // 15天
|
||||
|
||||
/**
|
||||
* 登录成功后缓存 session_key,用户态签名要用
|
||||
* @param int $userId
|
||||
* @param string $sessionKey
|
||||
*/
|
||||
public static function saveSessionKey($userId, $sessionKey)
|
||||
{
|
||||
if (!$userId || $sessionKey === '') {
|
||||
return;
|
||||
}
|
||||
Cache::set(self::SESSION_PREFIX . $userId, $sessionKey, self::CACHE_TTL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 wx.requestVirtualPayment 参数。signData 必须原样传给客户端,不能再 JSON.stringify
|
||||
* @param string $orderNo
|
||||
* @param float|string $amountYuan
|
||||
* @param string $openid
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function createClientPay($orderNo, $amountYuan, $openid)
|
||||
{
|
||||
$offerId = (string)config('common.virtual_pay.offer_id');
|
||||
if ($offerId === '') {
|
||||
throw new \Exception('未配置虚拟支付 OfferID');
|
||||
}
|
||||
$buyQuantity = self::coinQuantity($amountYuan);
|
||||
$outTradeNo = 'V' . date('ymdHis') . substr(md5(uniqid('', true)), 0, 10);
|
||||
$signData = json_encode([
|
||||
'offerId' => $offerId,
|
||||
'buyQuantity' => $buyQuantity,
|
||||
'env' => self::env(),
|
||||
'currencyType' => 'CNY',
|
||||
'outTradeNo' => $outTradeNo,
|
||||
'attach' => (string)$orderNo,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($signData === false) {
|
||||
throw new \Exception('虚拟支付参数编码失败');
|
||||
}
|
||||
$sessionKey = self::sessionKeyByOpenid($openid);
|
||||
Cache::set(self::TRADE_PREFIX . $outTradeNo, (string)$orderNo, self::CACHE_TTL);
|
||||
Cache::set(self::LATEST_PREFIX . $orderNo, $outTradeNo, self::CACHE_TTL);
|
||||
Log::info('虚拟支付下单 ' . json_encode([
|
||||
'order_no' => $orderNo,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'buy_quantity' => $buyQuantity,
|
||||
'env' => self::env(),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return [
|
||||
'mode' => 'short_series_coin',
|
||||
'signData' => $signData,
|
||||
'paySig' => hash_hmac('sha256', 'requestVirtualPayment&' . $signData, self::appKey()),
|
||||
'signature' => hash_hmac('sha256', $signData, $sessionKey),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 现金单已支付时扣减代币并入账。没有可确认的支付单时返回 false,调用方再拉起新的支付
|
||||
* @param string $orderNo
|
||||
* @param string $openid
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function settleIfPaid($orderNo, $openid)
|
||||
{
|
||||
$order = OrderService::getOne(['order_no' => $orderNo]);
|
||||
if (!$order) {
|
||||
throw new \Exception('订单不存在');
|
||||
}
|
||||
if ((int)$order->order_status !== OrderService::ORDER_STATUS_WAIT_PAY) {
|
||||
return in_array((int)$order->order_status, [
|
||||
OrderService::ORDER_STATUS_WAIT_SHIP,
|
||||
OrderService::ORDER_STATUS_SHIPPED,
|
||||
OrderService::ORDER_STATUS_FINISHED,
|
||||
], true);
|
||||
}
|
||||
$outTradeNo = Cache::get(self::LATEST_PREFIX . $orderNo);
|
||||
if (!$outTradeNo) {
|
||||
return false;
|
||||
}
|
||||
$res = self::queryOrder($openid, $outTradeNo);
|
||||
if ((int)($res['errcode'] ?? -1) !== 0) {
|
||||
Log::info('虚拟支付查单未支付 ' . json_encode([
|
||||
'order_no' => $orderNo,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'res' => $res,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
return false;
|
||||
}
|
||||
$wxOrder = $res['order'] ?? [];
|
||||
$status = (int)($wxOrder['status'] ?? 0);
|
||||
// 2已支付待发货 3发货中 4已发货。代币由平台入账,这三种都表示现金已支付
|
||||
if (!in_array($status, [2, 3, 4], true)) {
|
||||
return false;
|
||||
}
|
||||
self::deductCoins($order, $openid, $wxOrder);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理虚拟支付消息推送
|
||||
* @param array $msg
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function handleMessage(array $msg)
|
||||
{
|
||||
$event = (string)($msg['Event'] ?? '');
|
||||
if ($event === 'xpay_coin_pay_notify') {
|
||||
self::finishFromCoinNotify($msg);
|
||||
return ['ErrCode' => 0, 'ErrMsg' => 'success'];
|
||||
}
|
||||
if ($event === 'xpay_refund_notify') {
|
||||
self::handleRefundNotify($msg);
|
||||
return ['ErrCode' => 0, 'ErrMsg' => 'success'];
|
||||
}
|
||||
if ($event === 'xpay_subscribe_ios_refund_query_notify') {
|
||||
return self::iosRefundQuery($msg);
|
||||
}
|
||||
Log::info('虚拟支付推送 ' . $event . ' ' . json_encode($msg, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return ['ErrCode' => 0, 'ErrMsg' => 'success'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密安全模式下的消息,返回明文字符串
|
||||
* @param string $encrypt
|
||||
* @param string $msgSignature
|
||||
* @param string $nonce
|
||||
* @param string $timestamp
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function decryptMessage($encrypt, $msgSignature, $nonce, $timestamp)
|
||||
{
|
||||
$token = (string)config('common.virtual_pay.msg_token');
|
||||
$aesKey = (string)config('common.virtual_pay.aes_key');
|
||||
$appId = (string)config('common.wechat.app_id');
|
||||
if ($token === '' || $aesKey === '' || $appId === '') {
|
||||
throw new \Exception('未配置虚拟支付消息加解密参数');
|
||||
}
|
||||
$encryptor = new Encryptor($appId, $token, $aesKey);
|
||||
|
||||
return $encryptor->decrypt($encrypt, $msgSignature, $nonce, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $openid
|
||||
* @param string $outTradeNo
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function queryOrder($openid, $outTradeNo)
|
||||
{
|
||||
return self::postXpay('/xpay/query_order', [
|
||||
'openid' => $openid,
|
||||
'env' => self::env(),
|
||||
'order_id' => (string)$outTradeNo,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验现金金额后扣减代币,再把业务订单标成已支付
|
||||
* @param \think\Model $order
|
||||
* @param string $openid
|
||||
* @param array $wxOrder
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function deductCoins($order, $openid, array $wxOrder)
|
||||
{
|
||||
$paidFee = (int)($wxOrder['paid_fee'] ?? 0);
|
||||
if ($paidFee <= 0) {
|
||||
$paidFee = (int)($wxOrder['order_fee'] ?? 0);
|
||||
}
|
||||
$expect = self::amountFen($order->payment_amount);
|
||||
if ($paidFee !== $expect) {
|
||||
Log::error('虚拟支付金额不一致 ' . json_encode([
|
||||
'order_no' => $order->order_no,
|
||||
'paid_fee' => $paidFee,
|
||||
'expect' => $expect,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
throw new \Exception('支付金额与订单不一致');
|
||||
}
|
||||
$buyQuantity = self::coinQuantity($order->payment_amount);
|
||||
$res = self::postXpay('/xpay/currency_pay', [
|
||||
'openid' => $openid,
|
||||
'env' => self::env(),
|
||||
'user_ip' => self::userIp(),
|
||||
'amount' => $buyQuantity,
|
||||
'order_id' => (string)$order->order_no,
|
||||
'payitem' => json_encode([[
|
||||
'productid' => 'order',
|
||||
'unit_price' => $buyQuantity,
|
||||
'quantity' => 1,
|
||||
]], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'remark' => '订单' . $order->order_no,
|
||||
], self::sessionKeyByOpenid($openid));
|
||||
$errcode = (int)($res['errcode'] ?? -1);
|
||||
// 268490004:重复扣减,上一笔已经成功
|
||||
if ($errcode !== 0 && $errcode !== 268490004) {
|
||||
if ($errcode === 268490006) {
|
||||
throw new \Exception('支付确认中,请稍后在订单里再次点击支付,不会重复扣款');
|
||||
}
|
||||
throw new \Exception('代币扣减失败:' . ($res['errmsg'] ?? '未知错误'));
|
||||
}
|
||||
$transactionId = (string)($wxOrder['wxpay_order_id'] ?? ($wxOrder['channel_order_id'] ?? ''));
|
||||
$paidTime = (int)($wxOrder['paid_time'] ?? 0);
|
||||
OrderService::finishPaid(
|
||||
$order->order_no,
|
||||
$expect,
|
||||
$transactionId,
|
||||
'XPAY',
|
||||
$paidTime > 0 ? $paidTime : time(),
|
||||
'settle'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $msg
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function finishFromCoinNotify(array $msg)
|
||||
{
|
||||
$outTradeNo = (string)($msg['OutTradeNo'] ?? '');
|
||||
$orderNo = Cache::get(self::TRADE_PREFIX . $outTradeNo) ?: $outTradeNo;
|
||||
$coinInfo = isset($msg['CoinInfo']) && is_array($msg['CoinInfo']) ? $msg['CoinInfo'] : [];
|
||||
$order = OrderService::getOne(['order_no' => $orderNo]);
|
||||
if (!$order) {
|
||||
Log::error('虚拟支付代币通知未找到订单 ' . $outTradeNo);
|
||||
return;
|
||||
}
|
||||
$fen = isset($coinInfo['ActualPrice']) ? (int)$coinInfo['ActualPrice'] : self::amountFen($order->payment_amount);
|
||||
$payInfo = isset($msg['WeChatPayInfo']) && is_array($msg['WeChatPayInfo']) ? $msg['WeChatPayInfo'] : [];
|
||||
$paidTime = (int)($payInfo['PaidTime'] ?? 0);
|
||||
OrderService::finishPaid(
|
||||
$orderNo,
|
||||
$fen,
|
||||
(string)($payInfo['TransactionId'] ?? ''),
|
||||
'XPAY',
|
||||
$paidTime > 0 ? $paidTime : time(),
|
||||
'coin_notify'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未发货的订单退款成功后改为已退款。已发货或需求单只记日志,避免把已结算合同改回去
|
||||
* @param array $msg
|
||||
*/
|
||||
private static function handleRefundNotify(array $msg)
|
||||
{
|
||||
Log::info('虚拟支付退款通知 ' . json_encode($msg, JSON_UNESCAPED_UNICODE));
|
||||
if ((int)($msg['RetCode'] ?? 1) !== 0) {
|
||||
return;
|
||||
}
|
||||
$mchOrderId = (string)($msg['MchOrderId'] ?? '');
|
||||
$orderNo = Cache::get(self::TRADE_PREFIX . $mchOrderId) ?: $mchOrderId;
|
||||
$order = OrderService::getOne(['order_no' => $orderNo]);
|
||||
if (!$order) {
|
||||
Log::error('虚拟支付退款未找到订单 ' . $mchOrderId);
|
||||
return;
|
||||
}
|
||||
if ((int)$order->order_status === OrderService::ORDER_STATUS_WAIT_SHIP
|
||||
&& (int)$order->order_type === OrderService::ORDER_TYPE_SOFTWARE) {
|
||||
OrderService::where('order_no', $order->order_no)->update([
|
||||
'order_status' => OrderService::ORDER_STATUS_REFUNDED,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* iOS 退款问询:已交付建议拒绝,未交付建议退款。最终由苹果决定
|
||||
* @param array $msg
|
||||
* @return array
|
||||
*/
|
||||
private static function iosRefundQuery(array $msg)
|
||||
{
|
||||
$payOrderId = (string)($msg['pay_order_id'] ?? ($msg['PayOrderId'] ?? ''));
|
||||
$orderNo = $payOrderId ? (Cache::get(self::TRADE_PREFIX . $payOrderId) ?: $payOrderId) : '';
|
||||
$order = $orderNo ? OrderService::getOne(['order_no' => $orderNo]) : null;
|
||||
$delivered = $order && in_array((int)$order->order_status, [
|
||||
OrderService::ORDER_STATUS_SHIPPED,
|
||||
OrderService::ORDER_STATUS_FINISHED,
|
||||
], true);
|
||||
if ($delivered) {
|
||||
return [
|
||||
'result_code' => 1,
|
||||
'result_info' => '已交付',
|
||||
'evidence' => '虚拟商品已交付,建议拒绝退款',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'result_code' => 0,
|
||||
'result_info' => '未交付',
|
||||
'evidence' => '订单未交付,建议退款',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @param array $payload
|
||||
* @param string $sessionKey 需要用户态签名的接口才传
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function postXpay($uri, array $payload, $sessionKey = '')
|
||||
{
|
||||
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($body === false) {
|
||||
throw new \Exception('虚拟支付参数编码失败');
|
||||
}
|
||||
// access_token 由小程序 HTTP 客户端自动附在 query 上,不参与 pay_sig
|
||||
$query = [
|
||||
'pay_sig' => hash_hmac('sha256', $uri . '&' . $body, self::appKey()),
|
||||
];
|
||||
if ($sessionKey !== '') {
|
||||
$query['signature'] = hash_hmac('sha256', $body, $sessionKey);
|
||||
}
|
||||
try {
|
||||
$service = new EasyWeChatService();
|
||||
$response = $service->app->getClient()->post($uri, [
|
||||
'query' => $query,
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
'body' => $body,
|
||||
]);
|
||||
$result = $response->toArray(false);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('虚拟支付请求异常 ' . $uri . ' ' . $e->getMessage());
|
||||
throw new \Exception('虚拟支付请求失败');
|
||||
}
|
||||
Log::info('虚拟支付请求 ' . $uri . ' ' . json_encode([
|
||||
'payload' => $payload,
|
||||
'res' => $result,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return is_array($result) ? $result : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $openid
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function sessionKeyByOpenid($openid)
|
||||
{
|
||||
$userId = User::where('openid', $openid)->value('id');
|
||||
$sessionKey = $userId ? Cache::get(self::SESSION_PREFIX . $userId) : '';
|
||||
if (!$sessionKey) {
|
||||
throw new \Exception('登录态已过期,请退出小程序后重新进入再支付');
|
||||
}
|
||||
|
||||
return (string)$sessionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float|string $amountYuan
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function coinQuantity($amountYuan)
|
||||
{
|
||||
$fen = self::amountFen($amountYuan);
|
||||
if ($fen < 1) {
|
||||
throw new \Exception('支付金额不能小于0.01元');
|
||||
}
|
||||
$coinPerYuan = (int)config('common.virtual_pay.coin_per_yuan');
|
||||
if ($coinPerYuan < 1) {
|
||||
$coinPerYuan = 1;
|
||||
}
|
||||
if (($fen * $coinPerYuan) % 100 !== 0) {
|
||||
throw new \Exception('订单金额须为整数元才能按1元=1代币支付');
|
||||
}
|
||||
|
||||
return intdiv($fen * $coinPerYuan, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float|string $amountYuan
|
||||
* @return int
|
||||
*/
|
||||
private static function amountFen($amountYuan)
|
||||
{
|
||||
return (int)bcmul(sprintf('%.2f', $amountYuan), '100', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
private static function env()
|
||||
{
|
||||
return (int)config('common.virtual_pay.env') === 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function appKey()
|
||||
{
|
||||
$config = config('common.virtual_pay');
|
||||
$key = self::env() === 1 ? (string)($config['sandbox_app_key'] ?? '') : (string)($config['app_key'] ?? '');
|
||||
if ($key === '') {
|
||||
throw new \Exception('未配置虚拟支付 AppKey');
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private static function userIp()
|
||||
{
|
||||
$ip = request()->ip();
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return '127.0.0.1';
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,17 @@ return [
|
||||
// 免确认收款授权结果回调地址(公网可访问、HTTPS、不带参数)
|
||||
'transfer_authorization_notify_url' => Env::get('app.url') . '/api/account/transferAuthorizationNotify',
|
||||
],
|
||||
// 小程序虚拟支付(类目不支持普通微信支付时使用)
|
||||
// 代币比例须与 MP「虚拟支付-代币配置」一致:1元=1代币。buyQuantity 只能是整数,订单金额须为整数元
|
||||
'virtual_pay' => [
|
||||
'android_pay_fees' => 0.01,
|
||||
'ios_pay_fees' => 0.12,
|
||||
'offer_id' => Env::get('wechat.vp_offer_id', ''),
|
||||
'app_key' => Env::get('wechat.vp_app_key', ''), // 现网 AppKey,env=0
|
||||
'sandbox_app_key' => Env::get('wechat.vp_sandbox_app_key', ''), // 沙箱 AppKey,env=1
|
||||
'env' => (int)Env::get('wechat.vp_env', 0), // 0现网 1沙箱;正式版只能为 0
|
||||
'coin_per_yuan' => (int)Env::get('wechat.vp_coin_per_yuan', 1),
|
||||
'msg_token' => Env::get('wechat.vp_msg_token', ''), // 消息推送 Token
|
||||
'aes_key' => Env::get('wechat.vp_aes_key', ''), // 消息推送 EncodingAESKey,明文模式可空
|
||||
'android_pay_fees' => 0.01, //安卓支付费率
|
||||
'ios_pay_fees' => 0.12, //ios支付费率
|
||||
],
|
||||
];
|
||||
@@ -69,6 +69,7 @@ module.exports = {
|
||||
OrderDetail: ApiRootUrl + 'order/detail', //订单详情
|
||||
OrderSettleDetail: ApiRootUrl + 'order/settleDetail', //订单结算详情
|
||||
OrderCommitPay: ApiRootUrl + 'order/commitPay', //订单提交支付数据
|
||||
OrderVirtualPayConfirm: ApiRootUrl + 'order/virtualPayConfirm', //虚拟支付结果确认
|
||||
OrderDelete: ApiRootUrl + 'order/delete', //订单删除
|
||||
OrderCancel: ApiRootUrl + 'order/cancel', //取消订单
|
||||
OrderOperate: ApiRootUrl + 'order/orderOperate', //操作订单
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Dialog from 'tdesign-miniprogram/dialog/index';
|
||||
import Toast from 'tdesign-miniprogram/toast/index';
|
||||
|
||||
import { dispatchCommitPay } from '../../../services/order/orderConfirm';
|
||||
import { confirmVirtualPay, dispatchCommitPay } from '../../../services/order/orderConfirm';
|
||||
|
||||
// 真实的提交支付
|
||||
export const commitPay = (params) => {
|
||||
@@ -90,24 +90,64 @@ export const payFail = (payOrderInfo, resultMsg) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 微信支付方式
|
||||
function compareVersion(v1, v2) {
|
||||
const left = String(v1 || '0').split('.');
|
||||
const right = String(v2 || '0').split('.');
|
||||
const len = Math.max(left.length, right.length);
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const num1 = parseInt(left[i] || '0', 10);
|
||||
const num2 = parseInt(right[i] || '0', 10);
|
||||
if (num1 > num2) {
|
||||
return 1;
|
||||
}
|
||||
if (num1 < num2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function canVirtualPay() {
|
||||
if (typeof wx.canIUse === 'function' && wx.canIUse('requestVirtualPayment')) {
|
||||
return true;
|
||||
}
|
||||
const version = (wx.getSystemInfoSync() || {}).SDKVersion || '0.0.0';
|
||||
return compareVersion(version, '2.19.2') >= 0;
|
||||
}
|
||||
|
||||
// 虚拟支付。signData 使用服务端原文字符串,不能再 JSON.stringify
|
||||
export const wechatPayOrder = (payOrderInfo) => {
|
||||
const payInfo = payOrderInfo.payInfo;
|
||||
const { timeStamp, nonceStr, signType, paySign } = payInfo;
|
||||
const payInfo = payOrderInfo.payInfo || {};
|
||||
if (!canVirtualPay()) {
|
||||
payFail(payOrderInfo, '当前微信版本过低,请升级后再支付');
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
// 只有 wx.requestPayment 回调 success 才认为是支付成功,不能提前跳转支付结果页
|
||||
wx.requestPayment({
|
||||
timeStamp,
|
||||
nonceStr,
|
||||
package: payInfo.package,
|
||||
signType,
|
||||
paySign,
|
||||
success: function () {
|
||||
paySuccess(payOrderInfo);
|
||||
resolve();
|
||||
wx.requestVirtualPayment({
|
||||
signData: payInfo.signData,
|
||||
paySig: payInfo.paySig,
|
||||
signature: payInfo.signature,
|
||||
mode: payInfo.mode,
|
||||
success() {
|
||||
confirmVirtualPay(payOrderInfo.tradeNo).then((res) => {
|
||||
if (!res || res.code !== 1) {
|
||||
payFail(payOrderInfo, (res && res.msg) || '支付结果确认失败,请稍后在订单中再次支付,不会重复扣款');
|
||||
return;
|
||||
}
|
||||
paySuccess(payOrderInfo);
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
payFail(payOrderInfo, (err && err.msg) || '支付结果确认失败,请稍后在订单中再次支付,不会重复扣款');
|
||||
});
|
||||
},
|
||||
fail: function (err) {
|
||||
payFail(payOrderInfo, err.errMsg);
|
||||
fail(err) {
|
||||
const errCode = err && err.errCode;
|
||||
const errMsg = (err && err.errMsg) || '支付失败';
|
||||
if (errCode === -2 || errMsg.indexOf('cancel') !== -1) {
|
||||
payFail(payOrderInfo, 'requestPayment:fail cancel');
|
||||
return;
|
||||
}
|
||||
payFail(payOrderInfo, errMsg);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,11 @@ export function dispatchCommitPay(params) {
|
||||
return wx.$util.request(wx.$api.OrderCommitPay, params, 'POST');
|
||||
}
|
||||
|
||||
/** 虚拟支付收银台成功后,通知服务端查单并入账 */
|
||||
export function confirmVirtualPay(orderNo) {
|
||||
return wx.$util.request(wx.$api.OrderVirtualPayConfirm, { orderNo }, 'POST');
|
||||
}
|
||||
|
||||
/* 订单操作 */
|
||||
export function orderOperate(params) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user