'小红书解析

This commit is contained in:
eric
2026-03-16 04:28:16 -05:00
parent ff135563fb
commit ff31ef8001
15 changed files with 700 additions and 403 deletions

View File

@@ -28,8 +28,15 @@ export async function POST(request: NextRequest) {
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 || "").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 || "").trim();
const firstTime = String(formData.first_time || "").trim();
const agreeRules = !!formData.agree_rules;
@@ -43,32 +50,59 @@ export async function POST(request: NextRequest) {
intro = extra;
}
if (!nickname || !wechatOrPhone || !intro || !session || !agreeRules || !xiaohongshuUrl) {
const isDigitalNomad = session === "digital-nomad";
const contact = wechatOrPhone || wechat || phone;
if (!nickname || !intro || !session || !agreeRules || !xiaohongshuUrl) {
return NextResponse.json(
{ ok: false, error: "请填写必填项(含小红书主页地址)" },
{ ok: false, error: "请填写必填项(含小红书主页地址、自我介绍" },
{ status: 400 }
);
}
if (!contact) {
return NextResponse.json(
{ ok: false, error: "请填写微信号或手机号" },
{ status: 400 }
);
}
if (isDigitalNomad && (!education || !graduationYear)) {
return NextResponse.json(
{ ok: false, error: "请选择学历和毕业时间" },
{ status: 400 }
);
}
const userId = getUserIdFromCookie(request) || generateAnonId();
const EDUCATION_GRAD_AGE: Record<string, number> = {
"高中及以下": 18,
大专: 21,
本科: 22,
硕士: 25,
博士: 28,
};
const gradAge = EDUCATION_GRAD_AGE[education] ?? 22;
const estimatedAge = new Date().getFullYear() - parseInt(graduationYear, 10) + gradAge;
const ageRangeForRecord = isDigitalNomad && education && graduationYear
? `推算约${estimatedAge}岁(学历${education}+${graduationYear}毕业)`
: ageRange;
const record = {
user_id: userId,
nickname,
occupation: city,
occupation: isDigitalNomad ? profession : city,
reason: intro,
wechatId: wechatOrPhone,
wechatId: wechat || wechatOrPhone,
xiaohongshu_url: xiaohongshuUrl,
gender: "",
education: "",
gradYear: "",
phone: wechatOrPhone,
gender,
education: education,
gradYear: graduationYear,
phone: phone || wechatOrPhone,
single: "",
media: "",
media: xiaohongshuAvatarUrl,
type: session,
order_id: "",
amount: 0,
age_range: ageRange,
age_range: ageRangeForRecord,
first_time: firstTime,
};

View File

@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from "next/server";
import { resolvePaymentApiUrl } from "@/config/domain.config";
export const maxDuration = 60;
/** 开发环境优先使用本地 salonapi避免 PAYMENT_API_URL 指向远程时 404 */
function getSalonApiBase(): string {
if (process.env.NODE_ENV === "development") {
const port = process.env.PAYJSAPI_PORT || "8007";
const host = process.env.SALONAPI_HOST || "127.0.0.1";
return `http://${host}:${port}`;
}
return resolvePaymentApiUrl();
}
/** 代理到 salonapi 获取小红书用户资料 */
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const url = String(body?.url || "").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",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
signal: AbortSignal.timeout(50000),
});
const data = await res.json().catch(() => ({}));
if (res.ok && data?.ok) {
return NextResponse.json({
ok: true,
nickname: data.nickname,
gender: data.gender,
avatarUrl: data.avatarUrl ?? undefined,
followers: data.followersText ?? data.followers ?? 0,
followersText: data.followersText ?? undefined,
following: data.followingText ?? data.following ?? 0,
followingText: data.followingText ?? undefined,
likesAndCollects: data.likesAndCollectsText ?? data.likesAndCollects ?? 0,
likesAndCollectsText: data.likesAndCollectsText ?? undefined,
postCount: data.postCount ?? 0,
postCountText: data.postCountText ?? undefined,
redbookId: data.redbookId ?? undefined,
ipLocation: data.ipLocation ?? undefined,
personalLink: data.personalLink ?? undefined,
resolvedUrl: data.resolvedUrl ?? undefined,
});
}
const error = data?.detail ?? data?.error ?? "无法解析用户资料,请确认链接有效";
return NextResponse.json({ ok: false, error }, { status: res.status >= 400 ? res.status : 400 });
} catch (e) {
const msg = e instanceof Error ? e.message : "获取资料失败";
return NextResponse.json({ ok: false, error: msg }, { status: 500 });
}
}

View File

@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from "next/server";
/** 支持xiaohongshu.com/user/profile/xxx、xhslink.com/user/profile/xxx、xhslink.com/m/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;
function isValidUrl(url: string): boolean {
return XH_PROFILE_PATTERN.test(url) || XH_SHORT_PATTERN.test(url);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const url = String(body?.url || "").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 });
}
return NextResponse.json({ valid: true });
} catch {
return NextResponse.json({ valid: false, error: "校验失败" }, { status: 500 });
}
}