尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Civitai 分级执法系统(Strike System)后端基础设施设计实录

Civitai 分级执法系统(Strike System)后端基础设施设计实录 Civitai 分级执法系统Strike System后端基础设施设计实录【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai导读本文基于 Civitai 仓库中的 docs/strike-system-prd.md 设计文档完整还原其 Phase 1后端基础设施的落地过程从数据库 Schema、业务规则、升级Escalation引擎到 tRPC 端点、通知、邮件与定时任务并对照仓库真实源码src/server/services/strike.service.ts、src/server/jobs/process-strikes.ts、packages/civitai-db-schema/prisma/schema.full.prisma等验证实现细节。读完你将掌握Civitai 如何用计分式 strike替代二元 mute/ban 执法、2 分/3 分两档自动升级的完整逻辑、无限期 mute 与到期 mute 的共存机制以及如何通过测试 Webhook 对整条链路做安全演练。一、背景为什么需要 strike 系统Civitai 原有的审核工具是二元的mute禁言或 ban封禁。中间没有任何渐进式执法手段——没有对用户的警告、没有对惯犯的追踪、也没有自动升级auto-escalation。团队由此确立了 strike 系统的四个核心诉求在升级到 mute/ban 之前给用户清晰、有时限的警告为审核人员提供结构化的工作流建立用户违规行为的审计轨迹audit trail让 strike自动过期避免用户被永久惩罚。Phase 1 只做后端基础设施数据库、服务层、tRPC 端点、任务、通知前端 UI 属于 Phase 2。该 PRD 在仓库中的状态标注为Shipped正式的业务规则以 docs/moderator-app/strike-rules.md 为准本文档保留为 Phase 1 的设计记录design record。二、业务规则总览PRD 中的核心规则如下表规则详情每份被屏蔽内容记 1 次 strike每起事件 1 分每个用户每天最多 1 次自动 strike严重内容realistic minor R单起事件记 3 分2 个活跃积分无期限禁言mute直到用户重新接受 ToS 或积分降至 2 以下2026-08-25 修订见 docs/moderator-app/strike-rules.md §33 个活跃积分禁言 标记交由 LLM/人工审核Strike 过期自创建起 365 天Phase 1 期间为 30 天2026-08-24 修订为 365见 docs/moderator-app/strike-rules.md §2明确不在 Phase 1 范围内的事项不从图片扫描/屏蔽自动产生 strike误报率过高被屏蔽内容一律进入人工审核队列不做正式的上诉系统 UI申诉走客服渠道不接 report/scan 的自动触发点先把系统建好触发源后续再接。三、关键设计决策1. 基于积分的模型而非单纯计数每条 strike 记录带points字段1–3。一次严重违规产生一条 3 分的记录升级判断累加的是活跃积分的总和。这样既保持了一次违规对应一条记录的语义关联又支持不同严重程度。2. 新建StrikeReason枚举不复用BanReasonCode封禁理由SexualMinor、Nudify 等粒度与 strike 理由BlockedContent、TOSViolation、ManualModAction 等不同分开定义可以让两者各自独立演进。3. User 表新增muteExpiresAt列原有的 mute 是布尔值。新增的muteExpiresAt可空 DateTime承载审核员手动设置的限时禁言由每日任务在到期后解除。Strike 引发的禁言该列为 null——它不靠定时器结束而是在用户重新接受 ToS 或积分衰减后结束。4. 被推翻overturned的 strike 标记为Voided而非删除保留审计轨迹这是审核系统的基本要求。四、数据库 Schema新增枚举enum StrikeReason { BlockedContent RealisticMinorContent CSAMContent TOSViolation HarassmentContent ProhibitedContent ManualModAction } enum StrikeStatus { Active Expired Voided }新模型UserStrikemodel UserStrike { id Int id default(autoincrement()) userId Int user User relation(userStrikes, fields: [userId], references: [id], onDelete: Cascade) reason StrikeReason status StrikeStatus default(Active) points Int default(1) description String // 面向用户的说明团队讨论后确认关键字段 internalNotes String? // 仅审核员可见 entityType EntityType? // 触发该 strike 的内容类型 entityId Int? reportId Int? // 关联的原始举报 createdAt DateTime default(now()) expiresAt DateTime // 默认createdAt 365 天 voidedAt DateTime? voidedBy Int? voidedByUser User? relation(voidedStrikes, fields: [voidedBy], references: [id], onDelete: SetNull) voidReason String? issuedBy Int? issuedByUser User? relation(issuedStrikes, fields: [issuedBy], references: [id], onDelete: SetNull) index([userId, status]) index([userId, expiresAt]) index([status]) index([createdAt]) }仓库中的实际实现位于 packages/civitai-db-schema/prisma/schema.full.prisma与 PRD 基本一致仅细节略有差异description/internalNotes/voidReason被标注为db.VarChar1000/2000/1000关系名分别为strikeVoidedBy/strikeIssuedBy。User 模型变更在User上追加strikes UserStrike[] relation(userStrikes) issuedStrikes UserStrike[] relation(issuedStrikes) voidedStrikes UserStrike[] relation(voidedStrikes) muteExpiresAt DateTime?UserMeta 变更src/server/schema/user.schema.tsstrikeFlaggedForReview: z.boolean().optional(), strikeFlaggedAt: z.date().optional(),这两个字段在实现中确实存在且被审核面板与 ToS 释放逻辑读取。迁移 SQL迁移文件位于 packages/civitai-db-schema/prisma/migrations/20260209121754_add_strike_system/migration.sqlCREATE TYPE StrikeReason AS ENUM (BlockedContent, RealisticMinorContent, CSAMContent, TOSViolation, HarassmentContent, ProhibitedContent, ManualModAction); CREATE TYPE StrikeStatus AS ENUM (Active, Expired, Voided); ALTER TABLE User ADD COLUMN IF NOT EXISTS muteExpiresAt TIMESTAMP(3); CREATE TABLE IF NOT EXISTS UserStrike ( id SERIAL NOT NULL, userId INTEGER NOT NULL, reason StrikeReason NOT NULL, status StrikeStatus NOT NULL DEFAULT Active, points INTEGER NOT NULL DEFAULT 1, description VARCHAR(1000) NOT NULL, internalNotes VARCHAR(2000), entityType EntityType, entityId INTEGER, reportId INTEGER, createdAt TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, expiresAt TIMESTAMP(3) NOT NULL, voidedAt TIMESTAMP(3), voidedBy INTEGER, voidReason VARCHAR(1000), issuedBy INTEGER, CONSTRAINT UserStrike_pkey PRIMARY KEY (id) ); CREATE INDEX IF NOT EXISTS UserStrike_userId_status_idx ON UserStrike(userId, status); CREATE INDEX IF NOT EXISTS UserStrike_userId_expiresAt_idx ON UserStrike(userId, expiresAt); CREATE INDEX IF NOT EXISTS UserStrike_status_idx ON UserStrike(status); CREATE INDEX IF NOT EXISTS UserStrike_createdAt_idx ON UserStrike(createdAt); ALTER TABLE UserStrike ADD CONSTRAINT UserStrike_userId_fkey FOREIGN KEY (userId) REFERENCES User(id) ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE UserStrike ADD CONSTRAINT UserStrike_voidedBy_fkey FOREIGN KEY (voidedBy) REFERENCES User(id) ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE UserStrike ADD CONSTRAINT UserStrike_issuedBy_fkey FOREIGN KEY (issuedBy) REFERENCES User(id) ON DELETE SET NULL ON UPDATE CASCADE;五、Zod 输入校验 Schemasrc/server/schema/strike.schema.ts该文件在仓库中为新建文件定义了全部 tRPC 端点的输入校验。对照源码实际 schema 如下比 PRD 表格更精确含默认值与边界Schema校验要点createStrikeSchemauserId、reason枚举、points1–3默认 1、description1–1000 字符、internalNotes最多 2000、entityType?、entityId?、reportId?、expiresInDays1–365默认 365voidStrikeSchemastrikeId、voidReason1–1000 字符getStrikesSchema分页 userId?、username?、status[]?、reason[]?getUserStandingsSchema分页 hasActiveStrikes?、isMuted?、isFlaggedForReview?、sortpoints/score/lastStrike/created、sortOrdergetMyStrikesSchemaincludeExpired默认 false另外该文件还导出了两个前端也会用到的小工具strikeStatusColorSchemeActive → red、Expired → gray、Voided → yellow的状态配色accountStandingFromPoints(points)根据活跃积分给出账户状态0 分 Good standing / 1 分 Warning / 2 分 Restricted及对应颜色strikeReasonPublicLabel将七个StrikeReason映射为面向用户的、中性且不外露的文案如BlockedContent→ Content violated our Terms of Service这是唯一会通过邮件发给用户的理由文本自由文本description只用于站内/Retool绝不进入 strike 邮件。六、服务层核心业务逻辑src/server/services/strike.service.ts这是整个系统的引擎PRD 列出的函数在实现中全部存在且更丰富// CRUD createStrike(input { issuedBy?: number }) → UserStrike voidStrike(input { voidedBy: number }) → UserStrike acceptTosAfterMute({ userId, domain }) → { unmuted, reason? } // 实现中新增 // 查询 getActiveStrikePoints(userId) → number getStrikeSummary(userId) → { activeStrikes, totalActivePoints, nextExpiry } getStrikesForUser(userId, opts?) → StrikeSummary getStrikeHistoryForMod(userId) → 含用户档案的完整历史审核抽屉用 getStrikesForMod(filters, pagination) → PaginatedStrikes getUserStandings(input) → 审核面板的按用户聚合分页数据 // 升级引擎 evaluateStrikeEscalation(userId, { allowMute }) → { totalPoints, action } action ∈ none | muted | muted-and-flagged | unmuted // 限流 shouldRateLimitStrike(userId) → boolean // 每个用户每天最多 1 次自动 strike // 任务 expireStrikes() → { expiredCount } processTimedUnmutes() → { unmutedCount }1. 升级逻辑evaluateStrikeEscalation的源码级细节PRD 给出了 4 步逻辑实际实现在此基础上做了大量加固值得逐条展开锁定并求和活跃积分使用WITH locked AS MATERIALIZED (SELECT points FROM UserStrike WHERE ... FOR UPDATE) SELECT SUM(points) FROM locked的 CTE 形态。注释明确说明Postgres 直接拒绝在聚合查询上使用FOR UPDATE错误码 0A000所以必须把锁放在 CTE 层MATERIALIZED是关键——若内联 CTE 会把两层合并回一层导致每次调用都抛错。阈值检查降序且仅在调用方传入allowMute时才能执行禁言createStrike是唯一传入allowMute: true的地方totalPoints REVIEW_MUTE_POINTS (3)禁言、muteExpiresAt null、meta.strikeFlaggedForReview true、meta.strikeFlaggedAt new Date()、meta.muteReason strike-escalation→ 通知totalPoints MUTE_POINTS (2)禁言、muteExpiresAt null、meta.muteReason strike-escalation→ 通知 2释放本系统施加的禁言绝不触碰mutedAt非空的审核员禁言。禁言后invalidateSession(userId)释放后refreshSession(userId)。不叠加、不设定时器同一档位的第二条 strike 不会改变既有禁言状态。两个阈值常量被独立抽取到 src/shared/constants/strike.constants.ts原因是系统外部也有消费方如挑战资格判定把数值放进服务文件会拖拽通知/邮件依赖图export const MUTE_POINTS 2; export const REVIEW_MUTE_POINTS 3;实现中最关键的两个安全边界源码注释有非常详细的解释moderatorMutedmutedAt ! null是人工决定与系统自动的分水岭mutedAt只由审核员决策写入封禁维持、mute 开关、审核 App提示词审计、诈骗自动禁言、strike 升级等所有自动路径都留空它。strike 升级绝不覆盖或解除审核员的禁言——不解除、不延期、不动 expiry。注意实现用的是! null而非! null未 select 该字段的调用方若用严格比较会让所有禁言看起来都像审核员设置的从而冻结降级路径。mayLabelMute只有当muteReason已经是strike-escalation或当前未禁言时才给禁言打上这个标签。因为该标签是 ToS 门gate判断可以释放该账户的唯一依据绝不能给诈骗自动禁言等其它禁言错误打上 strike 标签。2. 限流shouldRateLimitStrikeSELECT COUNT(*) FROM UserStrike WHERE userId $1 AND createdAt CURRENT_DATE AND reason ! ManualModAction仅对自动/系统 strike 生效。人工审核员的ManualModAction永远绕过每日上限——审核员可以按需签发任意多条人工 strike。createStrike中非ManualModAction的 reason 会先查限流命中则记 Axiom 日志并返回null静默跳过。3.createStrike的完整流程含健壮性设计先校验用户存在不存在抛NOT_FOUND非人工 strike 做限流检查计算expiresAt now expiresInDays * 24h写入UserStrike记录非致命地记录审核活动trackModActivity注释明确指出strike 行已提交这里抛错会报告一次实际已落地的失败而审核员重试人工 strike 会因为绕过限流而开出第二条——所以失败只记 Axiom 日志调用evaluateStrikeEscalation(userId, { allowMute: true })发送strike-issued通知 发送strikeIssuedEmail均包裹 try/catch失败不阻断主流程。4.voidStrike原子状态守卫防竞态用updateMany的where: { id, status: Active }原子更新只有Active状态才能被 voidcount 0时再区分不存在与状态不对两种错误。void 之后重新调用evaluateStrikeEscalation不带allowMute若积分因此跌破 2 则立即释放禁言——无需等每日任务。5. ToS 重新接受即释放acceptTosAfterMute实现新增2 分禁言的两条出路之一另一条是积分衰减后由每日任务兜底。该方法由protectedProcedure暴露因此任何登录用户都可以直接调用——它用与禁言守卫完全相同的tosReacceptanceOffer谓词做资格判断muted、mutedAt为 null、muteReason strike-escalation、活跃积分 3并返回reason让客户端区分已解除 / 审核员禁言 / 待审核 / 未禁言 / 不符合条件。实现细节释放逻辑在写事务内重读并重判积分与状态避免第三次 strike 落在调用间隙、把strikeFlaggedForReview用陈旧 meta 覆盖掉的竞态接受时间戳也由本方法自己写入前端弹窗的 settings 写入不 await。七、控制器与 tRPC Router控制器src/server/controllers/strike.controller.ts薄处理器层遵循user.controller.ts的既有模式。PRD 中的 6 个 handler 全部存在并新增了acceptTosAfterMuteHandler与getUserStandingsHandler。值得注意的权限细节getMyStrikesHandler强制includeInternalNotes: false——内部备注永不暴露给用户getUserStrikeHistoryHandler会先用userMetaSchema.safeParse解析meta再输出flaggedForReview等字段createStrikeHandler/voidStrikeHandler把ctx.user.id注入为issuedBy/voidedBy实现责任到人。Routersrc/server/routers/strike.router.tsexport const strikeRouter router({ // 用户端点 getMyStrikes: protectedProcedure .meta({ requiredScope: TokenScope.Full }) .input(getMyStrikesSchema) .use(isFlagProtected(strikes)) .query(getMyStrikesHandler), acceptTosAfterMute: protectedProcedure .meta({ requiredScope: TokenScope.Full }) .use(isFlagProtected(strikes)) .mutation(acceptTosAfterMuteHandler), getMyStrikeSummary: protectedProcedure .meta({ requiredScope: TokenScope.Full }) .use(isFlagProtected(strikes)) .query(getMyStrikeSummaryHandler), // 审核端点 create: moderatorProcedure.input(createStrikeSchema).use(isFlagProtected(strikes)).mutation(createStrikeHandler), void: moderatorProcedure.input(voidStrikeSchema).use(isFlagProtected(strikes)).mutation(voidStrikeHandler), getAll: moderatorProcedure.input(getStrikesSchema).use(isFlagProtected(strikes)).query(getStrikesHandler), getUserStandings: moderatorProcedure.input(getUserStandingsSchema).use(isFlagProtected(strikes)).query(getUserStandingsHandler), getUserHistory: moderatorProcedure.input(z.object({ userId: z.number() })).use(isFlagProtected(strikes)).query(getUserStrikeHistoryHandler), });实现比 PRD 多出两点一是所有端点都加了isFlagProtected(strikes)功能开关保护二是用户端点额外要求TokenScope.Full。Router 在src/server/routers/index.ts注册为strike: strikeRouter。八、通知与邮件通知定义src/server/notifications/strike.notifications.ts通过createNotificationProcessor注册了5 种通知PRD 表格列了 4 种实际实现多了降级解禁的strike-de-escalation-unmuted类型触发时机是否可关闭文案要点strike-issuedcreateStrike()之后false注明点数1 分时特别说明计入 N 分strike-voidedvoidStrike()之后false说明移除原因strike-escalation-muted升级触发禁言时false区分无限期待审muteDays indefinite与等接受 ToS两种文案strike-expired过期任务运行时false告知账户状态已改善strike-de-escalation-unmuted积分下降解除禁言时false告知禁言已解除通知注册在src/server/notifications/utils.notifications.ts中import { strikeNotifications }并展开进notificationProcessors。邮件模板src/server/email/templates/strikeIssued.email.ts仿照既有邮件模板内容包括strike 原因与描述、本次积分与活跃积分总数、过期日期、账户状态页链接。九、定时任务src/server/jobs/process-strikes.ts任务Cron逻辑expire-strikes0 2 * * *每日 2AM将已过期的 strike 置为Expired批量通知受影响用户然后逐个顺序执行因每个都用FOR UPDATE事务重新评估升级状态process-timed-unmutes0 3 * * *每日 3AM只释放release-only。两类一类是 strike 禁言mutedAt IS NULL且muteReason strike-escalation或muteExpiresAt NOW()交给evaluateStrikeEscalation按积分决定是否释放另一类是审核员的限时禁言mutedAt IS NOT NULL且muteExpiresAt已过直接用clearedMuteFields解除并refreshSession两个任务都用createJob封装、带 Axiom 错误日志并在src/pages/api/webhooks/run-jobs/[[...run]].ts的 jobs 数组中注册。任务层也有对应测试src/server/jobs/tests/process-strikes.test.ts。十、与既有系统的集成点mute 系统strike 禁言无到期时间meta.muteReason strike-escalation打标靠 ToS 重新接受或积分衰减结束审核员的手动toggleMute保持不变本身无到期confirm-mutes任务src/server/jobs/confirm-mutes.ts处理的是已确认禁言的订阅取消与本系统无关自动解禁统一走新的process-timed-unmutes任务。举报系统createStrike接受可选reportId关联原始举报Phase 1 中审核员手动签发 strike不自动触发。未来审核员处理举报时 UI 可提供 Issue Strike 按钮。用户积分update-user-score.ts中的reportsAgainst独立追踪 ToS 违规Phase 2 可把 strike 纳入积分计算。会话/认证任何升级到禁言的路径都调用invalidateSession(userId)定时解禁后调用refreshSession(userId)。具体实现位于src/server/auth/session-invalidation.ts。ToS 门src/server/common/tos-reacceptance.ts中的STRIKE_MUTE_REASON strike-escalation与tosReacceptanceOffer()是只有 strike 禁言才向用户提供接受 ToS 即释放的唯一判定点接受后由src/server/services/tos-reacceptance.service.ts协同完成释放。十一、实施顺序与验证实施顺序PRD 原样继承Step 1: Schema Migration └─ prisma/schema.prisma └─ prisma/migrations/... └─ pnpm run db:generate Step 2可并行: ├─ src/server/schema/strike.schema.ts ├─ src/server/notifications/strike.notifications.ts └─ src/server/email/templates/strikeIssued.email.ts Step 3: src/server/services/strike.service.ts └─ 依赖schema、Prisma types Step 4: src/server/controllers/strike.controller.ts └─ 依赖service Step 5: src/server/routers/strike.router.ts └─ 依赖controller、schema Step 6可并行: ├─ src/server/routers/index.ts注册 router ├─ src/server/notifications/utils.notifications.ts注册通知 ├─ src/server/schema/user.schema.tsUserMeta 更新 └─ src/server/jobs/process-strikes.ts Step 7: src/pages/api/webhooks/run-jobs/[[...run]].ts注册任务 Step 8: Typecheck lint └─ pnpm run typecheck pnpm run lint验证计划pnpm run typecheck全部新文件编译通过pnpm run lint无 lint 问题本地库执行迁移确认表与索引创建成功pnpm run db:generateUserStrike类型可用手动 API 测试dev serverstrike.create/strike.getMyStrikes/strike.void/ 2 分自动禁言 / 3 分flaggedForReview任务测试通过 run-jobs webhook 手动触发expire-strikes与process-timed-unmutes。十二、测试 Webhook 端点src/pages/api/testing/strikes.ts用于手工集成测试GET /api/testing/strikes?tokenWEBHOOK_TOKENaction...通过WebhookEndpoint保护与 daily-challenge 测试端点同款支持dryRuntrue安全预览将要发生什么而不落库。对照实现源码需要注意一个与 PRD 的差异可写操作create/void已被迁移到带责任人的审核端点src/pages/api/mod/strike/create.tsissuedBy/voidedBy记录为调用审核员测试端点保留读操作 dry-run 任务触发。只读操作始终安全Action参数返回get-user-strikesuserId用户全部 strike、活跃积分、下次过期get-active-pointsuserId活跃 strike 积分之和check-rate-limituserId今天是否会被自动 strike 限流任务触发类操作可用dryRuntrue预览Action参数Dry Run 返回实际效果evaluate-escalationuserId当前状态 预测动作真正升级/降级expire无列出将过期的 strike过期 通知 重新评估unmute无列出禁言已到期用户 预测结果处理解禁注evaluate-escalation的 dry run 明确注释无法预测 mute——只有createStrike传allowMute从该端点触发的评估只可能释放dry-run 只预测unmuted/none。unmute的 dry run 则会对每名用户按活跃积分预测muted-and-flagged/muted/unmuted。冒烟测试序列PRD 原样继承TOKENyour_webhook_token BASEhttp://localhost:3000/api/testing/strikes # 1. 查看用户当前状态 curl $BASE?token$TOKENactionget-user-strikesuserId123 curl $BASE?token$TOKENactionget-active-pointsuserId123 # 2. Dry run 创建一条 strike curl $BASE?token$TOKENactioncreateuserId123reasonManualModActionpoints1descriptionTeststrikedryRuntrue # 3. 真正创建一条 strike curl $BASE?token$TOKENactioncreateuserId123reasonManualModActionpoints1descriptionTeststrike # 4. 检查升级状态 curl $BASE?token$TOKENactionevaluate-escalationuserId123dryRuntrue # 5. 创建第二条 strike应触发 2 分禁言 curl $BASE?token$TOKENactioncreateuserId123reasonManualModActionpoints1descriptionSecondteststrike # 6. 验证禁言已生效 curl $BASE?token$TOKENactionevaluate-escalationuserId123dryRuntrue # 7. Void 第二条 strike应降级/解禁 curl $BASE?token$TOKENactionvoidstrikeIdSTRIKE_IDvoidReasonTesting # 8. 验证降级 curl $BASE?token$TOKENactionevaluate-escalationuserId123dryRuntrue # 9. Void 第一条 strike 清理 curl $BASE?token$TOKENactionvoidstrikeIdFIRST_STRIKE_IDvoidReasonCleanupCron 任务的手动触发# 过期 strike正常每日 2AM 运行 GET /api/webhooks/run-jobs?runexpire-strikestokenWEBHOOK_TOKEN # 处理定时解禁正常每日 3AM 运行 GET /api/webhooks/run-jobs?runprocess-timed-unmutestokenWEBHOOK_TOKEN十三、代码评审修复清单PRD 记录实现均已落地降级会解禁用户——evaluateStrikeEscalation的 else 分支处理积分下降时的解禁processTimedUnmutes 解禁前重新查积分——调用evaluateStrikeEscalation而非直接清 muteinternalNotes 受保护——getStrikesForUser用selectincludeInternalNotes参数控制voidStrike 校验状态——原子updateMany带 status 守卫防竞态通知文案区分无限期——对 indefinite 与 timed mute 分别措辞通知错误内部消化——createNotification自行捕获并记 Axiom服务层无需额外 try-catchstrikeFlaggedForReview 清除——降级及从 3 分掉到 2 分时清除标记expireStrikes 重新评估——过期后对每个受影响用户调用evaluateStrikeEscalation校验用户存在——createStrike在任何工作前先确认用户存在重复禁言通知跳过——同档位已禁言时不重复通知降级解禁通知——禁言解除时通知用户。这些修复在 src/server/services/tests/strike.service.test.ts1381 行的 vitest 套件中都有对应断言——例如测试刻意不继承规范化的$transaction默认实现让每个写断言都必须是发生在事务锁内的写防止把锁外写入误判为锁内写入。十四、Phase 2 预览前端不属于本阶段用户账户状态页/user/account#strikes审核面板strike 日志、按用户过滤、签发/撤销 UI仓库中已有src/pages/moderator/strikes.tsx的雏形举报处理流程中的 Issue Strike 按钮状态页的积分集成通知链接端到端可用当前通知的 URL 均为 TODO等待 UI 页面落地。十五、关键参考文件索引用途文件Prisma schema完整版packages/civitai-db-schema/prisma/schema.full.prismaStrike 迁移 SQLpackages/civitai-db-schema/prisma/migrations/20260209121754_add_strike_system/migration.sqlZod 输入校验src/server/schema/strike.schema.ts服务层核心逻辑src/server/services/strike.service.ts服务层单元测试src/server/services/tests/strike.service.test.ts控制器src/server/controllers/strike.controller.tstRPC Routersrc/server/routers/strike.router.ts通知定义src/server/notifications/strike.notifications.ts定时任务src/server/jobs/process-strikes.ts任务测试src/server/jobs/tests/process-strikes.test.ts阈值常量src/shared/constants/strike.constants.tsToS 释放判定src/server/common/tos-reacceptance.ts测试 Webhooksrc/pages/api/testing/strikes.ts线上业务规则现行版docs/moderator-app/strike-rules.md小结Civitai 的 strike 系统用一张UserStrike表 一个积分升级引擎把警告 → 禁言 → 标记人工审核的渐进式执法完整落到后端并通过mutedAt/muteReason双信号严格区分系统自动禁言与审核员人工禁言保证自动逻辑永远不会覆盖人的决策。Phase 1 的边界清晰不做自动触发、不做 UI为 Phase 2 的账户状态页、审核面板和举报集成留好了完整接口。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表