ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从成就徽章到数字钥匙链:eChain的收藏玩法与MVP实现

从成就徽章到数字钥匙链:eChain的收藏玩法与MVP实现 我一直在想纸质时代的钥匙串为什么让人舍不得扔。上面每一把钥匙都对应一段记忆家门、工位、旧宿舍甚至是一把小锁的备用钥匙。后来手机把物理钥匙收纳进了门禁卡、二维码里但那种“一串小物件挂在腰间叮叮当当”的亲密感反而没了。eChainA Fun Digital Key Chain要做的就是把这种亲密感搬到线上它不是一个普通数字徽章墙而是一条能挂在个人主页上、随身携带的数字钥匙链。每一枚钥匙扣代表一次真实完成的小成就、一次线下活动参与、一个值得记住的时刻。下面我会从产品设计、数据模型到 MVP 实现完整拆一遍这个项目适合正在做数字收藏、游戏化激励、社区成长体系的产品经理和开发同学参考。1. 项目形态拆解eChain到底是个什么产品1.1 一个看得见、摸得着的“数字钥匙链”传统产品里的成就系统绝大多数做完就沉底了。用户完成打卡、获得一枚徽章然后在个人中心某个角落里看到一个灰扑扑的图标再也没有然后。eChain 想解决两件事一是让“完成”这件事变成“拥有”的感觉二是让这种拥有感具备社交传播力。我们参考了实体钥匙链的物理结构。一枚钥匙扣有环、有链、有挂孔不同材质的钥匙扣摸上去触感完全不一样塑料的轻飘飘金属的沉甸甸皮质的有温度。数字世界里没有触觉但我们可以用视觉和交互来模拟普通钥匙扣是哑光灰边史诗钥匙扣有流动光晕传说钥匙扣带金属拉丝纹理。用户把一枚新钥匙扣挂到自己的链上时会有一声响亮的“咔哒”反馈链条会轻轻摆动一下。这个细节花不了多少开发成本但用户就是会因为这一下“咔哒”多玩十分钟。和勋章墙相比钥匙链更像一个“空间叙事”。勋章墙是后台数据的汇总展示钥匙链则是用户主动排列、筛选、置顶的个人物件集合。有人会把最珍贵的传说钥匙扣挂到第一格有人会把第一次参加活动的纪念钥匙扣长期置顶。这个选择过程本身就是产品价值的体现。1.2 为什么叫Chain而不是Badge项目起名阶段我们列了十几个备选DigiBadge、AchievementGo、KeyRing……最后定了 eChain。很多人看到 Chain 会往“链”的方向联想但我们更想表达的是钥匙串上那根真实的金属链条同时它也暗示下一枚钥匙扣会不断接上来形成一个连续的收集链条。这个命名决定了产品基调。如果叫 Badge大家默认这是一个成就系统设计师会不自觉地在“完成度”上堆功能改成 Key Chain 之后所有设计语言都围绕“挂上去、戴出去、秀出来”展开。后来的实际效果也验证了这一点用户不太会去跟朋友说“我拿到了一个徽章”但会很自然地说“我钥匙串上加了一枚超酷的钥匙扣”。另外我们在设计初期就明确不做资产化、不做二级交易。钥匙扣可以转赠但转赠是一次性的转出后原链上不再保留。它更像朋友之间送实体小挂件而不是可炒作的数字商品。这个限制让整个项目避开了大量复杂的合规问题也让用户更关注收集本身的乐趣。1.3 目标用户和三类典型玩法eChain 的用户定位是 18-30 岁、喜欢收集和展示自我的年轻人同时为活动主办方和社群运营者提供了一个低门槛的用户激励工具。项目上线测试期我们验证了三类典型玩法活动型玩法是最直接的。主办方在活动页配置一个限定钥匙扣用户到场签到或线上参与后自动领取。比如城市漫步活动每走过一个地标就能获得对应的建筑造型钥匙扣技术社区办 Meetup参会者能领到当期限定款。这类玩法的特点是发放逻辑简单适合验证冷启动。行为型玩法用来做习惯养成。用户连续阅读七天获得一枚书本造型的钥匙扣连续运动十天获得一对哑铃钥匙扣。和打卡日历相比钥匙扣给了行为一个“可积累的实体感”。日历中断了会让人沮丧钥匙扣不会消失已经拿到的那一枚还挂在链上反而会激励用户继续补完下一个七天。社交型玩法是我们后来验证到最有潜力的方向。好友之间可以互赠钥匙扣也可以发起“合体”两个人各自把一枚同系列钥匙扣放一起合成一个双环组合链。这个玩法在测试群里被玩出了花有人专门为了合体钥匙扣拉上朋友一起报名活动。数字物品一旦有了赠送和组合的能力传播动力会明显上升。2. 功能架构与数据模型让每枚钥匙扣都有据可循2.1 核心数据表设计与字段说明eChain 的数据模型从第一版开始就坚持关系型建模。核心实体有四类用户、钥匙扣模板、颁发记录、用户持有的钥匙扣实例。很多做这类产品的人一开始会把“模板”和“实例”混在一张表里后面一旦出现同一款钥匙扣可以重复获得的情况就不得不开始加各种奇怪的冗余字段。钥匙扣模板表存的是“这一款钥匙扣长什么样”名称、描述、稀有度、视觉模板参数、所属系列、发行总量限制。颁发记录表存的是“谁在什么时间因为什么事件被授予了这枚钥匙扣”它是一张不可变的事件流水。用户持有表才是真正挂在用户链上的实例拥有独立序号、获取时间、展示顺序、是否置顶、是否隐藏。用 Prisma 表达核心关系大概是这样的enum Rarity { COMMON UNCOMMON RARE EPIC LEGENDARY } model Charm { id String id default(cuid()) code String unique name String description String? rarity Rarity default(COMMON) imageUrl String totalSupply Int? issuedCount Int default(0) seriesId String? series Series? relation(fields: [seriesId], references: [id]) grants Grant[] items KeychainItem[] } model Grant { id String id default(cuid()) charmId String charm Charm relation(fields: [charmId], references: [id]) userId String note String? createdAt DateTime default(now()) items KeychainItem[] } model KeychainItem { id String id default(cuid()) ownerId String charmId String grantId String serialNo Int obtainedAt DateTime default(now()) displayOrder Int default(0) pinned Boolean default(false) hidden Boolean default(false) charm Charm relation(fields: [charmId], references: [id]) grant Grant relation(fields: [grantId], references: [id]) }这里有一个关键设计为什么要单独建 Grant而不是在 KeychainItem 里直接记录来源因为同一个用户可能在两个不同活动中获得同一款纪念钥匙扣比如某系列在两个城市各发一次。如果只在 item 表里存来源字段就无法表达“同一款被获得过两次”。Grant 作为颁发事件天然支持一对多一次颁发事件产生一条 Grant每条 Grant 在用户钱包里生成一枚 KeychainItem。这样后续即使要做“重复获得自动合成升级款”之类的功能数据基础也完全够用。唯一约束也需要强调。Charm 表的 code 字段要设唯一索引这个 code 是给活动运营配置和系统内部引用用的不能用自增 id 做业务关联。KeychainItem 表建议加 (ownerId, charmId, serialNo) 的联合唯一索引防止同一枚限量钥匙扣被异常重复发放时出现脏数据。2.2 稀有度、套组和成长感怎么设计稀有度是整个收藏动力的第一引擎。我们参考了常见收集游戏的掉落分布初始配置了五个等级普通、罕见、稀有、史诗、传说。普通没有边框特效罕见加了细描边稀有开始有渐变背景史诗有流动光晕传说则使用动态金属拉丝纹理加特殊挂环造型。稀有度获得权重视觉表现对应场景普通50%灰色哑光边框日常打卡、活动基础参与罕见25%银灰色描边持续七天行为稀有15%渐变底色主题活动限量发放史诗7%光晕流动特效大型线下活动限定传说3%金属拉丝动态光效周年纪念、特殊贡献权重分布不直接告诉用户靠社区自己摸索传播反而能形成话题。上线后有人开始统计“第几枚必出稀有”我们内部知道没有保底机制但这份讨论热度确实帮助了自然传播。成长感则通过“链条本身”的变化来体现。用户获得第一枚钥匙扣时链上只有一个圆环第十枚时所有圆环会合并成一段更粗的链条第二十五枚时链条上会出现一个身份铭牌显示用户的收集等级。这种变化不打断收集体验但会持续给用户一个“我的链正在变长变粗”的直观反馈。套组机制是后期留存的关键。同一个系列集齐三枚、五枚、九枚时用户可以主动触发“串链”操作把多枚钥匙扣串成一个整体解锁系列专属的背景挂板。系列背景挂板会在个人主页展示时占据更大的视觉空间相当于给用户的收集行为颁发了一个可展示的成就。2.3 领取到分享的主链路拆解整个产品最核心的一条链路是发现活动、完成条件、领取钥匙扣、挂上链、生成分享卡片、好友扫码参与。六步里每一步都可能流失用户我们每一步都做了针对性设计。发现环节依赖活动落地页和二维码拿到活动页的用户会看到这枚钥匙扣的“未点亮”状态。未点亮状态比已获得状态更重要它展示的是钥匙扣的剪影轮廓和稀有度标签让用户提前产生“我想要”的念头。完成条件后领取按钮会从灰色变为彩色点击时立刻播放挂环的“咔哒”动画同时弹出一张全屏卡片卡片上带用户的专属编号。挂上链之后默认出现在链尾但我们允许用户长按拖拽调整顺序。这个拖拽操作在移动端看起来是个小功能实际开发时却花了不少功夫做手势判定和防误触。后来我们干脆把“第一格”设成了一个重要展示位用户把任意钥匙扣拖到第一格时会触发一次特殊的链条特效很多用户就是为了这个特效才反复调整排序。分享环节直接决定传播系数。分享出去的不是网页链接而是一张竖版长图上方是钥匙扣大图中间是用户昵称和拿到的时间下方是一句用户自定义的寄语底部附带活动二维码。长图的视觉效果必须足够好用户才愿意把它发到朋友圈和群里。关于分享图的生成我在第三章会详细讲实现方案。3. 实操实现从0到1跑通eChain的MVP3.1 技术选型为什么是Next.jsPrismaPostgreSQL第一版 MVP 没有引入任何微服务也没有做前后端分离直接用 Next.js 的 API Routes 包了服务端接口。这个选择在当时承担了不少质疑有人觉得 Next.js 只适合做展示型站点不适合做业务系统。实际上对于 eChain 这种以页面渲染为主、含大量社交分享场景的项目一体化的 Next.js 反而最顺手。选 PostgreSQL 而不是 MongoDB理由可以总结成三点第一Charm、Grant、KeychainItem 之间有明确的关系约束用外键和唯一索引能直接在数据库层拦住大量脏数据第二限量钥匙扣的发放需要行级锁和原子更新这是关系型数据库的舒适区文档数据库做这类操作要绕很多弯第三Prisma 提供了类型安全的 client 和 migration 流程表结构变更时不容易出错。Redis 在这个项目里不承担核心存储职责只用来做两类事热点数据的短期缓存和领取接口的限流。用户在拿完钥匙扣后打开个人主页这个动作是典型的读多写少场景我们会把前 N 条用户钥匙扣列表缓存一分钟避免每次都查库。但任何涉及发放、转赠、排序的写操作一律绕过缓存直写数据库。3.2 动态生成“钥匙扣”卡片并导出分享图分享图是整个传播闭环里最不能省的功能。市面上很多项目图省事直接把前端页面截图当成分享图结果不仅尺寸模糊还经常因为没有等待图片加载而缺元素。eChain 的做法是用服务端渲染 SVG再转成 PNG 输出。SVG 模板的好处是稀有度特效、边框颜色、文字排版都可以通过参数控制不需要为每一款钥匙扣单独做设计稿。核心渲染流程大概是这样的import { Resvg } from resvg/resvg-js; interface CharmRenderParams { name: string; serialNo: number; rarity: string; seriesName?: string; customNote?: string; } function renderCharmCard(params: CharmRenderParams): string { const frameColor getRarityFrameColor(params.rarity); return svg width640 height800 xmlnshttp://www.w3.org/2000/svg defs linearGradient idbg x10 y10 x21 y21 stop offset0% stop-color${frameColor.a}/ stop offset100% stop-color${frameColor.b}/ /linearGradient /defs rect width640 height800 rx24 fill#0F1115/ rect x24 y24 width592 height752 rx16 fillnone strokeurl(#bg) stroke-width4/ text x320 y360 text-anchormiddle font-familysans-serif font-size64 fill#FFFFFF${params.name}/text text x320 y440 text-anchormiddle font-familymonospace font-size28 fill#AAAAAANo.${params.serialNo}/text text x320 y700 text-anchormiddle font-familysans-serif font-size32 fill#CCCCCC${params.customNote}/text /svg ; } async function generateShareImage(params: CharmRenderParams) { const svg renderCharmCard(params); const png new Resvg(svg, { fitTo: { mode: width, value: 640 }, font: { loadSystemFonts: false } }).render().asPng(); return png; }这里的细节是字体处理。服务端环境往往没有中文字体如果直接用系统字体渲染中文会变成方块。我们前期踩过这个坑后来把一张开源中文字体文件放到项目静态目录里加载进 Resvg 的 font 配置才解决。渲染是很消耗 CPU 的操作所以绝对不能每次请求都实时生成。我们的策略是做成懒生成加缓存第一次请求时生成 PNG 并上传到对象存储后续直接返回图片 URL缓存 key 用 userId charmId serialNo 拼接。前端拿到图片 URL 后也不需要做任何重绘直接把这张 PNG 作为分享图展示给用户。用户长按图片就能保存在微信里直接发图也比发链接轻很多。3.3 提升分享率的四个交互细节分享卡片做好了还得让用户愿意分享。我们迭代了四个版本最终发现下面四个细节对分享率影响最大。第一个细节是分享文案里的“序号”必须清晰。用户看到“我在 eChain 收集的第 37 枚钥匙扣”比单纯看到“我获得了一枚钥匙扣”更有分享冲动。这里的 37 是用户在全局收集的累计序号不是某款钥匙扣的编号它表达的是“我已经玩了很久了”。第二个细节是自定义寄语的输入时机。我们原本把它放在领取后的分享卡片生成页结果填写率很低因为用户正在兴奋地想看卡片效果不愿意停下来打字。后来改成默认引用活动主题作为寄语用户想改可以在生成后的编辑页改填写率反而上升了。先用默认值把分享动作做轻再提供精细化编辑入口。第三个细节是二维码离分享图底部不能太近也不能太远。太近容易被其他 App 的图片裁剪功能切掉太远又会降低扫码转化。我们测试下来底部留白 80 像素左右最稳妥二维码尺寸不小于 120×120 像素扫码成功率才够高。第四个细节是“被领取通知”。当好友通过分享图扫码领取了同系列钥匙扣原分享者会收到一条站内通知加一条推送。这个通知创造了一种“我带动了别人”的成就感也是触发二次分享的核心动力。很多用户第一次分享没什么反馈但收到这条通知后会马上把卡片再发一遍。4. 实战避坑常见问题与排查记录4.1 领取成功但钥匙扣消失的状态一致性问题上线第一周我们收到一个反馈用户明明看到领取成功的动画但回到个人主页钥匙扣不见了。排查后发现问题出在领取接口的写入顺序上。初版代码先把 Grant 插入数据库再创建 KeychainItem。如果第二步因为序列号冲突等原因失败Grant 已经存在但用户链上没有任何新增。前端只接住了“领取成功”的返回没有校验实际生成的 item 是否落库。修复方案很直接把两步写入放进同一个数据库事务里要么全部成功要么全部回滚。同时接口的返回结构改成显式返回 itemId前端拿到这个 id 才播放领取成功动画。这样一来动画一旦播放数据一定是真实存在的。这里还有一个隐藏问题用户连续快速点击领取按钮可能会发出多个并发请求。我们在领接口做了用户维度的互斥锁同一用户同一秒只允许一个领取请求进入处理逻辑防止重复生成两条一模一样的钥匙扣。4.2 并发抢限量钥匙扣导致超发限量版钥匙扣是社区最关注的活动类型。比如某次活动只发 1000 枚用户同时涌入领取初版代码是先查出当前 issuedCount判断小于 totalSupply 再插入数据。并发场景下两个请求读到同一个 issuedCount就会突破限量这是经典的超发问题。后来改成单条 SQL 原子更新UPDATE Charm SET issuedCount issuedCount 1 WHERE id $1 AND (totalSupply IS NULL OR issuedCount totalSupply) RETURNING issuedCount;这条语句利用数据库的行锁保证并发安全只有 UPDATE 影响行数大于 0才允许继续创建 KeychainItem否则直接返回“已领完”。最开始我们也考虑过用 Redis 做一个原子扣减通过 Lua 脚本控制数量但 Redis 和数据库之间的事务一致性很难保证一旦 Redis 扣减成功而数据库写入失败限量数据就乱了。用数据库自带的行锁虽然压力大一点但在 MVP 规模下是更稳妥的方案。还有一个体验层的细节用户看到已领完时往往很失望但如果他其实已经获得了领取资格只是最后写入时库存没了界面会显示“资格已锁定等待补货”让用户觉得自己的参与没有白费。这个提示需要判断用户是否已经完成领取条件而不是看到发放失败就一律提示“已售罄”。4.3 图片加载慢和多端不同步分享图一旦生成就涉及到图片存储的访问速度。第一版我们直接把 PNG 原图放在对象存储的公共桶里一张图平均 800KB。朋友圈打开倒是不慢但个人主页里同时加载几十枚钥匙扣大图流量和延迟都扛不住。后来的解决方法是动态生成多个尺寸的缩略图列表页用 160×200详情页用 480×600分享场景才加载 640×800 原图并且给对象存储配了 CDN。列表页整体体积从原本可能几十 MB 降到了几 MB加载速度肉眼可见地提升。多端不同步的问题则主要发生在用户用手机领取、平板查看的场景。我们最初把用户链的排序数据缓存在本地导致一台设备上拖拽排序后另一台设备完全不变。排查后确认了原则设备端永远只做展示缓存排序、置顶、隐藏这些操作都以服务端为准。每个用户的钥匙扣列表在后端维护一个 version 字段前端拉取时带上 version如果版本落后就重新获取全量列表而不是做复杂的增量同步。这个策略在只有个人主页展示的情况下已经够用。如果后续要支持更复杂的多端实时同步再引入 WebSocket 推送也来得及。MVP 阶段千万别为不存在的复杂度提前设计。5. eChain给我留下的三个设计教训做这个项目最大的体会是数字收藏品的价值来自用户的付出而不是平台义无反顾的发放。我们的测试群里有人特意把第一次参加黑客松拿到的普通钥匙扣置顶哪怕后来他集齐了一整套传说款。因为那枚普通钥匙扣承载的是“第一次创造出一件完整作品”的记忆这种情感连接是任何算法都算不出来的。第二个教训是分享体验要先于收藏体验打磨。产品做到第二版时我们还在优化个人主页的动效结果用户增长数据毫无起色。后来把精力集中到分享图的尺寸、文案、二维码位置这些“看起来很小”的事情上分享率立刻涨了三分之一。在社交传播类产品里让用户愿意把一张图发出去比让用户在自己页面里多停留两分钟重要得多。第三个教训是权限和审核要提前留位。早期版本里用户自定义寄语没有做内容过滤结果有人在分享卡片上写了不合适的话被截图传播后处理了很久。后来加上了文本审核接口和人工举报入口这个问题才算解决。所有允许用户输入文本的地方上线第一天就要有过滤机制不能等出问题再补。最后再分享一个小技巧领取成功后的“咔哒”音效我们最开始用的是系统默认提示音效果非常廉价。后来自己录了一段钥匙扣碰撞的声音经过降噪和加速处理后作为应用内音效用户反馈明显变好。数字产品的质感往往就藏在这些细节里它不会被写进需求文档但用户一定能感知到。
返回列表