Files
gitlab-instance-0a899031_salon/app/api/join/route.ts
2026-03-18 02:01:48 -05:00

237 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextRequest, NextResponse } from "next/server";
import { postSalonApi } from "@/app/lib/salonApi";
import { getNowChinaStr } from "@/app/lib/dates";
import { getAdminToken } from "@/app/lib/pocketbase/admin";
import { getPocketBaseConfig } from "@/config/services.config";
import { COOKIE_NAME } from "@/app/lib/auth-cookie";
function getUserIdFromCookie(request: NextRequest): string | null {
const cookie = request.cookies.get(COOKIE_NAME)?.value;
if (!cookie) return null;
try {
const payload = JSON.parse(Buffer.from(cookie, "base64url").toString("utf-8"));
return payload?.userId ?? null;
} catch {
return null;
}
}
function generateAnonId(): string {
return "anon_" + crypto.randomUUID().replace(/-/g, "");
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const formData = body && typeof body === "object" ? body : {};
const nickname = String(formData.nickname || "").trim();
const city = String(formData.city || "").trim();
const session = String(formData.session || "").trim();
const ageRange = String(formData.age_range || "").trim();
const wechatOrPhone = String(formData.wechat_or_phone || formData.wechat || "").trim();
const wechat = String(formData.wechat || "").trim();
const phone = String(formData.phone || "").trim();
const profession = String(formData.profession || "").trim();
const education = String(formData.education || "").trim();
const graduationYear = String(formData.graduation_year || formData.gradYear || "").trim();
const xiaohongshuUrl = String(formData.xiaohongshu_url || "").trim();
const xiaohongshuAvatarUrl = String(formData.xiaohongshu_avatar_url || formData.media || "").trim();
const gender = String(formData.gender || "").trim() || "\u65e0";
let intro = String(formData.intro || formData.reason || "").trim();
const firstTime = String(formData.first_time || "").trim();
const agreeRules = !!formData.agree_rules;
const status = String(formData.status || "").trim();
const activityType = String(formData.activity_type || "").trim();
const whyJoin = String(formData.why_join || "").trim();
const region = String(formData.region || "").trim();
const availableTime = String(formData.available_time || "").trim();
const meetupdate = String(formData.meetupdate || "").trim();
const isVolunteer = !!formData.is_volunteer;
const qualify = !!formData.qualify; // 女性且小红书帖子>10 时自动通过审核
/** reason 仅存自我介绍,不合并其他字段 */
const contact = wechatOrPhone || wechat || phone;
if (!nickname || !session || !agreeRules) {
return NextResponse.json(
{ ok: false, error: "请填写必填项" },
{ status: 400 }
);
}
if (!contact) {
return NextResponse.json(
{ ok: false, error: "请填写微信号" },
{ status: 400 }
);
}
if (!xiaohongshuUrl) {
return NextResponse.json(
{ ok: false, error: "请填写小红书主页链接或笔记链接" },
{ status: 400 }
);
}
if (!intro) {
return NextResponse.json(
{ ok: false, error: "请填写自我介绍" },
{ status: 400 }
);
}
if (intro.length < 20) {
return NextResponse.json(
{ ok: false, error: "自我介绍不少于20字" },
{ status: 400 }
);
}
const userId = getUserIdFromCookie(request) || generateAnonId();
const ageRangeForRecord = ageRange || "";
/** occupation: 职业/城市reason: 仅自我介绍 */
const occupationValue = profession || city;
const userInfo = String(formData.userInfo || "").trim();
const record: Record<string, unknown> = {
user_id: userId,
nickname,
occupation: occupationValue,
reason: intro,
wechatId: wechat || wechatOrPhone,
xiaohongshu_url: xiaohongshuUrl,
gender,
education: education,
gradYear: graduationYear,
single: "",
media: xiaohongshuAvatarUrl,
type: session,
order_id: "",
amount: 0,
age_range: ageRangeForRecord,
first_time: firstTime,
is_volunteer: isVolunteer,
qualify,
userInfo: userInfo || "",
meetupdate: meetupdate || "",
};
if (phone.trim()) {
record.phone = phone.trim();
}
try {
const { status, data } = await postSalonApi(request, "/api/salon/join", record);
if (status >= 500) {
const payload = typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
return NextResponse.json(
{ ok: false, error: (payload.detail as string) || (payload.error as string) || "提交失败" },
{ status: 503 }
);
}
if (status >= 400) {
const payload = typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
const errMsg = (payload.error as string) || (payload.detail as string) || "提交失败";
if (status === 401) {
return tryPocketBaseDirect(record, errMsg);
}
return NextResponse.json({ ok: false, error: errMsg }, { status });
}
return NextResponse.json({ ok: true });
} catch {
return tryPocketBaseDirect(record, "服务暂时不可用");
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
console.error("Join API error:", e);
return NextResponse.json(
{ ok: false, error: `提交失败: ${msg}` },
{ status: 500 }
);
}
}
async function tryPocketBaseDirect(
record: Record<string, unknown>,
fallbackError: string
): Promise<NextResponse> {
const token = await getAdminToken();
if (!token) {
return NextResponse.json({ ok: false, error: fallbackError }, { status: 503 });
}
const pb = getPocketBaseConfig();
const base = pb.url.replace(/\/$/, "");
const wechatId = String(record.wechatId ?? "").trim();
if (wechatId) {
const escaped = wechatId.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
const filter = encodeURIComponent(`wechatId = "${escaped}"`);
const checkRes = await fetch(
`${base}/api/collections/${pb.joinCollection}/records?filter=${filter}&perPage=1`,
{
headers: { Authorization: `Bearer ${token}` },
cache: "no-store",
}
);
if (checkRes.ok) {
const checkData = await checkRes.json();
if (Array.isArray((checkData as { items?: unknown[] }).items) && (checkData as { items: unknown[] }).items.length > 0) {
return NextResponse.json(
{ ok: false, error: "该微信号已报名,请勿重复提交" },
{ status: 400 }
);
}
}
}
const url = `${base}/api/collections/${pb.joinCollection}/records`;
const qualify = !!(record.qualify ?? false);
const body: Record<string, unknown> = {
user_id: record.user_id,
nickname: record.nickname,
occupation: record.occupation ?? "",
reason: record.reason ?? "",
wechatId: record.wechatId ?? "",
gender: record.gender ?? "",
education: record.education ?? "",
gradYear: record.gradYear ?? "",
single: record.single ?? "",
media: record.media ?? "",
type: record.type ?? "",
order_id: record.order_id ?? "",
amount: record.amount ?? 0,
xiaohongshu_url: record.xiaohongshu_url ?? "",
is_volunteer: record.is_volunteer ?? false,
is_approved: qualify,
age_range: record.age_range ?? "",
first_time: record.first_time ?? "",
userInfo: record.userInfo ?? "",
};
if (record.meetupdate != null && String(record.meetupdate).trim()) {
body.meetupdate = String(record.meetupdate).trim();
}
body.submitted_at_cn = getNowChinaStr();
if (record.phone != null && String(record.phone).trim()) {
body.phone = record.phone;
}
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
const msg = (errData as { message?: string }).message || fallbackError;
return NextResponse.json({ ok: false, error: msg }, { status: res.status });
}
return NextResponse.json({ ok: true });
} catch (e) {
console.error("PocketBase direct write error:", e);
return NextResponse.json(
{ ok: false, error: fallbackError },
{ status: 503 }
);
}
}