'小红书解析
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
63
app/api/xiaohongshu/profile/route.ts
Normal file
63
app/api/xiaohongshu/profile/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
29
app/api/xiaohongshu/validate/route.ts
Normal file
29
app/api/xiaohongshu/validate/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user