Web 系统架构设计与开发实战
❓ 面试官:如果让你从零设计一个中型的企业级管理系统(CRM/OA/ERP/MES),你会如何设计整体架构?【中高级】
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论): 采用 "前后端分离 + 轻量级单体架构",前端 Vue3 + Element Plus/Ant Design Vue,后端 Spring Boot + MyBatis-Plus,数据库 MySQL + Redis 缓存。不盲目上微服务,优先保证快速交付和易维护性,1-3 个月内完成 MVP 版本上线。
📝 完整架构设计方案:
1. 技术选型(实用主义导向)
前端技术栈:
- 框架:Vue 3 + TypeScript + Vite
- UI 组件库:Element Plus(通用)或 Ant Design Vue(企业级更专业)
- 状态管理:Pinia(比 Vuex 更简洁)
- 路由:Vue Router 4
- HTTP 请求:Axios(封装拦截器处理 Token、错误统一提示)
- 图表:ECharts(数据看板、统计报表)
- 流程引擎:bpmn-js(工作流可视化,OA/ERP 必备)
后端技术栈:
- 框架:Spring Boot 2.7+(稳定版本)
- ORM:MyBatis-Plus(简化 CRUD,支持分页、条件构造器)
- 数据库:MySQL 8.0(主从复制预留扩展空间)
- 缓存:Redis 6+(热点数据缓存、分布式锁、Session 共享)
- 权限控制:Spring Security + JWT(无状态认证,支持多端登录)
- 工作流引擎:Flowable/Activiti(OA 审批流、ERP 业务流程)
- 文档:Knife4j(Swagger 增强版,在线接口文档)
- 工具类:Hutool(国产工具库,减少重复造轮子)
- Excel 处理:EasyExcel(大数据量导入导出,CRM/ERP 高频需求)
部署方案:
- 容器化:Docker + Docker Compose(一键部署)
- 反向代理:Nginx(静态资源托管、负载均衡、HTTPS)
- 日志:Logback + ELK(可选,初期直接看日志文件)
- 监控:Spring Boot Actuator + Prometheus + Grafana(可选)
2. 数据库设计核心要点
关键表结构设计:
-- 1. 用户表(支持多角色、多部门)
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(128) NOT NULL COMMENT '加密密码',
real_name VARCHAR(64) COMMENT '真实姓名',
phone VARCHAR(20) COMMENT '手机号',
email VARCHAR(128) COMMENT '邮箱',
dept_id BIGINT COMMENT '部门ID',
position VARCHAR(64) COMMENT '职位',
status TINYINT DEFAULT 1 COMMENT '状态:0禁用 1启用',
last_login_time DATETIME COMMENT '最后登录时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_phone (phone),
INDEX idx_email (email),
INDEX idx_dept_id (dept_id)
);
-- 2. 部门表(树形结构)
CREATE TABLE sys_dept (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
parent_id BIGINT DEFAULT 0 COMMENT '父部门ID',
dept_name VARCHAR(64) NOT NULL COMMENT '部门名称',
order_num INT DEFAULT 0 COMMENT '排序',
leader VARCHAR(64) COMMENT '负责人',
phone VARCHAR(20) COMMENT '联系电话',
email VARCHAR(128) COMMENT '邮箱',
status TINYINT DEFAULT 1 COMMENT '状态:0禁用 1启用'
);
-- 3. 角色表
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_code VARCHAR(64) NOT NULL UNIQUE COMMENT '角色编码:admin/sales/manager',
role_name VARCHAR(64) NOT NULL COMMENT '角色名称',
description VARCHAR(255) COMMENT '角色描述',
data_scope TINYINT DEFAULT 1 COMMENT '数据权限:1全部 2本部门 3本部门及子部门 4仅本人'
);
-- 4. 用户-角色关联表(多对多)
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id),
INDEX idx_role_id (role_id)
);
-- 5. 菜单/权限表(树形结构)
CREATE TABLE sys_menu (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
parent_id BIGINT DEFAULT 0 COMMENT '父菜单ID',
menu_name VARCHAR(64) NOT NULL COMMENT '菜单名称',
menu_type TINYINT COMMENT '类型:1目录 2菜单 3按钮',
path VARCHAR(128) COMMENT '路由路径',
component VARCHAR(128) COMMENT '组件路径',
permission VARCHAR(128) COMMENT '权限标识:customer:list',
sort_order INT DEFAULT 0 COMMENT '排序',
icon VARCHAR(64) COMMENT '图标',
visible TINYINT DEFAULT 1 COMMENT '是否显示'
);
-- 6. 客户表(CRM 核心)
CREATE TABLE crm_customer (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
customer_no VARCHAR(64) NOT NULL UNIQUE COMMENT '客户编号',
customer_name VARCHAR(128) NOT NULL COMMENT '客户名称',
customer_type TINYINT COMMENT '类型:1企业 2个人',
industry VARCHAR(64) COMMENT '所属行业',
source VARCHAR(64) COMMENT '客户来源',
level VARCHAR(32) COMMENT '客户等级:A/B/C',
owner_id BIGINT COMMENT '负责人ID(销售)',
phone VARCHAR(20) COMMENT '联系电话',
email VARCHAR(128) COMMENT '邮箱',
address VARCHAR(255) COMMENT '地址',
status TINYINT DEFAULT 1 COMMENT '状态:1潜在 2跟进中 3成交 4流失',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_owner_id (owner_id),
INDEX idx_status (status),
INDEX idx_customer_name (customer_name)
);
-- 7. 跟进记录表(CRM)
CREATE TABLE crm_follow_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
customer_id BIGINT NOT NULL COMMENT '客户ID',
follow_type TINYINT COMMENT '跟进方式:1电话 2拜访 3邮件 4微信',
content TEXT COMMENT '跟进内容',
next_follow_time DATETIME COMMENT '下次跟进时间',
operator_id BIGINT COMMENT '跟进人ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_customer_id (customer_id),
INDEX idx_operator_id (operator_id)
);
-- 8. 审批流程表(OA/ERP 核心)
CREATE TABLE oa_approval (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
approval_no VARCHAR(64) NOT NULL UNIQUE COMMENT '审批单号',
business_type VARCHAR(64) NOT NULL COMMENT '业务类型:leave/purchase/reimburse',
business_id BIGINT COMMENT '业务数据ID',
applicant_id BIGINT NOT NULL COMMENT '申请人ID',
status TINYINT DEFAULT 0 COMMENT '状态:0草稿 1审批中 2已通过 3已拒绝 4已撤回',
current_node VARCHAR(64) COMMENT '当前节点',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_applicant_id (applicant_id),
INDEX idx_status (status)
);
-- 9. 审批记录表
CREATE TABLE oa_approval_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
approval_id BIGINT NOT NULL COMMENT '审批单ID',
node_name VARCHAR(64) COMMENT '节点名称',
approver_id BIGINT COMMENT '审批人ID',
action TINYINT COMMENT '操作:1同意 2拒绝 3转交',
comment VARCHAR(500) COMMENT '审批意见',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_approval_id (approval_id)
);
-- 10. 物料表(ERP/MES 核心)
CREATE TABLE erp_material (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
material_no VARCHAR(64) NOT NULL UNIQUE COMMENT '物料编码',
material_name VARCHAR(128) NOT NULL COMMENT '物料名称',
material_type TINYINT COMMENT '类型:1原材料 2半成品 3成品',
spec VARCHAR(128) COMMENT '规格型号',
unit VARCHAR(32) COMMENT '单位',
price DECIMAL(10,2) COMMENT '单价',
stock_quantity DECIMAL(10,2) DEFAULT 0 COMMENT '库存数量',
safety_stock DECIMAL(10,2) COMMENT '安全库存',
supplier_id BIGINT COMMENT '默认供应商ID',
status TINYINT DEFAULT 1 COMMENT '状态:0停用 1启用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_material_no (material_no),
INDEX idx_material_type (material_type)
);
-- 11. 工单表(MES 核心)
CREATE TABLE mes_work_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '工单号',
product_id BIGINT NOT NULL COMMENT '产品ID',
quantity INT NOT NULL COMMENT '计划数量',
completed_quantity INT DEFAULT 0 COMMENT '已完成数量',
status TINYINT DEFAULT 0 COMMENT '状态:0待生产 1生产中 2已完成 3已暂停',
plan_start_time DATETIME COMMENT '计划开始时间',
plan_end_time DATETIME COMMENT '计划结束时间',
actual_start_time DATETIME COMMENT '实际开始时间',
actual_end_time DATETIME COMMENT '实际结束时间',
workshop_id BIGINT COMMENT '车间ID',
line_id BIGINT COMMENT '产线ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_order_no (order_no),
INDEX idx_status (status),
INDEX idx_plan_start_time (plan_start_time)
);
-- 12. 生产报工表(MES)
CREATE TABLE mes_production_report (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
work_order_id BIGINT NOT NULL COMMENT '工单ID',
process_id BIGINT COMMENT '工序ID',
quantity INT NOT NULL COMMENT '报工数量',
qualified_quantity INT COMMENT '合格数量',
defective_quantity INT COMMENT '不合格数量',
operator_id BIGINT COMMENT '操作员ID',
report_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '报工时间',
remark VARCHAR(500) COMMENT '备注',
INDEX idx_work_order_id (work_order_id),
INDEX idx_report_time (report_time)
);设计原则:
- 所有表必须有主键:使用 BIGINT 自增或雪花算法 ID
- 关键字段加索引:查询频繁的字段(如 owner_id、status、create_time)
- 冗余字段优化查询:避免频繁关联查询,适当冗余(如客户名称、物料名称)
- 软删除:重要业务表增加
deleted字段,不物理删除数据 - 审计字段:所有表都有
create_time和update_time,关键表增加create_by和update_by - 数据权限:通过
data_scope实现行级数据权限控制(本部门、本人等)
3. 后端分层架构设计
标准三层架构:
com.example.enterprise
├── controller # 控制层(接收请求、参数校验、返回响应)
│ ├── crm # CRM 模块控制器
│ ├── oa # OA 模块控制器
│ ├── erp # ERP 模块控制器
│ └── mes # MES 模块控制器
├── service # 业务层(核心业务逻辑、事务控制)
│ ├── impl # 业务实现类
│ └── workflow # 工作流服务
├── mapper # 数据访问层(MyBatis-Plus Mapper)
├── entity # 实体类(对应数据库表)
├── dto # 数据传输对象(接收前端参数)
├── vo # 视图对象(返回给前端的数据)
├── config # 配置类(Security、Redis、Swagger、Flowable 等)
├── common # 通用模块
│ ├── Result.java # 统一响应体
│ ├── ResultCode.java # 响应码枚举
│ ├── BusinessException.java # 业务异常
│ ├── GlobalExceptionHandler.java # 全局异常处理器
│ └── DataScopeHandler.java # 数据权限处理器
├── interceptor # 拦截器(JWT 验证、日志记录、数据权限)
├── aspect # AOP 切面(操作日志、数据权限)
└── utils # 工具类(JWT 生成、Excel 导出、工作流工具等)统一响应体设计:
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}全局异常处理:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(400, message);
}
/**
* 权限不足
*/
@ExceptionHandler(AccessDeniedException.class)
public Result<Void> handleAccessDeniedException(AccessDeniedException e) {
return Result.error(403, "权限不足,无法访问");
}
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error(500, "系统繁忙,请稍后再试");
}
}4. 权限控制设计(RBAC + 数据权限)
核心思路:基于角色的访问控制(RBAC)+ 行级数据权限控制
JWT Token 结构:
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration; // 过期时间(毫秒),默认 2 小时
/**
* 生成 Token
*/
public String generateToken(Long userId, String username, List<String> roles, Long deptId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("username", username)
.claim("roles", roles)
.claim("deptId", deptId) // 部门ID,用于数据权限
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
/**
* 解析 Token
*/
public Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 验证 Token
*/
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
}权限拦截器:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 1. 从请求头获取 Token
String token = resolveToken(request);
if (token != null && tokenProvider.validateToken(token)) {
// 2. 解析 Token 获取用户信息
Claims claims = tokenProvider.parseToken(token);
Long userId = Long.parseLong(claims.getSubject());
String username = claims.get("username", String.class);
List<String> roles = claims.get("roles", List.class);
Long deptId = claims.get("deptId", Long.class);
// 3. 构建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userId, null, roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())
);
// 4. 将用户信息放入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// 5. 将用户信息放入 ThreadLocal,供后续使用
UserContext.setUserId(userId);
UserContext.setUsername(username);
UserContext.setDeptId(deptId);
}
} catch (Exception e) {
log.error("JWT 认证失败", e);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}数据权限拦截(AOP 实现):
@Aspect
@Component
public class DataScopeAspect {
/**
* 拦截带有 @DataScope 注解的方法
*/
@Before("@annotation(dataScope)")
public void doBefore(JoinPoint point, DataScope dataScope) {
// 获取当前用户信息
Long userId = UserContext.getUserId();
Long deptId = UserContext.getDeptId();
List<String> roles = UserContext.getRoles();
// 如果是超级管理员,不限制数据权限
if (roles.contains("admin")) {
return;
}
// 根据角色的数据范围构造 SQL 过滤条件
String dataScopeFilter = buildDataScopeFilter(dataScope, userId, deptId);
// 将过滤条件放入参数中,供 Mapper 使用
Map<String, Object> params = new HashMap<>();
params.put("dataScope", dataScopeFilter);
// 如果方法参数中有 Map 类型,注入过滤条件
for (Object arg : point.getArgs()) {
if (arg instanceof Map) {
((Map<?, ?>) arg).putAll(params);
}
}
}
/**
* 构造数据权限过滤条件
*/
private String buildDataScopeFilter(DataScope dataScope, Long userId, Long deptId) {
// 根据角色查询数据权限
Integer dataScopeType = getDataScopeTypeByRole();
switch (dataScopeType) {
case 1: // 全部数据权限
return "";
case 2: // 本部门数据权限
return "AND dept_id = " + deptId;
case 3: // 本部门及子部门数据权限
return "AND dept_id IN (SELECT id FROM sys_dept WHERE ancestors LIKE '%" + deptId + "%')";
case 4: // 仅本人数据权限
return "AND create_by = " + userId;
default:
return "AND 1 = 0"; // 默认无权限
}
}
}
// 使用示例
@DataScope(deptAlias = "d", userAlias = "u")
List<CustomerVO> selectCustomerList(Map<String, Object> params);权限注解使用:
@RestController
@RequestMapping("/api/crm/customers")
public class CustomerController {
@GetMapping
@PreAuthorize("hasAuthority('crm:customer:list')") // 需要客户列表权限
public Result<Page<CustomerVO>> listCustomers(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
// ...
}
@PostMapping
@PreAuthorize("hasAuthority('crm:customer:add')") // 需要新增客户权限
public Result<Void> addCustomer(@RequestBody @Valid CustomerDTO dto) {
// ...
}
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('crm:customer:edit')") // 需要编辑客户权限
public Result<Void> updateCustomer(@PathVariable Long id, @RequestBody @Valid CustomerDTO dto) {
// ...
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('crm:customer:delete')") // 需要删除客户权限
public Result<Void> deleteCustomer(@PathVariable Long id) {
// ...
}
}5. 缓存策略设计
哪些数据需要缓存:
- 字典数据:客户类型、行业分类、物料类型、审批状态等(更新频率低,读取频繁)
- 用户信息:登录后的用户基本信息、权限列表(减少数据库查询)
- 组织架构:部门树、岗位列表(变化少,查询多)
- 验证码:短信/图形验证码(设置短过期时间 5 分钟)
- 统计数据:今日新增客户数、待审批数量、生产进度(定时刷新)
- 流程定义:工作流 BPMN XML(解析耗时,缓存提升性能)
Redis 缓存实现:
@Service
public class CustomerService {
@Autowired
private CustomerMapper customerMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String CUSTOMER_CACHE_KEY = "crm:customer:";
private static final long CACHE_EXPIRE_HOURS = 2; // 客户信息缓存 2 小时
/**
* 查询客户详情(带缓存)
*/
public CustomerVO getCustomerDetail(Long customerId) {
// 1. 先查缓存
String cacheKey = CUSTOMER_CACHE_KEY + customerId;
CustomerVO cachedCustomer = (CustomerVO) redisTemplate.opsForValue().get(cacheKey);
if (cachedCustomer != null) {
return cachedCustomer;
}
// 2. 缓存未命中,查数据库
Customer customer = customerMapper.selectById(customerId);
if (customer == null) {
throw new BusinessException(404, "客户不存在");
}
CustomerVO vo = convertToVO(customer);
// 3. 写入缓存
redisTemplate.opsForValue().set(cacheKey, vo, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
return vo;
}
/**
* 更新客户(删除缓存)
*/
@Transactional
public void updateCustomer(Long customerId, CustomerDTO dto) {
// 1. 更新数据库
Customer customer = new Customer();
BeanUtils.copyProperties(dto, customer);
customer.setId(customerId);
customerMapper.updateById(customer);
// 2. 删除缓存(下次查询时重新加载)
String cacheKey = CUSTOMER_CACHE_KEY + customerId;
redisTemplate.delete(cacheKey);
}
}缓存穿透防护:
/**
* 查询客户详情(防止缓存穿透)
*/
public CustomerVO getCustomerDetailWithProtection(Long customerId) {
String cacheKey = CUSTOMER_CACHE_KEY + customerId;
// 1. 查缓存
CustomerVO cachedCustomer = (CustomerVO) redisTemplate.opsForValue().get(cacheKey);
if (cachedCustomer != null) {
// 特殊值表示数据库中也不存在
if ("NULL".equals(cachedCustomer.toString())) {
return null;
}
return cachedCustomer;
}
// 2. 查数据库
Customer customer = customerMapper.selectById(customerId);
if (customer == null) {
// 3. 数据库中也不存在,缓存空值(短过期时间)
redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
return null;
}
CustomerVO vo = convertToVO(customer);
// 4. 写入缓存
redisTemplate.opsForValue().set(cacheKey, vo, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
return vo;
}企业级系统典型缓存场景:
- CRM - 客户公海池
// 缓存今日可领取的客户列表(每 10 分钟刷新)
@Scheduled(fixedRate = 10 * 60 * 1000)
public void refreshPublicCustomerCache() {
List<Customer> publicCustomers = customerMapper.selectPublicCustomers();
redisTemplate.opsForValue().set("crm:public:today", publicCustomers, 10, TimeUnit.MINUTES);
}- OA - 待办事项统计
// 缓存用户待办数量(实时更新,过期时间短)
public int getPendingApprovalCount(Long userId) {
String cacheKey = "oa:pending:" + userId;
Integer count = (Integer) redisTemplate.opsForValue().get(cacheKey);
if (count == null) {
count = approvalMapper.countPendingByUserId(userId);
redisTemplate.opsForValue().set(cacheKey, count, 1, TimeUnit.MINUTES);
}
return count;
}- ERP - 物料库存预警
// 缓存库存低于安全库存的物料列表(每小时刷新)
@Scheduled(fixedRate = 60 * 60 * 1000)
public void refreshLowStockCache() {
List<Material> lowStockMaterials = materialMapper.selectLowStockMaterials();
redisTemplate.opsForValue().set("erp:material:low_stock", lowStockMaterials, 1, TimeUnit.HOURS);
}- MES - 产线实时状态
// 缓存产线当前工单和进度(高频查询,短时间缓存)
public WorkOrderStatus getLineStatus(Long lineId) {
String cacheKey = "mes:line:" + lineId + ":status";
WorkOrderStatus status = (WorkOrderStatus) redisTemplate.opsForValue().get(cacheKey);
if (status == null) {
status = workOrderMapper.getCurrentWorkOrderByLineId(lineId);
redisTemplate.opsForValue().set(cacheKey, status, 30, TimeUnit.SECONDS);
}
return status;
}6. 前端工程化设计
项目结构:
src
├── api # API 接口封装
│ ├── crm # CRM 模块接口
│ │ ├── customer.ts # 客户管理
│ │ ├── contact.ts # 联系人
│ │ └── follow.ts # 跟进记录
│ ├── oa # OA 模块接口
│ │ ├── approval.ts # 审批流程
│ │ ├── leave.ts # 请假申请
│ │ └── notice.ts # 通知公告
│ ├── erp # ERP 模块接口
│ │ ├── material.ts # 物料管理
│ │ ├── purchase.ts # 采购管理
│ │ └── inventory.ts # 库存管理
│ ├── mes # MES 模块接口
│ │ ├── workorder.ts # 工单管理
│ │ ├── process.ts # 工序管理
│ │ └── report.ts # 生产报工
│ └── system # 系统管理接口
│ ├── user.ts # 用户管理
│ ├── role.ts # 角色管理
│ └── menu.ts # 菜单管理
├── components # 公共组件
│ ├── BusinessForm.vue # 业务表单通用组件
│ ├── SearchPanel.vue # 高级搜索面板
│ ├── DataPermission.vue # 数据权限选择器
│ ├── ApprovalFlow.vue # 审批流程展示
│ ├── FileUpload.vue # 文件上传(支持批量)
│ └── ExportExcel.vue # Excel 导出按钮
├── views # 页面组件
│ ├── crm
│ │ ├── customer
│ │ │ ├── List.vue # 客户列表
│ │ │ ├── Detail.vue # 客户详情
│ │ │ └── Edit.vue # 客户编辑
│ │ └── dashboard
│ │ └── Index.vue # CRM 数据看板
│ ├── oa
│ │ ├── approval
│ │ │ ├── Create.vue # 发起审批
│ │ │ ├── Pending.vue # 待我审批
│ │ │ └── History.vue # 我已审批
│ │ └── workflow
│ │ └── Designer.vue # 流程设计器
│ ├── erp
│ │ ├── material
│ │ │ ├── List.vue # 物料列表
│ │ │ └── Stock.vue # 库存查询
│ │ └── purchase
│ │ └── OrderList.vue # 采购订单
│ ├── mes
│ │ ├── workorder
│ │ │ ├── List.vue # 工单列表
│ │ │ └── Progress.vue # 生产进度
│ │ └── monitoring
│ │ └── Realtime.vue # 实时监控看板
│ └── system
│ ├── user
│ │ └── List.vue # 用户管理
│ └── dept
│ └── Tree.vue # 部门树形管理
├── router # 路由配置
│ └── index.ts
├── store # 状态管理
│ ├── user.ts # 用户信息、权限
│ ├── app.ts # 应用配置(主题、语言)
│ └── tagsView.ts # 标签页管理
├── utils # 工具函数
│ ├── request.ts # Axios 封装
│ ├── auth.ts # 认证工具
│ ├── permission.ts# 权限判断
│ ├── excel.ts # Excel 导入导出
│ └── validate.ts # 表单验证
├── types # TypeScript 类型定义
├── styles # 全局样式
└── layout # 布局组件
├── index.vue # 主布局
├── Sidebar.vue # 侧边栏菜单
└── TagsView.vue # 标签页Axios 封装: ``typescript // utils/request.ts import axios from 'axios'; import { ElMessage, ElLoading } from 'element-plus'; import router from '@/router'; import { useUserStore } from '@/store/user';
const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 30000 // 企业系统超时时间稍长 });
let loadingInstance: any = null;
// 请求拦截器 service.interceptors.request.use( config => { const userStore = useUserStore(); const token = userStore.token;
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 显示加载动画(可选)
if (config.showLoading !== false) {
loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
});
}
return config;
}, error => { if (loadingInstance) loadingInstance.close(); return Promise.reject(error); } );
// 响应拦截器 service.interceptors.response.use( response => { if (loadingInstance) loadingInstance.close();
const res = response.data;
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败');
// Token 过期或无效,跳转登录页
if (res.code === 401) {
const userStore = useUserStore();
userStore.logout();
router.push('/login');
}
return Promise.reject(new Error(res.message));
}
return res.data;
}, error => { if (loadingInstance) loadingInstance.close();
if (error.response) {
switch (error.response.status) {
case 403:
ElMessage.error('权限不足,无法访问');
break;
case 404:
ElMessage.error('请求的资源不存在');
break;
case 500:
ElMessage.error('服务器错误,请联系管理员');
break;
default:
ElMessage.error(error.message || '网络错误');
}
} else {
ElMessage.error('网络连接失败,请检查网络');
}
return Promise.reject(error);
} );
export default service;
**API 接口封装示例**:
``typescript
// api/crm/customer.ts
import request from '@/utils/request';
export interface CustomerListParams {
page: number;
size: number;
customerName?: string;
status?: number;
ownerId?: number;
startDate?: string;
endDate?: string;
}
export interface CustomerVO {
id: number;
customerNo: string;
customerName: string;
customerType: number;
industry: string;
level: string;
ownerName: string;
phone: string;
status: number;
createTime: string;
}
// 查询客户列表
export function getCustomerList(params: CustomerListParams) {
return request.get<any, { records: CustomerVO[]; total: number }>('/api/crm/customers', { params });
}
// 新增客户
export function addCustomer(data: any) {
return request.post('/api/crm/customers', data);
}
// 更新客户
export function updateCustomer(id: number, data: any) {
return request.put(`/api/crm/customers/${id}`, data);
}
// 删除客户
export function deleteCustomer(id: number) {
return request.delete(`/api/crm/customers/${id}`);
}
// 分配客户给销售
export function assignCustomer(customerId: number, ownerId: number) {
return request.post(`/api/crm/customers/${customerId}/assign`, { ownerId });
}
// 领取公海客户
export function claimCustomer(customerId: number) {
return request.post(`/api/crm/customers/${customerId}/claim`);
}
// 导出客户数据
export function exportCustomers(params: CustomerListParams) {
return request.get('/api/crm/customers/export', {
params,
responseType: 'blob' // 重要:指定响应类型为 blob
});
}Excel 导入导出工具: ``typescript // utils/excel.ts import * as XLSX from 'xlsx'; import { saveAs } from 'file-saver';
/**
- 导出 Excel */ export function exportExcel(data: any[], filename: string, headers: string[]) { // 转换数据格式 const wsData = [headers, ...data.map(item => headers.map(h => item[h]))];
// 创建工作簿 const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet(wsData);
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
// 导出文件 const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }); saveAs(new Blob([wbout], { type: 'application/octet-stream' }), ${filename}.xlsx); }
/**
导入 Excel */ export function importExcel(file: File): Promise<any[]> { return new Promise((resolve, reject) => { const reader = new FileReader();
reader.onload = (e) => { try { const data = e.target?.result; const workbook = XLSX.read(data, { type: 'binary' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const results = XLSX.utils.sheet_to_json(worksheet);
resolve(results);} catch (error) { reject(error); } };
reader.readAsBinaryString(file); }); }
**使用示例**:
``vue
<template>
<div>
<el-button @click="handleExport">导出客户</el-button>
<el-upload
action="#"
:auto-upload="false"
:on-change="handleImport"
accept=".xlsx,.xls"
>
<el-button>导入客户</el-button>
</el-upload>
</div>
</template>
<script setup lang="ts">
import { exportCustomers, getCustomerList } from '@/api/crm/customer';
import { exportExcel, importExcel } from '@/utils/excel';
// 导出
const handleExport = async () => {
try {
const blob = await exportCustomers({ page: 1, size: 10000 });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `客户列表_${new Date().getTime()}.xlsx`;
link.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('导出失败', error);
}
};
// 导入
const handleImport = async (file: any) => {
try {
const data = await importExcel(file.raw);
console.log('导入数据', data);
// 调用后端接口批量导入
} catch (error) {
console.error('导入失败', error);
}
};
</script>7. 性能优化策略
后端优化:
- 数据库索引优化:为查询频繁的字段添加索引(owner_id、status、create_time)
- 分页查询:避免一次性加载大量数据,使用 MyBatis-Plus 分页插件
- 批量操作:批量导入客户/物料使用
saveBatch()/updateBatchById() - 异步处理:非核心业务(如发送通知、记录日志)使用
@Async异步执行 - 连接池调优:HikariCP 最大连接数设置为
CPU 核数 * 2 + 1 - SQL 优化:避免 N+1 查询,使用 JOIN 或批量查询
- 缓存热点数据:字典、组织架构、权限列表等
前端优化:
- 路由懒加载:按需加载页面组件typescript
const CustomerList = () => import('@/views/crm/customer/List.vue'); - 表格虚拟滚动:大数据量表格使用虚拟滚动(如
el-table-v2) - 防抖节流:搜索框输入使用防抖,按钮点击使用节流
- 图片懒加载:客户头像、产品图片使用懒加载
- CDN 加速:第三方库(Vue、Element Plus)使用 CDN 引入
- Gzip 压缩:Nginx 开启 Gzip 压缩
- 浏览器缓存:静态资源设置长期缓存策略
企业系统特有优化:
- 大数据量导出:使用流式导出,避免内存溢出
- 复杂报表查询:预计算 + 定时任务生成统计表
- 工作流引擎优化:缓存流程定义,减少 XML 解析
- 权限判断优化:用户权限列表缓存到 Redis,避免频繁查询
❓ 面试官:你们的系统如何处理高并发场景?比如月初/月末的集中审批、大量数据导入。【中高级】
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论): 对于企业级系统,高并发场景主要是 "定时高峰"(月初报销、月末统计)和 "批量操作"(数据导入、批量审批)。采用 "异步处理 + 队列削峰 + 分批处理 + 限流保护" 即可应对。
📝 具体实施方案:
1. 批量数据导入(CRM 客户、ERP 物料)
问题:一次性导入 10 万条客户数据,直接插入会导致数据库压力过大、事务超时。
解决方案:分批处理 + 异步执行 + 进度反馈
@Service
public class CustomerImportService {
@Autowired
private ThreadPoolTaskExecutor importExecutor;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 批量导入客户
*/
public String importCustomers(MultipartFile file, Long userId) {
// 1. 生成任务 ID
String taskId = UUID.randomUUID().toString();
// 2. 初始化进度
Map<String, Object> progress = new HashMap<>();
progress.put("total", 0);
progress.put("success", 0);
progress.put("failed", 0);
progress.put("status", "processing");
redisTemplate.opsForValue().set("import:progress:" + taskId, progress, 1, TimeUnit.HOURS);
// 3. 异步处理
importExecutor.execute(() -> {
try {
// 解析 Excel
List<CustomerDTO> customers = parseExcel(file);
progress.put("total", customers.size());
// 分批处理(每批 500 条)
int batchSize = 500;
for (int i = 0; i < customers.size(); i += batchSize) {
int end = Math.min(i + batchSize, customers.size());
List<CustomerDTO> batch = customers.subList(i, end);
// 批量插入
int successCount = batchInsert(batch, userId);
// 更新进度
progress.put("success", (int) progress.get("success") + successCount);
redisTemplate.opsForValue().set("import:progress:" + taskId, progress, 1, TimeUnit.HOURS);
}
progress.put("status", "completed");
} catch (Exception e) {
log.error("导入失败", e);
progress.put("status", "failed");
progress.put("error", e.getMessage());
}
});
// 4. 立即返回任务 ID
return taskId;
}
/**
* 查询导入进度
*/
public Map<String, Object> getImportProgress(String taskId) {
return (Map<String, Object>) redisTemplate.opsForValue().get("import:progress:" + taskId);
}
}前端轮询进度:
// 启动导入
const taskId = await importCustomers(file);
// 轮询进度
const timer = setInterval(async () => {
const progress = await getImportProgress(taskId);
if (progress.status === 'completed') {
clearInterval(timer);
ElMessage.success(`导入完成!成功${progress.success}条,失败${progress.failed}条`);
} else if (progress.status === 'failed') {
clearInterval(timer);
ElMessage.error('导入失败:' + progress.error);
}
}, 1000);2. 集中审批场景(OA)
问题:月初大量员工提交报销申请,审批人集中处理导致系统卡顿。
解决方案:异步通知 + 批量审批 + 乐观锁
@Service
public class ApprovalService {
/**
* 批量审批
*/
@Transactional
public void batchApprove(List<Long> approvalIds, Long approverId, String action, String comment) {
for (Long approvalId : approvalIds) {
// 使用乐观锁防止重复审批
int updated = approvalMapper.updateStatusWithLock(
approvalId,
currentStatus, // 当前状态
newStatus, // 新状态
approverId,
comment
);
if (updated == 0) {
log.warn("审批失败,可能已被处理 | approvalId={}", approvalId);
continue;
}
// 异步通知申请人
notificationService.sendApprovalResult(approvalId, action);
}
}
}
// Mapper SQL
@Update("UPDATE oa_approval SET status = #{newStatus}, update_time = NOW() " +
"WHERE id = #{approvalId} AND status = #{currentStatus}")
int updateStatusWithLock(@Param("approvalId") Long approvalId,
@Param("currentStatus") Integer currentStatus,
@Param("newStatus") Integer newStatus,
@Param("approverId") Long approverId,
@Param("comment") String comment);3. 限流保护
网关层限流:Nginx 限制单个 IP 的请求频率 应用层限流:使用 Guava RateLimiter 或 Redis 滑动窗口 接口级限流:对高频接口(如查询、导出)单独限流
@Component
public class ApiRateLimiter {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
public boolean tryAcquire(String apiKey, double permitsPerSecond) {
RateLimiter limiter = limiters.computeIfAbsent(apiKey,
k -> RateLimiter.create(permitsPerSecond));
return limiter.tryAcquire();
}
}
// 使用
@GetMapping("/customers")
public Result<List<CustomerVO>> listCustomers() {
if (!rateLimiter.tryAcquire("customer:list", 10)) { // 每秒最多 10 次
return Result.error(429, "请求过于频繁,请稍后再试");
}
// 业务逻辑
}❓ 面试官:你们如何做系统监控和日志排查?线上出了问题怎么快速定位?【中级】
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论): 建立 "日志集中管理 + 关键指标监控 + 链路追踪" 三位一体的监控体系,配合告警机制做到早发现、早定位、早解决。
📝 监控体系搭建:
1. 日志规范
// 统一日志格式:时间 | 级别 | 线程名 | 类名 | traceId | 用户ID | 消息
@Slf4j
@Service
public class CustomerService {
public void createCustomer(CustomerDTO dto) {
String traceId = MDC.get("traceId");
Long userId = UserContext.getUserId();
log.info("[{}] 创建客户 | userId={} | customerName={}",
traceId, userId, dto.getCustomerName());
try {
// 业务逻辑
log.info("[{}] 客户创建成功 | customerId={}", traceId, customerId);
} catch (Exception e) {
log.error("[{}] 客户创建失败 | userId={}", traceId, userId, e);
throw e;
}
}
}2. 关键指标监控
- JVM 监控:堆内存使用率、GC 次数、线程数(使用 Prometheus + Grafana)
- 接口监控:QPS、平均响应时间、错误率(使用 Micrometer)
- 数据库监控:慢查询数量、连接池使用率
- 业务监控:
- CRM:今日新增客户数、跟进完成率
- OA:待审批数量、平均审批时长
- ERP:库存预警数量、采购订单完成率
- MES:工单完成率、产线稼动率
3. 告警规则
- 接口错误率 > 5% → 钉钉/企业微信告警
- 平均响应时间 > 2s → 检查慢查询
- JVM 堆内存使用率 > 80% → 准备扩容
- 数据库连接池使用率 > 90% → 检查是否有连接泄漏
- 待审批数量突增 → 通知管理人员
4. 快速定位问题
- 根据用户反馈的时间点和操作,在日志系统中搜索对应的
traceId - 查看完整的调用链路,定位是哪个环节出错
- 检查当时的系统指标(CPU、内存、数据库负载)
- 复现问题:在测试环境模拟相同场景
- 修复并验证:热修复或发布新版本