YUMINA 分享活动|工程实施计划 V3
版本:2026-07-16 状态:工程方案与业务实现已完成;testing 环境按发布流程验证 产品基线:yumina-share-event-v3.md
1. 结论
现有活动系统只能提交和审核世界卡,不能安全承载这次社媒分享活动。本次需要在保留旧 world 活动零回归的前提下,增加一条独立的 social_post 活动链路。
用户侧始终只看到三步:
- 参与并登记平台帖子。
- 自选是否补交最终数据。
- 等待官方核验和结算。
工程内部则必须将“平台投稿、帖子修订、私有证据、指标快照、账户结算、实际发奖”分开保存。这样才能同时保证每个平台独立计算、换帖不重复领初审奖励、发奖可重试但不重复、证据不公开,以及活动会员不覆盖 Stripe/WeChat 付费会员。
这不是改一份 Markdown 或给现有投稿表加几个可空字段就能完成的功能;它是一条新的活动 vertical slice,但可以复用现有活动 CMS 外壳、活动列表、Banner 上传和通知基础设施。
2. V3 唯一业务口径
以下规则必须进入 shared 常量、服务端校验和测试,不能只写在页面文案中:
- 参与登记:2026-07-16 00:00 至 2026-08-18 23:59,UTC+8。
- 可选补数据:2026-08-19 00:00 至 2026-08-21 23:59,UTC+8。
- 官方核验与发奖:2026-08-22 00:00 至 2026-08-28 23:59,UTC+8。
- 每个 UID 最多登记 5 个平台;同一平台只有一个稳定的平台槽位。
- 每个平台初审通过后各发 1,000 Mushies,最多先发 5,000。
- 每个平台独立计算:
点赞 + 收藏 + 有效评论(2026-07-16 owner 定稿:无加权,分享/转发不计分)。 - 每个平台独立套用累计档位:1,000 / 2,000 / 4,000 / 5,000 / 7,000 / 10,000 / 15,000 Mushies。
- 多个平台的 Mushies 直接相加,不再计算“最佳单帖”或额外多平台加成。
- 最多 5 个平台,理论总上限为 75,000 Mushies。
- 会员奖励取所有平台达到的最高档,只发一次 30 天:Gold=
go、Platinum=plus、Diamond=pro、Ascendant/超凡=ultra。 - 用户可按平台选择
user_evidence或official_link_check;用户不操作时默认由官方沿链接核验。 - 用户选择补交时,必须同时提交数字和有效证据;经审核的证据数字为最终数据。
- 链接无法打开且没有有效证据时,该平台传播分为 0;已通过初审取得的 1,000 保留,除非发现违规或造假。
- 活动不限参与名额,不设置奖池发完即止。
- 页面显示“最终解释、数据认定和争议裁定由 YUMINA 官方作出”。这句文案不能代替确定性的服务端计算和审计记录。
时间在数据库中使用 TIMESTAMPTZ,以半开区间判断;页面统一按 Asia/Hong_Kong 展示,不能把日期复制到各语言文件中硬编码。
3. 架构选择
3.1 保留旧 world 活动
community_event_submissions和当前世界卡审核流程继续服务submissionType="world"。- 新活动使用
submissionType="social_post"和独立表。 - 用户端和管理端入口按
submissionType分派;不把 world 与 social 的所有字段混进一个大而松散的 nullable 表。
3.2 服务端是唯一计分和发奖权威
- 客户端只能提交平台、链接、原始指标和证据 ID。
- 客户端及管理员均不能直接填写传播分、奖励金额或会员档位。
- shared 纯函数负责预览;服务端在审核/结算事务中重新计算并保存快照。
- 活动规则产生第一条正式投稿后锁定;如确需更改,必须产生新规则版本和管理员审计,不能静默改历史结果。
3.3 Banner 只做封面
- 公共 Banner 源图为 1920×600、16:5。
- 图片本身不放活动日期、奖励表、规则、步骤或 CTA。
- 标题、状态、活动时间、规则和参与按钮均由活动页面以 HTML 展示。
- 首版使用中央约 40% 安全区和
object-position:center适配手机裁切;可视化 focal point/crop 编辑器可作为后续增强,不阻塞本期活动。
4. Shared 契约与计算函数
新增:
packages/shared/src/types/community-event.tspackages/shared/src/validation/community-event.tspackages/shared/src/utils/community-event-rewards.ts
修改:
packages/shared/src/index.tspackages/shared/src/constants/limits.ts
需要定义:
EventSubmissionType = "world" | "social_post"。SocialPlatform、平台白名单及 URL/账号规范化配置。SocialEventRulesConfig、时间线和rulesVersion。SocialEventEntry、EntryRevision、FinalDataMode、MetricSnapshot、SettlementSummary。- 用户、管理员和通知 payload 的 discriminated unions。
纯函数:
calculatePlatformScore(metrics)
tierForPlatformScore(score)
calculateEventSettlement(entries, committedGrants)
deriveEventPhase(now, timeline, settlementState)
normalizeSocialPostUrl(platform, rawUrl)唯一奖励公式:
score[p] = likes[p] + favorites[p]
+ 2 * validComments[p]
+ 2 * shares[p]
platformEntitlement[p] = tierReward(score[p])
totalEntitlement = sum(platformEntitlement[p])
finalDue = max(totalEntitlement - committedEventMushies, 0)
membership = highestMembershipTier(entries)committedEventMushies 必须包含本活动该 UID 所有 pending / processing / failed / applied 且未取消的 Mushies grant,而不能只减已经到账的金额;否则初审奖励正在重试时,终审会重复补发它。
5. 数据库变动
5.1 扩展 community_events
新增可空字段,旧 world 活动无需回填:
registrationOpensAtregistrationClosesAtfinalDataOpensAtfinalDataClosesAtsettlementDeadlineAtrulesVersionrulesConfig jsonbrulesLockedAt
现有 draft / live / closed 继续表示可见性;实际阶段按时间和结算状态推导为:
upcoming → registration_open → final_data_open
→ official_verification → completed过了 8 月 28 日但仍有奖励未到账时应显示 overdue/处理中,不能仅因时间到达就伪装成 completed。
5.2 community_event_social_entries
一行代表“一个活动中,一个 UID 的一个平台槽位”。主要字段:
eventId,userId,platformsocialHandle,normalizedAccountKeycurrentRevisionId,replacementCountstatus,initialReviewEligibleAtfinalDataMode- 初审/终审管理员、时间、理由和风险标记
verifiedScore,platformEntitlement,membershipPlanIdversion,createdAt,updatedAt
关键约束:
UNIQUE(event_id, user_id, platform)。UNIQUE(event_id, platform, normalized_account_key)。- 每个 UID 最多 5 个平台;插入时锁定账户 settlement 后再计数,防止并发插入第 5、6个平台。
- 换帖沿用同一平台槽位,因此不会再获得一次初审 1,000。
5.3 community_event_social_revisions
不可变地保存首次帖子、纠错和换帖历史:
entryId,revisionNo,changeKind- 原始 URL、canonical URL、
canonicalPostKey postPublishedAt,submittedAt,supersededAtrulesVersion
关键约束:
UNIQUE(entry_id, revision_no)。UNIQUE(event_id, canonical_post_key)。- 每个 entry 只能有一个当前 revision。
短链和 tracking 参数必须在服务端规范化,避免同一帖子换一个 URL 再领一次奖励。
5.4 community_event_social_evidence
保存私有截图资产的元数据:
- 活动、用户、entry、revision/snapshot
purpose = initial | final_metrics | admin_capture- 私有
storageKey、MIME、size、checksum uploading | ready | attached | deleted- 创建、绑定、过期和删除时间
不得保存公共 CDN URL;业务表只引用 evidence ID。
5.5 community_event_social_metric_snapshots
每次核验都保存不可变快照:
resolution = user_evidence | official_link_check | unverifiable- likes、favorites、validComments、shares
- 链接状态、证据 ID、核验人、核验时间
- 服务端计算的 score/tier
user_evidence 必须绑定 ready 且归属正确的证据;unverifiable 必须将四项指标和传播分固定为 0。
5.6 community_event_social_settlements
每个活动、每个 UID 一行,UNIQUE(event_id, user_id):
status,versiontotalEntitlement,committedMushies,finalDueMushieshighestPlanId,membershipDurationDays- 每个平台的
calculationSnapshot - 冻结、审核、完成时间及管理员
检查约束:Mushies 总额和补发额均在 0..75000 之间。
5.7 community_event_reward_grants
每一项实际奖励一行:
- event/user/platform/entry/settlement 来源
phase = initial | final | adjustment | reversalkind = mushies | plan- 金额,或 plan/duration
idempotencyKey UNIQUEpending | processing | applied | failed | cancelled | reversed- attempt、lastError、ledger/entitlement ID、到账时间
稳定幂等键:
event:<eventId>:user:<userId>:platform:<platform>:initial:mushies
event:<eventId>:user:<userId>:final:mushies
event:<eventId>:user:<userId>:final:membership重新计算不会产生第二个 final grant;终审后的合法补差使用显式 adjustment:<version>,违规扣回使用 reversal,并保留历史。
同时给 credit_transactions 的 event reward reference 加 partial unique,作为第二道防双发约束。
5.8 plan_entitlements
活动 30 天会员不能直接复用当前 adminGrantStackedTimeLimitedPlan();该函数无来源幂等,会覆盖订阅来源,且可能让同级/更高级付费用户直接丢失活动时长。
新增独立 entitlement:
- user、plan、source=
event、sourceId idempotencyKey UNIQUE- duration/remaining duration
queued | active | consumed | cancelled- activated、resumed、ends 时间
Stripe/WeChat 继续维护基础订阅;活动 entitlement 作为可叠加的临时权益。effective plan 取基础订阅与当前活动权益中的最高档,不能把付费 subscriptionSource 改成 comp。
6. 服务端 API 与业务流程
6.1 用户 API
新增 packages/server/src/routes/community/social-event-submissions.ts:
GET /events/:eventId/participationPUT /events/:eventId/social-entries/:platform/draftPOST /events/:eventId/social-entriesPOST /events/:eventId/social-entries/:entryId/resubmitPOST /events/:eventId/social-entries/:entryId/replacePOST /events/:eventId/social-entries/:entryId/final-dataPOST /events/:eventId/evidence/upload-urlPOST /events/:eventId/evidence/:evidenceId/completeGET /events/:eventId/evidence/:evidenceId/contentDELETE /events/:eventId/evidence/:evidenceId
final-data 请求只允许:
{ mode: "user_evidence", metrics, evidenceIds }
{ mode: "official_link_check" }不操作在窗口关闭后等价于 official_link_check。用户不能提交 unverifiable、score、tier 或奖励金额。
6.2 管理员 API
新增 packages/server/src/routes/admin-social-events.ts:
- 分页/筛选初审、终审和结算队列。
- 初审
approve | needs_changes | reject | disqualify。 - 核验用户证据并记录经确认的数字。
- 沿公开链接录入官方指标快照。
- 链接不可访问且无证据时标记
unverifiable。 - settlement preview/finalize。
- reward grant 小批处理、失败重试和审计查询。
管理员确认的是事实和证据;score、tier、总额和补差全部只读,由服务端计算。
6.3 Entry 状态机
draft → initial_pending → initial_approved
↘ needs_changes → initial_pending
initial_approved → final_review_pending → settled
initial_pending → rejected
任一有效状态 → disqualified
replacement → 同一平台槽位的新 revision → initial_pendinginitialReviewEligibleAt = postPublishedAt + 24h。- 登记截止后仍允许处理已经登记的初审。
- 补数据窗口内,即使 entry 仍在初审排队,也可保存最终证据。
- 用户页面把内部状态压缩成三步,不暴露复杂后台流程。
7. 私有证明上传
现有 packages/server/src/routes/community/uploads.ts 会返回公共 CDN URL,不能复用来保存互动数据截图。
需要:
packages/server/src/lib/event-proof-storage.ts- 修改
packages/server/src/lib/s3.ts - 新增上述 evidence 上传/读取路由
实施方式:
- 使用独立 private bucket 最佳;退而求其次使用永不进入 CDN 白名单的
event-proofs/私有前缀。 - 服务端创建 evidence 记录并返回短时效 presigned PUT。
- 客户端直传后调用 complete;服务端通过 HeadObject 校验 owner、key、类型、大小和 checksum。
- 用户仅能读取自己的证明;管理员仅在审核权限下按需读取。
- 不把原始 key 或永久 URL返回前端,不在 localStorage 保存 File/Blob。
- 结算后按明确保留期清理;建议默认 90 天,发生争议/风控时可 hold。
8. 原子且可重试的发奖
8.1 每个平台初审 1,000
第一段主库事务:
- 锁定 entry,校验状态、公开满 24 小时及审核权限。
- 将 entry 标为初审通过。
- 用稳定平台幂等键
INSERT ... ON CONFLICT DO NOTHING创建 1,000 pending grant。 - 写 admin action,提交。
第二段奖励事务:
FOR UPDATE SKIP LOCKED取得 grant,并锁钱包行。- 增加余额和不会被月度刷新覆盖的活动/addon 余额。
- 在同一个 transaction 内写 hash ledger,reference 使用幂等键。
- 标记 grant applied 并保存 transaction ID。
- 任一步失败则整段回滚,grant 保留为可重试状态。
8.2 最终 Mushies
- 锁 settlement 和全部合格 entries。
- 冻结每个平台最终 metric snapshot。
- 逐平台计算累计 entitlement,再求和。
- 从总 entitlement 中减去所有已承诺但未取消的活动 Mushies。
- 创建全活动、全 UID 唯一的 final Mushies grant。
- 使用同一钱包/ledger 原子事务处理。
示例:平台 A 700 分得 7,000,平台 B 50 分得 2,000;总应得 9,000,初审已承诺 2,000,终审只补 7,000。
8.3 最高会员一次
- 从所有平台中选择最高会员档。
- 每个活动、每个 UID 只创建一个 plan grant 和 entitlement。
- Mushies 与 plan 分成两项 grant;一个成功、一个失败时只重试失败项。
- 同级/更高级付费会员存在时,将完整 30 天排队保存,不静默丢弃。
- Stripe/WeChat 续费只更新基础订阅,不清空活动 entitlement。
同时应修复现有 admin-events.ts 的 world 活动发奖路径:现在是先在事务外改钱包/会员,再在事务内更新审核状态,两个管理员并发或中途崩溃会双发或漏记状态。
9. App 用户端改动
修改:
packages/app/src/lib/community-events.ts:改用 shared discriminated types。community-event-detail.tsx:公开详情和登录 participation 分开加载,guest 401 不能卡住整页;Banner 为纯 16:5 图片,标题和规则移到图下;social 活动显示三步、公式、档位、平台卡和总计。community-event-submit.tsx:改为 world/social 分派壳。community-event-card.tsx:16:5 封面、动态活动类型、完整 i18n。community-home.tsx:social 搜索/计数和统一normalizeLang()。
新增建议:
world-event-submit.tsx:原 world picker 原样抽出。social-event-submit.tsx:平台、账号、链接、发布时间和初始证明的单页表单。social-event-entry-detail.tsx:单个平台详情、修改/换帖和最终数据。social-event-participation.tsx:三步时间线和账户总结。social-entry-card.tsx:平台级状态、得分、累计奖励和 CTA。social-final-evidence-form.tsx:二次提交二选一;只有选择补交时展开四项数字和证明上传。event-proof-uploader.tsx、event-proof-upload.ts:私有上传流程。social-event-api.ts:集中 API client。social-event-draft.ts、use-social-event-draft.ts:移动端草稿恢复。community.events.$eventId.entries.$entryId.tsx:通知可深链到具体平台;routeTree.gen.ts由 TanStack 生成器更新,禁止手改。
用户交互保持精简:首次登记为一个页面,不做多步 wizard;在补数据窗口,每个平台卡只有“补交最终数据”这个可选动作,不操作即交给官方。
移动端草稿以服务端 draft 为主,本地 cache 仅在切到社交 App/相册导致页面回收时兜底。localStorage 只保存文本和已完成上传的 evidence ID,不能保存私有 URL 或文件内容。
10. App 管理端改动
修改:
admin-event-form.tsx:支持social_post,配置三段时间、平台白名单、最多 5 平台、1/1/2/2 权重和七档奖励;首条正式投稿后锁规则。Banner 预览为纯图并显示中央安全区,格式不符时阻止发布。admin-events-page.tsx:表单值、clone/history、social 类型和阶段统计。admin-event-detail-page.tsx:编辑、规则锁定提示和 social 队列入口。admin-event-submissions-page.tsx:改为 world/social 分派;旧页面抽为admin-world-event-submissions.tsx。
新增:
admin-event-social-submissions-page.tsx:一个页面内用“初审 / 终审 / 结算 / 已完成”tabs,避免增加过多操作步骤。admin-social-entry-card.tsx:平台、账号、链接、24h 资格、修订和风险。social-proof-viewer.tsx:按权限加载私有证明。social-metrics-review.tsx:用户证据、官方链接和不可核验三种核验方式。event-settlement-summary.tsx:逐平台累计奖励、初审已承诺、最终补差、最高会员一次及 grant 状态。
普通审核不要求双人复核;只有人工 override、异常高风险、取消资格或负向 reversal 才进入第二管理员确认,以免后台步骤过重。
11. 通知、语言和页面文案
修改:
packages/app/src/features/notifications/notification-bell.tsxpackages/app/src/stores/notifications.tspackages/app/src/locales/{zh,zh-Hant,en,es,ja}/community.json- 对应五种语言的
common.json和admin.json - 服务端通知 payload 和
notify.ts
最小通知集:
- 平台初审通过并发放 1,000。
- 需要修改或被驳回。
- 可选补数据窗口提醒。
- 最终结算完成或奖励处理失败后恢复。
- 取消资格。
- 管理员待初审/待终审提醒。
通知保存结构化 eventId / entryId / platform / amount / i18nKey,前端本地化并深链;服务端不拼接单语言最终文案。
12. 现有活动系统必须顺手修复的问题
admin-events.ts的发奖与审核状态不原子,可能重复发奖。- world 投稿查重使用 read replica 且没有数据库唯一约束,存在并发竞态。
attachSubmissionCounts()把所有投稿读入 JS 后计数;改为数据库GROUP BY,并分别统计 world/social。- 管理员投稿列表没有可靠分页;social 队列必须服务端分页和索引筛选。
- 活动详情把公开 event 与需登录的 submission 用
Promise.all加载,guest 401 会卡 skeleton。 - 日语等语言分支存在手写判断,应统一使用
normalizeLang()。 - 现有公共 community uploader 不能承载敏感证据。
- Drizzle migration 编号和
_journal.json存在漂移,不能直接猜下一迁移编号。
13. 实施顺序
Phase 0:冻结口径与迁移预检
- 以 V3 文档为唯一基线,废止 V2 工程计划。
- 确认平台白名单、私有存储和活动会员的月度额度口径。
- 只读核对 dev/prod
__drizzle_migrations、真实 schema 和 migration journal。
Phase 1:Shared 契约与纯函数
- 添加类型、Zod 校验、时间阶段和奖励计算。
- 先完成边界测试,后续前后端只消费这一套规则。
Phase 2:Additive 数据库迁移
- 新增 social、evidence、snapshot、settlement、grant、entitlement 表和索引。
- 不改旧
community_event_submissions.world_id。 - 更新
db/index.ts的本地 PGlite parity,并添加 schema verify。
Phase 3:私有证据服务
- 实现 prepare/upload/complete/authorized-read/delete。
- 做 owner/admin ACL、对象校验、保留期和清理任务。
Phase 4:登记链路
- 服务端 draft、平台槽位、revision、链接规范化和最多 5 平台限制。
- 用户端单页登记、平台卡、移动端恢复。
Phase 5:初审与每平台 1,000
- 管理员初审队列和 24h 资格。
- durable grant、钱包/ledger 原子处理、失败重试和通知。
Phase 6:可选最终数据
- 用户的两种模式、证据数字和官方链接快照。
- 不可访问且无证据固定为 0 分。
Phase 7:最终结算与会员
- 逐平台计算、账户求和、初审承诺扣减和最高会员一次。
- plan entitlement 与 Stripe/WeChat 基础订阅隔离。
Phase 8:管理端、通知、i18n 与 Banner 适配
- 完成 social 管理队列、异常处理、五语言和纯封面展示。
- 把活动配置为 draft,做结算 preview 和 dry run。
Phase 9:Testing 与发布
- 仅在用户明确说“实施到 testing”后执行。
- shared/server/app 单测、类型检查、构建、真实 PostgreSQL 并发集成测试和手工 QA。
- 先部署 additive schema 并 verify,再部署读取新表的代码;活动 dry run 通过后才 promote 为 live。
建议并行:Phase 2 完成后,用户端登记 UI、管理员队列 UI、私有上传和奖励服务可以分四条工作流并行;数据库契约、shared 类型、最终集成和发布顺序必须串行。
14. Testing 验收矩阵
必须覆盖:
- 分数边界:29/30、99/100、199/200、499/500、999/1000、1999/2000。
- 传播分为点赞、收藏、有效评论的直接相加;分享/转发不计分(owner 定稿,取代早前 ×2 口径)。
- 两平台 700/50 分:总累计 9,000;初审承诺 2,000;终审补 7,000。
- 五平台均 2,000+:总计 75,000;只发一份 Ascendant 30 天。
- 五个平台初审分别发 1,000;同平台重复点击、换帖及 10 路并发仍只发一次。
- 第 5、6个平台并发提交时,最多成功 5 个。
- tracking URL、短链和同一账号不会绕过唯一约束。
user_evidence没有 ready proof 时拒绝;证据成立时按核验后的用户数字计算。- 用户不操作时走官方核验;链接打不开且无有效 proof 时分数为 0,但初审 1,000 保留。
- 初审 grant 为 pending/failed 时,终审不重复包含这 1,000。
- 终审 10/50 路并发只创建一份 final Mushies 和一份会员 entitlement。
- 钱包、ledger、grant 任一步故障都原子回滚并可安全重试。
- 免费、低档/同档/高档付费、Stripe、WeChat 和已有临时权益用户均不丢失活动 30 天。
- 私有证据的 owner/admin ACL、过期链接、类型/大小校验和删除保留期。
- guest 可正常查看详情;日语使用 ja;world 活动提交和审核零回归。
- 手机切 App/相册后草稿恢复,上传中断可重试,重复点击不创建第二 entry。
- Banner 在详情页完整显示为 16:5,信息在图外;375px Hub 裁切不丢失中央主体。
- social 管理队列分页,不一次加载全部投稿。
15. 实施前仍需锁定的三个小口径
这些不会改变总体架构,但在开始写代码前应写进 rulesConfig/运营配置:
- 平台白名单与 URL 规则:推荐首发只开放能稳定查看公开指标且可规范化链接的平台。
- 证据保留期:推荐结算后 90 天自动删除,争议/风控记录单独 hold。
- 会员月度 Mushies:推荐活动会员首次实际激活时只发一次该档正常月度额度;排队、续费和重试不得重复发。
除这三点外,V3 的时间、公式、每平台初审 1,000、5 平台上限、不限名额、二次提交选择、不可核验规则、最高会员一次和 Banner 纯封面均已锁定。
16. 本阶段明确不做
- 不生成或重做活动图片。
- 不新增强制公示/申诉主流程;争议走管理员审计与最终裁定。
- 不自动抓取任意社交平台或依赖不稳定的非官方 API。
- 不把证据上传到公共 CDN。
- 不让管理员自由填写活动奖励。
- 不在收到“实施到 testing”指令前修改业务代码、运行迁移或开始测试。
