diff --git a/package.json b/package.json index a1e6cdb..5cefb96 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "db:push": "prisma db push", "db:seed": "pnpm --filter @stamp/server seed", "db:seed-articles": "pnpm --filter @stamp/server seed-articles", - "db:seed-music": "pnpm --filter @stamp/server seed-music" + "db:seed-music": "pnpm --filter @stamp/server seed-music", + "db:update-brand-rules": "pnpm --filter @stamp/server update-brand-rules" }, "engines": { "node": ">=20" diff --git a/packages/server/package.json b/packages/server/package.json index b587a4d..10cec85 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -9,7 +9,8 @@ "start": "node dist/index.js", "seed": "tsx src/seed.ts", "seed-articles": "tsx src/seed-articles.ts", - "seed-music": "tsx src/seed-music.ts" + "seed-music": "tsx src/seed-music.ts", + "update-brand-rules": "tsx src/scripts/update-brand-rules.ts" }, "dependencies": { "@stamp/shared": "workspace:*", diff --git a/packages/server/src/routes/stamps.ts b/packages/server/src/routes/stamps.ts index 0c5f830..4d7f3fa 100644 --- a/packages/server/src/routes/stamps.ts +++ b/packages/server/src/routes/stamps.ts @@ -54,7 +54,10 @@ router.get("/", optionalAuth, async (req, res) => { }); router.get("/:id", async (req, res) => { - const stamp = await prisma.stamp.findUnique({ where: { id: req.params.id } }); + const stamp = await prisma.stamp.findUnique({ + where: { id: req.params.id }, + include: { prize: true }, + }); if (!stamp) { res.status(404).json({ success: false, error: { code: "NOT_FOUND", message: "图章不存在" } }); return; @@ -68,6 +71,15 @@ router.get("/:id", async (req, res) => { imageColor: stamp.imageColor, imageGrey: stamp.imageGrey, sortOrder: stamp.sortOrder, + prize: stamp.prize + ? { + id: stamp.prize.id, + name: stamp.prize.name, + description: stamp.prize.description, + stock: stamp.prize.stock, + enabled: stamp.prize.enabled, + } + : null, }, }); }); diff --git a/packages/server/src/scripts/update-brand-rules.ts b/packages/server/src/scripts/update-brand-rules.ts new file mode 100644 index 0000000..712aa69 --- /dev/null +++ b/packages/server/src/scripts/update-brand-rules.ts @@ -0,0 +1,125 @@ +import { prisma } from "@stamp/shared"; + +type Rule = { matchKey: string; canonicalName: string; description: string }; + +const RULES: Rule[] = [ + { matchKey: "孟令军", canonicalName: "孟令军炒货", + description: "进店消费 19.9 元,可赠送人气爆款谷物锅巴 1 份。" }, + { matchKey: "春山", canonicalName: "春山酒窖", + description: "全场产品 85 折优惠。" }, + { matchKey: "金陵绣男", canonicalName: "金陵绣男", + description: "全场产品 8 折优惠。" }, + { matchKey: "陶玉梅", canonicalName: "南京陶玉梅服饰(梦幻城店)", + description: "可获得「非遗宋锦书签制作」体验券 1 张。" }, + { matchKey: "LBZ", canonicalName: "LBZ 量不准咖啡", + description: "「三元巷」定制咖啡 8 折优惠。" }, + { matchKey: "芳婆", canonicalName: "芳婆糕团", + description: "单笔消费满 10 元,即赠糕点 1 块,每日限量 66 份,送完即止。" }, + { matchKey: "紫金", canonicalName: "紫金农商银行秦淮支行", + description: "到店拍照打卡即可获得「南京市民俗(非遗)博物馆 甘家大院」参观券 1 张。" }, + { matchKey: "尹氏", canonicalName: "尹氏汤包", + description: "可获得「亲子家庭汤包制作活动」体验券 1 张。" }, + { matchKey: "闲鱼", canonicalName: "闲鱼循环商店", + description: "到店参与寄卖服务,即可加盖闲鱼文创纪念章。" }, + { matchKey: "闽南", canonicalName: "闽南茶叶店", + description: "1. 全店茶叶产品 85 折优惠;2. 可获得「茶分享和体验活动」体验券 1 张。" }, + { matchKey: "移动", canonicalName: "南京移动朝天宫双塘分局", + description: "1. 免费手机贴膜 1 次(仅限直面屏手机);2. 免费领取 80G 流量体验卡 1 张;3. 购买 AI 手机返 100 元话费。" }, + { matchKey: "二条", canonicalName: "二条商店", + description: "全店当日单次实付满 380 元送二条原创小鲍挂件 1 个。" }, + { matchKey: "书", canonicalName: "锦创书城", + description: "1. 图书与咖啡产品享 7 折,文创产品享 8 折;2. 到店打卡赠送定制书签 1 枚;3. 可获得书城线下主题活动体验券 1 张。" }, + { matchKey: "魏", canonicalName: "魏虾神", + description: "到店就餐即赠奶皮子酸奶酪 1 份。" }, + { matchKey: "李记", canonicalName: "李记清真馆", + description: "可获得「亲子包锅贴体验活动」体验券 1 张。" }, + { matchKey: "农家", canonicalName: "农家小院", + description: "(小红书打卡、朋友圈转发)可享菜品 8 折优惠。" }, +]; + +async function main() { + const apply = process.argv.includes("--apply"); + const stamps = await prisma.stamp.findMany({ include: { prize: true } }); + console.log(`Found ${stamps.length} stamps in DB.\n`); + console.log("Current stamp names:"); + stamps.forEach((s) => console.log(` - ${s.name}`)); + console.log(); + + type Entry = { stampId: string; old: string; next: string; desc: string }; + const plan: Entry[] = []; + const issues: string[] = []; + const usedStampIds = new Set(); + + for (const rule of RULES) { + const hits = stamps.filter((s) => s.name.includes(rule.matchKey) && !usedStampIds.has(s.id)); + if (hits.length === 0) { + issues.push(`❌ "${rule.matchKey}" → 0 matches (target: ${rule.canonicalName})`); + continue; + } + if (hits.length > 1) { + issues.push( + `⚠️ "${rule.matchKey}" → ${hits.length} matches (${hits.map((h) => h.name).join(", ")}) — 使用更精确的 matchKey`, + ); + continue; + } + const stamp = hits[0]; + usedStampIds.add(stamp.id); + plan.push({ stampId: stamp.id, old: stamp.name, next: rule.canonicalName, desc: rule.description }); + } + + console.log("=== Match plan ==="); + plan.forEach((p) => { + const rename = p.old !== p.next ? ` [RENAME]` : ``; + console.log(` "${p.old}"${rename}`); + console.log(` ⟶ "${p.next}"`); + console.log(` ${p.desc}\n`); + }); + + if (issues.length) { + console.log("=== Issues ==="); + issues.forEach((i) => console.log(` ${i}`)); + console.log("\n请修正 matchKey 后重试。"); + process.exit(1); + } + + const leftover = stamps.filter((s) => !usedStampIds.has(s.id)); + if (leftover.length) { + console.log(`=== 未被映射覆盖的图章(保持不变) ===`); + leftover.forEach((s) => console.log(` - ${s.name}`)); + console.log(); + } + + if (!apply) { + console.log(`DRY RUN — 不写库。传入 --apply 执行写入。`); + return; + } + + console.log(`正在写入 ${plan.length} 条...\n`); + for (const p of plan) { + await prisma.$transaction(async (tx) => { + if (p.old !== p.next) { + await tx.stamp.update({ where: { id: p.stampId }, data: { name: p.next } }); + } + await tx.prize.upsert({ + where: { stampId: p.stampId }, + create: { + stampId: p.stampId, + name: `${p.next} · 品牌权益`, + description: p.desc, + stock: 100, + enabled: true, + }, + update: { description: p.desc }, + }); + }); + console.log(`✓ ${p.next}`); + } + console.log(`\n完成。已更新 ${plan.length} 枚图章。`); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/packages/web/public/poster.jpg b/packages/web/public/poster.jpg new file mode 100644 index 0000000..c68d358 Binary files /dev/null and b/packages/web/public/poster.jpg differ diff --git a/packages/web/src/admin/StampForm.tsx b/packages/web/src/admin/StampForm.tsx index 351331d..60de8ae 100644 --- a/packages/web/src/admin/StampForm.tsx +++ b/packages/web/src/admin/StampForm.tsx @@ -177,12 +177,12 @@ export default function StampForm({ open, id, onClose, onSaved }: Props) { /> - +