feat: 落地页改造 + 集章弹窗全状态品牌/奖品说明
- 落地页:顶部改为活动海报,底部替换为「活动规则」5 条编号列表 - 集章收集弹窗 (StampPopup):新增奖品规则卡片展示 Prize 信息 - 集章册 (AlbumPage / StampGrid):所有状态图章均可点击查看详情 - 兑换弹窗 (RedeemModal):新增 uncollected 分支,统一承载未收集/ 已集齐/已兑换三种状态;新增可选品牌说明区 - 后端 /api/stamps/:id 补充返回 prize 字段 - 管理后台字段标签改名:备注 → 品牌说明;奖品描述 → 奖品说明 - 新增一次性脚本 update-brand-rules,批量写入 16 条品牌权益文案 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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:*",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
125
packages/server/src/scripts/update-brand-rules.ts
Normal file
125
packages/server/src/scripts/update-brand-rules.ts
Normal file
@@ -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<string>();
|
||||
|
||||
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());
|
||||
Reference in New Issue
Block a user