Files
SoonDesign/backend-web/src/Controllers/PayController.php
T
24kycj 88c6ce8ccc 重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 18:17:39 +08:00

157 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
namespace Soon\Api\Controllers;
use Soon\Api\Core\Json;
use Soon\Api\Middleware\Auth;
use Soon\Api\Services\MembershipService;
use Soon\Api\Services\AlipayClient;
use Soon\Api\Services\PayService;
use Soon\Api\Services\WeChatPay\Client as WeChatClient;
final class PayController
{
public function listMyOrders(): void
{
$u = Auth::require();
$page = max(1, (int)($_GET['page'] ?? 1));
$size = max(1, min(50, (int)($_GET['size'] ?? 8)));
Json::ok(MembershipService::listOrders($u['id'], $page, $size));
}
public function createOrder(): void
{
$u = Auth::require();
$body = Json::readBody();
$planId = (int)($body['plan_id'] ?? 0);
$channel = (string)($body['channel'] ?? 'alipay');
if (!in_array($channel, ['alipay', 'wechat'], true)) {
Json::fail('bad_request', '不支持的支付方式', 400);
}
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
Json::ok(PayService::createOrder($u['id'], $planId, $channel, $ip));
}
public function checkoutOrder(string $orderNo): void
{
$u = Auth::require();
$body = Json::readBody();
$channel = isset($body['channel']) ? (string)$body['channel'] : null;
if ($channel !== null && !in_array($channel, ['alipay', 'wechat'], true)) {
Json::fail('bad_request', '不支持的支付方式', 400);
}
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
Json::ok(PayService::checkoutOrderForUser($u['id'], $orderNo, $ip, $channel));
}
public function cancelOrder(string $orderNo): void
{
$u = Auth::require();
PayService::cancelPendingForUser($u['id'], $orderNo);
Json::ok(['order_no' => $orderNo, 'status' => 'cancelled']);
}
public function showOrder(string $orderNo): void
{
$u = Auth::require();
$order = PayService::findByOrderNoForUser($orderNo, $u['id']);
if ($order === null) {
Json::fail('not_found', '订单不存在', 404);
}
Json::ok([
'order_no' => $order['order_no'],
'status' => $order['status'],
'amount_cents' => (int)$order['amount_cents'],
'channel' => $order['channel'],
'paid_at' => $order['paid_at'],
'refund_status' => $order['refund_status'] ?? 'none',
'refund_reason' => $order['refund_reason'] ?? null,
]);
}
public function requestRefund(string $orderNo): void
{
$u = Auth::require();
$body = Json::readBody();
$reason = trim((string)($body['reason'] ?? ''));
PayService::requestRefund($u['id'], $orderNo, $reason);
Json::ok(['order_no' => $orderNo, 'refund_status' => 'pending']);
}
public function alipayNotify(): void
{
$raw = file_get_contents('php://input') ?: '';
parse_str($raw, $params);
if (empty($params) && !empty($_POST)) {
$params = $_POST;
}
if (!AlipayClient::verifyNotify($params)) {
http_response_code(400);
echo 'fail';
exit;
}
$status = (string)($params['trade_status'] ?? '');
if (!in_array($status, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
echo 'success';
exit;
}
$orderNo = (string)($params['out_trade_no'] ?? '');
$tradeNo = (string)($params['trade_no'] ?? '');
$amountCents = (int)round((float)($params['total_amount'] ?? 0) * 100);
if (PayService::markPaid($orderNo, 'alipay', $tradeNo, $amountCents)) {
echo 'success';
} else {
echo 'fail';
}
exit;
}
public function wechatNotify(): void
{
$body = file_get_contents('php://input') ?: '';
$signature = $_SERVER['HTTP_WEIXINPAY_SIGNATURE'] ?? $_SERVER['HTTP_WEIXINPAY2_SIGNATURE'] ?? '';
$serial = $_SERVER['HTTP_WEIXINPAY_SERIAL'] ?? $_SERVER['HTTP_WEIXINPAY2_SERIAL'] ?? '';
$timestamp = $_SERVER['HTTP_WEIXINPAY_TIMESTAMP'] ?? $_SERVER['HTTP_WEIXINPAY2_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_WEIXINPAY_NONCE'] ?? $_SERVER['HTTP_WEIXINPAY2_NONCE'] ?? '';
if (!WeChatClient::verifyNotify($body, $signature, $serial, $timestamp, $nonce)) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['code' => 'FAIL', 'message' => '验签失败']);
exit;
}
$data = json_decode($body, true);
$plain = WeChatClient::decryptResource(
(string)($data['resource']['ciphertext'] ?? ''),
(string)($data['resource']['associated_data'] ?? ''),
(string)($data['resource']['nonce'] ?? ''),
(string)\Soon\Api\Core\Config::get('wechat.api_v3_key', '')
);
if ($plain === null) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(['code' => 'FAIL', 'message' => '解密失败']);
exit;
}
$decoded = json_decode($plain, true);
$orderNo = (string)($decoded['out_trade_no'] ?? '');
$txnId = (string)($decoded['transaction_id'] ?? '');
$amountCents = (int)($decoded['amount']['total'] ?? 0);
$tradeState = (string)($decoded['trade_state'] ?? '');
if ($tradeState !== '' && $tradeState !== 'SUCCESS') {
header('Content-Type: application/json');
echo json_encode(['code' => 'SUCCESS']);
exit;
}
if (!PayService::markPaid($orderNo, 'wechat', $txnId, $amountCents)) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(['code' => 'FAIL', 'message' => '订单处理失败']);
exit;
}
header('Content-Type: application/json');
echo json_encode(['code' => 'SUCCESS']);
exit;
}
}