Skip to content

YUMINA 分享活动|工程实施计划 V3

版本:2026-07-16 状态:工程方案与业务实现已完成;testing 环境按发布流程验证 产品基线:yumina-share-event-v3.md


1. 结论

现有活动系统只能提交和审核世界卡,不能安全承载这次社媒分享活动。本次需要在保留旧 world 活动零回归的前提下,增加一条独立的 social_post 活动链路。

用户侧始终只看到三步:

  1. 参与并登记平台帖子。
  2. 自选是否补交最终数据。
  3. 等待官方核验和结算。

工程内部则必须将“平台投稿、帖子修订、私有证据、指标快照、账户结算、实际发奖”分开保存。这样才能同时保证每个平台独立计算、换帖不重复领初审奖励、发奖可重试但不重复、证据不公开,以及活动会员不覆盖 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_evidenceofficial_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.ts
  • packages/shared/src/validation/community-event.ts
  • packages/shared/src/utils/community-event-rewards.ts

修改:

  • packages/shared/src/index.ts
  • packages/shared/src/constants/limits.ts

需要定义:

  • EventSubmissionType = "world" | "social_post"
  • SocialPlatform、平台白名单及 URL/账号规范化配置。
  • SocialEventRulesConfig、时间线和 rulesVersion
  • SocialEventEntryEntryRevisionFinalDataModeMetricSnapshotSettlementSummary
  • 用户、管理员和通知 payload 的 discriminated unions。

纯函数:

text
calculatePlatformScore(metrics)
tierForPlatformScore(score)
calculateEventSettlement(entries, committedGrants)
deriveEventPhase(now, timeline, settlementState)
normalizeSocialPostUrl(platform, rawUrl)

唯一奖励公式:

text
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 活动无需回填:

  • registrationOpensAt
  • registrationClosesAt
  • finalDataOpensAt
  • finalDataClosesAt
  • settlementDeadlineAt
  • rulesVersion
  • rulesConfig jsonb
  • rulesLockedAt

现有 draft / live / closed 继续表示可见性;实际阶段按时间和结算状态推导为:

text
upcoming → registration_open → final_data_open
         → official_verification → completed

过了 8 月 28 日但仍有奖励未到账时应显示 overdue/处理中,不能仅因时间到达就伪装成 completed。

5.2 community_event_social_entries

一行代表“一个活动中,一个 UID 的一个平台槽位”。主要字段:

  • eventId, userId, platform
  • socialHandle, normalizedAccountKey
  • currentRevisionId, replacementCount
  • status, initialReviewEligibleAt
  • finalDataMode
  • 初审/终审管理员、时间、理由和风险标记
  • verifiedScore, platformEntitlement, membershipPlanId
  • version, 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, supersededAt
  • rulesVersion

关键约束:

  • 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, version
  • totalEntitlement, committedMushies, finalDueMushies
  • highestPlanId, membershipDurationDays
  • 每个平台的 calculationSnapshot
  • 冻结、审核、完成时间及管理员

检查约束:Mushies 总额和补发额均在 0..75000 之间。

5.7 community_event_reward_grants

每一项实际奖励一行:

  • event/user/platform/entry/settlement 来源
  • phase = initial | final | adjustment | reversal
  • kind = mushies | plan
  • 金额,或 plan/duration
  • idempotencyKey UNIQUE
  • pending | processing | applied | failed | cancelled | reversed
  • attempt、lastError、ledger/entitlement ID、到账时间

稳定幂等键:

text
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/participation
  • PUT /events/:eventId/social-entries/:platform/draft
  • POST /events/:eventId/social-entries
  • POST /events/:eventId/social-entries/:entryId/resubmit
  • POST /events/:eventId/social-entries/:entryId/replace
  • POST /events/:eventId/social-entries/:entryId/final-data
  • POST /events/:eventId/evidence/upload-url
  • POST /events/:eventId/evidence/:evidenceId/complete
  • GET /events/:eventId/evidence/:evidenceId/content
  • DELETE /events/:eventId/evidence/:evidenceId

final-data 请求只允许:

