【寻迹校园 HarmonyOS NEXT 实战 31】举报去重怎么做:同一记录的重复投诉不应制造多条处理中案件

【寻迹校园 HarmonyOS NEXT 实战 31】举报去重怎么做:同一记录的重复投诉不应制造多条处理中案件

本章导读:这是"寻迹校园 HarmonyOS NEXT 实战"系列第 31 篇。本文结合 ModerationReportPageModerationService.submit()ModerationRepository.findByReportId() 与本地状态机测试,拆解举报原因、敏感信息拦截、重复案件回执和数据库唯一约束之间的边界。

上图为原创生成的举报去重流程插画,不是应用截图。同一个 targetReportId 再次提交时,当前单机实现返回已有案件,而不是继续生成新的待处理记录。

一、重复举报为什么不是"多存一条就行"

在失物招领场景中,用户可能因为网络迟缓、按钮重复点击、返回页面后再次进入,连续提交同一条内容举报。如果每次提交都生成案件,会出现三个直接问题:

  • 消息中心同时显示多条相同进度;
  • 治理人员可能对同一内容做出不同结论;
  • 一条记录隐藏后,其他案件仍停留在"处理中";
  • 案件统计把重复操作误算成多起问题;
  • 用户不知道应该跟踪哪个案件编号。

因此举报提交不是简单的 INSERT。在创建新案件之前,必须先回答"这条目标记录是否已有可复用案件"。

二、本文对应的真实文件

本章结论来自以下项目文件,而不是抽象的后台系统设想:

层级 文件 责任
ArkUI 页面 entry/src/main/ets/pages/ModerationReportPage.ets 选择原因、输入说明、展示提交状态
Service shared-business/src/main/ets/services/ModerationService.ets 校验、查重、创建案件、返回业务结果
Repository shared-business/src/main/ets/repositories/ModerationRepository.ets RelationalStore 与本地回退数据读写
Model common-core/src/main/ets/models/AppContracts.ets 原因、状态、动作和案件模型
测试 scripts/local-state-machines.test.mjs 验证重复提交返回同一个案件 ID

页面只维护输入草稿态。是否允许举报、是否重复以及返回哪个案件,统一由 Service 和 Repository 决定。

三、先固定五类举报原因

ModerationService 当前提供五个枚举值:

枚举 页面文案 主要用途
PRIVACY 泄露隐私 公开内容包含联系方式、精确地址等
FALSE_INFO 虚假信息 内容明显失实或误导
ILLEGAL 违法违规 涉及违法违规信息
INAPPROPRIATE_MEDIA 图片或内容不适 媒体或描述不适合展示
OTHER 其他问题 无法归入前四类的线索

固定枚举比自由填写原因更容易统计、筛选和制定处理规则。补充说明仍可保留细节,但它不能替代结构化原因。

四、页面禁用按钮不能代替 Service 校验

ModerationReportPage 会显示加载、提交中和错误状态,但真正的业务门禁在 submit() 中执行。即使未来从深链、消息页或其他入口发起请求,也必须经过相同校验。

ts 复制代码
const result: OperationResult<ModerationCase> =
  await moderationService.submit(
    this.reportId,
    this.selectedReason,
    this.note
  );

页面成功后跳转举报进度页;失败时展示 result.userMessage。它不会自行创建案件 ID,也不会直接操作数据库。

五、补充说明为什么限制为 200 字

当前 Service 对说明执行 trim(),并拒绝超过 200 字的内容。举报表单的目的不是开放第二个评论区,而是补充判断所需的最小事实。

过长文本会增加三个风险:用户粘贴整段聊天或个人信息、消息卡片难以摘要、未来服务端审核和日志存储成本上升。200 字是当前产品约束,不是 HarmonyOS 平台限制;正式上线后仍应根据真实治理流程调整。

六、敏感信息要在进入 Repository 前拦截

当前实现阻止手机号、身份证和"密码"等典型敏感内容:

ts 复制代码
private containsSensitiveContent(value: string): boolean {
  return value.includes('密码') ||
    value.includes('身份证') ||
    /1[3-9][0-9]{9}/.test(value);
}

这条规则只能覆盖典型误填,不是完整的数据防泄漏系统。用户仍可能通过空格、谐音或图片绕过。因此正式产品还需要字段最小化、图片治理、服务端规则和人工复核,不能把一条正则描述成"彻底消除隐私泄露"。

