18 KiB
18 KiB
PRD:UR#60 店铺联系电话精确查询
Status: ready-for-agent
Problem Statement
后台店铺列表目前不能按店铺联系电话检索。运营人员知道完整联系电话时,仍需通过店铺名称、编号或翻页人工定位,效率低且容易选错店铺。
本次核查还确认了三个与该查询直接相关、必须在同一需求闭环的问题:
- 店铺列表请求虽然声明了分页及筛选校验规则,但 Handler 当前没有执行整体验证,非法查询参数可能进入查询层。
- 未传
page、page_size时,数据库实际按第 1 页、每页 20 条查询,但响应元数据仍可能返回page=0、size=0。 - 企业账号当前可能进入核心店铺管理路由;由于其没有代理店铺范围,列表查询可能失去预期的数据隔离。企业账号不应访问核心店铺管理功能。
目标是在不扩大店铺模块架构迁移范围、不改变现有响应结构的前提下,提供可验证、权限安全、性能可接受的 11 位联系电话精确查询,并完成前后端交付和文档闭环。
Solution
在现有店铺列表接口 GET /api/admin/shops 增加可选查询参数 contact_phone。非空值必须由 11 个 ASCII 数字组成,查询使用等值匹配;它与店铺名称、店铺编号、上级店铺、层级、状态等已有筛选条件按 AND 组合。空值等同未传,不改变原查询结果。
同时完成以下配套改动:
- 对店铺列表请求执行完整 DTO 校验,并通过统一错误响应隐藏具体校验细节。
- 在请求进入查询前统一归一化分页默认值,使查询条件与响应中的分页元数据一致。
- 在核心店铺管理路由增加企业账号访问限制,同时保留超级管理员、平台账号和代理账号原有权限及代理数据范围。
- 为未软删除店铺的联系电话建立非唯一部分 B-tree 索引,支持重复联系电话并降低精确查询成本。
- 前端店铺列表增加联系电话筛选、校验、查询和清空能力,并沿用列表已有的加载、空态和失败反馈。
- 使用真实开发 PostgreSQL、Redis、JWT 配置完成 HTTP 集成验证,更新 OpenAPI 和需求总结文档后再进入联调与人工验收。
User Stories
- 作为有权限的后台运营人员,我希望输入完整的 11 位店铺联系电话并精确查找店铺,以便快速定位目标记录。
- 作为有权限的后台运营人员,我希望联系电话筛选与名称、编号、上级店铺、层级、状态等筛选条件同时生效,以便逐步收窄结果,而不是扩大结果集合。
- 作为有权限的后台运营人员,当多个可见店铺使用同一联系电话时,我希望看到全部符合其他筛选条件的记录,而不是只返回一条。
- 作为有权限的后台运营人员,当联系电话没有匹配记录时,我希望得到正常的空分页结果,而不是“资源不存在”错误。
- 作为有权限的后台运营人员,当我不填写联系电话时,我希望列表行为与改造前一致。
- 作为有权限的后台运营人员,我希望前端在联系电话不是 11 位 ASCII 数字时直接提示并阻止请求,以便及时修正输入。
- 作为 API 调用方,当我传入非法联系电话时,我希望后端仍独立拒绝请求,而不依赖前端校验保障数据安全。
- 作为 API 调用方,当我传入空联系电话时,我希望后端将其视为未提供该条件,不影响已有筛选。
- 作为 API 调用方,当我不传分页参数时,我希望查询和响应都明确使用第 1 页、每页 20 条。
- 作为 API 调用方,当我显式传入合法分页参数时,我希望后端严格使用该页码和每页数量,不自行重置页码。
- 作为 API 调用方,当店铺列表的任何已声明查询参数非法时,我希望收到统一的参数验证失败响应,而不是执行部分或错误查询。
- 作为代理账号,我希望搜索结果始终只包含本店铺及下级店铺范围内的数据,不能通过联系电话探测其他代理的数据。
- 作为超级管理员或平台账号,我希望在原有全局可见范围内使用联系电话筛选。
- 作为企业账号,我不应访问核心店铺管理接口,并应收到明确且一致的禁止访问响应。
- 作为维护人员,我希望联系电话查询使用适合软删除模型的数据库索引,同时允许多个店铺保存相同联系电话。
- 作为维护人员,我希望参数错误、权限错误和数据库错误都使用现有统一响应协议,且服务端错误不会把数据库细节暴露给客户端。
- 作为前端实现人员,我希望获得稳定的参数、响应和错误契约,以便无需猜测后端的分页、筛选或权限行为。
- 作为验收人员,我希望通过真实 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. 必须覆盖的后端用例
- 合法 11 位联系电话只返回联系电话完全相等且位于当前权限范围内的店铺。
- 前缀、后缀和包含关系均不匹配,证明查询不是模糊查询。
- 两个可见店铺联系电话相同均被返回,证明索引和查询没有引入唯一性假设。
- 电话联系条件与店铺名称、店铺编号、上级店铺、层级、状态分别及组合使用时均为 AND;
total与items使用相同条件。 - 未传和传空
contact_phone均保持原列表语义。 - 空格、全角数字、
+86、连字符、少于 11 位、超过 11 位、含字母等非空值均返回 HTTP 400 /code=1001,且没有执行店铺查询。 page=0、page_size=0、page_size>100、非法层级、非法状态、超长名称或编号等已有字段违反 DTO 约束时,也返回统一参数错误。- 未传分页参数时,实际查询第 1 页每页 20 条,响应同步为
page=1、size=20。 - 显式合法分页参数保持不变;后端不会因存在联系电话筛选而重置页码。
- 无匹配数据时返回 HTTP 200、空
items、total=0和正确分页元数据。 - 返回顺序保持
created_at DESC,软删除店铺不出现在结果中。 - 超级管理员和平台账号能在原有范围内查询;代理账号只能看到本店铺及下级店铺,即使范围外店铺使用相同电话也不可见。
- 企业账号访问列表返回 HTTP 403 /
code=1005/msg=无权限访问店铺管理功能。 - 企业账号访问创建、更新、删除、联级查询同样被核心路由组拒绝;独立的店铺角色及资金/佣金路由不因本次路由分组而被误拦截。
- 未认证、Token 无效或 Redis Token 状态无效时继续遵循现有认证错误契约。
- 数据库查询失败时返回脱敏的 HTTP 500 /
code=2001,响应不出现底层错误文本。 - 成功和失败响应均保持统一的
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 密钥或其他凭据,交付文档也不得复制这些值。