init: init prok
This commit is contained in:
36
packages/server/src/index.ts
Normal file
36
packages/server/src/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import authRoutes from "./routes/auth.js";
|
||||
import stampRoutes from "./routes/stamps.js";
|
||||
import redemptionRoutes from "./routes/redemption.js";
|
||||
import adminRoutes from "./routes/admin.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.SERVER_PORT || 3000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Serve uploaded files
|
||||
app.use("/uploads", express.static(path.join(__dirname, "../uploads")));
|
||||
|
||||
// Health check
|
||||
app.get("/api/health", (_req, res) => {
|
||||
res.json({ success: true, data: { status: "ok" } });
|
||||
});
|
||||
|
||||
// User-facing routes
|
||||
app.use("/api/auth", authRoutes);
|
||||
app.use("/api/stamps", stampRoutes);
|
||||
app.use("/api/redemption", redemptionRoutes);
|
||||
|
||||
// Admin routes
|
||||
app.use("/api/admin", adminRoutes);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
16
packages/server/src/lib/jwt.ts
Normal file
16
packages/server/src/lib/jwt.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const SECRET = process.env.JWT_SECRET || "dev-secret";
|
||||
const EXPIRES_IN = "7d";
|
||||
|
||||
export function signToken(userId: string): string {
|
||||
return jwt.sign({ sub: userId }, SECRET, { expiresIn: EXPIRES_IN });
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): { sub: string } | null {
|
||||
try {
|
||||
return jwt.verify(token, SECRET) as { sub: string };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
12
packages/server/src/lib/qrcode.ts
Normal file
12
packages/server/src/lib/qrcode.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import QRCode from "qrcode";
|
||||
|
||||
export async function generateQRCodeDataURL(
|
||||
url: string,
|
||||
options?: { width?: number }
|
||||
): Promise<string> {
|
||||
return QRCode.toDataURL(url, {
|
||||
width: options?.width ?? 300,
|
||||
margin: 2,
|
||||
color: { dark: "#1a1a2e", light: "#ffffff" },
|
||||
});
|
||||
}
|
||||
11
packages/server/src/middleware/admin.ts
Normal file
11
packages/server/src/middleware/admin.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
|
||||
export function requireAdmin(req: Request, res: Response, next: NextFunction) {
|
||||
const key = req.headers["x-admin-key"];
|
||||
const expected = process.env.ADMIN_API_KEY;
|
||||
if (!expected || key !== expected) {
|
||||
res.status(403).json({ success: false, error: { code: "FORBIDDEN", message: "管理员权限不足" } });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
36
packages/server/src/middleware/auth.ts
Normal file
36
packages/server/src/middleware/auth.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { verifyToken } from "../lib/jwt.js";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
userId?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header?.startsWith("Bearer ")) {
|
||||
res.status(401).json({ success: false, error: { code: "UNAUTHORIZED", message: "请先登录" } });
|
||||
return;
|
||||
}
|
||||
const payload = verifyToken(header.slice(7));
|
||||
if (!payload) {
|
||||
res.status(401).json({ success: false, error: { code: "TOKEN_INVALID", message: "登录已过期,请重新登录" } });
|
||||
return;
|
||||
}
|
||||
req.userId = payload.sub;
|
||||
next();
|
||||
}
|
||||
|
||||
export function optionalAuth(req: Request, _res: Response, next: NextFunction) {
|
||||
const header = req.headers.authorization;
|
||||
if (header?.startsWith("Bearer ")) {
|
||||
const payload = verifyToken(header.slice(7));
|
||||
if (payload) {
|
||||
req.userId = payload.sub;
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
197
packages/server/src/routes/admin.ts
Normal file
197
packages/server/src/routes/admin.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { Router } from "express";
|
||||
import { z } from "zod";
|
||||
import multer from "multer";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { prisma } from "@stamp/shared";
|
||||
import { requireAdmin } from "../middleware/admin.js";
|
||||
import { generateQRCodeDataURL } from "../lib/qrcode.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const uploadsDir = path.join(__dirname, "../../uploads");
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: uploadsDir,
|
||||
filename: (_req, file, cb) => {
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`);
|
||||
},
|
||||
});
|
||||
const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 } });
|
||||
|
||||
const router = Router();
|
||||
router.use(requireAdmin);
|
||||
|
||||
// ===== Stamps CRUD =====
|
||||
|
||||
router.get("/stamps", async (_req, res) => {
|
||||
const stamps = await prisma.stamp.findMany({ orderBy: { sortOrder: "asc" } });
|
||||
res.json({ success: true, data: stamps });
|
||||
});
|
||||
|
||||
const stampSchema = z.object({
|
||||
name: z.string().min(1, "图章名称不能为空"),
|
||||
note: z.string().optional(),
|
||||
imageColor: z.string().optional(),
|
||||
imageGrey: z.string().optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
router.post("/stamps", async (req, res) => {
|
||||
const parsed = stampSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, error: { code: "VALIDATION", message: parsed.error.issues[0].message } });
|
||||
return;
|
||||
}
|
||||
const stamp = await prisma.stamp.create({
|
||||
data: {
|
||||
name: parsed.data.name,
|
||||
note: parsed.data.note,
|
||||
imageColor: parsed.data.imageColor ?? "",
|
||||
imageGrey: parsed.data.imageGrey ?? "",
|
||||
sortOrder: parsed.data.sortOrder ?? 0,
|
||||
enabled: parsed.data.enabled ?? true,
|
||||
},
|
||||
});
|
||||
res.json({ success: true, data: stamp });
|
||||
});
|
||||
|
||||
router.put("/stamps/:id", async (req, res) => {
|
||||
const parsed = stampSchema.partial().safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, error: { code: "VALIDATION", message: parsed.error.issues[0].message } });
|
||||
return;
|
||||
}
|
||||
const stamp = await prisma.stamp.update({
|
||||
where: { id: req.params.id },
|
||||
data: parsed.data,
|
||||
}).catch(() => null);
|
||||
if (!stamp) {
|
||||
res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "图章不存在" } });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: stamp });
|
||||
});
|
||||
|
||||
router.delete("/stamps/:id", async (req, res) => {
|
||||
await prisma.stamp.delete({ where: { id: req.params.id } }).catch(() => null);
|
||||
res.json({ success: true, data: null });
|
||||
});
|
||||
|
||||
// Upload stamp image
|
||||
router.post("/stamps/:id/upload", upload.single("image"), async (req, res) => {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: { code: "NO_FILE", message: "请选择图片" } });
|
||||
return;
|
||||
}
|
||||
|
||||
const field = req.body.field as string;
|
||||
if (field !== "imageColor" && field !== "imageGrey") {
|
||||
res.status(400).json({ success: false, error: { code: "INVALID_FIELD", message: "field 必须是 imageColor 或 imageGrey" } });
|
||||
return;
|
||||
}
|
||||
|
||||
const imagePath = `/uploads/${req.file.filename}`;
|
||||
const stamp = await prisma.stamp.update({
|
||||
where: { id: req.params.id },
|
||||
data: { [field]: imagePath },
|
||||
}).catch(() => null);
|
||||
|
||||
if (!stamp) {
|
||||
res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "图章不存在" } });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: { path: imagePath } });
|
||||
});
|
||||
|
||||
// Generate QR code
|
||||
router.get("/stamps/:id/qrcode", async (req, res) => {
|
||||
const stamp = await prisma.stamp.findUnique({ where: { id: req.params.id } });
|
||||
if (!stamp) {
|
||||
res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "图章不存在" } });
|
||||
return;
|
||||
}
|
||||
|
||||
const siteUrl = process.env.SITE_URL || "http://localhost:5173";
|
||||
const collectUrl = `${siteUrl}/collect/${stamp.id}`;
|
||||
const qrDataUrl = await generateQRCodeDataURL(collectUrl, { width: 400 });
|
||||
|
||||
res.json({ success: true, data: { qrDataUrl, collectUrl, stampName: stamp.name } });
|
||||
});
|
||||
|
||||
// ===== Redemption Rules CRUD =====
|
||||
|
||||
router.get("/rules", async (_req, res) => {
|
||||
const rules = await prisma.redemptionRule.findMany({ orderBy: { sortOrder: "asc" } });
|
||||
res.json({ success: true, data: rules });
|
||||
});
|
||||
|
||||
const ruleSchema = z.object({
|
||||
name: z.string().min(1, "奖品名称不能为空"),
|
||||
description: z.string().optional(),
|
||||
threshold: z.number().int().min(1, "兑换门槛至少为 1"),
|
||||
enabled: z.boolean().optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
});
|
||||
|
||||
router.post("/rules", async (req, res) => {
|
||||
const parsed = ruleSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, error: { code: "VALIDATION", message: parsed.error.issues[0].message } });
|
||||
return;
|
||||
}
|
||||
const rule = await prisma.redemptionRule.create({
|
||||
data: {
|
||||
name: parsed.data.name,
|
||||
description: parsed.data.description,
|
||||
threshold: parsed.data.threshold,
|
||||
enabled: parsed.data.enabled ?? true,
|
||||
sortOrder: parsed.data.sortOrder ?? 0,
|
||||
},
|
||||
});
|
||||
res.json({ success: true, data: rule });
|
||||
});
|
||||
|
||||
router.put("/rules/:id", async (req, res) => {
|
||||
const parsed = ruleSchema.partial().safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, error: { code: "VALIDATION", message: parsed.error.issues[0].message } });
|
||||
return;
|
||||
}
|
||||
const rule = await prisma.redemptionRule.update({
|
||||
where: { id: req.params.id },
|
||||
data: parsed.data,
|
||||
}).catch(() => null);
|
||||
if (!rule) {
|
||||
res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "规则不存在" } });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: rule });
|
||||
});
|
||||
|
||||
router.delete("/rules/:id", async (req, res) => {
|
||||
await prisma.redemptionRule.delete({ where: { id: req.params.id } }).catch(() => null);
|
||||
res.json({ success: true, data: null });
|
||||
});
|
||||
|
||||
// ===== Redemption Records & Stats =====
|
||||
|
||||
router.get("/redemptions", async (_req, res) => {
|
||||
const records = await prisma.redemption.findMany({
|
||||
include: { user: { select: { username: true, phone: true } }, rule: { select: { name: true } } },
|
||||
orderBy: { redeemedAt: "desc" },
|
||||
});
|
||||
res.json({ success: true, data: records });
|
||||
});
|
||||
|
||||
router.get("/stats", async (_req, res) => {
|
||||
const [userCount, collectionCount, redemptionCount] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.collection.count(),
|
||||
prisma.redemption.count(),
|
||||
]);
|
||||
res.json({ success: true, data: { userCount, collectionCount, redemptionCount } });
|
||||
});
|
||||
|
||||
export default router;
|
||||
66
packages/server/src/routes/auth.ts
Normal file
66
packages/server/src/routes/auth.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Router } from "express";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@stamp/shared";
|
||||
import { signToken } from "../lib/jwt.js";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
|
||||
const registerSchema = z.object({
|
||||
username: z.string().min(1, "用户名不能为空").max(20, "用户名最长 20 字符"),
|
||||
phone: z.string().regex(phoneRegex, "手机号格式不正确"),
|
||||
});
|
||||
|
||||
const loginSchema = z.object({
|
||||
phone: z.string().regex(phoneRegex, "手机号格式不正确"),
|
||||
});
|
||||
|
||||
router.post("/register", async (req, res) => {
|
||||
const parsed = registerSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, error: { code: "VALIDATION", message: parsed.error.issues[0].message } });
|
||||
return;
|
||||
}
|
||||
const { username, phone } = parsed.data;
|
||||
|
||||
const existing = await prisma.user.findFirst({
|
||||
where: { OR: [{ username }, { phone }] },
|
||||
});
|
||||
if (existing) {
|
||||
const field = existing.username === username ? "用户名" : "手机号";
|
||||
res.status(409).json({ success: false, error: { code: "DUPLICATE", message: `该${field}已被注册` } });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await prisma.user.create({ data: { username, phone } });
|
||||
const token = signToken(user.id);
|
||||
res.json({ success: true, data: { user: { id: user.id, username: user.username, phone: user.phone }, token } });
|
||||
});
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
const parsed = loginSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, error: { code: "VALIDATION", message: parsed.error.issues[0].message } });
|
||||
return;
|
||||
}
|
||||
const user = await prisma.user.findUnique({ where: { phone: parsed.data.phone } });
|
||||
if (!user) {
|
||||
res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "该手机号未注册" } });
|
||||
return;
|
||||
}
|
||||
const token = signToken(user.id);
|
||||
res.json({ success: true, data: { user: { id: user.id, username: user.username, phone: user.phone }, token } });
|
||||
});
|
||||
|
||||
router.get("/me", requireAuth, async (req, res) => {
|
||||
const user = await prisma.user.findUnique({ where: { id: req.userId } });
|
||||
if (!user) {
|
||||
res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "用户不存在" } });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: { id: user.id, username: user.username, phone: user.phone } });
|
||||
});
|
||||
|
||||
export default router;
|
||||
79
packages/server/src/routes/redemption.ts
Normal file
79
packages/server/src/routes/redemption.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Router } from "express";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@stamp/shared";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/rules", async (_req, res) => {
|
||||
const rules = await prisma.redemptionRule.findMany({
|
||||
where: { enabled: true },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
select: { id: true, name: true, description: true, threshold: true },
|
||||
});
|
||||
res.json({ success: true, data: rules });
|
||||
});
|
||||
|
||||
const redeemSchema = z.object({
|
||||
ruleId: z.string().uuid("规则 ID 格式不正确"),
|
||||
});
|
||||
|
||||
router.post("/redeem", requireAuth, async (req, res) => {
|
||||
const parsed = redeemSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, error: { code: "VALIDATION", message: parsed.error.issues[0].message } });
|
||||
return;
|
||||
}
|
||||
|
||||
const rule = await prisma.redemptionRule.findUnique({ where: { id: parsed.data.ruleId, enabled: true } });
|
||||
if (!rule) {
|
||||
res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "兑换规则不存在" } });
|
||||
return;
|
||||
}
|
||||
|
||||
const collectionCount = await prisma.collection.count({ where: { userId: req.userId! } });
|
||||
if (collectionCount < rule.threshold) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: { code: "INSUFFICIENT", message: `需要收集 ${rule.threshold} 枚图章,当前只有 ${collectionCount} 枚` },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const redemption = await prisma.$transaction(async (tx) => {
|
||||
const record = await tx.redemption.create({
|
||||
data: { userId: req.userId!, ruleId: rule.id, stampCount: collectionCount },
|
||||
});
|
||||
await tx.collection.deleteMany({ where: { userId: req.userId! } });
|
||||
return record;
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: redemption.id,
|
||||
ruleName: rule.name,
|
||||
stampCount: redemption.stampCount,
|
||||
redeemedAt: redemption.redeemedAt.toISOString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/history", requireAuth, async (req, res) => {
|
||||
const records = await prisma.redemption.findMany({
|
||||
where: { userId: req.userId! },
|
||||
include: { rule: { select: { name: true } } },
|
||||
orderBy: { redeemedAt: "desc" },
|
||||
});
|
||||
|
||||
const data = records.map((r) => ({
|
||||
id: r.id,
|
||||
ruleName: r.rule.name,
|
||||
stampCount: r.stampCount,
|
||||
redeemedAt: r.redeemedAt.toISOString(),
|
||||
}));
|
||||
|
||||
res.json({ success: true, data });
|
||||
});
|
||||
|
||||
export default router;
|
||||
85
packages/server/src/routes/stamps.ts
Normal file
85
packages/server/src/routes/stamps.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Router } from "express";
|
||||
import { prisma } from "@stamp/shared";
|
||||
import { optionalAuth, requireAuth } from "../middleware/auth.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", optionalAuth, async (req, res) => {
|
||||
const stamps = await prisma.stamp.findMany({
|
||||
where: { enabled: true },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
});
|
||||
|
||||
let collections: Set<string> = new Set();
|
||||
let collectionMap: Map<string, Date> = new Map();
|
||||
|
||||
if (req.userId) {
|
||||
const userCollections = await prisma.collection.findMany({
|
||||
where: { userId: req.userId },
|
||||
select: { stampId: true, collectedAt: true },
|
||||
});
|
||||
userCollections.forEach((c) => {
|
||||
collections.add(c.stampId);
|
||||
collectionMap.set(c.stampId, c.collectedAt);
|
||||
});
|
||||
}
|
||||
|
||||
const data = stamps.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
note: s.note,
|
||||
imageColor: s.imageColor,
|
||||
imageGrey: s.imageGrey,
|
||||
sortOrder: s.sortOrder,
|
||||
collected: collections.has(s.id),
|
||||
collectedAt: collectionMap.get(s.id)?.toISOString() ?? null,
|
||||
}));
|
||||
|
||||
res.json({ success: true, data });
|
||||
});
|
||||
|
||||
router.get("/:id", async (req, res) => {
|
||||
const stamp = await prisma.stamp.findUnique({ where: { id: req.params.id } });
|
||||
if (!stamp) {
|
||||
res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "图章不存在" } });
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: stamp.id,
|
||||
name: stamp.name,
|
||||
note: stamp.note,
|
||||
imageColor: stamp.imageColor,
|
||||
imageGrey: stamp.imageGrey,
|
||||
sortOrder: stamp.sortOrder,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/:id/collect", requireAuth, async (req, res) => {
|
||||
const stamp = await prisma.stamp.findUnique({ where: { id: req.params.id, enabled: true } });
|
||||
if (!stamp) {
|
||||
res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "图章不存在" } });
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await prisma.collection.findUnique({
|
||||
where: { userId_stampId: { userId: req.userId!, stampId: stamp.id } },
|
||||
});
|
||||
if (existing) {
|
||||
res.status(409).json({ success: false, error: { code: "ALREADY_COLLECTED", message: "你已经收集了这枚图章" } });
|
||||
return;
|
||||
}
|
||||
|
||||
const collection = await prisma.collection.create({
|
||||
data: { userId: req.userId!, stampId: stamp.id },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { id: collection.id, stampId: collection.stampId, collectedAt: collection.collectedAt.toISOString() },
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
68
packages/server/src/seed.ts
Normal file
68
packages/server/src/seed.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { prisma } from "@stamp/shared";
|
||||
|
||||
async function seed() {
|
||||
console.log("Seeding database...");
|
||||
|
||||
// Create sample stamps
|
||||
const stamps = await Promise.all([
|
||||
prisma.stamp.create({
|
||||
data: { name: "古桥印记", note: "始建于明代的石拱桥", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 1 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "老街风韵", note: "百年历史的商业老街", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 2 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "园林雅趣", note: "江南古典园林", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 3 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "茶馆时光", note: "百年老茶馆", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 4 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "水乡晨曲", note: "清晨的水乡渔市", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 5 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "戏台余韵", note: "古戏台与昆曲", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 6 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "巷弄深处", note: "青石板铺就的幽深小巷", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 7 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "月下拱桥", note: "夜晚灯火映照的拱桥", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 8 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "匠心工坊", note: "传统手工艺作坊", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 9 },
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log(`Created ${stamps.length} stamps`);
|
||||
|
||||
// Create redemption rules
|
||||
const rules = await Promise.all([
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "纪念书签", description: "精美城市纪念书签一枚", threshold: 3, sortOrder: 1 },
|
||||
}),
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "手绘明信片套装", description: "一套 5 张手绘城市明信片", threshold: 5, sortOrder: 2 },
|
||||
}),
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "限定徽章礼盒", description: "城市限定版金属徽章礼盒", threshold: 7, sortOrder: 3 },
|
||||
}),
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "城市记忆礼包", description: "包含纪念 T 恤、帆布袋和城市画册", threshold: 9, sortOrder: 4 },
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log(`Created ${rules.length} redemption rules`);
|
||||
|
||||
// Print stamp IDs for QR code testing
|
||||
console.log("\nStamp IDs for testing:");
|
||||
stamps.forEach((s) => {
|
||||
console.log(` ${s.name}: /collect/${s.id}`);
|
||||
});
|
||||
|
||||
console.log("\nSeed complete!");
|
||||
}
|
||||
|
||||
seed()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user