七、为什么禁止举报当前设备发布的记录

Service 在创建案件前读取目标 ItemReport,并检查 isUserCreated。当前单机演示把"当前设备创建"作为近似身份边界,自有记录应走编辑、撤回或删除,而不是进入举报流程。

这不是正式账号权限。设备标记不能证明真实作者身份,也无法阻止多设备场景下的伪造。联网版应由服务端根据登录主体和记录所有者判断,而不是信任客户端布尔值。

八、查重的核心是 targetReportId

当前 Repository 提供:

ts 复制代码
async findByReportId(reportId: string):
  Promise<ModerationCase | undefined> {
  return this.findOne('target_report_id', reportId.trim(), this.context);
}

ModerationService.submit() 在插入前调用它。查到已有案件时,直接返回成功和原案件编号:

ts 复制代码
if (existing) {
  return new OperationResult<ModerationCase>(
    true,
    `该记录已有举报,案件编号:${existing.id}`,
    existing
  );
}

对用户而言,这是幂等式回执:重复操作不会制造新案件,同时仍能进入原进度页继续查看。

九、当前实现比大纲更严格:终态案件也会被复用

一个容易被忽略的真实细节是,findByReportId() 没有限制 PENDINGIN_REVIEW。只要该报告存在任何历史案件,包括 RESOLVEDREJECTED,再次举报都会返回旧案件。

这适合"同一条不可变内容只处理一次"的单机演示,却不一定适合正式产品。如果发布者修改了内容,或者出现新的违规事实,用户可能需要重新举报。

正式版可以把唯一范围改为"目标报告版本 + 活跃状态",或者引入 contentVersion:同一版本只允许一个活跃案件,新版本允许创建新案件,同时保留历史审计。

十、当前数据库没有 target_report_id 唯一索引

moderation_case 表只把 id 声明为主键,target_report_id 是普通非空列。这意味着"先查询、再插入"在单线程顺序执行中有效,但不是数据库级唯一保证。

两个并发请求可能同时查到不存在,然后分别插入不同的 CASE-*。本地时间戳 ID 也不是跨设备全局一致的业务约束。

正式服务端至少应选择一种方案:

  • 对活跃案件建立条件唯一索引;
  • 在事务内执行查重和插入;
  • 使用 reportId + contentVersion 作为幂等键;
  • 接口接收客户端操作 ID,并返回已创建结果;
  • 对冲突错误转换为"返回已有案件",而不是通用失败。

上图对比两种保护层级:上方应用层先查后写仍可能被并发穿透;下方把事务和唯一约束放入权威数据库,冲突请求只返回已有案件,不再产生第二条记录。

十一、举报 ID 为什么不能由页面生成

当前 Service 使用 CASE-${Date.now()} 创建单机案件编号。页面只接收保存后的 saved.id 并跳转。

如果页面先生成 ID,再分别写列表、消息和详情,重试时很容易产生多个编号。正式联网版应由服务端或统一 ID 服务生成稳定标识,并把幂等结果作为响应返回。

十二、重复提交应该返回成功还是失败

这里选择返回 success=true,因为用户的目标"确保这条记录已进入举报流程"已经满足。提示语明确告诉用户已有案件,而不是伪装成本次新建成功。

如果返回通用失败,用户可能继续点击;如果新建第二条,又会污染治理队列。幂等成功与重复创建不同:前者复用同一个资源,后者制造额外副作用。

十三、消息中心如何消费复用结果

提交成功后页面进入 ModerationProgressPage,消息中心通过 moderationService.listCases() 读取案件。因为重复操作没有新增记录,消息页也只显示一条治理状态卡片。

卡片使用案件标题、短编号、当前状态和 outcomeSummary,并提供"查看进度"按钮。它展示业务状态,不提供自由输入,因此不会把重复举报演变成重复对话线程。

十四、本地测试如何证明去重

项目测试按以下顺序执行:

  1. 插入目标拾得记录;
  2. PRIVACY 提交首次举报;
  3. 断言状态为 PENDING
  4. 再以 OTHER 提交重复举报;
  5. 断言第二次仍返回成功;
  6. 断言第二次返回的案件 ID 等于首次 ID;
  7. 继续验证越序隐藏被拒绝。
powershell 复制代码
powershell -ExecutionPolicy Bypass -File .\scripts\test-local-state-machines.ps1

