更新一下

This commit is contained in:
2026-07-21 15:26:07 +09:00
parent 2823ff13bf
commit 4902a02c87
32 changed files with 4138 additions and 294 deletions

View File

@@ -0,0 +1,201 @@
# 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 密钥或其他凭据,交付文档也不得复制这些值。