跳转至

EvoCode 开发规范(详细版)

适用于 backend / analyzer / frontend 三端 · 单人开发也必须遵守(保证毕业设计质量与可维护性) 版本:v1.3 · 2026-08-10

使用说明(灵活性声明):本规范是基线约定,不是死规定。附录 A 的代码骨架是"推荐写法"而非强制模板;当规范与实际情况冲突时,具体情况具体分析——记录决策(AD + devlog)后合理偏离,事后回填规范。任何偏离不得绕过两条底线:① 三端验证命令通过;② 不引入安全风险(见 §7)。


目录

  1. 协作与 Git 规范
  2. 后端规范(Spring Boot 3 / Java 17)
  3. 前端规范(Vue3 + TS)
  4. Analyzer 规范(Python / FastAPI)
  5. 接口契约管理
  6. 日志与异常规范
  7. 安全规范
  8. 环境与配置管理
  9. 测试规范
  10. 质量门禁与 CI
  11. 文档与知识沉淀
  12. Definition of Done(完成定义)

1. 协作与 Git 规范

1.1 分支模型(GitHub Flow)

main ──────────────────────────────── (始终可运行可演示)
  └── feature/{module}-{short}        (开发分支)
  └── docs/{topic}                    (文档分支)
  └── fix/{module}-{short}            (修复分支)
  • 禁止直接 push main;必须 PR(squash merge)
  • 单人开发也走 PR:PR 即 checkpoint,历史可回滚、可答辩展示
  • 分支命名示例:feature/project-uploadfeature/ai-reportfix/scan-ignore-ruledocs/db-design

1.2 Commit 规范(Conventional Commits)

<type>(<scope>): <subject>

body(必要时:为什么这么做,关联 issue)
type 含义 示例
feat 新功能 feat(user): 增加 zip 上传创建项目
fix 修复 fix(scan): 修复忽略规则漏掉 dist 目录
docs 文档 docs(db): 补充 analysis 表设计说明
refactor 重构(不改行为) refactor(analyzer): 重构 llm_client 降级链
test 测试 test(analyzer): 补充语言识别单测
chore 构建/依赖/杂项 chore(deps): 升级 spring boot 补丁版本
style 格式 style(front): 统一 prettier 配置
perf 性能 perf(scan): 大目录跳过加速 30%
  • scope 用模块名:user / scan / quality / arch / evolution / ai / report / front / analyzer / db / deps
  • subject 用中文(仓库统一中文描述,type/scope 保持英文)
  • 一个 commit 只做一件事;禁止混入无关改动

1.3 PR 流程

  1. 从 main 切分支 → 开发 → 本地三端验证(见 §10)
  2. 提交 PR,按模板填写(做了什么/为什么/验证方式/影响范围)
  3. 自己先 review 一遍 diff(换位评审),再合并
  4. squash merge,删除分支

1.4 Tag 与版本

  • 里程碑打 tag:v0.1.0(MVP)、v0.2.0v1.0.0
  • 版本号语义化:major.minor.patch

1.5 其他

  • 不使用 Git LFS(禁止入库大文件;项目代码存运行时目录,不入库)
  • .gitignore 必须包含:.envdata/logs/target/node_modules/__pycache__/.idea/.vscode/(工作区配置除外)、dist/
  • 示例项目(测试用 zip)放 samples/ 目录并入库(小体积)

2. 后端规范(Spring Boot 3 / Java 17)

2.1 分层职责(禁止越层)

