重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构 - 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐 - 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作 - 新增管理后台、支付与云端文件 API,更新 README 与项目规范 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user