初始化项目:添加后端代码、ThinkPHP框架、前端资源

This commit is contained in:
amb
2026-09-03 12:42:39 +08:00
commit 482bece22e
3726 changed files with 416708 additions and 0 deletions
+759
View File
@@ -0,0 +1,759 @@
<?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;
}
}