2026-08-23 本地实际运行通过。这个结果证明生产 Service 的本地回退路径遵守规则,不等于数据库并发唯一索引或多设备重复请求已经验证。

十五、异常回执要区分四类原因

用户看到的失败至少应区分:

场景 当前回执
原因非法 请选择举报原因
说明过长或含敏感信息 指向具体字段
记录不存在或属于当前设备 说明目标不可举报
Repository 异常 举报提交失败,请稍后重试

把所有问题都写成"提交失败"会诱发重复操作,也不利于定位是输入、权限、目标状态还是存储故障。

十六、正式版还要处理报告被编辑和删除

举报提交后,目标报告可能被编辑、撤回或删除。案件不能只保存标题,否则治理人员无法知道当时举报的是哪个版本。

更完整的案件应保存内容版本、必要的脱敏快照、举报时状态和证据摘要。快照必须控制最小字段并设置保留期限,不能因为审计需要就永久复制全部个人信息。

十七、工程验收清单

上线前可按以下不变量验收:

  • 同一目标版本最多一个活跃案件;
  • 重复请求返回相同案件 ID;
  • 请求重试不会增加消息卡片数量;
  • 终态案件是否允许重开有明确产品规则;
  • 自有记录由服务端身份判断;
  • 敏感说明不会写入日志;
  • 并发冲突由唯一约束或事务处理;
  • 被编辑或删除后仍保留可解释的最小审计证据;
  • 返回旧案件时明确告诉用户"已有举报"。

十八、工程复盘:查重规则必须包含时间维度

"按 reportId 查重"只是第一版答案。真正稳定的唯一语义通常是"目标内容的某个版本,在某个活跃窗口内,只允许一个治理案件"。缺少内容版本,历史驳回会阻塞对新内容的举报;缺少活跃状态,终态案件无法按规则重开;缺少数据库约束,并发仍能绕过 Service 查询。

因此实现前应先写唯一性表:唯一键由哪些字段组成,哪些状态算活跃,内容修改是否递增版本,案件关闭后多久允许新建,重复请求返回哪条记录。把这些问题留给页面按钮处理,会让多入口和多设备行为不一致。

十九、本文小结

举报去重的目标不是少存几行数据,而是让同一治理事实只有一个权威进度。当前"寻迹校园"已经在 Service 层校验原因、长度、敏感内容、自有记录,并在插入前按 targetReportId 复用已有案件;本地测试证明重复提交返回相同 ID。

但当前表结构没有目标唯一索引,查询也会复用终态案件。数据库并发、内容版本、真实身份、跨设备幂等和服务端审计仍未实现,不能从单机顺序测试外推。

系列导航:第 31 篇 / 共 50 篇。上一篇:《双方确认后才能结案》;下一篇:《受理、隐藏与驳回的强制状态顺序》。

相关推荐
贾伟康2 小时前
【中国方言题库|11】HarmonyOS ArkTS 学习统计实战:计算地区学习进度与收藏数量
harmonyos·arkts·arkui·数据统计·多设备适配
用户1269550879142 小时前
RK3588 + OpenHarmony 6.1 RKNN2 NPU 验证指南
harmonyos
m0_749690233 小时前
【寻迹校园 HarmonyOS NEXT 实战 35】先写全页面 Design Spec 再写 ArkUI:一个比赛项目的设计稿门禁实践
harmonyos·响应式设计·设计规范·arkui·ui设计
Magic-ZYJ3 小时前
HarmonyOS 日记类 App 的日期设计:本地自然日、月历与夏令时边界
华为·harmonyos·arkts·arkui·问题排查·移动端开发·独立开发者
贾伟康3 小时前
【中国方言题库|12】HarmonyOS ArkTS 题库列表组件实战:减少多地区页面重复并保证点击反馈
harmonyos·arkts·arkui·组件化·多设备适配
梦想不只是梦与想4 小时前
鸿蒙 AGC:华为开放能力管理(四)
harmonyos·agc·开发能力
大锅盖14 小时前
ArkUI声明式范式下的暗夜紫调沉浸式剧本杀组局社区:迷雾粒子双层特效与四套差异化弹框的工程化实践
华为·harmonyos
见山是山-见水是水13 小时前
鸿蒙Divider 分割线组件完全指南:内容分组、视觉分区与自定义样式
华为·harmonyos