Files
junhong_cmp_fiber/.scratch/ur60-shop-phone-search/PRD.md
2026-07-21 15:26:07 +09:00

18 KiB
Raw Blame History

PRDUR#60 店铺联系电话精确查询

Status: ready-for-agent


Problem Statement

后台店铺列表目前不能按店铺联系电话检索。运营人员知道完整联系电话时,仍需通过店铺名称、编号或翻页人工定位,效率低且容易选错店铺。

本次核查还确认了三个与该查询直接相关、必须在同一需求闭环的问题:

  1. 店铺列表请求虽然声明了分页及筛选校验规则,但 Handler 当前没有执行整体验证,非法查询参数可能进入查询层。
  2. 未传 pagepage_size 时,数据库实际按第 1 页、每页 20 条查询,但响应元数据仍可能返回 page=0size=0
  3. 企业账号当前可能进入核心店铺管理路由;由于其没有代理店铺范围,列表查询可能失去预期的数据隔离。企业账号不应访问核心店铺管理功能。

目标是在不扩大店铺模块架构迁移范围、不改变现有响应结构的前提下,提供可验证、权限安全、性能可接受的 11 位联系电话精确查询,并完成前后端交付和文档闭环。

Solution

在现有店铺列表接口 GET /api/admin/shops 增加可选查询参数 contact_phone。非空值必须由 11 个 ASCII 数字组成,查询使用等值匹配;它与店铺名称、店铺编号、上级店铺、层级、状态等已有筛选条件按 AND 组合。空值等同未传,不改变原查询结果。

同时完成以下配套改动:

  1. 对店铺列表请求执行完整 DTO 校验,并通过统一错误响应隐藏具体校验细节。
  2. 在请求进入查询前统一归一化分页默认值,使查询条件与响应中的分页元数据一致。
  3. 在核心店铺管理路由增加企业账号访问限制,同时保留超级管理员、平台账号和代理账号原有权限及代理数据范围。
  4. 为未软删除店铺的联系电话建立非唯一部分 B-tree 索引,支持重复联系电话并降低精确查询成本。
  5. 前端店铺列表增加联系电话筛选、校验、查询和清空能力,并沿用列表已有的加载、空态和失败反馈。
  6. 使用真实开发 PostgreSQL、Redis、JWT 配置完成 HTTP 集成验证,更新 OpenAPI 和需求总结文档后再进入联调与人工验收。

User Stories

  1. 作为有权限的后台运营人员,我希望输入完整的 11 位店铺联系电话并精确查找店铺,以便快速定位目标记录。
  2. 作为有权限的后台运营人员,我希望联系电话筛选与名称、编号、上级店铺、层级、状态等筛选条件同时生效,以便逐步收窄结果,而不是扩大结果集合。
  3. 作为有权限的后台运营人员,当多个可见店铺使用同一联系电话时,我希望看到全部符合其他筛选条件的记录,而不是只返回一条。
  4. 作为有权限的后台运营人员,当联系电话没有匹配记录时,我希望得到正常的空分页结果,而不是“资源不存在”错误。
  5. 作为有权限的后台运营人员,当我不填写联系电话时,我希望列表行为与改造前一致。
  6. 作为有权限的后台运营人员,我希望前端在联系电话不是 11 位 ASCII 数字时直接提示并阻止请求,以便及时修正输入。
  7. 作为 API 调用方,当我传入非法联系电话时,我希望后端仍独立拒绝请求,而不依赖前端校验保障数据安全。
  8. 作为 API 调用方,当我传入空联系电话时,我希望后端将其视为未提供该条件,不影响已有筛选。
  9. 作为 API 调用方,当我不传分页参数时,我希望查询和响应都明确使用第 1 页、每页 20 条。
  10. 作为 API 调用方,当我显式传入合法分页参数时,我希望后端严格使用该页码和每页数量,不自行重置页码。
  11. 作为 API 调用方,当店铺列表的任何已声明查询参数非法时,我希望收到统一的参数验证失败响应,而不是执行部分或错误查询。
  12. 作为代理账号,我希望搜索结果始终只包含本店铺及下级店铺范围内的数据,不能通过联系电话探测其他代理的数据。
  13. 作为超级管理员或平台账号,我希望在原有全局可见范围内使用联系电话筛选。
  14. 作为企业账号,我不应访问核心店铺管理接口,并应收到明确且一致的禁止访问响应。
  15. 作为维护人员,我希望联系电话查询使用适合软删除模型的数据库索引,同时允许多个店铺保存相同联系电话。
  16. 作为维护人员,我希望参数错误、权限错误和数据库错误都使用现有统一响应协议,且服务端错误不会把数据库细节暴露给客户端。
  17. 作为前端实现人员,我希望获得稳定的参数、响应和错误契约,以便无需猜测后端的分页、筛选或权限行为。
  18. 作为验收人员,我希望通过真实 PostgreSQL、Redis 和认证链路验证该接口,以确认权限过滤和实际 SQL 行为,而不只是验证模拟对象。