职责 禁忌
controller 参数校验、调用 service、返回 Result 业务逻辑、直接调 mapper/analyzer
service 业务逻辑、事务、跨模块编排 直接返回 entity(转 DTO)、HTTP 细节
mapper 数据访问(MyBatis Plus) 业务逻辑
entity 表映射(贫血模型) 业务方法
dto 入参出参(XxxReq / XxxResp 映射到 DB 逻辑
config Bean、配置类 业务
common Result、异常、错误码、工具
enums 枚举常量(状态机等)

2.2 命名规范

规范 示例
PascalCase ProjectService
接口/实现 接口 XxxService + 实现 XxxServiceImpl
方法 camelCase,动词开头 createProject
常量 UPPER_SNAKE_CASE MAX_UPLOAD_SIZE
com.evocode.{layer}.{module} com.evocode.service.project
表/字段 snake_case;实体驼峰映射开启 last_analyzed_atlastAnalyzedAt
DTO XxxReq(入)/ XxxResp(出) AnalyzeReq
枚举类 类名 XxxType,常量大写 AnalysisStatus.SUCCEEDED

2.3 代码风格

  • Java 17;统一项目编码 UTF-8;EditorConfig 生效
  • 优先 record 承载不可变 DTO(Java 17 支持);需要可变再退用 class
  • 空值处理:Optional 用于可能为空的返回值;禁止到处判空嵌套(用工具类 ObjectUtil
  • 集合操作用 Stream;超过 3 层嵌套一律抽方法
  • Lombok:允许 @Data @Builder @Slf4j;禁止 @AllArgsConstructor 滥用;禁止 Lombok 生成对 entity 的 setter 直改(状态流转走 service 方法)
  • 依赖注入用构造器注入(@RequiredArgsConstructor),禁止字段注入
  • 禁止 System.out;禁止捕获异常后吞掉(至少 log.warn)
  • 日期:LocalDateTime + 统一 Asia/Shanghai(或 UTC 存储 + 展示转换,全局统一一种,不允许混用

2.4 异常与错误码

  • 业务异常:throw new BusinessException(ErrorCode.xxx, "消息")
  • 全局 GlobalExceptionHandler 兜底:BusinessException / 参数校验 / 兜底 Exception
  • 错误码分段(common/enums/ErrorCode):
含义 示例
1xxx 参数错误 1001 参数缺失、1002 格式错误、1003 分页非法
2xxx 业务错误 2001 项目不存在、2002 分析任务占用、2003 文件非法、2005 文件越权、2006 会话不存在、2007 发送过频、2008 重生成中、2009 仓库克隆失败
3xxx 分析器错误 3001 analyzer 不可达、3002 扫描超时、3003 LLM 失败
5xxx 系统错误 5001 磁盘不足、5002 数据库异常、5003 服务重启中断

错误码唯一来源:docs/06-API契约.md §2.2(含 2004 预留说明);本表仅列分段与常见示例,新增/修改只改 06。

  • 返回格式统一:{"code": 0, "message": "ok", "data": ...},code=0 表示成功

2.5 日志规范(见 §6 统一)

2.6 数据库规范

  • DDL 统一放 db/migration/(命名 V{version}__{desc}.sql,如 V001__init.sql;版本递增不修改旧文件)
  • 字段规范:主键 bigint;时间 TIMESTAMPTZ;金额/大数 numeric;灵活结构 jsonb;枚举短码 varchar(20)(不用 DB enum,便于演进)
  • 逻辑删除:业务表保留 deleted 字段(0/1),Mapper 全局配置逻辑删除
  • 索引:外键、筛选高频字段、(project_id, id DESC) 组合;索引命名 idx_{table}_{cols}
  • 事务:@Transactional(rollbackFor = Exception.class) 在 service;禁止跨服务循环注入;事务内不做 LLM 调用(长事务)
  • 大 JSONB(报告):读多写少,查询用投影(SELECT 指定列

2.7 异步与并发

  • 分析任务用独立 @Async("analysisExecutor") 线程池:核心 2 / 最大 4 / 队列 8(可配置)
  • 同一项目并发保护:启动任务前加 SELECT ... FOR UPDATE 校验,重复发起返回 2002
  • 线程安全:Spring 单例 Bean 无状态;可变的工具类静态状态必须 synchronized 或不可变

2.8 配置规范

  • application.yml(公共)+ application-dev.yml(开发)+ application-prod.yml(生产)
  • 敏感配置一律 ${ENV_VAR:默认值} 读取;.env.example 列全所有变量
  • 配置项集中 config 包下 @ConfigurationProperties 类,禁止散落 @Value

2.9 后端验证命令

mvn -q compile                          # 编译
mvn test                                # 单测
mvn -q package -DskipTests             # 打包

3. 前端规范(Vue3 + TS + Vite)

3.1 目录规范

src/
├── api/            # 每资源一个文件:project.ts / analysis.ts / ...
├── views/          # 页面:project-list/ project-detail/(index.vue + 子组件)
├── components/     # 通用组件:base/(按钮/空态/加载态) charts/(图表封装) feature/
├── stores/         # Pinia:project.ts(当前项目)、user.ts(预留)
├── utils/          # request.ts(axios 封装)、format.ts、markdown.ts
├── types/          # 与后端 DTO 对应的 TS 类型(api 返回类型统一放这)
├── router/
└── styles/         # 全局样式、CSS 变量

3.2 命名与代码风格

规范
组件文件 PascalCase:ProjectCard.vueRelationChart.vue
页面文件 kebab-case 目录 + index.vue:views/project-detail/index.vue
组件名 多词 PascalCase,前缀语义(不强制前缀)
props camelCase;事件 on- 前缀(@on-close)或 emit('close')
函数 camelCase 动词开头:fetchReport()
TS strict: true;api 返回值类型定义在 types/;禁止 any(万不得已 unknown + 收窄)
模板 统一 <script setup lang="ts">;逻辑抽 composables/(useProject.ts)

3.3 请求层

  • 统一 utils/request.tsbaseURL=/api/v1;请求拦截器注入 token(预留);响应拦截器统一处理:code≠0 → 错误 toast + 可选跳转;401/403 处理;blob 下载特例
  • API 模块只导出函数(export const getProject = (id) => ...),返回类型来自 types/
  • 页面用 loading 状态 + 骨架屏,不做全局 loading 遮罩(分析页除外)

3.4 状态与路由

  • Pinia 只放跨页面状态(当前项目、会话列表);单页数据用组件本地状态
  • 路由懒加载 () => import(...);路由元信息:title、图标
  • 查询参数用 useRoute 读取后转 store,避免页面间耦合

3.5 样式

  • 组件样式一律 scoped;全局主题变量(颜色/间距/圆角)定义在 styles/variables.css
  • 颜色只用主题变量;禁止硬编码色值散落
  • 图表组件(ECharts):封装 base/ChartContainer.vue(resize/主题/loading 统一处理);图表配置抽 charts/options/xxx.ts

3.6 其他

  • 文案统一中文;数字格式化工具 utils/format.ts(万/亿、千分位)
  • Markdown 渲染用 marked + 自定义样式;代码高亮用 highlight.js
  • Monaco 仅 AI 医生/文件预览用(按需加载,控制包体)

3.7 前端验证命令

npm run lint        # ESLint + Prettier 检查
npm run build       # 类型检查 + 打包

4. Analyzer 规范(Python / FastAPI)

4.1 模块职责

目录 职责 禁忌
scanners/ 文件扫描、忽略规则、语言识别、技术栈识别、LOC 调用 LLM
parsers/ tree-sitter 解析(符号/调用关系) 执行被分析代码
quality/ sonar-scanner 封装、结果归一化
git_analyzer/ git 统计(subprocess 调 git CLI) 拼接不可信参数进 shell
ai/ LLM 调用、报告生成、RAG、prompt 模板 业务数据处理逻辑
utils/ path 安全、忽略规则、限流

4.2 契约与类型

  • 所有入参出参用 pydantic 模型(schemas.py),字段名与后端 DTO 一致;契约文件是唯一事实来源(见 §5)
  • 禁止 dict 裸传;json.dumps 统一 ensure_ascii=False
  • 全量类型注解;ruff 规则全开(除 line-length 88)

4.3 安全铁律

  • 绝不执行被分析代码;禁止 eval/exec/subprocess 执行项目内文件
  • git 命令:参数经白名单校验(只允许 log 固定参数),仓库路径用绝对路径并验证前缀
  • 文件读取:全部走 utils/path.py(校验在项目根内 + 只读)
  • 解压:逐文件校验(..、绝对路径、符号链接),用 zipfile + 显式路径拼接
  • LLM 调用:超时(默认 60s)+ 重试(2 次指数退避)+ 输出 JSON 解析失败重试

4.4 性能

  • 大目录扫描:忽略规则前置(os.walk 时剪枝);大文件跳过;流式读文件(不整体加载 >1MB 进内存)
  • 解析结果内存控制:边解析边入库/写文件,不全程驻留
  • 单次分析总超时(配置),超时返回部分结果 + 标记

4.5 其他

  • 依赖锁定:requirements.txt + pip freeze;生产 pip 安装用 --no-cache-dir
  • 日志用 logging(带模块名);禁止 print
  • 新增可解析语言:新增 parser 模块 + 注册表 + 单测,三处同步
  • 依赖方向检查:import-linter 配置 layers(schemas ← core ← parsers/analyzers ← ai/api),纳入 pytest 或 CI,防模块串层

4.6 Analyzer 验证命令

ruff check . && ruff format --check .   # lint
pytest -q                                # 测试(含 fixtures 小样本)
import-linter enforce                    # 依赖方向检查(pyproject [tool.importlinter] 配置 layers)
uvicorn app.main:app --port 8091         # 本地联调

5. 接口契约管理

5.1 契约来源

  • Analyzer 的 schemas.py 为分析结果契约的唯一事实来源;后端 DTO 与前端 types 由它派生
  • 对外 REST API:后端 knife4j(OpenAPI 3)自动生成文档,前端以 .json 为准做类型
  • 新增/修改字段流程:改 schemas.py → 同步后端 DTO → 同步前端 types → 三端跑通 → 在 PR 描述标注"契约变更"

5.2 错误码(三端共用,见 §2.4)

5.3 API 版本

  • 对外:/api/v1 前缀;破坏性变更升 v2
  • analyzer 内部:/analyze/v1/...;内部契约允许同版本内演进,但必须后端同步

5.4 命名统一

  • JSON 字段:camelCase(后端序列化配置)
  • 枚举值:全大写(SUCCEEDED);前端映射中文展示在组件层,不改变传输值

6. 日志与异常规范

6.1 统一格式(三端尽量一致)

[2026-08-10 14:03:22.123] [INFO ] [analysis-executor-1] [com.evocode.service.AnalysisService] 消息: 参数

6.2 级别选择

级别 用途
ERROR 系统错误、任务失败(含异常栈)
WARN 可恢复异常(LLM 重试、降级发生)、跳过文件
INFO 业务入口/出口、任务状态流转(不多于每任务 10 条)
DEBUG 详细入参、分析中间结果(默认关闭)

6.3 内容要求

  • 业务入口 INFO 打印:方法、关键入参(脱敏)
  • 任务日志带 analysisId 前缀(便于按任务 grep)
  • 禁止打印:LLM Key、文件全文、cookie/token
  • 异常统一 log.error("描述", e),禁止只 log 消息不传异常

6.4 日志存储

  • 后端/analyzer:按天滚动 logs/,保留 14 天(logback / logging.config)
  • 前端:错误捕获(app.config.errorHandler)仅 console + 可选上报本地文件

7. 安全规范

7.1 代码与依赖

  • 依赖引入前评估:优先官方维护、周下载量大的包;锁版本
  • 前端禁止 v-html 渲染不可信内容(markdown 渲染需 sanitize)
  • 后端禁止 SQL 拼接(全走 MyBatis Plus / 参数化);排序字段白名单

7.2 数据与密钥

  • .env 不入库;.env.example 入库存模板(值置空)
  • LLM Key 只存服务端环境变量;前端接口永不返回密钥
  • 上传文件大小/类型/路径三重校验;下载接口校验路径在项目目录内

7.3 服务

  • analyzer 只绑定 127.0.0.1:8091;后端调用 analyzer 不暴露公网
  • PostgreSQL/Redis 端口仅 127.0.0.1 映射(docker-compose)
  • CORS 只允许前端源

7.4 安全测试清单(每阶段跑一遍)

  • [ ] 恶意 zip 上传被拒(路径穿越、超限)
  • [ ] 文件内容接口:../ / 绝对路径越权被拒、超 2MB 被拒、二进制拒绝(P0-3 修复后)
  • [ ] 超长文件名/中文文件名正常
  • [ ] 删除项目后磁盘无残留
  • [ ] 日志无密钥/无文件全文
  • [ ] analyzer 无法执行被分析代码(无 shell 注入点)

8. 环境与配置管理

8.1 环境划分

环境 用途 配置
dev 本机开发 docker-compose 起 postgres/redis/sonarqube
prod 演示/答辩 同 dev,可加 systemd 托管

8.2 配置清单(.env.example 内容)

POSTGRES_DB=evocode
POSTGRES_USER=evocode
POSTGRES_PASSWORD=change-me
LLM_BASE_URL=https://api.deepseek.com
LLM_API_KEY=
LLM_MODEL=deepseek-chat
EMBEDDING_MODEL=bge-m3
DATA_ROOT=./data

8.3 本地启动(一键)

  • 根目录 scripts/start-dev.sh / .ps1(win):docker compose up → 起 analyzer → 起 backend → 起 frontend
  • 顺序固定:基础设施 → analyzer → backend → frontend;失败即停并提示

9. 测试规范

9.1 覆盖目标

必测范围 目标覆盖率
backend service 核心逻辑(状态机、降级、错误码)、工具类 service ≥70%,工具 ≥90%
analyzer 忽略规则、语言识别、解析器、schemas 校验 关键模块 ≥80%
frontend 请求封装、格式化工具、关键页面冒烟(vitest + happy-dom 可选) 低门槛,不强制

9.2 命名与结构

  • 后端:XxxServiceTest,按"given-when-then"组织(可中文注释场景名)
  • analyzer:tests/test_{module}.py;fixtures 放 tests/fixtures/(小样本工程),禁止在测试里造大文件
  • 前端:utils/__tests__/format.spec.ts

9.3 Mock 原则

  • LLM:backend 测试 mock AnalyzerClient(LLM 出口在 analyzer,backend 永不直连);analyzer 测试用 tests/fixtures/llm_stub.py(固定响应 + 异常注入)
  • 外部服务(Sonar/git):测试内 stub,不真实调用
  • 数据库:后端单测用 H2(兼容模式)或 mock mapper;集成测试才连 PostgreSQL

9.4 手工冒烟(每阶段)

见开发指导 §13 测试策略:完整流程冒烟 + 降级冒烟 + 安全冒烟。


10. 质量门禁与 CI

10.1 提交前门禁(本地)

backend:  mvn test
analyzer: ruff check . && ruff format --check . && pytest -q
frontend: npm run lint && npm run build

10.2 可选 CI(GitHub Actions,建议 v0.2 后加)

# .github/workflows/ci.yml 要点
- name: Backend test        # actions/setup-java@v4, java 17, mvn test
- name: Analyzer lint+test  # actions/setup-python@v5, ruff + pytest
- name: Frontend build      # actions/setup-node@v4, npm ci && lint && build
  • 三条全绿才允许合 PR(本地单人开发至少保证三命令全过)

10.3 门禁补充

  • PR 描述含验证截图/输出 → 合
  • 破坏性契约变更未同步 → 不合

11. 文档与知识沉淀

位置 内容 频率
docs/decisions/AD-xxx.md 技术决策(背景/方案对比/结论) 每次重大选型
docs/devlog/YYYY-MM-DD.md 做了什么/踩坑/下周计划 每周
docs/screenshots/ 阶段演示截图 每阶段
docs/api/ 导出后的 OpenAPI 快照 契约变更时
根 README 项目简介 + 一键启动 随版本更新
三端 README 各自启动/测试命令 随版本更新

AD 模板:

# AD-004:健康分算法采用规则+AI 修正
- 日期 / 状态:已采纳
- 背景:报告需要可解释、可复现的评分
- 方案对比:纯规则(可解释但生硬)/ 纯 AI(灵活但不可复现)/ 规则+AI 修正(折中)
- 结论:规则基础分 + LLM 修正(幅度 ±10,需给理由)
- 影响:报告 JSON 含 score 与 score_detail

12. Definition of Done(DoD)

每个功能/PR 必须满足:

  • [ ] 功能按需求文档实现(对照 FR 编号)
  • [ ] 三端验证命令通过(§10.1)
  • [ ] 关键逻辑有测试
  • [ ] 契约同步(analyzer schemas ↔ 后端 DTO ↔ 前端 types)
  • [ ] 日志、错误码符合规范;无调试残留
  • [ ] 无密钥/敏感信息入库
  • [ ] 相关文档更新(README/AD/devlog)
  • [ ] UI 变更附演示截图
  • [ ] 手工冒烟通过(含降级场景)
  • [ ] 合并走 PR(squash),描述完整

附录 A:代码骨架示例(推荐写法)

A.1 统一响应与异常(backend/common)

// Result.java
@Data
public class Result<T> {
    private int code;      // 0=成功
    private String message;
    private T data;
    public static <T> Result<T> ok(T data) { ... }
    public static <T> Result<T> fail(int code, String message) { ... }
}

// ErrorCode.java(枚举段示例;完整枚举以 06 §2.2 为唯一来源)
public enum ErrorCode {
    PARAM_MISSING(1001, "参数缺失"),
    PARAM_INVALID(1002, "参数格式错误"),
    PAGE_PARAM_INVALID(1003, "分页参数非法"),
    PROJECT_NOT_FOUND(2001, "项目不存在"),
    ANALYSIS_BUSY(2002, "该项目已有运行中的分析任务"),
    FILE_ILLEGAL(2003, "上传文件非法"),
    PROJECT_STATE_FORBIDDEN(2004, "项目状态不允许该操作"), // 预留
    FILE_CONTENT_FORBIDDEN(2005, "文件内容越权或超限"),
    SESSION_NOT_FOUND(2006, "会话不存在"),
    CHAT_TOO_FREQUENT(2007, "发送过于频繁/重复提交"),
    REPORT_REGENERATING(2008, "该分析正在重新生成报告中"),
    ANALYZER_UNREACHABLE(3001, "分析服务不可达或内部错误"),
    SCAN_TIMEOUT(3002, "扫描超时"),
    LLM_FAILED(3003, "AI 服务调用失败"),
    DISK_FULL(5001, "磁盘空间不足"),
    DB_ERROR(5002, "数据库异常"),
    TASK_INTERRUPTED(5003, "服务重启导致任务中断");
    // code, message, httpStatus 字段 + 构造器(httpStatus 见 06 §2.1 映射)
}

// BusinessException.java
@Getter
public class BusinessException extends RuntimeException {
    private final int code;
    public BusinessException(ErrorCode ec, String detail) {
        super(ec.getMessage() + (detail == null ? "" : ":" + detail));
        this.code = ec.getCode();
    }
}

A.2 Controller 示例(分层示范)

@RestController
@RequestMapping("/api/v1/projects")
@RequiredArgsConstructor
public class ProjectController {

    private final ProjectService projectService;

    @PostMapping
    public Result<ProjectResp> create(@RequestPart("file") MultipartFile file,
                                      @RequestParam("name") String name) {
        return Result.ok(projectService.createFromZip(name, file));
    }

    @PostMapping("/{id}/analyses")
    public Result<AnalysisResp> startAnalysis(@PathVariable Long id,
                                              @RequestBody @Valid AnalyzeReq req) {
        return Result.ok(projectService.startAnalysis(id, req.getType()));
    }

    @GetMapping("/{id}")
    public Result<ProjectDetailResp> detail(@PathVariable Long id) {
        return Result.ok(projectService.detail(id));
    }
}

A.3 分析状态机(service 核心,伪代码)

@Transactional
public AnalysisResp startAnalysis(Long projectId, String type) {
    // 并发保护:同一项目仅一个运行中任务
    if (analysisMapper.countRunning(projectId) > 0) {
        throw new BusinessException(ErrorCode.ANALYSIS_BUSY, null);
    }
    Analysis analysis = new Analysis(projectId, type, Status.PENDING, 0);
    analysisMapper.insert(analysis);
    analysisAsyncRunner.run(analysis.getId());   // @Async
    return AnalysisResp.of(analysis);
}

@Async("analysisExecutor")
public void run(Long analysisId) {
    Analysis a = analysisMapper.selectById(analysisId);
    updateStatus(a, Status.RUNNING, 5, "SCAN");
    try {
        ScanResult scan = analyzerClient.scan(a.getProjectId());     // 阶段1:超时10min
        updateStatus(a, Status.RUNNING, 70, "SCAN_DONE");
        fileNodeService.saveSnapshot(a.getProjectId(), analysisId, scan);
        projectService.updateProfile(a.getProjectId(), scan);

        updateStatus(a, Status.RUNNING, 75, "REPORT");
        ReportResult report = reportService.generate(a, scan);        // 阶段2:LLM或降级
        updateStatus(a, Status.RUNNING, 95, "REPORT");
        a.setReportJson(report);
        updateStatus(a, Status.SUCCEEDED, 100, "DONE");
    } catch (Exception e) {
        log.error("[analysisId={}] 分析失败", analysisId, e);
        updateStatus(a, Status.FAILED, a.getProgress(), e.getMessage());  // 失败不置 -1(progress 恒在 0-100)
    }
}

A.4 AnalyzerClient(后端 → analyzer,Spring RestClient)

@Service
public class AnalyzerClient {
    private final RestClient restClient;   // base-url 指向 127.0.0.1:8091
    private final EvocodeProperties props;

    public ScanResult scan(Long projectId, String codeDir) {
        try {
            ScanReq req = new ScanReq(projectId, codeDir);
            return restClient.post().uri("/analyze/v1/scan")
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(req)
                    .retrieve()
                    .body(ScanResult.class);
        } catch (ResourceAccessException e) {
            throw new BusinessException(ErrorCode.ANALYZER_UNREACHABLE, e.getMessage());
        }
    }
}

A.5 analyzer schemas.py(scan 契约,唯一事实来源)

from pydantic import BaseModel, Field

class ScanFile(BaseModel):
    path: str
    language: str = "OTHER"
    loc: int = 0
    size_bytes: int = 0

class ScanRequest(BaseModel):
    project_id: int
    code_dir: str

class ScanResult(BaseModel):
    languages: dict[str, float] = Field(default_factory=dict)   # {"Java":61.2}
    loc_total: int
    file_count: int
    ignored_count: int = 0
    frameworks: list[str] = Field(default_factory=list)
    has_backend: bool = False
    has_frontend: bool = False
    db_hint: list[str] = Field(default_factory=list)
    files: list[ScanFile] = Field(default_factory=list)

A.6 analyzer llm_client.py(含降级骨架)

import json, logging
import httpx

logger = logging.getLogger(__name__)

class LLMError(Exception): ...

class LLMClient:
    def __init__(self, settings):
        self.base_url, self.api_key, self.model = settings.llm_base_url, settings.llm_api_key, settings.llm_model
        self.timeout = settings.llm_timeout_seconds
        self.retries = settings.llm_max_retries

    def chat_json(self, system: str, user: str, max_retries: int | None = None) -> dict:
        if not self.api_key:
            raise LLMError("LLM_API_KEY 未配置")     # 上层捕获 → 降级规则版
        for attempt in range((max_retries or self.retries) + 1):
            try:
                resp = httpx.post(
                    f"{self.base_url}/chat/completions",
                    headers={"Authorization": f"Bearer {self.api_key}"},
                    json={
                        "model": self.model,
                        "temperature": 0.3,
                        "response_format": {"type": "json_object"},   # 部分厂商支持
                        "messages": [{"role": "system", "content": system},
                                     {"role": "user", "content": user}],
                    },
                    timeout=self.timeout,
                )
                resp.raise_for_status()
                text = resp.json()["choices"][0]["message"]["content"]
                return json.loads(text.strip().removeprefix("```json").removesuffix("```"))
            except Exception as e:
                logger.warning("LLM 调用失败(第%s次): %s", attempt + 1, e)
        raise LLMError("LLM 重试后仍失败")

A.7 前端 utils/request.ts

import axios from 'axios'

const request = axios.create({ baseURL: '/api/v1', timeout: 30000 })

request.interceptors.response.use(
  (resp) => {
    const { code, message, data } = resp.data
    if (code !== 0) return Promise.reject(new Error(message))
    return data            // 直接返回业务数据,页面拿到的就是 data
  },
  (err) => Promise.reject(err)
)

export default request

A.8 前端组件示例(ProjectCard.vue 骨架)

<script setup lang="ts">
import type { ProjectSummary } from '@/types/api'

defineProps<{ project: ProjectSummary }>()
const emit = defineEmits<{ (e: 'open', id: number): void }>()
</script>

<template>
  <article class="project-card" @click="emit('open', project.id)">
    <h3>{{ project.name }}</h3>
    <div class="lang-bar" title="语言占比">
      <span v-for="(pct, lang) in project.langStats" :key="lang"
            :style="{ width: pct + '%' }" class="lang-seg" />
    </div>
    <div class="meta">
      <span>{{ project.locTotal }} LOC</span>
      <span>{{ project.healthScore ?? '--' }}</span>
    </div>
  </article>
</template>

<style scoped>
.project-card { border: 1px solid var(--color-border); border-radius: 8px; padding: 12px; cursor: pointer; }
/* 颜色只用主题变量 */
</style>

附录 B:Self-Review 自评清单(合并 PR 前自己过一遍)

  • [ ] diff 逐行看过,无调试代码(console.log / System.out / print / 注释掉的代码)
  • [ ] 无魔法数字散落(提取常量/枚举)
  • [ ] 分支覆盖了错误路径与空值(空列表、null、超时)
  • [ ] 命名准确表达意图,无缩写混乱(usr / repo / info 混用)
  • [ ] 事务/异步边界正确(LLM 调用不在事务内、@Async 方法独立 Bean)
  • [ ] 轮询/重试有上限,不会无限循环
  • [ ] 新增 API 有 knife4j 注解或文档说明(analyzer 有 docstring)
  • [ ] 前端新增组件有 loading/空态/错误三态
  • [ ] 契约字段:后端 DTO ↔ 前端 types ↔ analyzer schema 三处一致(grep 核对)
  • [ ] 日志包含关键 ID(projectId/analysisId),便于排查
  • [ ] 未引入新依赖(如引入,记录理由到 AD)
  • [ ] 性能意识:列表查询有分页/索引;大 JSON 不整表查询
  • [ ] 本 PR 对应的 FR 编号与 AC 已确认

附录 C:规范未覆盖时怎么办(决策流程)

遇到规范/文档未覆盖的问题或冲突
  ↓
1. 判断影响面:
   仅实现细节(命名/写法)→ 选最贴近现有代码的方式,不打断工作
   影响架构/契约/数据 → 进入第 2 步
  ↓
2. 记录决策:docs/decisions/AD-xxx.md(背景/方案对比/结论/影响)
  ↓
3. 实现并验证(三端命令 + 冒烟)
  ↓
4. 回填:更新对应文档(需求 FR / 开发指导附录 / 规范条款)+ devlog 周记
  ↓
5. 单人开发提示:把"当时为什么这么选"写进 AD,答辩被问到时直接引用

底线(不可违反):不执行被分析代码;密钥不入库;三端验证命令通过;AC 用例不倒退。