Files

760 lines
26 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\api\controller;
use app\common\controller\Api;
use app\common\library\pay\VirtualPayService;
use app\common\model\order\Order;
use app\common\model\order\Log as OrderLog;
use think\Db;
/**
* 虚拟支付
*/
class VirtualPay extends Api
{
protected $noNeedLogin = [];
protected $noNeedRight = ['*'];
/**
* @var VirtualPayService
*/
protected $virtualPayService = null;
/**
* 初始化虚拟支付服务
*/
public function _initialize()
{
parent::_initialize();
$this->virtualPayService = new VirtualPayService();
}
/**
* 获取虚拟支付配置信息(是否开启等)
* @return mixed
*/
public function getConfig()
{
$virtualPayConfig = \app\common\model\config\System::getConfig('virtual_pay');
$data = [
'status' => $virtualPayConfig['status'] ?? 'close',
'ios_refund_policy' => $virtualPayConfig['ios_refund_policy'] ?? 'none',
'virtual_goods_types' => $virtualPayConfig['virtual_goods_types'] ?? [],
'exchange_rate' => $virtualPayConfig['exchange_rate'] ?? '1',
];
$this->success('获取成功', $data);
}
/**
* 查询用户代币余额
* @return mixed
*/
public function queryBalance()
{
$oauth = \app\common\model\user\Oauth::where([
'user_id' => $this->auth->id,
'provider' => 'Wechat',
'platform' => 'wxMiniProgram'
])->find();
if (!$oauth || empty($oauth->openid)) {
$this->error('用户微信小程序授权信息不存在');
}
$result = $this->virtualPayService->queryUserBalance($oauth->openid);
if (isset($result['errcode']) && $result['errcode'] !== 0) {
$this->error('查询余额失败:' . ($result['errmsg'] ?? '未知错误'));
}
$this->success('查询成功', $result);
}
/**
* 检查余额并支付(代币模式)
* 支付前先查询余额,如果足够直接扣减代币完成支付
* 如果不足返回需要充值的信息,充值后再次调用此接口
* @return mixed
*/
public function checkAndPay()
{
$orderNo = $this->request->post('order_no');
$mode = $this->request->post('mode', 'short_series_coin');
if (empty($orderNo)) {
$this->error('订单号不能为空');
}
$order = Order::where([
'order_no' => $orderNo,
'status' => \app\common\constant\order\Status::STATUS_UNPAID
])->find();
if (!$order) {
$this->error('订单不存在或已支付');
}
if ($order['user_id'] != $this->auth->id) {
$this->error('无权操作此订单');
}
$oauth = $this->getOrderWxOauth($order);
try {
$balanceResult = $this->virtualPayService->queryUserBalance($oauth->openid);
if (isset($balanceResult['errcode']) && $balanceResult['errcode'] !== 0) {
$this->error('查询余额失败:' . ($balanceResult['errmsg'] ?? '未知错误'));
}
$currentBalance = intval($balanceResult['balance'] ?? 0);
$requiredAmount = $this->getRequiredCoinAmount($order, $mode);
if ($currentBalance >= $requiredAmount) {
return $this->executeCurrencyPay($order, $oauth, $requiredAmount);
} else {
$deficit = $requiredAmount - $currentBalance;
return $this->returnRechargeInfo($order, $currentBalance, $requiredAmount, $deficit, $balanceResult);
}
} catch (\Exception $e) {
$this->error('检查余额异常:' . $e->getMessage());
}
}
/**
* 获取订单需要的代币数量
* @param Order $order 订单对象
* @param string $mode 支付模式
* @return int
*/
private function getRequiredCoinAmount($order, $mode)
{
$exchangeRate = VirtualPayService::getExchangeRate();
$priceInYuan = floatval($order->real_price);
if ($mode === 'short_series_coin') {
return intval($priceInYuan * $exchangeRate);
}
return intval($priceInYuan * $exchangeRate);
}
/**
* 执行代币扣减支付
* @param Order $order 订单对象
* @param mixed $oauth 用户授权信息
* @param int $amount 扣减的代币数量
* @return void
*/
private function executeCurrencyPay($order, $oauth, $amount)
{
$payItem = $this->buildPayItem($order);
try {
$result = $this->virtualPayService->currencyPay(
$oauth->openid,
$amount,
$order->order_no,
$payItem
);
if (isset($result['errcode']) && $result['errcode'] !== 0) {
$errMsg = $result['errmsg'] ?? '未知错误';
if ($result['errcode'] == 268490006) {
$this->error('代币余额不足,请先充值');
} elseif ($result['errcode'] == 268490004) {
$this->success('支付成功(重复操作)', [
'status' => 'success',
'order_no' => $order->order_no,
'balance' => $result['balance'] ?? 0,
'message' => '订单已处理'
]);
} else {
$this->error('扣减代币失败:' . $errMsg);
}
}
$newBalance = $result['balance'] ?? 0;
$this->success('支付成功', [
'status' => 'success',
'order_no' => $order->order_no,
'amount' => $amount,
'balance' => $newBalance,
'used_present' => $result['used_present_amount'] ?? 0,
'message' => '代币扣减成功,等待微信推送确认'
]);
} catch (\Exception $e) {
$this->error('执行代币支付异常:' . $e->getMessage());
}
}
/**
* 返回充值信息(余额不足时)
* @param Order $order 订单对象
* @param int $currentBalance 当前余额
* @param int $requiredAmount 需要的金额
* @param int $deficit 差额
* @param array $balanceResult 余额查询结果
* @return void
*/
private function returnRechargeInfo($order, $currentBalance, $requiredAmount, $deficit, $balanceResult)
{
$this->success('余额不足,请先充值', [
'status' => 'insufficient_balance',
'order_no' => $order->order_no,
'current_balance' => $currentBalance,
'required_amount' => $requiredAmount,
'deficit' => $deficit,
'present_balance' => $balanceResult['present_balance'] ?? 0,
'sum_save' => $balanceResult['sum_save'] ?? 0,
'sum_present' => $balanceResult['sum_present'] ?? 0,
'first_save_flag' => $balanceResult['first_save_flag'] ?? false,
'message' => "当前代币余额:{$currentBalance},需要:{$requiredAmount},还差:{$deficit}"
]);
}
/**
* 构建支付项信息
* @param Order $order 订单对象
* @return array
*/
private function buildPayItem($order)
{
$orderItems = \app\admin\model\order\Item::where(['order_no' => $order->order_no])->select()->toArray();
$payItems = [];
foreach ($orderItems as $item) {
$payItems[] = [
'productid' => $item['item_id'] ?? '',
'unit_price' => floatval($item['unit_price'] ?? $item['real_price'] ?? 0),
'quantity' => intval($item['goods_count'] ?? 1),
'title' => $item['item_name'] ?? '',
'goods_type' => $item['goods_type'] ?? ''
];
}
return $payItems;
}
/**
* 拉起虚拟支付(客户端调用wx.requestVirtualPayment前获取参数)
* @return mixed
*/
public function handle()
{
$orderNo = $this->request->post('order_no');
$code = $this->request->post('code', '');
$order = Order::where([
'order_no' => $orderNo,
'status' => 'unpaid'
])->find();
if (!$order) {
$this->error('订单不存在或已支付');
}
if ($order['user_id'] != $this->auth->id) {
$this->error('无权支付此订单');
}
$oauth = \app\common\model\user\Oauth::where([
'user_id' => $this->auth->id,
'provider' => 'Wechat',
'platform' => 'wxMiniProgram'
])->find();
if (!$oauth || empty($oauth->openid)) {
$this->error('请先登录微信小程序');
}
$this->tryResumeExistingWxOrder($order, $oauth);
if (empty($code)) {
$this->error('请先调用wx.login并传入code');
}
$wechat = new \app\common\library\Wechat('wxMiniProgram');
$session = $wechat->code($code);
if (empty($session['session_key'])) {
$this->error('获取session_key失败:' . ($session['errmsg'] ?? '未知错误'));
}
if (!empty($session['openid']) && $session['openid'] !== $oauth->openid) {
$this->error('微信登录态与当前用户不一致,请重新登录');
}
$sessionKey = $session['session_key'];
$oauth->session_key = $sessionKey;
$oauth->expiretime = time() + 7200;
$oauth->save();
$orderData = [
'openid' => $oauth->openid,
'session_key' => $sessionKey,
'out_trade_no' => $order->order_no,
'total_fee' => $order->real_price,
'mode' => $this->request->post('mode', 'short_series_coin'),
'product_id' => $this->request->post('product_id', ''),
];
try {
$result = $this->virtualPayService->getClientPayConfig(
$orderData['openid'],
$orderData['session_key'],
$orderData['out_trade_no'],
$orderData['total_fee'],
$orderData['mode'],
$orderData['product_id']
);
} catch (\Exception $e) {
$this->error('获取支付参数失败:' . $e->getMessage());
}
$this->success('获取成功', $result);
}
/**
* 历史订单再次发起支付前,先检查微信侧是否已经存在同订单号的支付单。
* 如果微信侧已成功,则直接补齐本地订单;如果已存在未完成支付单,则阻止重复拉起。
* @param Order $order
* @param mixed $oauth
* @return void
*/
private function tryResumeExistingWxOrder($order, $oauth)
{
try {
$queryResult = $this->virtualPayService->queryOrder($oauth->openid, $order->order_no);
} catch (\Exception $e) {
return;
}
if (isset($queryResult['errcode']) && $queryResult['errcode'] !== 0) {
return;
}
$wxOrder = $queryResult['order'] ?? [];
$status = intval($wxOrder['status'] ?? -1);
if (in_array($status, [2, 3, 4], true)) {
if ($order['status'] == \app\common\constant\order\Status::STATUS_UNPAID) {
if ($order['is_virtual_pay'] == 1) {
$this->processCoinPayAfterRecharge($order, $oauth, $queryResult);
} else {
$this->processCashOrderDeliver($order, $oauth, $queryResult);
}
}
$this->success('订单已存在微信支付记录,已自动处理', [
'status' => 'completed',
'order_no' => $order->order_no,
]);
}
if ($status >= 0) {
$this->error('该订单已存在微信侧支付单,请先查询支付结果;若需重新支付,请重新下单');
}
}
/**
* 虚拟支付成功回调(小程序端支付成功后调用,用于确认发货)
* 余额不足充值场景:queryOrder确认充值成功 -> query_user_balance -> currency_pay -> paySuccess
* @return mixed
*/
public function notify()
{
$orderNo = $this->request->post('order_no');
$order = Order::where('order_no', $orderNo)->find();
if (!$order) {
$this->error('订单不存在');
}
if ($order->user_id != $this->auth->id) {
$this->error('无权处理此订单');
}
$oauth = $this->getOrderWxOauth($order);
if ($order->status != \app\common\constant\order\Status::STATUS_UNPAID) {
$this->success('订单已处理');
}
try {
$orderResult = $this->virtualPayService->queryOrder($oauth->openid, $orderNo);
} catch (\Exception $e) {
$this->error('查询虚拟支付订单失败:' . $e->getMessage());
}
if (isset($orderResult['errcode']) && $orderResult['errcode'] !== 0) {
$this->error('查询虚拟支付订单失败:' . ($orderResult['errmsg'] ?? '未知错误'));
}
$wxOrder = $orderResult['order'] ?? [];
if (!in_array(intval($wxOrder['status'] ?? -1), [2, 3, 4])) {
$this->error('微信侧订单尚未支付成功');
}
if ($order['is_virtual_pay'] == 1) {
$this->processCoinPayAfterRecharge($order, $oauth, $orderResult);
} else {
$this->processCashOrderDeliver($order, $oauth, $orderResult);
}
$this->success('发货通知成功');
}
/**
* 处理代币充值成功后的扣币流程
* queryOrder确认充值成功 -> query_user_balance -> currency_pay -> paySuccess
* @param Order $order 订单对象
* @param mixed $oauth 用户授权信息
* @param array $orderResult 微信订单查询结果
* @return void
*/
private function processCoinPayAfterRecharge($order, $oauth, $orderResult)
{
$orderNo = $order->order_no;
try {
$balanceResult = $this->virtualPayService->queryUserBalance($oauth->openid);
if (isset($balanceResult['errcode']) && $balanceResult['errcode'] !== 0) {
throw new \Exception('查询余额失败:' . ($balanceResult['errmsg'] ?? '未知错误'));
}
$currentBalance = intval($balanceResult['balance'] ?? 0);
$requiredAmount = $this->getRequiredCoinAmount($order, 'short_series_coin');
if ($currentBalance < $requiredAmount) {
throw new \Exception("充值后余额仍不足,当前:{$currentBalance},需要:{$requiredAmount}");
}
$payItem = $this->buildPayItem($order);
$result = $this->virtualPayService->currencyPay(
$oauth->openid,
$requiredAmount,
$orderNo,
$payItem
);
if (isset($result['errcode']) && $result['errcode'] !== 0) {
if ($result['errcode'] == 268490004) {
$this->handleDuplicateCurrencyPay($order, $oauth, $orderResult);
return;
}
throw new \Exception('扣减代币失败:' . ($result['errmsg'] ?? '未知错误'));
}
$notify = [
'order_no' => $orderNo,
'transaction_id' => $orderResult['order']['wxpay_order_id'] ?? ($orderResult['order']['wx_order_id'] ?? $orderNo),
'pay_time' => time(),
'real_pay_price' => $order->real_price,
'pay_type' => 'virtual_pay',
'payment_json' => json_encode([
'query_order' => $orderResult,
'balance_result' => $balanceResult,
'currency_pay' => $result,
], JSON_UNESCAPED_UNICODE),
];
Db::transaction(function () use ($order, $notify) {
$order->paySuccess($order, $notify);
});
OrderLog::set($orderNo, "充值后扣减代币成功,余额:{$result['balance'] ?? 0}");
} catch (\Exception $e) {
OrderLog::set($orderNo, "充值后扣币失败:{$e->getMessage()}");
$this->error($e->getMessage());
}
}
/**
* 处理重复扣减代币的情况(currency_pay返回268490004
* 查询微信订单确认是否已扣币,然后完成本地订单
* @param Order $order 订单对象
* @param mixed $oauth 用户授权信息
* @param array $orderResult 微信订单查询结果
* @return void
*/
private function handleDuplicateCurrencyPay($order, $oauth, $orderResult)
{
$orderNo = $order->order_no;
try {
$queryResult = $this->virtualPayService->queryOrder($oauth->openid, $orderNo);
if (isset($queryResult['errcode']) && $queryResult['errcode'] !== 0) {
throw new \Exception('查询订单失败:' . ($queryResult['errmsg'] ?? '未知错误'));
}
$wxOrder = $queryResult['order'] ?? [];
$orderStatus = intval($wxOrder['status'] ?? -1);
if ($orderStatus === 3 || $orderStatus === 4) {
$notify = [
'order_no' => $orderNo,
'transaction_id' => $wxOrder['wxpay_order_id'] ?? ($wxOrder['wx_order_id'] ?? $orderNo),
'pay_time' => time(),
'real_pay_price' => $order->real_price,
'pay_type' => 'virtual_pay',
'payment_json' => json_encode([
'query_order' => $orderResult,
'duplicate_check' => $queryResult,
], JSON_UNESCAPED_UNICODE),
];
Db::transaction(function () use ($order, $notify) {
$order->paySuccess($order, $notify);
});
OrderLog::set($orderNo, '重复扣币,订单已完成支付');
} else {
throw new \Exception('订单状态异常,status=' . $orderStatus);
}
} catch (\Exception $e) {
OrderLog::set($orderNo, "重复扣币处理异常:{$e->getMessage()}");
$this->error($e->getMessage());
}
}
/**
* 处理现金订单发货(道具直购模式)
* @param Order $order 订单对象
* @param mixed $oauth 用户授权信息
* @param array $orderResult 微信订单查询结果
* @return void
*/
private function processCashOrderDeliver($order, $oauth, $orderResult)
{
$orderNo = $order->order_no;
try {
$result = $this->virtualPayService->notifyProvideGoods($orderNo);
} catch (\Exception $e) {
$this->error('通知发货失败:' . $e->getMessage());
}
if (isset($result['errcode']) && $result['errcode'] !== 0) {
$this->error('通知发货失败:' . ($result['errmsg'] ?? '未知错误'));
}
$wxOrder = $orderResult['order'] ?? [];
$notify = [
'order_no' => $orderNo,
'transaction_id' => $wxOrder['wxpay_order_id'] ?? ($wxOrder['wx_order_id'] ?? $orderNo),
'pay_time' => time(),
'real_pay_price' => $order->real_price,
'pay_type' => 'virtual_pay',
'payment_json' => json_encode(['query_order' => $orderResult, 'notify_provide_goods' => $result], JSON_UNESCAPED_UNICODE),
];
Db::transaction(function () use ($order, $notify) {
$order->paySuccess($order, $notify);
});
}
/**
* 代币支付退款
* @return mixed
*/
public function refund()
{
$orderNo = $this->request->post('order_no');
$amount = $this->request->post('amount');
$order = Order::where('order_no', $orderNo)->find();
if (!$order) {
$this->error('订单不存在');
}
if ($order->user_id != $this->auth->id) {
$this->error('无权处理此订单');
}
if (!is_numeric($amount) || intval($amount) <= 0) {
$this->error('退款金额不合法');
}
if (!in_array($order->status, [
\app\common\constant\order\Status::STATUS_PAID,
\app\common\constant\order\Status::STATUS_UNRECEIVE,
\app\common\constant\order\Status::STATUS_UNSEND,
\app\common\constant\order\Status::STATUS_SUREUNRECEIVE,
\app\common\constant\order\Status::STATUS_SUCCESS,
])) {
$this->error('订单状态不支持退款');
}
$oauth = $this->getOrderWxOauth($order);
$refundId = 'RF' . date('YmdHis') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
try {
$result = $this->virtualPayService->cancelCurrencyPay(
$oauth->openid,
$amount,
$orderNo,
$refundId
);
} catch (\Exception $e) {
$this->error('退款失败:' . $e->getMessage());
}
if (isset($result['errcode']) && $result['errcode'] !== 0) {
$this->error('退款失败:' . ($result['errmsg'] ?? '未知错误'));
}
$this->success('退款成功', $result);
}
/**
* 查询虚拟支付订单
* 余额不足充值场景:queryOrder确认充值成功 -> query_user_balance -> currency_pay -> paySuccess
* @return mixed
*/
public function queryOrder()
{
$orderNo = $this->request->post('order_no', '');
$wxOrderId = $this->request->post('wx_order_id', '');
$autoComplete = $this->request->post('auto_complete', 0);
if (empty($orderNo) && empty($wxOrderId)) {
$this->error('订单号不能为空');
}
if (!empty($orderNo)) {
$order = Order::where('order_no', $orderNo)->find();
if (!$order) {
$this->error('订单不存在');
}
if ($order->user_id != $this->auth->id) {
$this->error('无权查询此订单');
}
$oauth = $this->getOrderWxOauth($order);
} else {
$oauth = $this->getCurrentWxOauth();
}
$result = $this->virtualPayService->queryOrder($oauth->openid, $orderNo, $wxOrderId);
if (isset($result['errcode']) && $result['errcode'] !== 0) {
$this->error('查询失败:' . ($result['errmsg'] ?? '未知错误'));
}
if ($autoComplete && !empty($orderNo) && $order['is_virtual_pay'] == 1
&& $order['status'] == \app\common\constant\order\Status::STATUS_UNPAID) {
$wxOrder = $result['order'] ?? [];
if (in_array(intval($wxOrder['status'] ?? -1), [2, 3, 4])) {
$this->processCoinPayAfterRecharge($order, $oauth, $result);
}
}
$this->success('查询成功', $result);
}
/**
* 查询现金订单并启动退款
* @return mixed
*/
public function refundOrder()
{
$orderNo = $this->request->post('order_no');
$refundFee = $this->request->post('refund_fee', null);
$reason = $this->request->post('refund_reason', '0');
$reqFrom = $this->request->post('req_from', '3');
if (empty($orderNo)) {
$this->error('订单号不能为空');
}
$order = Order::where('order_no', $orderNo)->find();
if (!$order) {
$this->error('订单不存在');
}
if ($order->user_id != $this->auth->id) {
$this->error('无权处理此订单');
}
$oauth = $this->getOrderWxOauth($order);
$orderResult = $this->virtualPayService->queryOrder($oauth->openid, $orderNo);
if (isset($orderResult['errcode']) && $orderResult['errcode'] !== 0) {
$this->error('查询订单失败:' . ($orderResult['errmsg'] ?? '未知错误'));
}
$leftFee = intval($orderResult['order']['left_fee'] ?? 0);
if ($leftFee <= 0) {
$this->error('订单无可退金额');
}
if ($refundFee === null || $refundFee === '') {
$refundFee = $leftFee;
} else {
$refundFee = intval($refundFee);
}
if ($refundFee <= 0 || $refundFee > $leftFee) {
$this->error('退款金额不合法');
}
$refundId = 'RF' . date('YmdHis') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
$result = $this->virtualPayService->refundOrder($oauth->openid, $orderNo, $refundId, $leftFee, $refundFee, $reason, $reqFrom);
if (isset($result['errcode']) && $result['errcode'] !== 0) {
$this->error('退款失败:' . ($result['errmsg'] ?? '未知错误'));
}
$this->success('退款任务已启动', $result);
}
/**
* 获取订单所属用户的小程序授权信息。
* @param Order $order
* @return \think\Model
*/
private function getOrderWxOauth($order)
{
$oauth = \app\common\model\user\Oauth::where([
'user_id' => $order->user_id,
'provider' => 'Wechat',
'platform' => 'wxMiniProgram'
])->find();
if (!$oauth || empty($oauth->openid)) {
$this->error('用户微信授权信息不存在');
}
return $oauth;
}
/**
* 获取当前用户的小程序授权信息。
* @return \think\Model
*/
private function getCurrentWxOauth()
{
$oauth = \app\common\model\user\Oauth::where([
'user_id' => $this->auth->id,
'provider' => 'Wechat',
'platform' => 'wxMiniProgram'
])->find();
if (!$oauth || empty($oauth->openid)) {
$this->error('用户微信授权信息不存在');
}
return $oauth;
}
}