注册支持用户名/手机号/邮箱 + 镜像站部署脚本 + AI 助手 Tool Calling 重构
- Prisma User 模型新增 username 字段(唯一索引) - 注册先查重复再创建,返回友好中文提示(非 500) - 登录支持用户名/手机号/邮箱三种方式 - 前端注册表单增加用户名输入框,预校验 2-20 位格式 - 新增 scripts/deploy.sh:一键构建并部署主站+镜像站+重启后端+重载 Nginx - 镜像站 www.yuzhiran.com.cn Nginx 配置与主站同步 - AI 助手架构升级:用户端/管理后台均采用完整 Tool Calling 架构 - 新增 UserAiAssistantService(18 工具)+ AiAssistantController - admin 助手新增 search + mark-all-notifications-read 工具 - 修复注册 500 错误:catch Prisma P2002 → BadRequestException - Baidu Analytics Script 注入 root layout
This commit is contained in:
Regular → Executable
+60
-62
@@ -4,59 +4,68 @@ import { useEffect, useState, useRef } from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
import { apiFetch } from '@/lib/auth';
|
||||
import { isWeChatBrowser } from '@/lib/wechat';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
interface PayResult {
|
||||
prepay_id?: string;
|
||||
nonceStr?: string;
|
||||
timeStamp?: string;
|
||||
package?: string;
|
||||
paySign?: string;
|
||||
signType?: string;
|
||||
gatewayOrderId?: string;
|
||||
payUrl?: string;
|
||||
qrcode?: string;
|
||||
codeUrl?: string;
|
||||
redirectUrl?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
orderNo: string;
|
||||
payResult: PayResult;
|
||||
tradeType: 'JSAPI' | 'NATIVE';
|
||||
payChannel: 'wxpay' | 'alipay';
|
||||
onPaid: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function PaymentModal({ open, orderNo, payResult, tradeType, onPaid, onClose }: Props) {
|
||||
export default function PaymentModal({ open, orderNo, payResult, payChannel, onPaid, onClose }: Props) {
|
||||
const t = useT();
|
||||
const [status, setStatus] = useState<'pending' | 'paid' | 'failed'>('pending');
|
||||
const [qrDataUrl, setQrDataUrl] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const wechatBridgeCalled = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setStatus('pending');
|
||||
setMessage('');
|
||||
wechatBridgeCalled.current = false;
|
||||
if (pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; }
|
||||
return;
|
||||
}
|
||||
|
||||
// NATIVE: render QR code
|
||||
if (tradeType === 'NATIVE' && payResult?.codeUrl) {
|
||||
QRCode.toDataURL(payResult.codeUrl, { margin: 1, width: 280 }, (err, url) => {
|
||||
const qrCode = payResult.qrcode || payResult.codeUrl;
|
||||
const redirectUrl = payResult.payUrl || payResult.redirectUrl;
|
||||
|
||||
if (payChannel === 'alipay') {
|
||||
setMessage(t.member.alipayRedirect);
|
||||
if (redirectUrl && redirectUrl !== 'mock://pay') {
|
||||
window.open(redirectUrl, '_blank');
|
||||
}
|
||||
startPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
// WeChat NATIVE: render QR code
|
||||
if (qrCode) {
|
||||
QRCode.toDataURL(qrCode, { margin: 1, width: 280 }, (err, url) => {
|
||||
if (!err) setQrDataUrl(url);
|
||||
});
|
||||
setMessage('请使用微信扫描二维码完成支付');
|
||||
setMessage(t.member.scanQrCode);
|
||||
startPolling();
|
||||
}
|
||||
|
||||
// JSAPI in WeChat: call WeixinJSBridge
|
||||
if (tradeType === 'JSAPI' && isWeChatBrowser() && !wechatBridgeCalled.current) {
|
||||
wechatBridgeCalled.current = true;
|
||||
setMessage('正在调起微信支付...');
|
||||
callWechatJsapi(payResult);
|
||||
// WeChat JSAPI in browser
|
||||
if (isWeChatBrowser() && payResult.gatewayOrderId) {
|
||||
setMessage(t.member.alipayRedirect);
|
||||
startPolling();
|
||||
}
|
||||
}, [open, tradeType, payResult?.codeUrl]);
|
||||
}, [open, payChannel, payResult?.qrcode, payResult?.codeUrl, payResult?.payUrl]);
|
||||
|
||||
function startPolling() {
|
||||
if (pollingRef.current) clearInterval(pollingRef.current);
|
||||
@@ -64,9 +73,10 @@ export default function PaymentModal({ open, orderNo, payResult, tradeType, onPa
|
||||
try {
|
||||
const res = await apiFetch(`/payment/wxpay/query?outTradeNo=${orderNo}`);
|
||||
const data = await res.json();
|
||||
if (data.trade_state === 'SUCCESS' || data.localStatus === 'PAID') {
|
||||
const isPaid = data.trade_state === 'SUCCESS' || data.localStatus === 'PAID' || data.gatewayStatus === 'paid';
|
||||
if (isPaid) {
|
||||
setStatus('paid');
|
||||
setMessage('支付成功!');
|
||||
setMessage(t.member.orderPaid);
|
||||
if (pollingRef.current) clearInterval(pollingRef.current);
|
||||
setTimeout(onPaid, 1500);
|
||||
}
|
||||
@@ -74,54 +84,21 @@ export default function PaymentModal({ open, orderNo, payResult, tradeType, onPa
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function callWechatJsapi(params: PayResult) {
|
||||
if (typeof WeixinJSBridge === 'undefined') {
|
||||
document.addEventListener('WeixinJSBridgeReady', () => doInvoke(params), false);
|
||||
} else {
|
||||
doInvoke(params);
|
||||
}
|
||||
}
|
||||
|
||||
function doInvoke(params: PayResult) {
|
||||
WeixinJSBridge.invoke(
|
||||
'getBrandWCPayRequest',
|
||||
{
|
||||
appId: '', // filled by WeChat
|
||||
timeStamp: params.timeStamp || '',
|
||||
nonceStr: params.nonceStr || '',
|
||||
package: params.package || '',
|
||||
signType: params.signType || 'RSA',
|
||||
paySign: params.paySign || '',
|
||||
},
|
||||
(res: any) => {
|
||||
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||
setStatus('paid');
|
||||
setMessage('支付成功!');
|
||||
if (pollingRef.current) clearInterval(pollingRef.current);
|
||||
setTimeout(onPaid, 1500);
|
||||
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||||
setMessage('已取消支付');
|
||||
setStatus('pending');
|
||||
} else {
|
||||
setMessage('支付失败,请重试');
|
||||
setStatus('failed');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => { if (pollingRef.current) clearInterval(pollingRef.current); };
|
||||
}, []);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const title = payChannel === 'alipay' ? t.member.alipay : t.member.wechatPay;
|
||||
const qrCode = payResult.qrcode || payResult.codeUrl;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
||||
<div className="bg-card rounded-2xl p-8 w-full max-w-sm mx-4 shadow-xl border border-border" onClick={e => e.stopPropagation()}>
|
||||
<h3 className="text-lg font-semibold text-foreground text-center mb-4">支付</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground text-center mb-4">{title}</h3>
|
||||
|
||||
{tradeType === 'NATIVE' && (
|
||||
{payChannel === 'wxpay' && qrCode && (
|
||||
<div className="flex justify-center mb-4">
|
||||
{qrDataUrl ? (
|
||||
<img src={qrDataUrl} alt="支付二维码" className="w-56 h-56 rounded-xl border border-border" />
|
||||
@@ -131,6 +108,17 @@ export default function PaymentModal({ open, orderNo, payResult, tradeType, onPa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{payChannel === 'alipay' && status === 'pending' && (
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="w-56 h-56 bg-muted rounded-xl flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-5xl mb-2">💳</div>
|
||||
<p className="text-xs text-muted-foreground">{t.member.alipayRedirect}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'paid' ? (
|
||||
<div className="text-center">
|
||||
<div className="text-5xl mb-3">✅</div>
|
||||
@@ -139,12 +127,22 @@ export default function PaymentModal({ open, orderNo, payResult, tradeType, onPa
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground text-center mb-4">{message}</p>
|
||||
{payChannel === 'alipay' && (payResult.payUrl || payResult.redirectUrl) && (
|
||||
<a
|
||||
href={payResult.payUrl || payResult.redirectUrl || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full py-2.5 mb-2 text-sm font-medium text-center text-white bg-blue-600 rounded-xl hover:bg-blue-700"
|
||||
>
|
||||
{t.member.openAlipay}
|
||||
</a>
|
||||
)}
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="w-2 h-2 bg-brand-600 rounded-full animate-pulse" />
|
||||
等待支付中...
|
||||
{t.member.waitingPay}
|
||||
</div>
|
||||
<button onClick={onClose} className="mt-4 w-full py-2 text-sm text-muted-foreground border border-border rounded-xl hover:bg-accent">
|
||||
取消支付
|
||||
{t.member.cancelPay}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user