Implementation Decisions

1. 接口及筛选契约

  • 复用现有 GET /api/admin/shops,不新增 Handler 或新接口。
  • 新增可选字符串查询参数 contact_phone,含义为“店铺联系电话精确查询”。
  • 非空 contact_phone 必须完整匹配 ^[0-9]{11}$:恰好 11 个 ASCII 数字。
  • 不接受前后空格、全角数字、+86、连字符或其他格式;后端不做 trim、格式修复或号码归一化。
  • 不校验中国大陆手机号号段或首位规则,只校验 11 位 ASCII 数字。
  • 参数未传或值为空字符串时视为没有联系电话筛选,不影响原查询。
  • 联系电话使用数据库等值条件,不使用 LIKE、包含、前缀或后缀匹配。
  • contact_phone 与所有已提供的已有筛选参数按 AND 组合;不是 OR 查询。
  • 保留已有筛选语义:店铺名称模糊匹配;店铺编号精确匹配;上级店铺、层级、状态精确匹配。
  • 允许联系电话重复;相同电话的所有可见匹配店铺均进入结果,再统一分页。
  • 没有匹配项时返回成功空分页:items 为空数组、total=0,不返回 404。
  • 结果继续按 created_at DESC 排序;本需求不增加次级排序规则。
  • 继续排除已软删除店铺,不改变列表项字段和现有分页响应结构。

2. 请求校验与分页

  • 店铺列表 Handler 在完成查询参数解析后,对整个列表请求 DTO 执行 Validator 校验,而不是仅单独校验联系电话。
  • 完整校验规则为:page 可省略,提供时不小于 1page_size 可省略,提供时为 1 至 100shop_name 最长 100shop_code 最长 50parent_id 提供时不小于 1level 提供时为 1 至 7status 提供时只能为 0 或 1contact_phone 非空时满足 11 位 ASCII 数字规则。
  • 参数解析失败或任一字段验证失败时,记录包含请求上下文和具体校验原因的服务端日志;客户端统一收到 HTTP 400、code=1001msg=参数验证失败data=null 和时间戳,不返回 Validator 或底层解析错误文本。
  • 未提供 page 时,在查询前归一化为 1未提供 page_size 时,在查询前归一化为 20。
  • 查询实际使用的页码、每页数量必须与响应 data.pagedata.size 完全一致。
  • 显式提供的合法 pagepage_size 原样生效;后端绝不因新增或改变筛选条件而自行把页码重置为 1。
  • 成功响应继续使用现有统一结构HTTP 200、code=0msg=successdata 包含 itemstotalpagesize,并返回时间戳。

3. 权限边界

  • 身份认证仍由现有后台认证中间件负责;本需求不创建新的认证机制。
  • 核心店铺管理路由仅指店铺列表、创建、更新、删除和联级查询。
  • 超级管理员和平台账号保持当前核心店铺管理访问能力;店铺列表可查看其原有范围内的数据。
  • 代理账号保持当前访问能力,列表继续应用“本店铺及全部下级店铺”数据范围;联系电话和其他筛选只能在该范围内生效。
  • 企业账号访问任一核心店铺管理路由时,统一返回 HTTP 403、code=1005msg=无权限访问店铺管理功能data=null 和时间戳。
  • 企业账号拦截必须只挂在核心店铺管理路由组,不得因复用 /shops 前缀而误伤店铺角色接口、代理商资金概况、提现、佣金、钱包流水等独立路由。
  • 本需求不重新定义这些独立路由各自已有的权限校验。

4. 查询实现与架构边界

  • 这是现有列表的简单只读筛选,沿用当前 Handler → Service → Store → GORM/DTO 调用链。
  • 不为该筛选迁移整个店铺模块不新建聚合根、Repository 抽象、Query 模块、工厂或其他无业务价值的层次。
  • Service 将非空联系电话加入筛选集合Store 在同一 GORM 查询上追加等值条件。所有条件必须共同作用于计数查询和分页数据查询。
  • 继续复用现有店铺数据权限过滤;不得在增加联系电话条件时绕过、覆盖或改写权限范围。
  • 列表读取不新增缓存,不使用 Redis 缓存查询结果。Redis 仅按现有认证及代理范围链路参与测试和运行。
  • 数据库查询错误转换为现有内部错误码并保留服务端上下文;客户端收到 HTTP 500、code=2001msg=内部服务器错误data=null,不得泄露 SQL、主机、库名或驱动错误。
  • 这是只读操作,不新增幂等键、分布式锁、审计业务日志或异步任务;现有 HTTP 访问日志继续记录请求。

