'城市详情'
This commit is contained in:
114
app/api/join/route.ts
Normal file
114
app/api/join/route.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPocketBaseConfig } from "@/config/services.config";
|
||||
|
||||
function getOrCreateUserId(): string {
|
||||
return `user${Date.now()}`;
|
||||
}
|
||||
|
||||
async function getAdminToken(): Promise<string | null> {
|
||||
const email = process.env.POCKETBASE_EMAIL;
|
||||
const password = process.env.POCKETBASE_PASSWORD;
|
||||
if (!email || !password) return null;
|
||||
|
||||
const pb = getPocketBaseConfig();
|
||||
const res = await fetch(`${pb.url}/api/admins/auth-with-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: email, password }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return data?.token ?? null;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = getOrCreateUserId();
|
||||
const body = await request.json();
|
||||
const formData = body && typeof body === "object" ? body : {};
|
||||
|
||||
const {
|
||||
nickname,
|
||||
gender,
|
||||
single,
|
||||
profession,
|
||||
intro,
|
||||
wechat,
|
||||
education,
|
||||
graduationYear,
|
||||
phone,
|
||||
media: mediaUrl,
|
||||
} = formData;
|
||||
|
||||
const record: Record<string, string | number> = {
|
||||
nickname: nickname || "",
|
||||
occupation: profession || "",
|
||||
reason: intro || "",
|
||||
single: single || "",
|
||||
wechatId: wechat || "",
|
||||
gender: gender || "",
|
||||
education: education || "",
|
||||
gradYear: graduationYear || "",
|
||||
phone: phone || "",
|
||||
user_id: userId,
|
||||
amount: 0,
|
||||
order_id: "",
|
||||
type: "",
|
||||
};
|
||||
|
||||
if (mediaUrl && typeof mediaUrl === "string") {
|
||||
(record as Record<string, string>)["media"] = mediaUrl;
|
||||
}
|
||||
|
||||
const pb = getPocketBaseConfig();
|
||||
const collection = pb.joinCollection;
|
||||
|
||||
const doCreate = async (authToken?: string) => {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (authToken) headers["Authorization"] = authToken;
|
||||
|
||||
return fetch(`${pb.url}/api/collections/${collection}/records`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(record),
|
||||
});
|
||||
};
|
||||
|
||||
let res = await doCreate();
|
||||
|
||||
if (res.status === 403) {
|
||||
const token = await getAdminToken();
|
||||
if (token) res = await doCreate(token);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text();
|
||||
console.error("PocketBase create error:", res.status, errText);
|
||||
let errMsg = "提交失败,请稍后重试";
|
||||
try {
|
||||
const errJson = JSON.parse(errText);
|
||||
if (errJson?.message) errMsg = errJson.message;
|
||||
if (errJson?.data && typeof errJson.data === "object") {
|
||||
const details = Object.entries(errJson.data)
|
||||
.map(([k, v]) => `${k}: ${(v as { message?: string })?.message || v}`)
|
||||
.join("; ");
|
||||
if (details) errMsg += ` (${details})`;
|
||||
}
|
||||
} catch {
|
||||
if (errText.length < 200) errMsg = errText;
|
||||
}
|
||||
return NextResponse.json({ ok: false, error: errMsg }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, user_id: userId });
|
||||
} 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
97
app/api/pay/route.ts
Normal file
97
app/api/pay/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPaymentConfig } from "@/config/services.config";
|
||||
import { SITE_URL } from "@/app/lib/env";
|
||||
|
||||
async function createPayOrder(
|
||||
user_id: string,
|
||||
return_url: string,
|
||||
total_fee?: number,
|
||||
type?: string,
|
||||
channel = "alipay"
|
||||
) {
|
||||
const config = getPaymentConfig();
|
||||
if (!config.apiUrl) throw new Error("未配置 PAYMENT_API_URL");
|
||||
|
||||
const payload = {
|
||||
user_id,
|
||||
total_fee: Number(total_fee) || config.defaultAmount,
|
||||
type: type || config.defaultType,
|
||||
channel,
|
||||
return_url,
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const res = await fetch(`${config.apiUrl}/payh5`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal,
|
||||
}).finally(() => clearTimeout(timeout));
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || data?.status !== "ok") {
|
||||
throw new Error(data?.detail || data?.msg || data?.message || "支付创建失败");
|
||||
}
|
||||
|
||||
const params = data.params || data.xorpay_params || {};
|
||||
const payUrl = data.pay_url || data.xorpay_url || config.zpaySubmitUrl;
|
||||
if (!params || typeof params !== "object") {
|
||||
throw new Error("payjsapi 未返回支付参数");
|
||||
}
|
||||
return { params: JSON.parse(JSON.stringify(params)), payUrl };
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
let body: Record<string, string | number>;
|
||||
const contentType = request.headers.get("content-type") || "";
|
||||
if (contentType.includes("application/json")) {
|
||||
body = await request.json();
|
||||
} else {
|
||||
const form = await request.formData();
|
||||
body = Object.fromEntries(form.entries()) as Record<string, string>;
|
||||
}
|
||||
|
||||
const user_id = String(body?.user_id || "").trim();
|
||||
const return_url = String(body?.return_url || "").trim() || `${SITE_URL}/zh/join/paid`;
|
||||
const total_fee = Number(body?.total_fee) || 80;
|
||||
const type = String(body?.type || "meetup");
|
||||
const channel = String(body?.channel || "alipay");
|
||||
const device = String(body?.device || "pc");
|
||||
|
||||
if (!user_id) {
|
||||
return NextResponse.json({ ok: false, error: "缺少 user_id" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { params, payUrl } = await createPayOrder(user_id, return_url, total_fee, type, channel);
|
||||
|
||||
const wantHtml = String(body?.html) === "1" || (request.headers.get("accept") || "").includes("text/html");
|
||||
if (wantHtml) {
|
||||
const escapeAttr = (s: string) =>
|
||||
String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
const inputs = Object.entries(params as Record<string, unknown>)
|
||||
.filter(([, v]) => v != null && String(v).trim() !== "")
|
||||
.map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(String(v))}" />`)
|
||||
.join("\n");
|
||||
const orderId = String((params as Record<string, unknown>)?.out_trade_no || "").trim();
|
||||
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>跳转支付...</title></head><body><p>正在跳转到支付页面...</p><form id="f" action="${escapeAttr(payUrl)}" method="POST">${inputs}</form><script>document.getElementById("f").submit();</script></body></html>`;
|
||||
const res = new NextResponse(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
||||
if (orderId) {
|
||||
res.cookies.set("join_pay_order", `${orderId}|${Date.now()}`, { path: "/", maxAge: 900 });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, pay_url: payUrl, params, provider: "zpay" });
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error(String(e));
|
||||
console.error("Pay API error:", err);
|
||||
return NextResponse.json({ ok: false, error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
23
app/api/pay/status/route.ts
Normal file
23
app/api/pay/status/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPaymentConfig } from "@/config/services.config";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const orderId = request.nextUrl.searchParams.get("order_id");
|
||||
if (!orderId) {
|
||||
return NextResponse.json({ ok: false, error: "缺少 order_id" }, { status: 400 });
|
||||
}
|
||||
const config = getPaymentConfig();
|
||||
if (!config.apiUrl) {
|
||||
return NextResponse.json({ ok: false, error: "未配置 PAYMENT_API_URL" }, { status: 500 });
|
||||
}
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${config.apiUrl}/zpay_order_status?out_trade_no=${encodeURIComponent(orderId)}`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json({ ok: true, paid: !!data?.paid });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ ok: false, paid: false, error: String(e) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
39
app/api/upload-media/route.ts
Normal file
39
app/api/upload-media/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { uploadBuffer, generateObjectKey } from "@/app/lib/minio";
|
||||
|
||||
const MAX_SIZE = 20 * 1024 * 1024; // 20MB
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const form = await request.formData();
|
||||
const file = form.get("file");
|
||||
const fileObj =
|
||||
file != null && typeof file === "object" && "size" in file
|
||||
? (file as File | Blob)
|
||||
: null;
|
||||
|
||||
if (!fileObj || fileObj.size === 0) {
|
||||
return NextResponse.json({ ok: false, error: "请选择文件" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (fileObj.size > MAX_SIZE) {
|
||||
return NextResponse.json({ ok: false, error: "文件大小不能超过 20MB" }, { status: 400 });
|
||||
}
|
||||
|
||||
const ext =
|
||||
(fileObj instanceof File ? fileObj.name : "").split(".").pop() ||
|
||||
(fileObj.type?.startsWith("video/") ? "mp4" : "jpg");
|
||||
const objectKey = generateObjectKey(ext);
|
||||
|
||||
const buffer = Buffer.from(await fileObj.arrayBuffer());
|
||||
const contentType = (fileObj as File).type || "application/octet-stream";
|
||||
|
||||
const { url } = await uploadBuffer(buffer, objectKey, contentType);
|
||||
|
||||
return NextResponse.json({ ok: true, url });
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Upload media error:", e);
|
||||
return NextResponse.json({ ok: false, error: `上传失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user