text
{ 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 状态机

text
draft → initial_pending → initial_approved
                      ↘ needs_changes → initial_pending
initial_approved → final_review_pending → settled
initial_pending → rejected
任一有效状态 → disqualified
replacement → 同一平台槽位的新 revision → initial_pending
  • initialReviewEligibleAt = 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 上传/读取路由

实施方式:

  1. 使用独立 private bucket 最佳;退而求其次使用永不进入 CDN 白名单的 event-proofs/ 私有前缀。
  2. 服务端创建 evidence 记录并返回短时效 presigned PUT。
  3. 客户端直传后调用 complete;服务端通过 HeadObject 校验 owner、key、类型、大小和 checksum。
  4. 用户仅能读取自己的证明;管理员仅在审核权限下按需读取。
  5. 不把原始 key 或永久 URL返回前端,不在 localStorage 保存 File/Blob。
  6. 结算后按明确保留期清理;建议默认 90 天,发生争议/风控时可 hold。

8. 原子且可重试的发奖

8.1 每个平台初审 1,000

第一段主库事务:

  1. 锁定 entry,校验状态、公开满 24 小时及审核权限。
  2. 将 entry 标为初审通过。
  3. 用稳定平台幂等键 INSERT ... ON CONFLICT DO NOTHING 创建 1,000 pending grant。
  4. 写 admin action,提交。

第二段奖励事务:

  1. FOR UPDATE SKIP LOCKED 取得 grant,并锁钱包行。
  2. 增加余额和不会被月度刷新覆盖的活动/addon 余额。
  3. 在同一个 transaction 内写 hash ledger,reference 使用幂等键。
  4. 标记 grant applied 并保存 transaction ID。
  5. 任一步失败则整段回滚,grant 保留为可重试状态。

8.2 最终 Mushies

  1. 锁 settlement 和全部合格 entries。
  2. 冻结每个平台最终 metric snapshot。
  3. 逐平台计算累计 entitlement,再求和。
  4. 从总 entitlement 中减去所有已承诺但未取消的活动 Mushies。
  5. 创建全活动、全 UID 唯一的 final Mushies grant。
  6. 使用同一钱包/ledger 原子事务处理。

示例:平台 A 700 分得 7,000,平台 B 50 分得 2,000;总应得 9,000,初审已承诺 2,000,终审只补 7,000。

8.3 最高会员一次

  1. 从所有平台中选择最高会员档。
  2. 每个活动、每个 UID 只创建一个 plan grant 和 entitlement。
  3. Mushies 与 plan 分成两项 grant;一个成功、一个失败时只重试失败项。
  4. 同级/更高级付费会员存在时,将完整 30 天排队保存,不静默丢弃。
  5. 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.tsxevent-proof-upload.ts:私有上传流程。
  • social-event-api.ts:集中 API client。
  • social-event-draft.tsuse-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.tsx
  • packages/app/src/stores/notifications.ts
  • packages/app/src/locales/{zh,zh-Hant,en,es,ja}/community.json
  • 对应五种语言的 common.jsonadmin.json
  • 服务端通知 payload 和 notify.ts

最小通知集:

  • 平台初审通过并发放 1,000。
  • 需要修改或被驳回。
  • 可选补数据窗口提醒。
  • 最终结算完成或奖励处理失败后恢复。
  • 取消资格。
  • 管理员待初审/待终审提醒。

通知保存结构化 eventId / entryId / platform / amount / i18nKey,前端本地化并深链;服务端不拼接单语言最终文案。


12. 现有活动系统必须顺手修复的问题

  1. admin-events.ts 的发奖与审核状态不原子,可能重复发奖。
  2. world 投稿查重使用 read replica 且没有数据库唯一约束,存在并发竞态。
  3. attachSubmissionCounts() 把所有投稿读入 JS 后计数;改为数据库 GROUP BY,并分别统计 world/social。
  4. 管理员投稿列表没有可靠分页;social 队列必须服务端分页和索引筛选。
  5. 活动详情把公开 event 与需登录的 submission 用 Promise.all 加载,guest 401 会卡 skeleton。
  6. 日语等语言分支存在手写判断,应统一使用 normalizeLang()
  7. 现有公共 community uploader 不能承载敏感证据。
  8. 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 验收矩阵

必须覆盖:

  1. 分数边界:29/30、99/100、199/200、499/500、999/1000、1999/2000。
  2. 传播分为点赞、收藏、有效评论的直接相加;分享/转发不计分(owner 定稿,取代早前 ×2 口径)。
  3. 两平台 700/50 分:总累计 9,000;初审承诺 2,000;终审补 7,000。
  4. 五平台均 2,000+:总计 75,000;只发一份 Ascendant 30 天。
  5. 五个平台初审分别发 1,000;同平台重复点击、换帖及 10 路并发仍只发一次。
  6. 第 5、6个平台并发提交时,最多成功 5 个。
  7. tracking URL、短链和同一账号不会绕过唯一约束。
  8. user_evidence 没有 ready proof 时拒绝;证据成立时按核验后的用户数字计算。
  9. 用户不操作时走官方核验;链接打不开且无有效 proof 时分数为 0,但初审 1,000 保留。
  10. 初审 grant 为 pending/failed 时,终审不重复包含这 1,000。
  11. 终审 10/50 路并发只创建一份 final Mushies 和一份会员 entitlement。
  12. 钱包、ledger、grant 任一步故障都原子回滚并可安全重试。
  13. 免费、低档/同档/高档付费、Stripe、WeChat 和已有临时权益用户均不丢失活动 30 天。
  14. 私有证据的 owner/admin ACL、过期链接、类型/大小校验和删除保留期。
  15. guest 可正常查看详情;日语使用 ja;world 活动提交和审核零回归。
  16. 手机切 App/相册后草稿恢复,上传中断可重试,重复点击不创建第二 entry。
  17. Banner 在详情页完整显示为 16:5,信息在图外;375px Hub 裁切不丢失中央主体。
  18. social 管理队列分页,不一次加载全部投稿。

15. 实施前仍需锁定的三个小口径

这些不会改变总体架构,但在开始写代码前应写进 rulesConfig/运营配置:

  1. 平台白名单与 URL 规则:推荐首发只开放能稳定查看公开指标且可规范化链接的平台。
  2. 证据保留期:推荐结算后 90 天自动删除,争议/风控记录单独 hold。
  3. 会员月度 Mushies:推荐活动会员首次实际激活时只发一次该档正常月度额度;排队、续费和重试不得重复发。

除这三点外,V3 的时间、公式、每平台初审 1,000、5 平台上限、不限名额、二次提交选择、不可核验规则、最高会员一次和 Banner 纯封面均已锁定。


16. 本阶段明确不做

  • 不生成或重做活动图片。
  • 不新增强制公示/申诉主流程;争议走管理员审计与最终裁定。
  • 不自动抓取任意社交平台或依赖不稳定的非官方 API。
  • 不把证据上传到公共 CDN。
  • 不让管理员自由填写活动奖励。
  • 不在收到“实施到 testing”指令前修改业务代码、运行迁移或开始测试。