5. 数据库索引与迁移

  • 新增名为 idx_shop_contact_phone 的非唯一 B-tree 索引,索引列为店铺表的 contact_phone
  • 索引仅覆盖 deleted_at IS NULL 的记录,与列表默认排除软删除记录的查询条件一致。
  • 索引不得设为唯一;业务允许不同店铺使用同一联系电话。
  • 不新增字段,不修改字段类型,不回填、清洗或删除历史联系电话数据。
  • 历史记录中不符合 11 位规则的值继续保留,只是无法被合法的本接口联系电话参数精确命中。
  • 迁移回滚只删除该索引,不变更任何业务数据。
  • 迁移随七月迭代维护窗口发布;上线和回滚均需核验索引状态。

6. 前端交付契约

  • 后台店铺列表筛选区新增“联系电话”输入框,并提供现有风格的查询和清空操作。
  • 前端仅允许提交 11 位 ASCII 数字;非法值不发起请求,并展示清晰的中文校验提示。
  • 有值时以 contact_phone 查询参数提交;空值时不提交该参数。
  • 联系电话与页面当前已有筛选参数一并提交,后端按 AND 查询。
  • 清空联系电话后恢复为不含该条件的列表查询,同时保留产品现有的其他筛选交互规则。
  • 前端如何维护或改变页码属于其现有页面状态策略;后端契约只要求尊重实际收到的 page,本需求不新增“改变筛选必须重置页码”的规则。
  • 查询期间展示加载状态;成功无数据展示空态;请求失败展示可重试的错误反馈,不把旧结果伪装成新查询结果。
  • 当前后端仓库不包含前端源码;后端接口和 OpenAPI 就绪后按项目既有交付方式通知前端仓库实施并联调。

7. 文档与发布闭环

  • 更新店铺列表请求的 OpenAPI 定义,准确描述联系电话格式、精确匹配、分页限制和现有筛选字段;重新生成接口文档。
  • 因为复用现有 Handler无需向文档生成器新增 Handler 实例,但必须确认生成结果中该参数真实可见。
  • 实施阶段新增 UR#60 中文总结文档,并更新项目 README 的相关索引或说明。
  • 后端代码、迁移、自动化测试、OpenAPI 和总结文档完成后才可交给前端联调。
  • 前后端联调和人工验收通过后,才可在禅道中更新本需求状态;自动化测试通过不等同于整项需求完成。

Testing Decisions

1. 最高价值测试接缝

  • 主要验收采用 HTTP 集成测试:测试进程内启动 Fiber App通过真实路由进入认证中间件、权限中间件、Handler、Service、Store、GORM 和统一响应处理。
  • 测试加载项目 .env.local 中的真实开发 PostgreSQL、Redis 和 JWT 配置;不把数据库或 Redis 替换为内存实现或 Mock。
  • Fiber App 在测试进程内启动,不要求开发者事先在 127.0.0.1:3000 运行独立 API 进程;:3000 只是正常开发启动时的 API 监听地址,与远程 PostgreSQL、Redis 地址无关。
  • 认证用例使用真实 JWT 配置和 Redis Token 状态穿过现有认证中间件,不能通过直接向 Fiber Context 填充用户信息绕过认证。
  • 测试数据必须使用唯一标识创建并严格清理;数据库测试记录和 Redis Token/范围缓存均不得污染共享开发环境,也不得依赖环境中偶然存在的业务数据。
  • 测试不接入网关、对象存储、短信等第三方系统,因为本需求链路不需要它们。

2. 必须覆盖的后端用例

  1. 合法 11 位联系电话只返回联系电话完全相等且位于当前权限范围内的店铺。
  2. 前缀、后缀和包含关系均不匹配,证明查询不是模糊查询。
  3. 两个可见店铺联系电话相同均被返回,证明索引和查询没有引入唯一性假设。
  4. 电话联系条件与店铺名称、店铺编号、上级店铺、层级、状态分别及组合使用时均为 ANDtotalitems 使用相同条件。
  5. 未传和传空 contact_phone 均保持原列表语义。
  6. 空格、全角数字、+86、连字符、少于 11 位、超过 11 位、含字母等非空值均返回 HTTP 400 / code=1001,且没有执行店铺查询。
  7. page=0page_size=0page_size>100、非法层级、非法状态、超长名称或编号等已有字段违反 DTO 约束时,也返回统一参数错误。
  8. 未传分页参数时,实际查询第 1 页每页 20 条,响应同步为 page=1size=20
  9. 显式合法分页参数保持不变;后端不会因存在联系电话筛选而重置页码。
  10. 无匹配数据时返回 HTTP 200、空 itemstotal=0 和正确分页元数据。
  11. 返回顺序保持 created_at DESC,软删除店铺不出现在结果中。
  12. 超级管理员和平台账号能在原有范围内查询;代理账号只能看到本店铺及下级店铺,即使范围外店铺使用相同电话也不可见。
  13. 企业账号访问列表返回 HTTP 403 / code=1005 / msg=无权限访问店铺管理功能
  14. 企业账号访问创建、更新、删除、联级查询同样被核心路由组拒绝;独立的店铺角色及资金/佣金路由不因本次路由分组而被误拦截。
  15. 未认证、Token 无效或 Redis Token 状态无效时继续遵循现有认证错误契约。
  16. 数据库查询失败时返回脱敏的 HTTP 500 / code=2001,响应不出现底层错误文本。
  17. 成功和失败响应均保持统一的 codemsgdatatimestamp 外层结构。

