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

202 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PRDUR#60 店铺联系电话精确查询
Status: ready-for-agent
---
## Problem Statement
后台店铺列表目前不能按店铺联系电话检索。运营人员知道完整联系电话时,仍需通过店铺名称、编号或翻页人工定位,效率低且容易选错店铺。
本次核查还确认了三个与该查询直接相关、必须在同一需求闭环的问题:
1. 店铺列表请求虽然声明了分页及筛选校验规则,但 Handler 当前没有执行整体验证,非法查询参数可能进入查询层。
2. 未传 `page``page_size` 时,数据库实际按第 1 页、每页 20 条查询,但响应元数据仍可能返回 `page=0``size=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` 可省略,提供时不小于 1`page_size` 可省略,提供时为 1 至 100`shop_name` 最长 100`shop_code` 最长 50`parent_id` 提供时不小于 1`level` 提供时为 1 至 7`status` 提供时只能为 0 或 1`contact_phone` 非空时满足 11 位 ASCII 数字规则。
- 参数解析失败或任一字段验证失败时,记录包含请求上下文和具体校验原因的服务端日志;客户端统一收到 HTTP 400、`code=1001``msg=参数验证失败``data=null` 和时间戳,不返回 Validator 或底层解析错误文本。
- 未提供 `page` 时,在查询前归一化为 1未提供 `page_size` 时,在查询前归一化为 20。
- 查询实际使用的页码、每页数量必须与响应 `data.page``data.size` 完全一致。
- 显式提供的合法 `page``page_size` 原样生效;后端绝不因新增或改变筛选条件而自行把页码重置为 1。
- 成功响应继续使用现有统一结构HTTP 200、`code=0``msg=success``data` 包含 `items``total``page``size`,并返回时间戳。
### 3. 权限边界
- 身份认证仍由现有后台认证中间件负责;本需求不创建新的认证机制。
- 核心店铺管理路由仅指店铺列表、创建、更新、删除和联级查询。
- 超级管理员和平台账号保持当前核心店铺管理访问能力;店铺列表可查看其原有范围内的数据。
- 代理账号保持当前访问能力,列表继续应用“本店铺及全部下级店铺”数据范围;联系电话和其他筛选只能在该范围内生效。
- 企业账号访问任一核心店铺管理路由时,统一返回 HTTP 403、`code=1005``msg=无权限访问店铺管理功能``data=null` 和时间戳。
- 企业账号拦截必须只挂在核心店铺管理路由组,不得因复用 `/shops` 前缀而误伤店铺角色接口、代理商资金概况、提现、佣金、钱包流水等独立路由。
- 本需求不重新定义这些独立路由各自已有的权限校验。
### 4. 查询实现与架构边界
- 这是现有列表的简单只读筛选,沿用当前 `Handler → Service → Store → GORM/DTO` 调用链。
- 不为该筛选迁移整个店铺模块不新建聚合根、Repository 抽象、Query 模块、工厂或其他无业务价值的层次。
- Service 将非空联系电话加入筛选集合Store 在同一 GORM 查询上追加等值条件。所有条件必须共同作用于计数查询和分页数据查询。
- 继续复用现有店铺数据权限过滤;不得在增加联系电话条件时绕过、覆盖或改写权限范围。
- 列表读取不新增缓存,不使用 Redis 缓存查询结果。Redis 仅按现有认证及代理范围链路参与测试和运行。
- 数据库查询错误转换为现有内部错误码并保留服务端上下文;客户端收到 HTTP 500、`code=2001``msg=内部服务器错误``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. 电话联系条件与店铺名称、店铺编号、上级店铺、层级、状态分别及组合使用时均为 AND`total``items` 使用相同条件。
5. 未传和传空 `contact_phone` 均保持原列表语义。
6. 空格、全角数字、`+86`、连字符、少于 11 位、超过 11 位、含字母等非空值均返回 HTTP 400 / `code=1001`,且没有执行店铺查询。
7. `page=0``page_size=0``page_size>100`、非法层级、非法状态、超长名称或编号等已有字段违反 DTO 约束时,也返回统一参数错误。
8. 未传分页参数时,实际查询第 1 页每页 20 条,响应同步为 `page=1``size=20`
9. 显式合法分页参数保持不变;后端不会因存在联系电话筛选而重置页码。
10. 无匹配数据时返回 HTTP 200、空 `items``total=0` 和正确分页元数据。
11. 返回顺序保持 `created_at DESC`,软删除店铺不出现在结果中。
12. 超级管理员和平台账号能在原有范围内查询;代理账号只能看到本店铺及下级店铺,即使范围外店铺使用相同电话也不可见。
13. 企业账号访问列表返回 HTTP 403 / `code=1005` / `msg=无权限访问店铺管理功能`
14. 企业账号访问创建、更新、删除、联级查询同样被核心路由组拒绝;独立的店铺角色及资金/佣金路由不因本次路由分组而被误拦截。
15. 未认证、Token 无效或 Redis Token 状态无效时继续遵循现有认证错误契约。
16. 数据库查询失败时返回脱敏的 HTTP 500 / `code=2001`,响应不出现底层错误文本。
17. 成功和失败响应均保持统一的 `code``msg``data``timestamp` 外层结构。
### 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 密钥或其他凭据,交付文档也不得复制这些值。