From 742c293e1459326b3796d25ad0cefcde469081a6 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 18 Mar 2026 00:53:28 -0500 Subject: [PATCH] 's' --- app/api/join/route.ts | 23 ++++- app/api/xiaohongshu/profile/route.ts | 5 +- app/api/xiaohongshu/validate/route.ts | 17 ++-- app/join/page.tsx | 128 ++++++++++++++++++++++++-- 4 files changed, 153 insertions(+), 20 deletions(-) diff --git a/app/api/join/route.ts b/app/api/join/route.ts index 2541393..25c4835 100644 --- a/app/api/join/route.ts +++ b/app/api/join/route.ts @@ -37,7 +37,7 @@ export async function POST(request: NextRequest) { 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 || "").trim(); + 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(); @@ -61,6 +61,24 @@ export async function POST(request: NextRequest) { { 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(); @@ -68,6 +86,7 @@ export async function POST(request: NextRequest) { /** occupation: 职业/城市,reason: 仅自我介绍 */ const occupationValue = profession || city; + const userInfo = String(formData.userInfo || "").trim(); const record: Record = { user_id: userId, nickname, @@ -87,6 +106,7 @@ export async function POST(request: NextRequest) { first_time: firstTime, is_volunteer: isVolunteer, qualify, + userInfo: userInfo || "", }; if (phone.trim()) { record.phone = phone.trim(); @@ -177,6 +197,7 @@ async function tryPocketBaseDirect( is_approved: qualify, age_range: record.age_range ?? "", first_time: record.first_time ?? "", + userInfo: record.userInfo ?? "", }; if (record.phone != null && String(record.phone).trim()) { body.phone = record.phone; diff --git a/app/api/xiaohongshu/profile/route.ts b/app/api/xiaohongshu/profile/route.ts index b50a2df..9a5694f 100644 --- a/app/api/xiaohongshu/profile/route.ts +++ b/app/api/xiaohongshu/profile/route.ts @@ -13,16 +13,15 @@ function getSalonApiBase(): string { return resolvePaymentApiUrl(); } -/** 代理到 salonapi 获取小红书用户资料 */ +/** 代理到 salonapi 获取小红书用户资料,支持主页链接、笔记链接 xhslink.com/o/ */ export async function POST(request: NextRequest) { try { const body = await request.json(); - const url = String(body?.url || "").trim(); + const url = String(body?.url || body?.id || "").trim(); if (!url) { return NextResponse.json({ ok: false, error: "请输入小红书链接" }, { status: 400 }); } - const apiBase = getSalonApiBase(); const res = await fetch(`${apiBase}/api/salon/xiaohongshu/profile`, { method: "POST", diff --git a/app/api/xiaohongshu/validate/route.ts b/app/api/xiaohongshu/validate/route.ts index 4ba2b33..faf8062 100644 --- a/app/api/xiaohongshu/validate/route.ts +++ b/app/api/xiaohongshu/validate/route.ts @@ -1,28 +1,31 @@ import { NextRequest, NextResponse } from "next/server"; -/** 支持:xiaohongshu.com/user/profile/xxx、xhslink.com/user/profile/xxx、xhslink.com/m/xxx 短链 */ +/** 支持:主页 xiaohongshu.com/user/profile/xxx、xhslink.com/m/xxx;笔记 xhslink.com/o/xxx、xiaohongshu.com/explore/xxx */ const XH_PROFILE_PATTERN = /^https?:\/\/(www\.)?(xiaohongshu\.com|xhslink\.com)\/user\/profile\/[a-zA-Z0-9_-]+/i; const XH_SHORT_PATTERN = /^https?:\/\/xhslink\.com\/m\/[a-zA-Z0-9_-]+/i; +const XH_NOTE_PATTERN = /^https?:\/\/(xhslink\.com\/o\/|(www\.)?xiaohongshu\.com\/explore\/)[a-zA-Z0-9_-]+/i; function isValidUrl(url: string): boolean { - return XH_PROFILE_PATTERN.test(url) || XH_SHORT_PATTERN.test(url); + const s = url.trim(); + if (!s) return false; + if (XH_PROFILE_PATTERN.test(s) || XH_SHORT_PATTERN.test(s) || XH_NOTE_PATTERN.test(s)) return true; + return false; } export async function POST(request: NextRequest) { try { const body = await request.json(); - const url = String(body?.url || "").trim(); + const url = String(body?.url || body?.id || "").trim(); if (!url) { return NextResponse.json({ valid: false, error: "请输入小红书链接" }, { status: 400 }); } - const valid = isValidUrl(url); - if (!valid) { - return NextResponse.json({ valid: false, error: "小红书link异常" }, { status: 200 }); + if (!isValidUrl(url)) { + return NextResponse.json({ valid: false, error: "请输入有效的主页链接或笔记链接(xhslink.com/m/xxx 或 xhslink.com/o/xxx)" }, { status: 200 }); } - return NextResponse.json({ valid: true }); + return NextResponse.json({ valid: true, url }); } catch { return NextResponse.json({ valid: false, error: "校验失败" }, { status: 500 }); } diff --git a/app/join/page.tsx b/app/join/page.tsx index 4c0b68b..a2d6c99 100644 --- a/app/join/page.tsx +++ b/app/join/page.tsx @@ -19,7 +19,7 @@ const ageRangeOptions = ["18-22", "23-27", "28-32", "33-37", "38+"]; /** 微信号仅允许英文字母、数字、下划线、连字符 */ const WECHAT_REGEX = /^[a-zA-Z0-9_-]*$/; -/** 从粘贴文本中提取小红书 URL,如 "我在小红书收获了536次赞与收藏>> https://xhslink.com/m/9H50QYiVOVx" */ +/** 从粘贴文本中提取小红书 URL(主页或笔记链接),清除前后空格 */ function extractXiaohongshuUrl(text: string): string { const trimmed = text.trim(); if (!trimmed) return ""; @@ -133,6 +133,7 @@ export default function JoinPage() { const [checkingWechat, setCheckingWechat] = useState(false); const [xiaohongshuUrl, setXiaohongshuUrl] = useState(""); + const [intro, setIntro] = useState(""); const [profession, setProfession] = useState(""); const [gender, setGender] = useState(""); const [ageRange, setAgeRange] = useState(""); @@ -146,6 +147,14 @@ export default function JoinPage() { ipLocation?: string; avatarUrl?: string; resolvedUrl?: string; + followers?: number; + followersText?: string; + following?: number; + followingText?: string; + likesAndCollects?: number; + likesAndCollectsText?: string; + redbookId?: string; + personalLink?: string; } | null>(null); const [validating, setValidating] = useState(false); const [submitted, setSubmitted] = useState(false); @@ -210,7 +219,7 @@ export default function JoinPage() { const handleUrlBlur = async () => { const extracted = extractXiaohongshuUrl(xiaohongshuUrl); - if (extracted !== xiaohongshuUrl) { + if (extracted !== xiaohongshuUrl.trim()) { setXiaohongshuUrl(extracted); } const url = extracted; @@ -230,7 +239,7 @@ export default function JoinPage() { }); const validateData = await validateRes.json(); if (!validateData.valid) { - setUrlError(validateData.error || "小红书link异常"); + setUrlError(validateData.error || "小红书链接异常"); return; } const profileRes = await fetch("/api/xiaohongshu/profile", { @@ -248,6 +257,14 @@ export default function JoinPage() { ipLocation: profileData.ipLocation, avatarUrl: profileData.avatarUrl, resolvedUrl: profileData.resolvedUrl, + followers: profileData.followers, + followersText: profileData.followersText, + following: profileData.following, + followingText: profileData.followingText, + likesAndCollects: profileData.likesAndCollects, + likesAndCollectsText: profileData.likesAndCollectsText, + redbookId: profileData.redbookId, + personalLink: profileData.personalLink, }); } else { setUrlError(profileData.error || "获取资料失败"); @@ -270,10 +287,22 @@ export default function JoinPage() { setError("该微信号已报名,请勿重复提交"); return; } + if (!xiaohongshuUrl.trim()) { + setError("请填写小红书主页链接或笔记链接"); + return; + } if (xiaohongshuUrl.trim() && urlError) { setError("请先修正小红书链接"); return; } + if (!intro.trim()) { + setError("请填写自我介绍"); + return; + } + if (intro.trim().length < 20) { + setError("自我介绍不少于20字"); + return; + } if (!profession.trim()) { setError("请填写职业"); return; @@ -310,7 +339,7 @@ export default function JoinPage() { const postCountOk = (profile?.postCount ?? 0) > 10; const qualify = formGenderFemale && postCountOk; const genderLabel = gender === "女" ? "女" : gender === "男" ? "男" : "无"; - const submitXiaohongshuUrl = profile?.resolvedUrl || xiaohongshuUrl.trim() || ""; + const submitXiaohongshuUrl = profile?.resolvedUrl || extractXiaohongshuUrl(xiaohongshuUrl) || ""; const res = await fetch("/api/join", { method: "POST", @@ -323,7 +352,27 @@ export default function JoinPage() { xiaohongshu_url: submitXiaohongshuUrl, xiaohongshu_avatar_url: profile?.avatarUrl || "", gender: genderLabel, - intro: "", + intro: intro.trim(), + reason: intro.trim(), + userInfo: profile + ? JSON.stringify({ + 昵称: profile.nickname, + 性别: profile.gender, + 笔记数: profile.postCount, + 笔记数文案: profile.postCountText, + IP属地: profile.ipLocation, + 头像链接: profile.avatarUrl, + 主页链接: profile.resolvedUrl, + 粉丝数: profile.followers, + 粉丝数文案: profile.followersText, + 关注数: profile.following, + 关注数文案: profile.followingText, + 获赞与收藏: profile.likesAndCollects, + 获赞与收藏文案: profile.likesAndCollectsText, + 小红书号: profile.redbookId, + 个人链接: profile.personalLink, + }) + : "", age_range: ageRange, city: "深圳", session: fromVolunteer ? "volunteer" : "digital-nomad", @@ -377,6 +426,7 @@ export default function JoinPage() { setUserRecord(null); setWechat(""); setXiaohongshuUrl(""); + setIntro(""); setProfession(""); setGender(""); setAgeRange(""); @@ -813,19 +863,79 @@ export default function JoinPage() { />
- -

💡 建议填写,可加快审核

+ { setXiaohongshuUrl(e.target.value); setUrlError(null); }} onBlur={handleUrlBlur} - placeholder="https://www.xiaohongshu.com/user/profile/xxx 或粘贴分享文案" + required className="form-input" /> {validating &&

🔄 校验中…

} {urlError && !validating &&

⚠️ {urlError}

} - {profile && !urlError &&

已验证

} + {profile && !urlError && ( + <> +

已验证

+ {process.env.NODE_ENV === "development" && ( +
+
+ {profile.avatarUrl ? ( + + ) : ( +
+ 👤 +
+ )} +
+

{profile.nickname}

+ {profile.gender &&

性别 {profile.gender}

} + {profile.redbookId &&

小红书号 {profile.redbookId}

} + {profile.ipLocation &&

IP属地 {profile.ipLocation}

} +
+
+
+ 关注 {profile.followingText ?? profile.following ?? "—"} + 粉丝 {profile.followersText ?? profile.followers ?? "—"} + 获赞与收藏 {profile.likesAndCollectsText ?? profile.likesAndCollects ?? "—"} + 笔记 {profile.postCountText ?? profile.postCount ?? "—"} +
+ {profile.personalLink && ( +

个人链接 {profile.personalLink}

+ )} + {profile.resolvedUrl && ( +

Profile {profile.resolvedUrl}

+ )} +
+ )} + + )} +
+
+ +
+