Files
cunkebao_v3/Server/application/common/controller/PaymentService.php
Manus AI 5517457929 sync: 以本地为准同步全量变更至 GitHub
含四端需求文档、前端/后端/超管/触客宝迭代及部署脚本更新;未拉取远程。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 04:24:56 +08:00

496 lines
20 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\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);
}
}