Files
Jyotisha/frontend/src/app/api/synastry/route.ts
T
Jesse_ChenandClaude Opus 5.5 96adbce3a9 feat(account): self-service deletion with a 7-day cooling-off period
Request signs out everywhere and freezes paid routes (423 + DB triggers);
signing in within 7 days shows the pending gate with 撤销注销. A periodic
idempotent worker purges personal content afterwards and keeps finance
rows against a tombstoned identity. Read-only admin list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-09-30 09:42:11 +08:00

246 lines
11 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 { NextResponse } from "next/server";
import { refuseWhenAccountDeletionPending } from "@/lib/account-deletion-server";
import { z } from "zod";
import { isProductEnabled } from "@/lib/product-access";
import { consumeUserRequestRateLimit } from "@/lib/request-rate-limit";
import { globalBirthProfileFromAccountRow } from "@/lib/server-owned-birth-profile";
import { ConsultationSubjectError, loadSubjectBirth } from "@/lib/subject-birth";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { synastryBirthPayload } from "@/lib/global-birth-payloads";
const apiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
const synastryRequestSchema = z.object({
partnerChartProfileId: z.string().uuid(),
relationshipType: z.enum(["romance", "business", "family", "general"]).optional(),
}).strict();
function moonLongitude(chart: Record<string, unknown>) {
const planets = chart.planets && typeof chart.planets === "object" ? chart.planets as Record<string, unknown> : {};
const moon = planets.Moon && typeof planets.Moon === "object" ? planets.Moon as Record<string, unknown> : {};
const value = moon.lon ?? moon.longitude ?? moon.degree;
const numeric = Number(value);
if (!Number.isFinite(numeric)) throw new Error("moon_longitude_missing");
return numeric;
}
function moonSummary(chart: Record<string, unknown>) {
const planets = chart.planets && typeof chart.planets === "object" ? chart.planets as Record<string, unknown> : {};
const moon = planets.Moon && typeof planets.Moon === "object" ? planets.Moon as Record<string, unknown> : {};
return {
sign: moon.sign,
nakshatra: moon.nakshatra,
pada: moon.nakshatra_pada,
lord: moon.nakshatra_lord,
longitude: moonLongitude(chart),
};
}
function d9Summary(varga: Record<string, unknown>) {
const result = varga.result && typeof varga.result === "object" ? varga.result as Record<string, unknown> : {};
const d9 = result.D9_Navamsa && typeof result.D9_Navamsa === "object" ? result.D9_Navamsa as Record<string, unknown> : {};
const ascendant = d9.ascendant && typeof d9.ascendant === "object" ? d9.ascendant as Record<string, unknown> : {};
const planets = d9.planets && typeof d9.planets === "object" ? d9.planets as Record<string, unknown> : {};
return {
ascendant,
moon: planets.Moon,
venus: planets.Venus,
mars: planets.Mars,
source: varga.source,
};
}
function planetSign(point: unknown) {
return point && typeof point === "object" && "sign" in point
? String((point as Record<string, unknown>).sign || "unknown")
: "unknown";
}
function relationshipReport(synastry: Record<string, unknown>, selfD9: Record<string, unknown>, partnerD9: Record<string, unknown>) {
const total = Number(synastry.total_score ?? 0);
const max = Number(synastry.max_score ?? 36);
const ratio = max > 0 ? total / max : 0;
const band = ratio >= 0.72 ? "supportive" : ratio >= 0.5 ? "mixed" : "challenging";
const self = d9Summary(selfD9);
const partner = d9Summary(partnerD9);
return {
status: "evidence_summary",
scoreBand: band,
headline: band === "supportive"
? "基础匹配度偏支持,但仍需结合现实互动与长期运势。"
: band === "mixed"
? "基础匹配度中等,适合重点观察沟通节奏、价值观与关系承诺。"
: "基础匹配度偏谨慎,需要先处理冲突模式与现实条件。",
strengths: [
`Ashtakoot ${total}/${max}`,
`本人 D9 Moon:${planetSign(self.moon)}`,
`对方 D9 Moon:${planetSign(partner.moon)}`,
],
risks: [
"这不是完整婚恋结论;尚未纳入双方 Dasha、UL/DK 与长期时机。",
`D9 Venus/Mars 需要继续解释:本人 ${planetSign(self.venus)}/${planetSign(self.mars)},对方 ${planetSign(partner.venus)}/${planetSign(partner.mars)}。`,
],
nextEvidence: ["双方 Dasha", "UL/DK", "D9 7宫/7主", "现实关系时间线"],
};
}
function vargaPlanetSign(varga: Record<string, unknown>, division: string, planet: string) {
const result = varga.result && typeof varga.result === "object" ? varga.result as Record<string, unknown> : {};
const chart = result[division] && typeof result[division] === "object" ? result[division] as Record<string, unknown> : {};
const planets = chart.planets && typeof chart.planets === "object" ? chart.planets as Record<string, unknown> : {};
return planetSign(planet === "Ascendant" ? chart.ascendant : planets[planet]);
}
function businessReport(selfVargas: Record<string, unknown>, partnerVargas: Record<string, unknown>) {
return {
status: "partial_evidence",
scoreBand: "not_scored",
headline: "已完成基础合作结构筛查;这不是合作成败、收益或契约保证。",
strengths: [
`D10 事业轴:本人 ${vargaPlanetSign(selfVargas, "D10_Dasamsa", "Ascendant")} / 对方 ${vargaPlanetSign(partnerVargas, "D10_Dasamsa", "Ascendant")}`,
`D2 财富轴:本人 Moon ${vargaPlanetSign(selfVargas, "D2_Hora", "Moon")} / 对方 Moon ${vargaPlanetSign(partnerVargas, "D2_Hora", "Moon")}`,
`D11 收益轴:本人 Sun ${vargaPlanetSign(selfVargas, "D11_Rudramsa", "Sun")} / 对方 Sun ${vargaPlanetSign(partnerVargas, "D11_Rudramsa", "Sun")}`,
],
risks: [
"尚未完成 A10、功能吉凶、双方 Vimshottari + Narayana、Shadbala/AV 与外部数值一致性,不得据此断言合作结果或精确时点。",
],
nextEvidence: ["A10", "功能吉凶", "双方 Vimshottari + Narayana", "D10/D2/D11 原始度数与外部校验"],
};
}
async function postPython(path: string, body: unknown) {
const response = await fetch(`${apiBase}${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(45_000),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data || typeof data !== "object") {
throw new Error(`jyotish_api_${response.status}`);
}
return data as Record<string, unknown>;
}
export async function POST(request: Request) {
try {
const supabase = await createServerSupabaseClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const frozen = await refuseWhenAccountDeletionPending(user.id);
if (frozen) return frozen;
if (!await isProductEnabled("compatibility")) {
return NextResponse.json(
{ error: "合盘服务暂未开放", code: "compatibility_product_disabled" },
{ status: 503 },
);
}
const limited = consumeUserRequestRateLimit("synastry", user.id);
if (!limited.ok) {
return NextResponse.json({
error: "请求过于频繁",
code: "rate_limited",
retryAfterSeconds: limited.retryAfterSeconds,
}, { status: 429, headers: { "Retry-After": String(limited.retryAfterSeconds) } });
}
const parsed = synastryRequestSchema.safeParse(await request.json().catch(() => null));
if (!parsed.success) {
return NextResponse.json({ error: "请选择已保存的对方星盘" }, { status: 400 });
}
let selfResolved;
try {
selfResolved = await loadSubjectBirth(supabase, user.id, "self");
} catch (error) {
if (error instanceof ConsultationSubjectError) {
return NextResponse.json({ error: "请先完成本人出生资料" }, { status: 409 });
}
throw error;
}
if (!selfResolved.chartable) {
return NextResponse.json({ error: "请先完成本人出生资料" }, { status: 409 });
}
let partnerResolved;
try {
partnerResolved = await loadSubjectBirth(supabase, user.id, parsed.data.partnerChartProfileId);
} catch (error) {
if (error instanceof ConsultationSubjectError && error.code === "subject_unavailable") {
return NextResponse.json({ error: "暂时无法读取对方星盘" }, { status: 503 });
}
if (error instanceof ConsultationSubjectError) {
return NextResponse.json({ error: "请先把对方星盘保存到云端星盘库" }, { status: 404 });
}
throw error;
}
if (!partnerResolved.chartable) {
return NextResponse.json({ error: "请先把对方星盘保存到云端星盘库" }, { status: 404 });
}
const [selfPayload, partnerPayload] = await Promise.all([
synastryBirthPayload(globalBirthProfileFromAccountRow(selfResolved.row)),
synastryBirthPayload(globalBirthProfileFromAccountRow(partnerResolved.row)),
]);
const selfChart = await postPython("/api/chart", selfPayload);
const partnerChart = await postPython("/api/chart", partnerPayload);
const relationshipType = parsed.data.relationshipType === "business" || parsed.data.relationshipType === "family" || parsed.data.relationshipType === "general"
? parsed.data.relationshipType
: "romance";
if (relationshipType === "business") {
const [selfVargas, partnerVargas] = await Promise.all([
postPython("/api/varga_full", { ...selfPayload, planets: selfChart.planets, ascendant: selfChart.ascendant, divisions: ["D2", "D10", "D11"] }),
postPython("/api/varga_full", { ...partnerPayload, planets: partnerChart.planets, ascendant: partnerChart.ascendant, divisions: ["D2", "D10", "D11"] }),
]);
return NextResponse.json({
status: "ok",
relationshipType,
claimStatus: "partial",
method: "d2_d10_d11_business_screening_partial",
evidenceLayers: ["d2_hora", "d10_dashamsa", "d11_ekadashamsa"],
blockedLayers: ["A10", "functional_benefic_malefic", "vimshottari_narayana", "shadbala_ashtakavarga", "external_engine_parity"],
relationshipReport: businessReport(selfVargas, partnerVargas),
});
}
if (relationshipType !== "romance") {
return NextResponse.json({
status: "blocked",
relationshipType,
message: "该关系类型尚无可验证的专用合盘计算合同;已保留问题草稿。",
});
}
const selfD9 = await postPython("/api/varga_full", {
...selfPayload,
planets: selfChart.planets,
ascendant: selfChart.ascendant,
divisions: ["D9"],
});
const partnerD9 = await postPython("/api/varga_full", {
...partnerPayload,
planets: partnerChart.planets,
ascendant: partnerChart.ascendant,
divisions: ["D9"],
});
const synastry = await postPython("/api/synastry", {
male_moon: moonLongitude(selfChart),
female_moon: moonLongitude(partnerChart),
});
return NextResponse.json({
status: "ok",
method: "ashtakoot_plus_moon_nakshatra_d9",
evidenceLayers: ["ashtakoot", "moon_nakshatra", "d9_navamsa"],
selfChart: { moon: moonSummary(selfChart), d9: d9Summary(selfD9) },
partnerChart: { moon: moonSummary(partnerChart), d9: d9Summary(partnerD9) },
synastry,
relationshipReport: relationshipReport(synastry, selfD9, partnerD9),
});
} catch (error) {
return NextResponse.json({
status: "blocked",
error: error instanceof Error ? error.message : "synastry_unavailable",
message: "合盘计算暂时不可用;可先保留合盘问题草稿。",
}, { status: 503 });
}
}