EvoCode 开发规范(详细版)
适用于 backend / analyzer / frontend 三端 · 单人开发也必须遵守(保证毕业设计质量与可维护性)
版本:v1.3 · 2026-08-10
使用说明(灵活性声明):本规范是基线约定,不是死规定。附录 A 的代码骨架是"推荐写法"而非强制模板;当规范与实际情况冲突时,具体情况具体分析——记录决策(AD + devlog)后合理偏离,事后回填规范。任何偏离不得绕过两条底线:① 三端验证命令通过;② 不引入安全风险(见 §7)。
目录
- 协作与 Git 规范
- 后端规范(Spring Boot 3 / Java 17)
- 前端规范(Vue3 + TS)
- Analyzer 规范(Python / FastAPI)
- 接口契约管理
- 日志与异常规范
- 安全规范
- 环境与配置管理
- 测试规范
- 质量门禁与 CI
- 文档与知识沉淀
- 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-upload、feature/ai-report、fix/scan-ignore-rule、docs/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 流程
- 从 main 切分支 → 开发 → 本地三端验证(见 §10)
- 提交 PR,按模板填写(做了什么/为什么/验证方式/影响范围)
- 自己先 review 一遍 diff(换位评审),再合并
- squash merge,删除分支
1.4 Tag 与版本
- 里程碑打 tag:
v0.1.0(MVP)、v0.2.0、v1.0.0
- 版本号语义化:
major.minor.patch
1.5 其他
- 不使用 Git LFS(禁止入库大文件;项目代码存运行时目录,不入库)
.gitignore 必须包含:.env、data/、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_at ↔ lastAnalyzedAt |
| 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.vue、RelationChart.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.ts:baseURL=/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 用例不倒退。