- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源 - 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调) - Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录 - 门禁:mkdocs build --strict 通过(零警告)
20 KiB
06 证据驱动质量审计(开工前基线)
- 审计人:EvidenceQA(Evidence Collector)
- 日期:2026-09-03
- 方式:只读审计(ls / grep / 读文件 / git 只读命令)。未运行构建、未启动服务,所有涉及运行时行为的结论均标注「静态分析,需运行验证」。
- 验收依据:
/home/lx/workspace/patbond/patbond-doc/docs/development/development-plan.md第 9 节(测试与质量门禁)、第 10 节(Definition of Done)、第 8 节(第一迭代任务)、M1 验收标准(第 212-223 行)。
1. 测试资产审计
1.1 patbond-api:零测试
- 全仓库文件清单中不存在任何
src/test目录(find patbond-api -type f结果仅含src/main,见仓库文件列表)。 - 三个模块的 pom 中均无
spring-boot-starter-test依赖(grep -rn "spring-boot-starter-test" patbond-api --include="pom.xml"无任何匹配)——即写测试的前置依赖都还没有。 - 与文档第 9 节要求的差距(
development-plan.md:294-300):单元测试、集成测试(Testcontainers)、契约测试、安全测试全部缺失,缺口为 100%。 - 后果:CI 最低门禁
mvn clean test(development-plan.md:313-314)在当前仓库上是空转通过——0 个测试也会绿。这正是文档第 11 节风险 6(development-plan.md:354)自认的状态,审计确认属实。
1.2 patbond-flutter:仅 1 个冒烟测试
test/目录只有 1 个文件:/home/lx/workspace/patbond/patbond-flutter/test/widget_test.dart,共 21 行。- 内容:单个
testWidgets,断言主导航 5 个 Tab 文案,以及两条写死的演示数据字符串:test/widget_test.dart:18→expect(find.text('北京 · 朝阳区'), findsOneWidget)test/widget_test.dart:19→expect(find.text('28°C 晴'), findsOneWidget)- 这两个值来自
lib/data/demo_data.dart:11-19(locationWeatherOptions第一项:city '北京'、district '朝阳区'、temperature 28、conditionText '晴')。演示数据一改,唯一的测试即失败。
- 与文档第 9 节 Flutter 要求的差距(
development-plan.md:302-308):- 单元测试(DTO 映射、Repository、缓存、状态转换):0 个。
lib/state/app_state.dart有可测的加载/持久化/回退逻辑(约 120 行),无对应测试。 - Widget 测试(登录、宠物编辑、动态互动等):0 个(登录页本身不存在)。
- Golden/响应式测试:0 个。
- Integration/E2E:0 个(
integration_test/目录不存在)。 - loading/empty/error/retry/离线状态覆盖:0 个。
- 单元测试(DTO 映射、Repository、缓存、状态转换):0 个。
- 工具链本身可用:
analysis_options.yaml引入package:flutter_lints/flutter.yaml(analysis_options.yaml:10),pubspec.yaml:39-48有flutter_test与flutter_lints ^6.0.0。
1.3 门禁落地情况
- 两个代码仓库均无任何 CI 配置:
ls -a未见.github、.gitlab-ci.yml、Jenkinsfile 等(patbond-api 根目录仅.git、.gitignore、.idea、三个模块与pom.xml、Readme.md)。 - 无 Maven Wrapper(
mvnw不存在),与 M0 目标(development-plan.md:118、204 行前后)尚有差距——这是计划内待办,但意味着「CI 最低门禁」目前只存在于文档里,没有任何机制执行它。
2. 代码质量风险审计(逐条附证据)
2.1 认证与鉴权(后端)
- token 不可验证:
patbond-api/patbond-auth/src/main/java/com/patbond/patbond/auth/service/AuthService.java:47-56——buildToken返回UUID.randomUUID().toString().replace("-", ""),expiresAt只是响应里的展示字段。无签名、无存储、无校验途径、无 refresh token。与 M1 验收标准(development-plan.md:223)和风险 2(development-plan.md:350)一致,审计确认。 /internal/**完全无访问控制:patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/controller/UserController.java:18映射/internal/users,全仓库 grep 无任何 Spring Security、拦截器或过滤器。GET /internal/users/{id}(UserController.java:37-40)返回的UserProfile含phone字段(UserService.java:69-71的toProfile传入user.getPhone())——任何能访问 8082 端口的人可枚举用户资料含手机号。对应风险 3(development-plan.md:351)。- 用户仅存内存:
patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/service/UserService.java:21-23——AtomicLong idGenerator+ 两个ConcurrentHashMap。重启即丢全部用户,直接不满足 M1 验收「注册后重启服务数据不丢失」(development-plan.md:223)。 - 错误状态码折叠(静态分析,需运行验证):
AuthService.java:58-63的requireData把所有失败折叠为HttpStatus.BAD_REQUEST。而 user 服务用ResponseStatusException返回 409/401/404(UserService.java:29,48,56,64),Feign 客户端(patbond-auth/.../client/UserClient.java)收到非 2xx 时会抛FeignException而非进入requireData的业务分支——登录密码错误的调用链大概率对客户端表现为 500 或语义丢失的 400,违反契约规范「错误必须同时返回正确 HTTP 状态码和稳定业务错误码」(development-plan.md:171)。
2.2 硬编码密钥 / 密码 / 敏感信息
- 后端源码中未发现硬编码密钥、密码或密文。
grep -rniE "password|secret|api[_-]?key|token"在patbond-api三个模块src下的命中全部是 DTO 字段赋值(如LoginRequest.java:26、CreateUserRequest.java:27,45),无真实凭据。诚实结论:这一项没有问题,不编造。 - 配置卫生良好:仓库只提交
.yml.sample(patbond-auth/src/main/resources/application.yml.sample、patbond-user/.../application.yml.sample),真实application.yml被.gitignore忽略(patbond-api/.gitignore末段:patbond-*/src/main/resources/application.yml+!...yml.sample)。sample 中仅有${NACOS_SERVER_ADDR:127.0.0.1:8848}之类的本地默认值,无敏感值。 - 开发 fixture 凭据集中在 SQL:
patbond-doc/docs/database/patbond_postgresql.sql:- 第 1269 行:注释明文声明
accounts use the password: Patbond@123; - 第 1289-1293 行:三个 fixture 账号共用同一 bcrypt 哈希
$2y$12$UYCn5Zm...; - 第 1283-1286 行:fixture 手机号
+8613800000001等(明显假号段,风险低); - 约第 1296-1308 行:
identity.auth_sessions插入固定refresh_token_hash = decode(repeat('ab', 32), 'hex')和access_token_jti = 'fixture-access-token-jti'。 - 文件自身已注明「Remove this section from the production Flyway baseline」(第 1266-1269 行),且这是开发种子数据而非泄露的生产密钥。风险在于:bootstrap 是单文件,结构与种子数据未拆分(第 4.3 节第 105 行的拆分要求未完成),误导入生产即产生三个已知密码账号和一个可预测 refresh token。
- 第 1269 行:注释明文声明
2.3 TODO / FIXME / 注释掉的代码
grep -rn "TODO|FIXME|HACK|XXX"在patbond-api、patbond-flutter/lib、patbond-flutter/test的 java/dart/yml 文件中:0 命中。- 注释掉的代码块:Java 侧
grep -rnE "^\s*//.*(;|\{)\s*$"0 命中;Dart 侧同类模式 0 命中(仅pubspec.yaml、analysis_options.yaml中的脚手架模板注释,属正常)。 - 诚实结论:这两类问题当前不存在,不虚报。
2.4 硬编码 URL 与展示型字段(Flutter)
- 22 处硬编码 Unsplash 图片 URL,集中在:
lib/data/demo_data.dart(4、6、8、128、136、146、151、159、178、191、203、216、229、245、252、259 行)、lib/features/home/home_page.dart:551,556、lib/features/pets/pets_page.dart:357-358。属演示数据(文档第 33 行认可其非契约地位),但 DoD 要求「不依赖 Demo 常量」(development-plan.md:340),验收时必须逐页替换。 - 展示字符串被当数据持久化:
lib/data/demo_data.dart:117(time: '2小时前')、130、138、147、161 行;distance: '1.2km'等在 172、185、197、210、223 行。违反第 4.3 节「相对时间、距离由事实字段计算,不持久化展示字符串」(development-plan.md:111),对应风险 4(development-plan.md:352)。 - 无网络层、无登录页、无安全存储:
lib/下无http/dio依赖(pubspec.yaml:30-37仅cupertino_icons+shared_preferences),AppState直接持久化宠物/疫苗/帖子/天气到SharedPreferences(lib/state/app_state.dart:9-12,97-118)。第一迭代需按 4.2 节新建全部网络与鉴权层;届时 token 必须走安全存储而非SharedPreferences(development-plan.md:97)。
3. 文档一致性审计
| 检查项 | 结论 | 证据 |
|---|---|---|
| API Readme 端点 vs 代码 | 一致 | Readme.md:34-44 的 6 个端点与 AuthController.java:24-31、UserController.java:27-45 逐一对应 |
| API Readme 端口 vs 配置 | 一致 | Readme.md:33,40(8081/8082)= 两个 application.yml.sample:2 |
| API Readme 的 NACOS_SERVER_ADDR 说明 | 一致 | Readme.md:48-53 vs sample 第 12 行 ${NACOS_SERVER_ADDR:127.0.0.1:8848} |
| API Readme 启动步骤完整性 | 不一致 | Readme.md:22-26 只有 mvn compile,未提及必须先复制 application.yml.sample 为 application.yml(该步骤只在 development-plan.md:127-138),也无 spring-boot:run 命令。干净检出仅按 README 无法把服务跑起来——文档风险 5(development-plan.md:353)确认属实 |
| Readme 技术栈 RabbitMQ vs 计划 | 内部一致、与计划冲突 | Readme.md:19 列 RabbitMQ,patbond-common/pom.xml:120-123 确实引入 spring-boot-starter-amqp;但计划 4.1 节明确 common「不应让所有服务被动引入 RabbitMQ、Feign 等依赖」(development-plan.md:79) |
| Flutter README 平台声明 | 一致 | README.md:5 声明的 Android/iOS/Web/桌面对应仓库 android/ ios/ web/ linux/ macos/ windows/ 目录均存在 |
| Flutter README shared_preferences 声明 | 一致 | README.md:13 vs lib/state/app_state.dart:9-12 四个持久化 key |
| Flutter README 校验命令 vs 计划 CI 门禁 | 不一致 | README.md:34 为 dart format --set-exit-if-changed lib test(缺 --output=none,实际会改写文件);计划门禁是 dart format --output=none --set-exit-if-changed lib test(development-plan.md:317) |
| 计划 5.2 节命令中的文件路径 | 一致 | development-plan.md:130-133 引用的两个 .yml.sample 均存在于对应路径 |
| mkdocs.yml 导航 | 一致 | patbond-doc/mkdocs.yml nav 引用的 index.md、development/development-plan.md 均存在;docs/index.md:14 链接的 database/patbond_postgresql.sql 存在 |
| 其他观察 | — | patbond-flutter 工作区有未提交改动(git status: M README.md);patbond-api/.idea/ 在磁盘上但未被 git 跟踪(git ls-files 无 .idea 条目),无泄露 |
mkdocs build --strict 门禁未实际执行(审计约束禁止构建),仅完成静态引用核对。
4. 问题清单(按严重程度排序)
Blocker
B1 后端自动化测试为零,CI 门禁空转
证据:patbond-api 无任何 src/test 目录;三个 pom 均无 spring-boot-starter-test。
影响:第 9 节全部后端测试类别缺口 100%;mvn clean test 0 测试也通过,门禁无意义。第一迭代任务 5「注册、登录、刷新、退出和鉴权集成测试」(development-plan.md:285)从零开始。
B2 认证纵切三件套全部缺位:内存用户 + 不可验证 token + /internal 裸奔
证据:UserService.java:21-23(ConcurrentHashMap 存储)、AuthService.java:47-56(随机 UUID 当 token)、UserController.java:18(/internal/users 无鉴权且经 UserService.java:69-71 返回手机号)。
影响:M1 全部四条验收标准(development-plan.md:223)当前均不满足;未鉴权的 /internal 是当前唯一对外可见的真实安全暴露面。
Major
M1 错误状态码在 auth→user 调用链上丢失(静态分析,需运行验证)
证据:AuthService.java:58-63 将一切失败折叠为 400;user 侧用 ResponseStatusException 抛 409/401/404(UserService.java:29,48,56,64),Feign 非 2xx 会抛异常绕过该分支。
影响:违反契约规范 development-plan.md:171;客户端无法区分「用户名已存在」「密码错误」。第一迭代任务 3 的统一异常响应必须覆盖此链路,验收时需用真实 curl 记录证明。
M2 patbond-common 强制全体服务引入 AMQP/Feign/LoadBalancer/Nacos
证据:patbond-common/pom.xml:120-139(spring-boot-starter-amqp、spring-cloud-starter-openfeign、spring-cloud-starter-loadbalancer、两个 nacos starter 全部为 compile 依赖)。
影响:与架构原则 development-plan.md:79 直接冲突;后续每个新业务模块都会被动携带消息队列与服务发现依赖。
M3 两个代码仓库均无 CI 配置和 Maven Wrapper
证据:ls -a 无 .github/.gitlab-ci.yml/mvnw。
影响:第 9 节 CI 最低门禁(development-plan.md:310-323)没有执行载体,DoD 中「代码通过 CI」(development-plan.md:345)无法核查。
M4 API Readme 启动步骤不完整,干净检出无法照做启动
证据:patbond-api/Readme.md:22-26 仅 mvn compile;配置复制步骤只存在于 development-plan.md:127-138;.gitignore 忽略 application.yml 且仓库只有 .sample。
影响:违反 M0 验收「新机器仅依据仓库文档即可启动」(development-plan.md:210)。
M5 数据库 bootstrap 单文件混装结构与开发凭据
证据:patbond_postgresql.sql:1266-1269(明文声明 fixture 密码 Patbond@123)、1289-1293(三账号同 bcrypt 哈希)、约 1296-1308(固定 refresh_token_hash 与 JTI 的预置会话)。
影响:结构/种子未拆分(development-plan.md:105 要求拆开),一旦整文件被当生产 baseline 导入,即产生已知密码账号与可预测会话。风险 7(development-plan.md:355)确认属实。
Minor
m1 Flutter 唯一测试断言写死演示数据
证据:test/widget_test.dart:18-19 断言 '北京 · 朝阳区'、'28°C 晴',值来自 lib/data/demo_data.dart:11-19。
影响:演示数据或默认城市一改,唯一的测试即挂;该测试对回归防护价值趋近于零。
m2 Flutter README 校验命令与 CI 门禁不一致
证据:patbond-flutter/README.md:34 缺 --output=none,会改写文件;门禁版本在 development-plan.md:317。
影响:开发者本机「校验」实际是格式化,CI(若建立)与本机行为不一致。
m3 展示字符串与硬编码图片 URL 持久化在演示数据中
证据:lib/data/demo_data.dart:117,130,138,147,161('2小时前' 等)、172,185,197,210,223('1.2km' 等)、22 处 Unsplash URL(见 2.4 节行号清单)。
影响:与 development-plan.md:111 及 DoD「不依赖 Demo 常量」冲突;属已知风险 4,逐页替换时必须清除,验收时应 grep 证明。
m4 patbond-flutter 工作区有未提交改动
证据:git status --short → M README.md。
影响:基线不干净,审计快照与远端不一致;开工前应提交或还原。
5. 第一迭代「登录纵切」验收证据清单
依据:第 8 节任务 1-8、M1 验收标准(development-plan.md:223)、第 9 节门禁、第 10 节 DoD。没有下列证据即不通过验收,任何「已完成」的口头声明不作数。
5.1 自动化测试输出(原始终端输出,不接受转述)
mvn clean test完整输出:显示测试总数 > 0,且包含注册/登录/刷新/退出/鉴权的集成测试类名与用例数;Testcontainers 启动 PostgreSQL 16 的日志行可见。- 每个接口至少覆盖:成功、参数错误、资源不存在、无权限、并发冲突、幂等重试(
development-plan.md:300)——以测试报告中的用例名逐条对应。 dart format --output=none --set-exit-if-changed lib test、flutter analyze、flutter test三条命令的退出码为 0 的完整输出;flutter test中包含登录页 widget 测试(loading/error/成功三态)。mkdocs build --strict成功输出(文档更新后)。- CI 运行链接或日志:以上门禁在 CI 中执行并全绿(M3 问题修复的证明)。
5.2 接口调用记录(curl/httpie 全文:请求 + 响应头 + 响应体)
按顺序一份完整 transcript:
POST /api/v1/auth/register→ 201/200,响应体含 UUID 格式 userId 与{code, message, data}包裹。- 重复用户名注册 → HTTP 409 + 稳定业务错误码(验证 M1 问题修复:不再折叠为 400/500)。
- 错误密码登录 → HTTP 401 + 业务错误码。
POST /api/v1/auth/login成功 → 含 access + refresh token。GET /api/v1/me带 token → 200;不带/伪造 token → 401(证明 token 可验证)。POST /api/v1/auth/refresh→ 新 token 对;随后用旧 refresh token 重放 → 401(轮换生效)。POST /api/v1/auth/logout→ 成功;再用已撤销 refresh token → 401(M1 验收「退出后 refresh token 不可再次使用」)。- 未携带服务间凭据直接调用
/internal/users/{id}→ 被拒绝(401/403),对照当前裸奔状态(B2)。
5.3 数据库查询结果(psql 原始输出)
- 注册后:
SELECT id, username, status FROM identity.users WHERE username='...'显示 UUID 主键行。 SELECT hash_algorithm, left(password_hash, 7) FROM identity.user_credentials ...显示 bcrypt/argon2id 前缀——同时证明非明文。- 重启持久化证据:注册 → 服务重启(附带重启时间戳的服务日志)→ 登录成功 + 上述查询仍有该行(M1 验收「重启数据不丢失」,直接针对 B2 内存存储)。
- refresh 轮换后:
SELECT ... FROM identity.auth_sessions显示旧会话 revoked/新会话 active。 SELECT version, description, success FROM flyway_schema_history显示 identity/media baseline 迁移(任务 1),且在全新 PostgreSQL 16 实例执行过一次(development-plan.md:325)。- 生产 baseline 不含 fixture:对迁移产物
grep -c "Patbond@123"为 0(针对 M5)。
5.4 界面截图(真机或模拟器,标注设备与时间)
- 登录页:初始态、提交中 loading 态、错误态(错误密码后的可读提示)、成功跳转后首页。
- 注册页同三态。
- 登录态恢复:登录 → 完全杀掉 App → 重新打开直接进入已登录态(M1 验收),两张前后截图 + 中间的杀进程操作说明。
- 退出登录后回到未登录态的截图。
- 安全存储证据:代码评审指向 token 写入 secure storage 的调用点(文件+行号),并
grep -rn "SharedPreferences" lib输出证明 token 未落入SharedPreferences(development-plan.md:97)。
5.5 附加核查项(DoD)
- 日志片段 +
grep -inE "password|token" <日志文件>输出:证明日志不含密码与 token 全文(development-plan.md:175,344)。 - OpenAPI 文件路径 + 契约测试输出(任务 6)。
- 端到端用例(注册 → 登录 → 获取当前用户 → 退出,任务 8)的单次完整执行记录,与 5.2 的 transcript 可为同一份。
验收纪律
- 每条证据必须可复现:附命令、路径、时间。截图必须来自本次交付的构建,不接受历史截图。
- 声明「零问题」「production ready」而不附上述证据的交付,直接按 FAILED 处理并退回。
- 本报告第 4 节的 B1、B2 未关闭前,第一迭代不具备进入验收的资格。
附:本次审计执行的命令类别
find(文件清单)、ls -a(CI/wrapper 探测)、grep -rn(TODO/密钥/URL/展示字符串/测试依赖)、cat/sed(读源码与 SQL)、wc -l(体量)、git status --short / git ls-files / git log --oneline(只读仓库状态)。未修改任何被审计仓库的文件。