496 lines
20 KiB
PHP
496 lines
20 KiB
PHP
<?php
|
||
|
||
namespace app\common\controller;
|
||
|
||
use app\chukebao\model\TokensCompany;
|
||
use app\chukebao\model\TokensRecord;
|
||
use app\common\model\Order;
|
||
use app\common\model\User;
|
||
use app\common\service\payment\AlipayNativeGateway;
|
||
use app\common\service\payment\WechatPayV3Gateway;
|
||
use app\cunkebao\service\PaymentConfigService;
|
||
use think\Db;
|
||
use think\facade\Env;
|
||
use think\facade\Log;
|
||
use think\facade\Request;
|
||
|
||
/**
|
||
* 官方支付服务(微信 V3 Native + 支付宝 precreate)
|
||
*/
|
||
class PaymentService
|
||
{
|
||
/**
|
||
* 统一下单
|
||
*
|
||
* @param array $order orderNo, money(分), goodsName, service, notify_url, companyId, userId, orderType, goodsId, goodsSpecs
|
||
*/
|
||
public function createOrder(array $order): string
|
||
{
|
||
$channel = $this->resolveChannel($order['service'] ?? '');
|
||
|
||
if (PaymentConfigService::allowMockPayment()) {
|
||
return $this->createMockOrder($order, $channel);
|
||
}
|
||
|
||
if ($channel === 'wechat' && !PaymentConfigService::isWechatReady()) {
|
||
return json_encode(['code' => 500, 'msg' => '微信官方支付未配置']);
|
||
}
|
||
if ($channel === 'alipay' && !PaymentConfigService::isAlipayReady()) {
|
||
return json_encode(['code' => 500, 'msg' => '支付宝官方支付未配置']);
|
||
}
|
||
|
||
Db::startTrans();
|
||
try {
|
||
$wechatCfg = PaymentConfigService::getWechatConfig();
|
||
Order::create([
|
||
'mchId' => $channel === 'wechat' ? (int)$wechatCfg['mch_id'] : 0,
|
||
'companyId' => (int)($order['companyId'] ?? 0),
|
||
'userId' => (int)($order['userId'] ?? 0),
|
||
'orderType' => (int)($order['orderType'] ?? 1),
|
||
'status' => 0,
|
||
'goodsId' => (int)($order['goodsId'] ?? 0),
|
||
'goodsName' => (string)($order['goodsName'] ?? ''),
|
||
'money' => (int)($order['money'] ?? 0),
|
||
'goodsSpecs' => json_encode($order['goodsSpecs'] ?? [], JSON_UNESCAPED_UNICODE),
|
||
'orderNo' => (string)($order['orderNo'] ?? ''),
|
||
'ip' => Request::ip(),
|
||
'nonceStr' => md5(($order['orderNo'] ?? '') . microtime(true)),
|
||
'createTime' => time(),
|
||
'payType' => $channel === 'alipay' ? 2 : 1,
|
||
]);
|
||
|
||
$codeUrl = '';
|
||
$payMode = 'qr';
|
||
$redirectUrl = '';
|
||
if ($channel === 'wechat') {
|
||
$gw = new WechatPayV3Gateway();
|
||
$notify = $order['notify_url'] ?? PaymentConfigService::resolveWechatNotifyUrl();
|
||
$result = $gw->createNativeOrder(
|
||
(string)$order['orderNo'],
|
||
(int)$order['money'],
|
||
(string)$order['goodsName'],
|
||
$notify
|
||
);
|
||
$codeUrl = $result['codeUrl'];
|
||
} else {
|
||
$gw = new AlipayNativeGateway();
|
||
$notify = $order['notify_url'] ?? PaymentConfigService::resolveAlipayNotifyUrl();
|
||
$returnUrl = PaymentConfigService::resolveAlipayReturnUrl(
|
||
'/recharge/buy?paid=1&orderNo=' . rawurlencode((string)$order['orderNo'])
|
||
);
|
||
$result = $gw->createWapPayUrl(
|
||
(string)$order['orderNo'],
|
||
((int)$order['money']) / 100,
|
||
(string)$order['goodsName'],
|
||
$notify,
|
||
$returnUrl
|
||
);
|
||
$redirectUrl = $result['redirectUrl'];
|
||
$codeUrl = $redirectUrl;
|
||
$payMode = 'redirect';
|
||
}
|
||
|
||
Db::commit();
|
||
return $this->encodeCreateOrderSuccess($codeUrl, false, $payMode, $redirectUrl);
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
Log::error('[Payment] createOrder failed', ['error' => $e->getMessage(), 'orderNo' => $order['orderNo'] ?? '']);
|
||
return json_encode(['code' => 500, 'msg' => '订单创建失败:' . $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 开发环境模拟支付成功(算力充值等 mock 订单)
|
||
*/
|
||
public function mockMarkPaid(string $orderNo, int $companyId, int $userId = 0): string
|
||
{
|
||
if (!PaymentConfigService::allowMockPayment()) {
|
||
return json_encode(['code' => 500, 'msg' => '未开启 mock 支付']);
|
||
}
|
||
|
||
$query = Order::where('orderNo', $orderNo)->where('companyId', $companyId);
|
||
if ($userId > 0) {
|
||
$query->where('userId', $userId);
|
||
}
|
||
$order = $query->find();
|
||
if (!$order) {
|
||
return json_encode(['code' => 500, 'msg' => '订单不存在']);
|
||
}
|
||
|
||
$specs = json_decode($order->goodsSpecs ?: '{}', true) ?: [];
|
||
if (empty($specs['mock'])) {
|
||
return json_encode(['code' => 500, 'msg' => '仅 mock 订单可模拟支付']);
|
||
}
|
||
if ((int)$order->status === 1) {
|
||
return json_encode(['code' => 200, 'msg' => '订单已支付']);
|
||
}
|
||
|
||
Db::startTrans();
|
||
try {
|
||
$order->status = 1;
|
||
$order->payType = (int)($order->payType ?: 1);
|
||
$order->payTime = time();
|
||
$order->transactionId = 'MOCK_' . $orderNo;
|
||
$order->save();
|
||
$this->processOrder($order);
|
||
Db::commit();
|
||
return json_encode(['code' => 200, 'msg' => '模拟支付成功']);
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
Log::error('[Payment] mockMarkPaid failed', ['error' => $e->getMessage(), 'orderNo' => $orderNo]);
|
||
return json_encode(['code' => 500, 'msg' => '模拟支付失败:' . $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
/** 微信官方回调 JSON */
|
||
public function notifyWechat(): string
|
||
{
|
||
$raw = file_get_contents('php://input');
|
||
$payload = json_decode($raw, true);
|
||
if (!is_array($payload) || empty($payload['resource'])) {
|
||
return $this->wechatNotifyFail('invalid body');
|
||
}
|
||
|
||
try {
|
||
$gw = new WechatPayV3Gateway();
|
||
$data = $gw->decryptNotifyResource($payload['resource']);
|
||
if (($data['trade_state'] ?? '') !== 'SUCCESS') {
|
||
return $this->wechatNotifyOk();
|
||
}
|
||
return $this->markPaidFromNotify(
|
||
(string)($data['out_trade_no'] ?? ''),
|
||
(string)($data['transaction_id'] ?? ''),
|
||
$this->parseWechatPayTime($data['success_time'] ?? ''),
|
||
1
|
||
) ? $this->wechatNotifyOk() : $this->wechatNotifyFail('process failed');
|
||
} catch (\Throwable $e) {
|
||
Log::error('[Payment] wechat notify', ['error' => $e->getMessage()]);
|
||
return $this->wechatNotifyFail($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/** 支付宝官方回调 form */
|
||
public function notifyAlipay(): string
|
||
{
|
||
$params = Request::param();
|
||
if (empty($params)) {
|
||
$params = $_POST;
|
||
}
|
||
|
||
try {
|
||
$gw = new AlipayNativeGateway();
|
||
if (!$gw->verifyNotify($params)) {
|
||
Log::error('[Payment] alipay notify sign fail', ['params' => array_keys($params)]);
|
||
return 'fail';
|
||
}
|
||
|
||
if (($params['trade_status'] ?? '') !== 'TRADE_SUCCESS'
|
||
&& ($params['trade_status'] ?? '') !== 'TRADE_FINISHED') {
|
||
return 'success';
|
||
}
|
||
|
||
$ok = $this->markPaidFromNotify(
|
||
(string)($params['out_trade_no'] ?? ''),
|
||
(string)($params['trade_no'] ?? ''),
|
||
!empty($params['gmt_payment']) ? (strtotime($params['gmt_payment']) ?: time()) : time(),
|
||
2
|
||
);
|
||
return $ok ? 'success' : 'fail';
|
||
} catch (\Throwable $e) {
|
||
Log::error('[Payment] alipay notify', ['error' => $e->getMessage()]);
|
||
return 'fail';
|
||
}
|
||
}
|
||
|
||
/** 兼容旧路由 /v1/pay/notify */
|
||
public function notify()
|
||
{
|
||
$contentType = Request::header('content-type', '');
|
||
if (stripos($contentType, 'json') !== false) {
|
||
return $this->notifyWechat();
|
||
}
|
||
if (!empty($_POST['sign']) || !empty(Request::param('sign'))) {
|
||
return $this->notifyAlipay();
|
||
}
|
||
Log::warning('[Payment] legacy notify unknown format');
|
||
return 'fail';
|
||
}
|
||
|
||
public function queryOrder($orderNo = ''): string
|
||
{
|
||
if ($orderNo === '') {
|
||
return json_encode(['code' => 422, 'msg' => '订单号缺失']);
|
||
}
|
||
|
||
$order = Order::where('orderNo', $orderNo)->find();
|
||
if (!$order) {
|
||
return json_encode(['code' => 500, 'msg' => '订单不存在']);
|
||
}
|
||
if ((int)$order->status === 1) {
|
||
return json_encode(['code' => 200, 'msg' => '支付成功']);
|
||
}
|
||
|
||
$specs = json_decode($order->goodsSpecs ?: '{}', true) ?: [];
|
||
$payType = $specs['payType'] ?? '';
|
||
$isAlipay = in_array($payType, ['alipayNative', 'alipay'], true) || (int)$order->payType === 2;
|
||
|
||
try {
|
||
if ($isAlipay && PaymentConfigService::isAlipayReady()) {
|
||
$result = (new AlipayNativeGateway())->queryByOutTradeNo($orderNo);
|
||
} elseif (PaymentConfigService::isWechatReady()) {
|
||
$result = (new WechatPayV3Gateway())->queryByOutTradeNo($orderNo);
|
||
} else {
|
||
return json_encode(['code' => 500, 'msg' => '支付渠道未配置']);
|
||
}
|
||
|
||
if (empty($result['paid'])) {
|
||
return json_encode(['code' => 500, 'msg' => '支付未完成', 'data' => $result]);
|
||
}
|
||
|
||
Db::startTrans();
|
||
$order = Order::where('orderNo', $orderNo)->lock(true)->find();
|
||
if ($order && (int)$order->status !== 1) {
|
||
$order->status = 1;
|
||
$order->transactionId = $result['transaction_id'] ?? '';
|
||
$order->payTime = (int)($result['pay_time'] ?? time());
|
||
$order->save();
|
||
$this->processOrder($order);
|
||
}
|
||
Db::commit();
|
||
return json_encode(['code' => 200, 'msg' => '支付成功']);
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return json_encode(['code' => 500, 'msg' => '查询失败:' . $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
public function processOrder($order = [])
|
||
{
|
||
if (empty($order)) {
|
||
return false;
|
||
}
|
||
|
||
switch ($order['orderType']) {
|
||
case 1:
|
||
$user = User::where([
|
||
'id' => $order->userId,
|
||
'companyId' => $order->companyId,
|
||
])->find();
|
||
$isAdmin = (!empty($user) && isset($user->isAdmin) && $user->isAdmin == 1) ? 1 : 0;
|
||
|
||
$token = TokensCompany::where(['companyId' => $order->companyId, 'userId' => $order->userId])->find();
|
||
$goodsSpecs = json_decode($order->goodsSpecs, true);
|
||
if (!empty($token)) {
|
||
$token->tokens = $token->tokens + $goodsSpecs['tokens'];
|
||
$token->updateTime = time();
|
||
$token->save();
|
||
$newTokens = $token->tokens;
|
||
} else {
|
||
$tokensCompany = new TokensCompany();
|
||
$tokensCompany->userId = $order->userId;
|
||
$tokensCompany->companyId = $order->companyId;
|
||
$tokensCompany->tokens = $goodsSpecs['tokens'];
|
||
$tokensCompany->isAdmin = $isAdmin;
|
||
$tokensCompany->createTime = time();
|
||
$tokensCompany->updateTime = time();
|
||
$tokensCompany->save();
|
||
$newTokens = $tokensCompany->tokens;
|
||
}
|
||
$record = new TokensRecord();
|
||
$record->companyId = $order->companyId;
|
||
$record->userId = $order->userId;
|
||
$record->type = 1;
|
||
$record->form = 5;
|
||
$record->wechatAccountId = 0;
|
||
$record->friendIdOrGroupId = 0;
|
||
$record->remarks = '购买算力【' . $goodsSpecs['name'] . '】';
|
||
$record->tokens = $goodsSpecs['tokens'];
|
||
$record->balanceTokens = $newTokens;
|
||
$record->createTime = time();
|
||
$record->save();
|
||
break;
|
||
case 2:
|
||
try {
|
||
$specs = json_decode($order->goodsSpecs, true) ?: [];
|
||
$flowOrderNo = (string)($specs['flowOrderNo'] ?? $order->orderNo);
|
||
$fp = \app\store\model\FlowPackageOrderModel::where('orderNo', $flowOrderNo)->find();
|
||
if ($fp && (int)$fp['payStatus'] !== 1) {
|
||
\app\store\model\FlowPackageOrderModel::where('id', $fp['id'])->update([
|
||
'payStatus' => 1,
|
||
'status' => 1,
|
||
'payTime' => time(),
|
||
'updateTime'=> time(),
|
||
]);
|
||
$flowPackage = \app\store\model\FlowPackageModel::where('id', $fp['packageId'])->where('isDel', 0)->find();
|
||
if ($flowPackage) {
|
||
$now = time();
|
||
$expireTime = $now + (intval($flowPackage['duration']) * 30 * 86400);
|
||
\app\store\model\UserFlowPackageModel::create([
|
||
'userId' => (int)$fp['userId'],
|
||
'packageId' => (int)$fp['packageId'],
|
||
'orderId' => (int)$fp['id'],
|
||
'packageName' => (string)$flowPackage['name'],
|
||
'monthlyFlow' => $flowPackage['monthlyFlow'],
|
||
'duration' => $flowPackage['duration'],
|
||
'totalFlow' => $flowPackage->totalFlow ?? ($flowPackage['monthlyFlow'] * $flowPackage['duration']),
|
||
'usedFlow' => 0,
|
||
'startTime' => $now,
|
||
'expireTime' => $expireTime,
|
||
'status' => 1,
|
||
'isDel' => 0,
|
||
]);
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {
|
||
Log::error('[Payment] flow_package fulfill: ' . $e->getMessage());
|
||
}
|
||
break;
|
||
case 3:
|
||
try {
|
||
$specs = json_decode($order->goodsSpecs, true) ?: [];
|
||
$vendorOrderNo = (string)($specs['vendorOrderNo'] ?? $order->orderNo);
|
||
$vo = \app\store\model\VendorOrderModel::where('orderNo', $vendorOrderNo)->find();
|
||
if ($vo && (int)$vo->status < \app\store\model\VendorOrderModel::STATUS_PAID) {
|
||
$vo->status = \app\store\model\VendorOrderModel::STATUS_PAID;
|
||
$vo->payTime = time();
|
||
$vo->updateTime = time();
|
||
$vo->save();
|
||
}
|
||
} catch (\Throwable $e) {
|
||
Log::error('[Payment] vendor fulfill: ' . $e->getMessage());
|
||
}
|
||
break;
|
||
case 8:
|
||
try {
|
||
$svc = new \app\cunkebao\service\PaymentAcquisitionService();
|
||
$svc->markOrderPaid($order);
|
||
} catch (\Throwable $e) {
|
||
Log::error('[PaymentAcquisition] markOrderPaid: ' . $e->getMessage());
|
||
}
|
||
break;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private function resolveChannel(string $serviceType): string
|
||
{
|
||
if (in_array($serviceType, ['alipayNative', 'pay.alipay.native', 'alipayQr', 'alipay'], true)) {
|
||
return 'alipay';
|
||
}
|
||
return 'wechat';
|
||
}
|
||
|
||
/** 本地调试 mock 下单(官方网关未配时) */
|
||
private function createMockOrder(array $order, string $channel): string
|
||
{
|
||
Db::startTrans();
|
||
try {
|
||
$specs = $order['goodsSpecs'] ?? [];
|
||
if (!is_array($specs)) {
|
||
$specs = json_decode((string)$specs, true) ?: [];
|
||
}
|
||
$specs['mock'] = true;
|
||
$specs['payType'] = $channel === 'alipay' ? 'alipayNative' : 'qrCode';
|
||
|
||
$wechatCfg = PaymentConfigService::getWechatConfig();
|
||
Order::create([
|
||
'mchId' => $channel === 'wechat' ? (int)($wechatCfg['mch_id'] ?? 0) : 0,
|
||
'companyId' => (int)($order['companyId'] ?? 0),
|
||
'userId' => (int)($order['userId'] ?? 0),
|
||
'orderType' => (int)($order['orderType'] ?? 1),
|
||
'status' => 0,
|
||
'goodsId' => (int)($order['goodsId'] ?? 0),
|
||
'goodsName' => (string)($order['goodsName'] ?? ''),
|
||
'money' => (int)($order['money'] ?? 0),
|
||
'goodsSpecs' => json_encode($specs, JSON_UNESCAPED_UNICODE),
|
||
'orderNo' => (string)($order['orderNo'] ?? ''),
|
||
'ip' => Request::ip() ?: '127.0.0.1',
|
||
'nonceStr' => md5(($order['orderNo'] ?? '') . microtime(true)),
|
||
'createTime' => time(),
|
||
'payType' => $channel === 'alipay' ? 2 : 1,
|
||
]);
|
||
|
||
$h5Base = rtrim((string)Env::get('app.host', 'http://localhost:3100'), '/');
|
||
$codeUrl = $h5Base . '/recharge?mockPay=1&orderNo=' . urlencode((string)$order['orderNo']);
|
||
|
||
Db::commit();
|
||
return $this->encodeCreateOrderSuccess($codeUrl, true, 'qr', '');
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
Log::error('[Payment] createMockOrder failed', ['error' => $e->getMessage(), 'orderNo' => $order['orderNo'] ?? '']);
|
||
return json_encode(['code' => 500, 'msg' => 'mock 订单创建失败:' . $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
private function encodeCreateOrderSuccess(
|
||
string $codeUrl,
|
||
bool $mock,
|
||
string $payMode = 'qr',
|
||
string $redirectUrl = ''
|
||
): string {
|
||
return json_encode([
|
||
'code' => 200,
|
||
'msg' => '订单创建成功',
|
||
'data' => $codeUrl,
|
||
'qrImageUrl' => $payMode === 'qr' ? PaymentConfigService::buildQrImageUrl($codeUrl) : '',
|
||
'mock' => $mock,
|
||
'realPayment' => !$mock,
|
||
'payMode' => $mock ? 'qr' : $payMode,
|
||
'redirectUrl' => $redirectUrl,
|
||
], JSON_UNESCAPED_UNICODE);
|
||
}
|
||
|
||
private function markPaidFromNotify(string $orderNo, string $transactionId, int $payTime, int $payType): bool
|
||
{
|
||
if ($orderNo === '') {
|
||
return false;
|
||
}
|
||
|
||
Db::startTrans();
|
||
try {
|
||
$order = Order::where('orderNo', $orderNo)->lock(true)->find();
|
||
if (!$order) {
|
||
Db::rollback();
|
||
return false;
|
||
}
|
||
if ((int)$order->status === 1) {
|
||
Db::commit();
|
||
return true;
|
||
}
|
||
|
||
$order->status = 1;
|
||
$order->payType = $payType;
|
||
$order->payTime = $payTime > 0 ? $payTime : time();
|
||
$order->transactionId = $transactionId;
|
||
$order->save();
|
||
$this->processOrder($order);
|
||
Db::commit();
|
||
return true;
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
Log::error('[Payment] markPaidFromNotify', ['error' => $e->getMessage(), 'orderNo' => $orderNo]);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private function parseWechatPayTime(string $successTime): int
|
||
{
|
||
if ($successTime === '') {
|
||
return time();
|
||
}
|
||
$ts = strtotime($successTime);
|
||
return $ts ?: time();
|
||
}
|
||
|
||
private function wechatNotifyOk(): string
|
||
{
|
||
return json_encode(['code' => 'SUCCESS', 'message' => 'OK'], JSON_UNESCAPED_UNICODE);
|
||
}
|
||
|
||
private function wechatNotifyFail(string $msg): string
|
||
{
|
||
return json_encode(['code' => 'FAIL', 'message' => $msg], JSON_UNESCAPED_UNICODE);
|
||
}
|
||
}
|