注册支持用户名/手机号/邮箱 + 镜像站部署脚本 + 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
Regular → Executable
Regular → Executable
Regular → Executable
+64
-30
@@ -6,16 +6,15 @@ import { apiFetch } from '../../../lib/auth';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import PaymentModal from '@/components/ui/payment-modal';
|
||||
import { useT } from '@/i18n';
|
||||
import { isWeChatBrowser, getOpenidFromUrl, isMiniProgram } from '@/lib/wechat';
|
||||
import { isWeChatBrowser } from '@/lib/wechat';
|
||||
|
||||
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 Subscription {
|
||||
@@ -23,7 +22,7 @@ interface Subscription {
|
||||
}
|
||||
|
||||
interface Order {
|
||||
id: number; orderNo: string; amount: number; planType: string; status: string; createdAt: string;
|
||||
id: number; orderNo: string; amount: number; planType: string; status: string; payChannel?: string; createdAt: string;
|
||||
}
|
||||
|
||||
const PLANS = [
|
||||
@@ -41,9 +40,10 @@ export default function MemberPage() {
|
||||
const [quota, setQuota] = useState<{ used: number; remaining: number; dailyLimit: number } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [payLoading, setPayLoading] = useState<string | null>(null);
|
||||
const [payChannel, setPayChannel] = useState<'wxpay' | 'alipay'>('alipay');
|
||||
const [paymentModal, setPaymentModal] = useState<{
|
||||
open: boolean; orderNo: string; payResult: PayResult; tradeType: 'JSAPI' | 'NATIVE';
|
||||
}>({ open: false, orderNo: '', payResult: {}, tradeType: 'NATIVE' });
|
||||
open: boolean; orderNo: string; payResult: PayResult; payChannel: 'wxpay' | 'alipay';
|
||||
}>({ open: false, orderNo: '', payResult: {}, payChannel: 'alipay' });
|
||||
|
||||
useEffect(() => { loadData(); }, []);
|
||||
|
||||
@@ -66,16 +66,11 @@ export default function MemberPage() {
|
||||
async function handleSubscribe(planType: string) {
|
||||
setPayLoading(planType);
|
||||
try {
|
||||
const inWeChat = isWeChatBrowser();
|
||||
const openid = getOpenidFromUrl();
|
||||
const useJsapi = inWeChat && openid && isMiniProgram();
|
||||
const tradeType = useJsapi ? 'JSAPI' : 'NATIVE';
|
||||
|
||||
const body: Record<string, any> = {
|
||||
amount: planType === 'MONTHLY' ? 49.9 : 299,
|
||||
planType, payChannel: 'wxpay', tradeType,
|
||||
planType,
|
||||
payChannel,
|
||||
};
|
||||
if (useJsapi && openid) body.openid = openid;
|
||||
|
||||
const res = await apiFetch('/orders/create', {
|
||||
method: 'POST',
|
||||
@@ -84,13 +79,17 @@ export default function MemberPage() {
|
||||
const data = await res.json();
|
||||
|
||||
if (data.order && data.payResult) {
|
||||
// Mock mode → auto-completed by backend
|
||||
if (data.payResult.codeUrl === 'mock://pay') {
|
||||
const payUrl = data.payResult.payUrl || data.payResult.redirectUrl;
|
||||
const qrCode = data.payResult.qrcode || data.payResult.codeUrl;
|
||||
|
||||
if ((payUrl === 'mock://pay' || qrCode === 'mock://pay') || data.message === '模拟支付完成') {
|
||||
loadData();
|
||||
} else {
|
||||
} else if (payUrl || qrCode) {
|
||||
setPaymentModal({
|
||||
open: true, orderNo: data.order.orderNo,
|
||||
payResult: data.payResult, tradeType,
|
||||
open: true,
|
||||
orderNo: data.order.orderNo,
|
||||
payResult: data.payResult,
|
||||
payChannel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -164,10 +163,40 @@ export default function MemberPage() {
|
||||
))}
|
||||
</div>
|
||||
{plan.id !== 'FREE' && (
|
||||
<button onClick={() => handleSubscribe(plan.id)} disabled={payLoading === plan.id || isCurrent}
|
||||
className={`w-full py-2.5 rounded-xl text-sm font-medium transition-all ${isCurrent ? 'bg-muted text-muted-foreground cursor-default' : plan.popular ? 'bg-brand-600 text-white hover:bg-brand-700' : 'border border-border text-foreground hover:bg-accent'} disabled:opacity-50`}>
|
||||
{payLoading === plan.id ? t.member.processing : isCurrent ? t.member.currentPlan_badge : t.member.subscribe}
|
||||
</button>
|
||||
<div className="space-y-3">
|
||||
{!isCurrent && (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => { setPayChannel('alipay'); handleSubscribe(plan.id); }}
|
||||
disabled={payLoading === plan.id}
|
||||
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-all disabled:opacity-50 ${
|
||||
plan.popular
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'border border-border text-foreground hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{payLoading === plan.id ? t.member.processing : t.member.alipay}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setPayChannel('wxpay'); handleSubscribe(plan.id); }}
|
||||
disabled={payLoading === plan.id}
|
||||
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-all disabled:opacity-50 ${
|
||||
plan.popular
|
||||
? 'bg-brand-600 text-white hover:bg-brand-700'
|
||||
: 'border border-border text-foreground hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{payLoading === plan.id ? t.member.processing : t.member.wechatPay}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isCurrent && (
|
||||
<button disabled
|
||||
className="w-full py-2.5 rounded-xl text-sm font-medium bg-muted text-muted-foreground cursor-default">
|
||||
{t.member.currentPlan_badge}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -188,9 +217,14 @@ export default function MemberPage() {
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-foreground">¥{order.amount}</div>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${order.status === 'PAID' ? 'bg-green-100 text-green-700' : order.status === 'PENDING' ? 'bg-yellow-100 text-yellow-700' : 'bg-muted text-muted-foreground'}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
{order.payChannel && (
|
||||
<span className="text-xs text-muted-foreground">{order.payChannel === 'alipay' ? '支付宝' : '微信'}</span>
|
||||
)}
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${order.status === 'PAID' ? 'bg-green-100 text-green-700' : order.status === 'PENDING' ? 'bg-yellow-100 text-yellow-700' : 'bg-muted text-muted-foreground'}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -201,7 +235,7 @@ export default function MemberPage() {
|
||||
open={paymentModal.open}
|
||||
orderNo={paymentModal.orderNo}
|
||||
payResult={paymentModal.payResult}
|
||||
tradeType={paymentModal.tradeType}
|
||||
payChannel={paymentModal.payChannel}
|
||||
onPaid={handlePaymentPaid}
|
||||
onClose={() => setPaymentModal(prev => ({ ...prev, open: false }))}
|
||||
/>
|
||||
|
||||
Regular → Executable
Regular → Executable
Reference in New Issue
Block a user