3. 迁移、文档与前端验证

  • 在可控开发环境验证迁移向上执行后存在 idx_shop_contact_phone,它是非唯一、仅含 contact_phone 且带 deleted_at IS NULL 条件的 B-tree 索引。
  • 验证迁移回滚只删除该索引,随后再次向上迁移恢复索引;全程不改变店铺业务数据。
  • 运行目标包测试和全仓 Go 测试,执行格式化、静态检查及构建;任何失败必须在交付前处理。
  • 重新生成 OpenAPI并核验店铺列表中出现 contact_phone 及正确约束,且生成结果无无关漂移。
  • 前端验证合法输入发起正确请求、非法输入不发请求、清空后不携带联系电话、组合筛选参数完整、加载/空态/失败反馈正确。
  • 联调以浏览器网络请求和真实接口响应共同核对;不得只凭页面展示判断 AND 条件、分页或权限是否正确。

4. 完成标准

  • 后端实现、迁移、HTTP 集成测试、全仓回归、OpenAPI、中文总结文档和 README 更新全部完成。
  • 前端实现与联调完成,超级管理员/平台/代理/企业四类账号的关键行为通过人工验收。
  • 精确查询、AND 组合、默认分页、重复电话、空结果、非法参数、代理隔离和企业 403 均有可复现证据。
  • 维护窗口的发布检查和索引回滚路径已演练或核验。
  • 人工验收通过并更新禅道后UR#60 才能标记完成。

Out of Scope

  • 不新增独立的联系电话搜索接口。
  • 不做模糊、前缀、后缀、分词或跨字段 OR 搜索。
  • 不验证运营商号段、号码真实性、号码归属地,也不规范化 +86、空格、连字符或全角字符。
  • 不修改店铺创建、更新接口的联系电话校验规则;这两个接口的历史验证差异不在 UR#60 内处理。
  • 不清洗、回填或删除历史联系电话数据。
  • 不禁止多个店铺使用相同联系电话,不建立唯一约束。
  • 不改变已有列表项字段、排序、软删除语义或响应外层结构。
  • 不调整前端自身的页码状态策略;后端只按收到的参数查询。
  • 不重构整个店铺模块,不迁移未触碰用例到 DDD 或新的 Query 目录。
  • 不为只读列表增加 Redis 结果缓存、审计业务日志、幂等控制或异步任务。
  • 不借本次核心路由权限修复重新设计店铺角色、资金、佣金、提现、钱包流水等独立接口的授权规则。
  • 不接入或改造网关、对象存储、短信等第三方系统。
  • 不在本 Spec 阶段实施代码、生成整轮七月迭代 PRD、拆分 tickets 或创建 OpenSpec 产物。

Further Notes

  • 本 Spec 仅覆盖七月迭代中的 UR#60“店铺联系电话精确查询”不代表整轮七月迭代范围。
  • 需求事实以当前会话确认、七月迭代标准评审稿、当前独立方案和对应禅道草稿为顺序收敛;当前代码和开发环境仅用于核实系统现状。
  • 当前代码已证明列表真实查询默认值与响应分页元数据存在偏差,也证明企业账号可能因无店铺范围而获得过宽列表结果,因此两项修复属于本需求的验收边界,不是额外优化。
  • 本需求没有引入新的领域术语、业务状态或跨模块长期架构决策,因此无需修改领域词汇表或新增 ADR。
  • 本地开发 PostgreSQL 和 Redis 已验证网络可达,但 API 进程是否已独立启动不影响测试设计;集成测试应自行构建并启动进程内 Fiber App。
  • .env.local 含敏感配置实施和测试日志不得打印连接密码、JWT 密钥或其他凭据,交付文档也不得复制这些值。