dd8e2ad9c7
Deploy staging to test server / deploy (push) Successful in 3m42s
Use direct PostgreSQL access for package and Z-Pay administration so self-hosted staging can load and save settings reliably, while restoring admin scrolling and default channel collapse.
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { requireAdminSession } from "@/lib/admin/auth";
|
|
import { queryAdminRows } from "@/lib/admin/database";
|
|
import { adminErrorResponse } from "@/lib/admin/http";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
const schema = z.object({
|
|
name: z.string().trim().min(1).max(80),
|
|
description: z.string().trim().max(500),
|
|
priceCents: z.number().int().positive().max(100_000_000),
|
|
credits: z.number().int().positive().max(10_000_000),
|
|
sortOrder: z.number().int().min(-100_000).max(100_000),
|
|
enabled: z.boolean(),
|
|
}).strict();
|
|
const updateSchema = schema.extend({ id: z.string().uuid() });
|
|
const idSchema = z.object({ id: z.string().uuid() }).strict();
|
|
|
|
type PackageRow = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
price_cents: number;
|
|
credits: number;
|
|
sort_order: number;
|
|
enabled: boolean;
|
|
created_at: Date;
|
|
updated_at: Date;
|
|
};
|
|
|
|
function output(row: PackageRow) {
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
description: row.description,
|
|
priceCents: row.price_cents,
|
|
credits: row.credits,
|
|
sortOrder: row.sort_order,
|
|
enabled: row.enabled,
|
|
createdAt: row.created_at.toISOString(),
|
|
updatedAt: row.updated_at.toISOString(),
|
|
};
|
|
}
|
|
|
|
export async function GET() {
|
|
try {
|
|
await requireAdminSession("read");
|
|
const rows = await queryAdminRows<PackageRow>(`
|
|
select id, name, description, price_cents, credits, sort_order, enabled, created_at, updated_at
|
|
from public.payment_packages
|
|
order by sort_order, created_at
|
|
`);
|
|
return NextResponse.json({ packages: rows.map(output) });
|
|
} catch (error) {
|
|
return adminErrorResponse(error);
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const auth = await requireAdminSession("write");
|
|
const parsed = schema.safeParse(await request.json().catch(() => null));
|
|
if (!parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 });
|
|
const p = parsed.data;
|
|
const rows = await queryAdminRows<PackageRow>(`
|
|
insert into public.payment_packages
|
|
(name, description, price_cents, credits, sort_order, enabled, created_by)
|
|
values ($1, $2, $3, $4, $5, $6, $7)
|
|
returning id, name, description, price_cents, credits, sort_order, enabled, created_at, updated_at
|
|
`, [p.name, p.description, p.priceCents, p.credits, p.sortOrder, p.enabled, auth.user.id]);
|
|
return NextResponse.json({ package: output(rows[0]) }, { status: 201 });
|
|
} catch (error) {
|
|
return adminErrorResponse(error);
|
|
}
|
|
}
|
|
|
|
export async function PATCH(request: Request) {
|
|
try {
|
|
await requireAdminSession("write");
|
|
const parsed = updateSchema.safeParse(await request.json().catch(() => null));
|
|
if (!parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 });
|
|
const p = parsed.data;
|
|
const rows = await queryAdminRows<PackageRow>(`
|
|
update public.payment_packages
|
|
set name = $2, description = $3, price_cents = $4, credits = $5,
|
|
sort_order = $6, enabled = $7, updated_at = clock_timestamp()
|
|
where id = $1
|
|
returning id, name, description, price_cents, credits, sort_order, enabled, created_at, updated_at
|
|
`, [p.id, p.name, p.description, p.priceCents, p.credits, p.sortOrder, p.enabled]);
|
|
if (!rows[0]) return NextResponse.json({ error: "套餐不存在" }, { status: 404 });
|
|
return NextResponse.json({ package: output(rows[0]) });
|
|
} catch (error) {
|
|
return adminErrorResponse(error);
|
|
}
|
|
}
|
|
|
|
export async function DELETE(request: Request) {
|
|
try {
|
|
await requireAdminSession("write");
|
|
const parsed = idSchema.safeParse(await request.json().catch(() => null));
|
|
if (!parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 });
|
|
const rows = await queryAdminRows<{ id: string }>(`
|
|
update public.payment_packages
|
|
set enabled = false, updated_at = clock_timestamp()
|
|
where id = $1
|
|
returning id
|
|
`, [parsed.data.id]);
|
|
if (!rows[0]) return NextResponse.json({ error: "套餐不存在" }, { status: 404 });
|
|
return NextResponse.json({ ok: true });
|
|
} catch (error) {
|
|
return adminErrorResponse(error);
|
|
}
|
|
}
|