88c6ce8ccc
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构 - 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐 - 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作 - 新增管理后台、支付与云端文件 API,更新 README 与项目规范 Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
5.9 KiB
PHP
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;
|
|
}
|
|
}
|