feat(pricing): implement tariff management with peak, valley, and flat pricing feat(api): add tariff API for fetching and updating pricing configurations feat(tariff-schema): create database schema for tariff configuration feat(pricing-page): create UI for displaying and managing pricing tiers fix(sidebar): update sidebar to include pricing settings link
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import { Hono } from "hono";
|
|
import { eq } from "drizzle-orm";
|
|
import { useDrizzle } from "@/lib/db.js";
|
|
import { tariff } from "@/db/schema.js";
|
|
import type { HonoEnv } from "@/types/hono.ts";
|
|
import type { PriceTier, TariffSlot } from "@/db/tariff-schema.ts";
|
|
|
|
const app = new Hono<HonoEnv>();
|
|
|
|
/** GET /api/tariff — 返回当前生效的电价配置(任何已登录用户) */
|
|
app.get("/", async (c) => {
|
|
const db = useDrizzle();
|
|
const [row] = await db.select().from(tariff).where(eq(tariff.isActive, true)).limit(1);
|
|
|
|
if (!row) return c.json(null);
|
|
|
|
return c.json(rowToPayload(row));
|
|
});
|
|
|
|
/** PUT /api/tariff — 更新/初始化电价配置(仅管理员) */
|
|
app.put("/", async (c) => {
|
|
const currentUser = c.get("user");
|
|
if (currentUser?.role !== "admin") {
|
|
return c.json({ error: "Forbidden" }, 403);
|
|
}
|
|
|
|
type TierPricing = { electricityPrice: number; serviceFee: number };
|
|
type TariffBody = {
|
|
slots: TariffSlot[];
|
|
prices: Record<PriceTier, TierPricing>;
|
|
};
|
|
|
|
let body: TariffBody;
|
|
try {
|
|
body = await c.req.json<TariffBody>();
|
|
} catch {
|
|
return c.json({ error: "Invalid JSON" }, 400);
|
|
}
|
|
|
|
const { slots, prices } = body;
|
|
if (!Array.isArray(slots) || !prices) {
|
|
return c.json({ error: "Missing slots or prices" }, 400);
|
|
}
|
|
|
|
const db = useDrizzle();
|
|
|
|
// 停用旧配置
|
|
await db.update(tariff).set({ isActive: false });
|
|
|
|
const [row] = await db
|
|
.insert(tariff)
|
|
.values({
|
|
id: crypto.randomUUID(),
|
|
slots,
|
|
peakElectricityPrice: fenFromCny(prices.peak?.electricityPrice),
|
|
peakServiceFee: fenFromCny(prices.peak?.serviceFee),
|
|
valleyElectricityPrice: fenFromCny(prices.valley?.electricityPrice),
|
|
valleyServiceFee: fenFromCny(prices.valley?.serviceFee),
|
|
flatElectricityPrice: fenFromCny(prices.flat?.electricityPrice),
|
|
flatServiceFee: fenFromCny(prices.flat?.serviceFee),
|
|
isActive: true,
|
|
})
|
|
.returning();
|
|
|
|
return c.json(rowToPayload(row));
|
|
});
|
|
|
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function fenFromCny(cny: number | undefined): number {
|
|
if (typeof cny !== "number" || isNaN(cny)) return 0;
|
|
return Math.round(cny * 100);
|
|
}
|
|
|
|
function rowToPayload(row: typeof tariff.$inferSelect) {
|
|
return {
|
|
id: row.id,
|
|
slots: row.slots as TariffSlot[],
|
|
prices: {
|
|
peak: {
|
|
electricityPrice: row.peakElectricityPrice / 100,
|
|
serviceFee: row.peakServiceFee / 100,
|
|
},
|
|
valley: {
|
|
electricityPrice: row.valleyElectricityPrice / 100,
|
|
serviceFee: row.valleyServiceFee / 100,
|
|
},
|
|
flat: {
|
|
electricityPrice: row.flatElectricityPrice / 100,
|
|
serviceFee: row.flatServiceFee / 100,
|
|
},
|
|
},
|
|
createdAt: row.createdAt,
|
|
updatedAt: row.updatedAt,
|
|
};
|
|
}
|
|
|
|
export default app;
|