8月5号新增筛选条件
商户预警筛选功能设计一个「看起来简单」的筛选功能背后隐藏了几个值得深挖的架构判断。一、需求回顾这个功能的起点是一句话连续3天未交易的通过审核商户记录下来方便查哪天有哪些人不交易。随着需求的展开它演变成了一个带有多维筛选的预警系统按公司信息搜索公司名/法人/注册号按审核状态过滤已通过/待审核/已驳回按指定上级手机号查其所有下级的流失数据每个筛选维度背后都有一个值得说说的架构决策。二、筛选维度一公司信息搜索数据来源公司信息法人、注册号、公司名本属于 cn_user_company但我们选择了反规范化——在每日快照写入时把这些字段冗余到 cn_user_churn_warning_record。为什么不联表查最直接的方案是查询时 JOIN cn_user_company但我没有这么做。原因历史快照语义快照记录的是「统计那一天用户的状态」。如果用户之后修改了公司信息JOIN 拿到的是现在的公司信息而不是快照时间点的。冗余字段保证了历史快照的完整性。查询性能cn_user_company 有近万行高频查询时 JOIN 每次都要关联冗余后直接走快照表索引。筛选简洁搜索直接 LIKE business_name 或 LIKE register_number不需要跨表。代价冗余字段在公司信息变更后不自动同步但快照本来就是历史记录这个代价可以接受。三、筛选维度二审核状态过滤设计决策// 前端默认传 companyStatus 1后端 SQL 加过滤if testcompanyStatus ! nulland r.company_status #{companyStatus}/if默认值的选择是这里最重要的决策默认 status1已通过而不是「全部」。理由运营看这个页面的目的是「找需要跟进的活跃商户」未审核或被驳回的商户在这个场景下几乎没有运营价值。所以默认过滤掉降低认知负担。但允许选「全部/待审核/已驳回」给运营留了灵活空间——比如想看「即使是待审核用户也3天没交易」的情况。四、筛选维度三指定上级手机号这是最有技术含量的一个维度也是我做过几次方案选择的地方。数据结构系统的邀请关系通过两个字段记录inviter_id: 直接上级inviter_path: /10/25/30/ 完整邀请链路径inviter_path 是一种树形结构的路径压缩存储用 LIKE 可以高效查任意层级的下级-- 查 user_id25 的所有下级直接间接WHERE inviter_path LIKE %/25/%方案选择Java层 vs SQL层我考虑了两种实现方式方案ASQL子查询AND w.user_id IN (SELECT user_id FROM cn_user WHERE inviter_path LIKE %/X/%)方案BJava层处理// 先查下级列表ListLong subordinates cnUserMapper.getInviterIds2(targetUserId);// 再传给Mapper做IN过滤query.setUserIdList(subordinates);选择方案B理由报错友好手机号不存在时可以提前返回「该用户不存在」而不是静默返回空数据代码清晰查下级和过滤预警是两个独立逻辑放在一起的子查询难以维护符合项目风格getInviterIds2 在项目中已有多处复用不新造轮子扩展灵活下级 ID 列表可以在 Service 层做更多处理如限量、排重但方案B有个隐患下级列表可能非常大间接下级可能数千甚至上万。IN 列表过长会导致 SQL 性能下降。当前数据规模下没有问题但上规模后需要考虑分页或改用关联查询。五、一个踩坑记录XML 不转义在实现「下级过滤」时我在 CnUserMapper.xml 里写了and u.user_id #{getUserId} 在 XML 里是标签起始符必须写成 lt;。这导致 MyBatis 解析 XML 失败整个应用启动退出码1。正确写法and u.user_id lt;gt; #{getUserId}这是 XML 开发中最经典的低级错误之一。MyBatis Mapper 文件是 XML 格式SQL 里涉及比较运算符时必须转义原符号 XML 转义 lt; gt;严格来说 不强制但建议转义 lt;gt; lt;更好的防护方式是用 ![CDATA[...]] 包裹含有特殊字符的 SQL 片段![CDATA[ and u.user_id #{getUserId} ]]CDATA 块内的内容不会被 XML 解析器处理可以写原始 SQL 字符更直观。六、数据快照的语义边界筛选功能的背后其实是「快照表」和「实时联表查询」之间的权衡。快照表的核心价值是历史可追溯 查询性能但它有一个固有的语义问题快照是某一时刻的状态不随时间自动更新。这对筛选有直接影响筛选字段 数据时效 可能的语义问题business_name 快照时间点 用户改了公司名后快照里是旧名字legal_person 快照时间点 同上company_status 快照时间点 状态变更后快照不反映last_trade_time 快照时间点 每天更新较准确当前处理方式每次手动统计或定时任务跑时ON DUPLICATE KEY UPDATE 会刷新当天快照公司信息随之更新。所以实际上每天的快照是当天最新状态只有历史快照如昨天的才可能有偏差。对于运营来说看最新一次快照默认行为时数据是准确的。七、架构总结这个筛选功能的设计体现了几条可以复用的原则原则 体现反规范化换查询效率 冗余公司字段进快照表避免 JOIN默认值决定用户心智 默认 status1降低认知负担Java层做报错SQL层做过滤 下级查询在 Service 层控制边界SQL 层做 IN 过滤XML 特殊字符必须转义 → lt;gt;或用 CDATA 块快照语义需明确边界 快照是时间点状态每日更新保证最新快照准确性筛选功能不难但把它做对——既高效又语义清晰——需要在每个维度上都做出合理的判断。这大概是架构工作的本质不是找最复杂的解而是找在当前约束下最合适的解。先读一下现有的 Mapper 和 Service 代码再写这篇分析。先读一下现有的 Mapper 和 Service 代码再写这篇分析。智能体读取商户预警相关代码思考▼