This commit is contained in:
eric
2026-03-16 19:17:06 -05:00
parent 3ba00e9a93
commit e30d4ad950
2 changed files with 92 additions and 45 deletions

View File

@@ -13,29 +13,10 @@ const empty = {
vip: false, vip: false,
}; };
/** 按 wechatId 查询 solanRed 记录,优先 salonapi */ async function getPocketBaseJoinByWechat(wechatId: string) {
export async function GET(request: NextRequest) {
const wechatId = request.nextUrl.searchParams.get("wechat_id")?.trim();
if (!wechatId) {
return NextResponse.json(empty);
}
try {
const { status, data } = await getSalonApi(
request,
`/api/salon/join/by-wechat?wechat_id=${encodeURIComponent(wechatId)}`,
{ timeoutMs: 8000 }
);
if (status === 200 && data && typeof data === "object") {
return NextResponse.json(data);
}
} catch {
// salonapi 不可用,回退到直连 PocketBase
}
const token = await getAdminToken(); const token = await getAdminToken();
if (!token) { if (!token) {
return NextResponse.json(empty); return null;
} }
const pb = getPocketBaseConfig(); const pb = getPocketBaseConfig();
@@ -45,7 +26,7 @@ export async function GET(request: NextRequest) {
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000); const timeoutId = setTimeout(() => controller.abort(), 4000);
const res = await fetch( const res = await fetch(
`${base}/api/collections/${pb.joinCollection}/records?filter=${filter}&sort=-created&perPage=1`, `${base}/api/collections/${pb.joinCollection}/records?filter=${filter}&sort=-created&perPage=1`,
{ {
@@ -54,7 +35,7 @@ export async function GET(request: NextRequest) {
signal: controller.signal, signal: controller.signal,
} }
).finally(() => clearTimeout(timeoutId)); ).finally(() => clearTimeout(timeoutId));
if (!res.ok) return NextResponse.json(empty); if (!res.ok) return null;
const data = await res.json(); const data = await res.json();
const items = Array.isArray(data?.items) ? data.items : []; const items = Array.isArray(data?.items) ? data.items : [];
const rec = items[0] ?? null; const rec = items[0] ?? null;
@@ -66,7 +47,7 @@ export async function GET(request: NextRequest) {
const isRejected = !!rec?.is_rejected; const isRejected = !!rec?.is_rejected;
const vip = !!rec?.vip; const vip = !!rec?.vip;
return NextResponse.json({ return {
hasRecord, hasRecord,
record: rec, record: rec,
isComplete, isComplete,
@@ -74,8 +55,36 @@ export async function GET(request: NextRequest) {
isApproved, isApproved,
isRejected, isRejected,
vip, vip,
}); };
} catch { } catch {
return NextResponse.json(empty); return null;
} }
} }
/** 按 wechatId 查询 solanRed 记录。优先 salonapi记录由其创建线上部署时更可靠失败时回退直连 PocketBase */
export async function GET(request: NextRequest) {
const wechatId = request.nextUrl.searchParams.get("wechat_id")?.trim();
if (!wechatId) {
return NextResponse.json(empty);
}
try {
const { status, data } = await getSalonApi(
request,
`/api/salon/join/by-wechat?wechat_id=${encodeURIComponent(wechatId)}`,
{ timeoutMs: 6000 }
);
if (status === 200 && data && typeof data === "object") {
return NextResponse.json(data);
}
} catch {
/* salonapi 不可用,回退直连 PocketBase */
}
const pbData = await getPocketBaseJoinByWechat(wechatId);
if (pbData) {
return NextResponse.json(pbData);
}
return NextResponse.json(empty);
}

View File

@@ -77,6 +77,28 @@ interface UserRecord {
vip: boolean; vip: boolean;
} }
const EMPTY_USER_RECORD: UserRecord = {
hasRecord: false,
record: null,
isComplete: false,
isWechatFriend: false,
isApproved: false,
isRejected: false,
vip: false,
};
function toUserRecord(data: Partial<UserRecord> & { record?: UserRecord["record"] }): UserRecord {
return {
hasRecord: !!data.hasRecord,
record: data.record ?? null,
isComplete: !!data.isComplete,
isWechatFriend: !!data.isWechatFriend,
isApproved: !!data.isApproved,
isRejected: !!data.isRejected,
vip: !!data.vip,
};
}
function UserProfileCard({ record, showVolunteerTag }: { record: UserRecord["record"]; showVolunteerTag?: boolean }) { function UserProfileCard({ record, showVolunteerTag }: { record: UserRecord["record"]; showVolunteerTag?: boolean }) {
if (!record || (!record.nickname && !record.media && !record.wechatId)) return null; if (!record || (!record.nickname && !record.media && !record.wechatId)) return null;
const isVolunteer = showVolunteerTag && record.is_volunteer; const isVolunteer = showVolunteerTag && record.is_volunteer;
@@ -146,23 +168,20 @@ export default function JoinPage() {
const SUBMIT_THROTTLE_MS = 3000; const SUBMIT_THROTTLE_MS = 3000;
const fetchByWechat = async (w: string) => { const fetchByWechat = async (w: string) => {
if (!w.trim()) return; if (!w.trim()) return null;
setError(null); setError(null);
setCheckingWechat(true); setCheckingWechat(true);
try { try {
const res = await fetch(`/api/join/by-wechat?wechat_id=${encodeURIComponent(w.trim())}`); const res = await fetch(`/api/join/by-wechat?wechat_id=${encodeURIComponent(w.trim())}`, {
const data = await res.json(); cache: "no-store",
setUserRecord({
hasRecord: !!data.hasRecord,
record: data.record ?? null,
isComplete: !!data.isComplete,
isWechatFriend: !!data.isWechatFriend,
isApproved: !!data.isApproved,
isRejected: !!data.isRejected,
vip: !!data.vip,
}); });
const data = await res.json();
const nextRecord = toUserRecord(data);
setUserRecord(nextRecord);
return nextRecord;
} catch { } catch {
setUserRecord({ hasRecord: false, record: null, isComplete: false, isWechatFriend: false, isApproved: false, isRejected: false, vip: false }); setUserRecord(EMPTY_USER_RECORD);
return null;
} finally { } finally {
setCheckingWechat(false); setCheckingWechat(false);
} }
@@ -331,13 +350,23 @@ export default function JoinPage() {
setQualify(qualify); setQualify(qualify);
setSubmitted(true); setSubmitted(true);
try { try {
await fetchByWechat(wechat.trim()); localStorage.setItem(WECHAT_STORAGE_KEY, wechat.trim());
} catch { } catch {
/* ignore */
}
let result = await fetchByWechat(wechat.trim());
const maxRetries = 5;
const retryDelayMs = 1500;
for (let i = 0; i < maxRetries && !result?.hasRecord; i++) {
await new Promise((r) => setTimeout(r, retryDelayMs));
result = await fetchByWechat(wechat.trim());
}
if (!result?.hasRecord) {
try { try {
localStorage.setItem(WECHAT_STORAGE_KEY, wechat.trim()); window.location.replace("/join");
} catch {} } catch {
window.location.replace("/join"); /* ignore */
return; }
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "网络错误,请重试"); setError(err instanceof Error ? err.message : "网络错误,请重试");
@@ -618,6 +647,12 @@ export default function JoinPage() {
</p> </p>
)} )}
{paying && (
<div className="mt-6 flex items-center justify-center gap-2 rounded-xl bg-amber-50 dark:bg-amber-900/20 py-3 px-4 text-amber-700 dark:text-amber-400 text-sm">
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-amber-500 border-t-transparent" />
</div>
)}
<button <button
type="button" type="button"
onClick={handlePayNow} onClick={handlePayNow}
@@ -648,7 +683,7 @@ export default function JoinPage() {
} }
// 微信号输入区:失焦时查询,查询中不展示其他元素 // 微信号输入区:失焦时查询,查询中不展示其他元素
const wechatOnlyView = !userRecord || checkingWechat; const wechatOnlyView = !userRecord;
if (wechatOnlyView) { if (wechatOnlyView) {
return ( return (
@@ -733,7 +768,7 @@ export default function JoinPage() {
</ul> </ul>
</div> </div>
)} )}
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} action="javascript:void(0)" className="space-y-5">
<div> <div>
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">💬 <span className="text-red-500">*</span></label> <label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">💬 <span className="text-red-500">*</span></label>
<input <input
@@ -797,6 +832,9 @@ export default function JoinPage() {
{error && ( {error && (
<p className="rounded-lg bg-red-50 dark:bg-red-900/30 px-4 py-3 text-sm text-red-600 dark:text-red-400"> {error}</p> <p className="rounded-lg bg-red-50 dark:bg-red-900/30 px-4 py-3 text-sm text-red-600 dark:text-red-400"> {error}</p>
)} )}
{submitted && checkingWechat && (
<p className="text-sm text-amber-600 dark:text-amber-400"></p>
)}
<button type="submit" disabled={submitting || !!urlError || validating} className="w-full rounded-xl bg-amber-600 py-3.5 text-base font-medium text-white transition-colors hover:bg-amber-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50"> <button type="submit" disabled={submitting || !!urlError || validating} className="w-full rounded-xl bg-amber-600 py-3.5 text-base font-medium text-white transition-colors hover:bg-amber-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50">
{submitting ? "🔄 提交中…" : fromVolunteer ? "📤 提交志愿者申请" : "📤 提交报名"} {submitting ? "🔄 提交中…" : fromVolunteer ? "📤 提交志愿者申请" : "📤 提交报名"}
</button> </button>