项目设计与 CodeReview
Project Design & Code Review Guide — 从零设计项目到高质量交付,面试中讲出令人信服的项目经验
1. 需求分析
1.1 为什么需求分析是第一步
很多初级开发者拿到需求就写代码,这是最大的错误。先想清楚再做,是区分初级和高级工程师的第一道分水岭。
需求分析就像建工厂之前的可行性研究报告。如果没搞清楚要生产什么、产量多大、原料从哪来就开工建厂房,建成后要么产能不足,要么设备用不上,要么物流动线混乱 — 全部推倒重来。
1.2 高级工程师如何读需求
读需求不是"看一遍就过了",而是要追问五个维度:
五个关键问题 (必须问清楚)
| 维度 | 问题 | 为什么重要 | 影响什么 |
|---|---|---|---|
数据量 |
预计有多少数据?增长速度? | 决定数据库选型和分表策略 | 1万条 vs 1亿条是完全不同的架构 |
并发量 |
同时多少用户访问?QPS 多少? | 决定是否需要缓存、消息队列 | 10 QPS 和 10000 QPS 差1000倍 |
实时性 |
数据需要实时还是准实时? | 决定是否需要 WebSocket/轮询 | 秒级 vs 分钟级 vs 小时级 |
用户数 |
目标用户群多大? | 决定权限体系和部署方案 | 内部5人 vs 全厂500人 vs 对外 |
峰值 |
有没有流量高峰时段? | 决定弹性扩容策略 | 月结报表时访问量是平时10倍 |
1.3 功能性 vs 非功能性需求
| 类型 | 定义 | 示例 (制造看板) | 如何量化 |
|---|---|---|---|
| 功能性 | 系统"做什么" | 展示OEE指标、设备状态、产量趋势 | 功能清单 + 验收标准 |
| 非功能性 | 系统"做得怎样" | 页面加载 <2s、可用性 99.9%、支持50并发 | 性能指标 + SLA |
1.4 需求分析 Checklist
Production Case: 需求理解偏差导致整个模块重做
开发了一个设备维修管理模块,花了3周完成。交付后车间主任说:"我要的不是修设备,是设备保养计划 — 预防性的,不是事后维修记录。"
只看了需求文档标题"设备维护",没追问具体业务场景。"维护"在IT眼里是"修坏的东西",在车间眼里是"定期保养"。
3周代码全部废弃。重新做需求分析 — 跟车间主任聊了2小时,明确了是预防性保养计划:基于设备运行时长自动触发保养提醒,保养完成后记录并重置计时。
需求文档里的每一个名词都要确认语义。尤其在不同行业,"同一个词"可能含义完全不同。写代码之前,先用白话把功能描述给业务方确认一遍。
2. 技术选型
2.1 选型决策框架
技术选型不是"哪个新用哪个",而是要综合考虑四个维度:
用团队最熟悉的,而不是最热门的。一个用 Vue 做过3个项目的团队,比用 React 从零开始的团队,交付速度快3倍。面试官问"为什么选这个技术"时,你要能说出理由,而不仅仅是"我喜欢"。
2.2 制造看板项目 — 完整选型表
以一个生产可视化看板项目为例,展示完整的技术选型过程:
| 层级 | 选择 | 为什么 | 备选方案 | 不选备选的原因 |
|---|---|---|---|---|
| 前端框架 | Vue 3 | 团队熟悉、中文社区好、组合式API灵活 | React 18 | 团队无 React 经验,学习成本高 |
| 图表库 | ECharts | 国内最流行、工业图表支持好、大数据量性能优 | D3.js / Chart.js | D3 学习曲线太陡;Chart.js 图表类型不够 |
| UI 组件 | Element Plus | Vue 3 生态最成熟、表单/表格组件丰富 | Ant Design Vue | 都可以,Element 的文档更适合快速上手 |
| 后端框架 | Java + Spring Boot | 企业级首选、生态完善、MyBatis/JPA 成熟、运维工具链全 | Node.js Express / Python Flask | Node.js 单线程不适合 CPU 密集场景;Python 性能弱于 Java |
| 关系数据库 | MySQL 8 | 工业数据是结构化的、查询场景明确、运维成熟 | PostgreSQL | 都可以,MySQL DBA 更好招 |
| 缓存 | Redis | 看板数据高频读取、会话管理、排行榜/实时指标 | Memcached | Redis 支持更多数据结构,适用场景更广 |
| 部署 | Docker + Nginx | 环境一致性、快速部署、Nginx 反向代理+静态资源 | K8s / 裸机部署 | K8s 对小团队太复杂;裸机不好维护和迁移 |
技术选型就像选生产线设备。不是最贵最好的就合适 — 如果你的工厂只需要年产1000件,买一条年产100万件的全自动线就是浪费。同样,一个5人用的内部系统,不需要 Kubernetes + 微服务 + Kafka。
3. 架构设计
3.1 系统架构图: 制造看板系统
下面是完整的制造看板系统架构。面试中画架构图是必考环节,你需要能从左到右讲清楚每一层的职责。
面试中如何讲架构
"我们的系统分为5层。最底层是数据源,从MES和SCADA系统采集数据,经过ETL清洗后存入MySQL。Redis作为缓存层,存放高频访问的实时指标。后端是Java+Spring Boot+MyBatis,提供RESTful API。Nginx做反向代理和静态资源托管。前端是Vue3+ECharts,支持PC和大屏两种展示方式。"
3.2 架构模式选择
| 模式 | 适合场景 | 优点 | 缺点 | 什么时候用 |
|---|---|---|---|---|
| 单体架构 | 小型项目、团队 <5人 | 开发快、部署简单、调试方便 | 代码耦合、不好扩展 | 你的第一个项目 → 先从这开始 |
| 微服务 | 大型项目、多团队协作 | 独立部署、技术栈灵活 | 运维复杂、网络开销 | QPS >10000、团队 >10人时考虑 |
| 分层架构 | 大多数企业应用 | 职责清晰、便于维护 | 跨层调用有性能损耗 | 推荐的默认架构 |
3.3 设计原则
关注点分离 (Separation of Concerns)
sendEmail() + auth() +
saveToDB() + exportPDF()
4. 数据库设计
4.1 数据库设计五步法
4.2 制造看板数据库设计 — ER 图
4.3 索引设计原则
很多人在建表时只写字段不建索引,等项目上线后查询慢了才想起来加。这时候表里已经有几百万条数据,加索引时数据库会被锁住,线上服务直接卡死。
| 原则 | 说明 | 示例 |
|---|---|---|
| 查询驱动 | 根据 WHERE / JOIN / ORDER BY 的字段建索引 | production_records 经常按 machine_id + record_date 查询 |
| 最左前缀 | 联合索引 (a,b,c) 可以覆盖 a / a,b / a,b,c | INDEX(machine_id, record_date) 覆盖按机器查和按机器+日期查 |
| 避免过度 | 索引不是越多越好,每个索引都增加写入开销 | 一张表 5-8 个索引合理,超过 10 个要注意 |
| 选择性高 | 区分度高的字段放前面 | status 只有3个值,不适合做索引第一个字段 |
-- production_records 的索引设计
CREATE INDEX idx_machine_date ON production_records(machine_id, record_date);
CREATE INDEX idx_product_date ON production_records(product_id, record_date);
CREATE INDEX idx_record_date ON production_records(record_date);
-- downtime_records 的索引设计
CREATE INDEX idx_downtime_machine ON downtime_records(machine_id, start_time);
CREATE INDEX idx_downtime_date ON downtime_records(start_time);
Production Case: 后期加索引很痛苦
看板页面打开需要 12秒。用户投诉"比以前慢了很多"。数据库CPU持续90%+。
production_records 表有 380万条 数据,查询 OEE 的SQL没有任何索引,每次都是全表扫描。EXPLAIN 显示 type: ALL,扫描了全部380万行。
执行 ALTER TABLE production_records ADD INDEX idx_machine_date(machine_id, record_date); — 这条语句跑了8分钟,期间表被锁定,所有写入请求超时。最终查询时间从12秒降到 0.05秒。
在建表时就设计好索引。如果后期必须加索引,要在低峰期操作,或使用 pt-online-schema-change 等工具在线加索引。面试中讲这个故事非常有说服力。
5. API 设计
5.1 RESTful API 设计规范
- 名词用复数:
/api/machines而不是/api/machine - 动词用HTTP方法: GET(查) / POST(增) / PUT(改) / DELETE(删)
- 版本控制:
/api/v1/machines为未来兼容留余量 - 过滤用参数:
?line_id=1&date=2025-06-01
5.2 制造看板 API 列表
| 方法 | 路径 | 功能 | 关键参数 |
|---|---|---|---|
| GET | /api/v1/oee | 获取OEE指标(可用率×性能率×质量率) | machine_id, line_id, start_date, end_date |
| GET | /api/v1/production | 获取产量数据 | line_id, date, shift_id |
| GET | /api/v1/downtime | 获取停机记录 | machine_id, start_date, end_date |
| GET | /api/v1/quality | 获取质量数据 | product_id, date |
| GET | /api/v1/energy | 获取能耗数据 | workshop, date |
| GET | /api/v1/machines | 获取设备列表及状态 | line_id, status |
| GET | /api/v1/machines/:id | 获取单个设备详情 | — |
| POST | /api/v1/production | 录入产量数据 | Body: JSON |
| POST | /api/v1/downtime | 录入停机记录 | Body: JSON |
| PUT | /api/v1/production/:id | 修改产量记录 | Body: JSON |
| DELETE | /api/v1/downtime/:id | 删除停机记录 | — |
5.3 响应格式标准
// 标准成功响应
{
"code": 200,
"message": "success",
"data": {
"list": [...],
"pagination": {
"page": 1,
"page_size": 20,
"total": 1580,
"total_pages": 79
}
},
"timestamp": "2025-06-09T14:30:00Z"
}
// 标准错误响应
{
"code": 400,
"message": "Invalid parameter: machine_id is required",
"error_detail": {
"field": "machine_id",
"reason": "must be a positive integer"
},
"timestamp": "2025-06-09T14:30:00Z"
}
5.4 分页/排序/过滤设计
// 分页 + 排序 + 过滤 统一参数格式
GET /api/v1/production?page=1&page_size=20&sort_by=record_date&sort_order=desc&line_id=3&start_date=2025-06-01&end_date=2025-06-09
// 后端实现 (Node.js / Express)
const { page = 1, page_size = 20, sort_by = 'record_date', sort_order = 'desc' } = req.query;
const offset = (page - 1) * page_size;
// MySQL 查询
const sql = `SELECT * FROM production_records
WHERE line_id = ? AND record_date BETWEEN ? AND ?
ORDER BY ${sort_by} ${sort_order}
LIMIT ? OFFSET ?`;
Production Case: API没有分页,一次返回50万条数据
前端页面直接白屏卡死。Chrome DevTools 显示接口返回了 120MB 的 JSON,浏览器内存溢出。后端日志显示单次查询耗时 45秒。
GET /api/v1/production 接口没有分页参数,直接 SELECT * FROM production_records,返回了全部 50万条数据。
1. 加了 page 和 page_size 参数,默认每页20条
2. 数据库层面加 LIMIT OFFSET
3. 强制最大 page_size=100,超过自动截断
4. 加了接口超时限制:超过10秒自动中断
所有列表接口必须有分页,这是硬性要求。即使现在数据量小,将来也会增长。好的API设计是面向未来3-5年的。
6. CodeReview 指南
6.1 为什么 CodeReview 很重要
CodeReview 就像工厂里的质检工序。没有质检,次品直接流到客户手中 — 在软件里,次品就是 bug、安全漏洞、性能问题。Review 不是"找茬",而是两个人的智慧总比一个人强。
| 收益 | 具体表现 | 量化指标 |
|---|---|---|
| 提高质量 | 在合并前发现 bug | Google 数据: Review 阻止了 60-90% 的严重 bug 上线 |
| 知识共享 | 团队成员互相学习 | 新成员通过 Review 快速了解代码风格和架构 |
| 统一风格 | 代码库保持一致性 | 减少 "看谁写的" 的时间 |
| 安全兜底 | 发现 SQL 注入、XSS 等问题 | 安全漏洞在 Review 阶段被发现,成本比线上修复低 100倍 |
6.2 CodeReview 流程图
6.3 Review Checklist (20项)
a, b, temp, data)console.log(err) 就完事了)6.4 Review 评论: 好与坏
| 风格 | 评论示例 | 为什么好/坏 |
|---|---|---|
| 坏的 | "这代码写得不行" | 攻击性、没有建设性、没有具体建议 |
| 坏的 | "重构一下" | 太笼统,不知道要重构什么、怎么重构 |
| 好的 | "这个函数有60行,建议拆成3个小函数,每个负责一个职责。参考 XXX 模块的写法。" | 具体、有建议、有参考 |
| 好的 | "这里的 SQL 用了字符串拼接,有注入风险。建议改用参数化查询: pool.query(sql, [params])" |
指出风险、给出方案、附带代码示例 |
6.5 如何接收 Review 反馈
- Review 是对事不对人 — 评价的是代码,不是你这个人
- 不懂就问 — "能解释一下为什么这样改更好吗?" 是好问题
- 记录学到的东西 — 每次被 Review 都是一次免费学习
- 不要反驳后不改 — 如果 Reviewer 说得有道理,就改
- 有理有据地讨论 — 如果你不同意,用数据/文档说明原因
Production Case: CodeReview发现了一个SQL注入漏洞
Review一段查询接口代码时,发现了这行:
// 危险! 直接拼接用户输入
const sql = `SELECT * FROM machines WHERE line_id = ${req.query.line_id}`;
pool.query(sql, (err, results) => { ... });
如果攻击者传入 line_id=1; DROP TABLE machines; --,就会执行 DROP TABLE,整个表被删除。这就是经典的 SQL 注入攻击。
// 安全: 使用参数化查询
const sql = 'SELECT * FROM machines WHERE line_id = ?';
pool.query(sql, [req.query.line_id], (err, results) => { ... });
永远不要拼接 SQL。这条规则刻在脑子里。这个漏洞如果上线,攻击者可以读取、修改、删除数据库中的任何数据。Review 就是最后一道防线。
7. 面试中如何讲项目
7.1 STAR 法则
面试中讲项目,永远用 STAR 法则。这是全球通用的结构化表达方式:
花80%的时间讲背景,20%讲做了什么。 面试官想听的核心是 Action 和 Result。建议分配: S(15%) + T(10%) + A(50%) + R(25%)。
7.2 三个完整 STAR 项目范例
Project 1: 生产可视化看板
Vue + ECharts + Node.js + MySQL一家汽车零部件制造厂,有3条产线、45台设备。之前用Excel记录产量,每天花2小时手动汇总。管理层看不到实时数据,决策滞后。工厂决定上一套生产可视化看板系统。
我是唯一的全栈开发,负责从需求分析到上线部署的全流程。需要在3个月内交付可用的看板系统,支持实时OEE展示和产量趋势分析。
需求分析: 与车间主任沟通,确定5个核心指标:OEE、产量、停机时长、不良率、能耗
技术选型: Vue3+ECharts(图表丰富)+Java Spring Boot(企业级后端,MyBatis+JPA生态)+MySQL(结构化工业数据)+Redis(实时指标缓存)
数据库设计: 设计了维度表(设备/产线/班次/产品)+事实表(生产记录/停机记录/质量记录),提前建好联合索引
性能优化: 引入Redis缓存OEE指标(5分钟刷新),查询从3秒降到50ms。大屏数据用WebSocket推送,实时性从5分钟提升到秒级
部署: Docker容器化+Nginx反向代理,一键部署到工厂内网服务器
✓ 看板页面加载 <1.5秒,数据刷新延迟 <3秒
✓ 每天节省2小时人工汇总时间,3条产线年节省约 1500工时
✓ 设备异常响应时间从30分钟缩短到3分钟(看板实时报警)
✓ OEE从85%提升到91%(可视化暴露了停机损失)
✓ 系统稳定运行6个月0故障,日活用户45人
Project 2: 报表自动化系统
Python + SQL Server + Docker质量部门每月要生成30+份报表,包含质量趋势、不良品分析、供应商评分等。之前全部手工从SQL Server导数据到Excel,再手动画图。一个报表平均要4小时,每月花在报表上的时间超过 120小时。
我负责开发一套报表自动化系统: 配置好数据源和模板后,一键生成PDF报表。要求支持定时自动运行,并将结果邮件发送给相关人员。
架构设计: 三层结构 — 配置层(定义数据源+模板)、执行层(Python脚本+SQL查询)、输出层(生成PDF+发送邮件)
模板引擎: 用 Jinja2 模板定义报表结构,支持变量替换和条件渲染。用户只需修改模板,不需要改代码
SQL优化: 原来手工查数据用嵌套子查询(跑5分钟),我改写成窗口函数(跑8秒),性能提升37倍
定时调度: 用 APScheduler 实现定时任务,支持 cron 表达式。每月1号自动生成上月报表
容器化: Docker 打包,包含所有依赖,在新机器上5分钟部署完成
✓ 30+份报表从每月120小时降到2小时(自动生成+人工校验)
✓ 报表准确率从92%提升到99.8%(消除人工复制粘贴错误)
✓ SQL查询优化后,单次报表生成从5分钟降到8秒
✓ 系统运行1年,累计生成360+份报表,节省约 1400+工时
Project 3: ETL数据平台
Python + pandas + SQL Server + Redis工厂数据分散在5个系统中: MES(生产)、QMS(质量)、SCADA(设备)、ERP(物料)、Excel(人工补录)。数据格式不统一,口径不一致,做一份跨系统分析需要3天。急需一个统一的数据平台。
我负责设计并开发一套 ETL数据平台: 从多个异构数据源抽取(Extract)数据,经过清洗转换(Transform),加载(Load)到统一的数据仓库中,供看板和报表使用。
数据抽取: 用 Python 连接 MES/MSSQL/Oracle/Excel 四种数据源,统一抽取为 DataFrame
数据清洗: 用 pandas 处理缺失值(用前值填充)、异常值(3-sigma检测)、重复数据(按时间戳+设备ID去重)
数据转换: 统一字段命名规范(machine_id vs MachineID)、时间格式(UTC+8)、单位换算
缓存层: Redis 缓存热点查询结果,ETL任务状态用 Redis 管理,支持断点续传
监控: 每次 ETL 记录日志(抽取行数/耗时/异常),异常时自动发邮件通知
最大挑战: SCADA 数据每秒1000条,直接写数据库导致 I/O 瓶颈。解决方案: 先缓存到 Redis List,每分钟批量写入数据库,写入性能提升50倍
✓ 整合5个数据源,日处理数据量 200万+条
✓ 跨系统分析从3天降到10分钟
✓ ETL 任务执行时间从45分钟优化到6分钟
✓ 数据一致性校验通过率 99.5%
✓ 系统稳定运行8个月,累计处理 5亿+条数据记录
7.3 高频面试问答
回答模板:
S: "在开发ETL数据平台时,SCADA系统每秒产生1000条数据,直接写入SQL Server导致数据库I/O打满,ETL任务频繁超时。"
T: "我需要在保证数据不丢失的前提下,解决高频写入的性能瓶颈。"
A: "我设计了缓冲队列方案: 先将数据缓存到Redis List中,然后用定时任务每分钟批量从Redis取出数据,用批量INSERT写入数据库。同时加了监控,如果Redis积压超过阈值就报警。"
R: "写入性能从每秒50条提升到每秒2500条(50倍),ETL任务超时率从30%降到0%,数据零丢失。"
回答模板:
"最有技术含量的是ETL数据平台。技术难点在于三点: 第一,整合5个异构数据源,需要处理不同的数据库驱动、编码、时间格式。第二,SCADA高频数据的实时写入,我设计了Redis缓冲+批量写入的方案,写入性能提升50倍。第三,数据质量保证 — 我设计了数据校验规则引擎,能自动检测缺失值、异常值、重复数据,校验通过率99.5%。"
回答模板:
"我主要考虑四个维度: 团队技能(我们团队熟悉 Java 和 Vue)、项目规模(内部系统,50并发以内)、时间线(3个月交付)、生态成熟度(出问题能快速找到解决方案)。基于这四个维度,前端选Vue3而不是React,后端选 Java Spring Boot — 企业级首选,生态成熟,团队最熟悉。数据库选MySQL因为工业数据是高度结构化的。Redis做缓存是因为看板有高频读取场景。"
- 用数字说话: "提升了50%"比"提升了"有说服力100倍
- 讲挑战不讲流水账: 面试官不关心你用了什么组件,关心你解决了什么难题
- 准备3个故事: 一个技术挑战 + 一个协作挑战 + 一个性能优化
- 准备好追问: "为什么不用X?" "如果重新做你会怎么改?" "你学到了什么?"
- 诚实: 没做过的不要说做过,但可以说"我了解原理,没有实际项目经验"
8. 15个设计原则
面试中经常问到设计原则,不需要背教科书,但要能用自己的话解释清楚,并结合实际举例。
// Bad: 重复的日期格式化代码出现5次
const date1 = d.toLocaleDateString();
const date2 = d.toLocaleDateString();
// Good: 提取公共函数
const formatDate = (d) => d.toLocaleDateString();
// Bad: 过度设计
class DateFormatterFactory { ... }
// Good: 简单直接
const formatDate = (d) => d.toLocaleDateString();
// Bad: 提前设计了不需要的多语言支持
const i18n = { zh: {...}, en: {...}, ja: {...} };
// Good: 先只做中文,需要时再加
const labels = { title: '生产看板' };
// routes/oee.js - 只管路由
router.get('/oee', oeeController.getOEE);
// controllers/oee.js - 只管业务逻辑
exports.getOEE = (req, res) => { ... };
// models/production.js - 只管数据
exports.getByMachine = (id) => { ... };
// Bad: 一个函数做三件事
function processOrder() { /* 验证 + 计算 + 发邮件 */ }
// Good: 拆成三个函数
function validateOrder() { }
function calculateTotal() { }
function sendNotification() { }
// Bad: 每加一个报表类型就改switch
switch(type) { case 'oee': ...; case 'quality': ...; }
// Good: 注册机制,新报表只需注册
const reports = {};
const registerReport = (type, fn) => reports[type] = fn;
// Bad: 子类改变了父类行为
class Bird { fly() {} }
class Penguin extends Bird {
fly() { throw new Error("can't fly"); }
}
// Good: 更合理的继承结构
class Bird { move() {} }
class FlyingBird extends Bird { fly() {} }
// Bad: 一个大接口包含所有方法
interface IWorker { work(); eat(); sleep(); }
// Good: 拆成小接口
interface IWorkable { work(); }
interface IFeedable { eat(); }
// Bad: 直接依赖具体实现
const db = new MySQLDatabase();
// Good: 依赖抽象,可替换
const db = createDatabase(config);
// 可以是 MySQL / PostgreSQL / SQLite
// Bad: 深层继承
class AdminUser extends User { }
// Good: 组合
const user = {
auth: new RoleAuth('admin'),
logger: new ActivityLogger()
};
a.b.c.d。// Bad: 知道太多内部结构
order.customer.address.city;
// Good: 通过方法获取
order.getShippingCity();
// Bad: 不校验,错误到后面才爆
function createOrder(data) { save(data); }
// Good: 入口就校验
function createOrder(data) {
if (!data.items) throw new Error('items required');
save(data);
}
// 修 bug 时顺便:
// - 重命名一个不清楚的变量
// - 加一条有用的注释
// - 删掉一行无用代码
// 每次一点点,代码库越来越好
// Bad: 还没上线就加缓存
const cache = new Map();
// Good: 上线后,用数据证明需要缓存
// "这个接口平均响应 3s,加了缓存降到 50ms"
// Bad: 每个路由都配置一遍
app.get('/api/v1/oee', ...);
app.get('/api/v1/production', ...);
// Good: 约定目录结构自动注册路由
// routes/oee.js → /api/v1/oee
// routes/production.js → /api/v1/production
autoRegisterRoutes(app);
8.1 SOLID 原则速查表
| 字母 | 原则 | 一句话 | 违反的标志 |
|---|---|---|---|
| S | Single Responsibility | 一个类只做一件事 | 改一个需求要改这个类的3个地方 |
| O | Open/Closed | 加功能不改老代码 | 每次加功能都要改switch/if-else |
| L | Liskov Substitution | 子类能替换父类 | 子类throw了父类没声明的异常 |
| I | Interface Segregation | 接口要小而专 | 实现接口时有空方法(用不到) |
| D | Dependency Inversion | 依赖抽象不依赖具体 | 直接new一个具体类,不好测试 |