Appearance
传统OA办公系统(亮点与难点)
上面这位学员简历的项目没什么亮点和难点,投出去面试机会可能不多,经过和老师沟通后,着重优化了第三个项目《OA办公系统》,优化如下:
项目三:企业级分布式协同办公系统
项目描述:
该项目是面向集团级企业的大型分布式协同办公平台,服务用户规模3000+,日均处理业务数据约10万+。平台采用微服务架构,实现了信息发布中心、流程审批中心、远程办公中心等核心功能模块,支持全面的信创环境适配。
技术架构:
- 前端:Vue.js + Element UI
- 后端:SpringBoot + SpringCloud Alibaba + Redis + Rabbitmq + MySQL + MinIO + ElasticSearch
- 监控:Prometheus + Grafana + ELK
技术难点及解决方案:
- 文档处理性能优化:
- 设计基于MinIO的分布式文件存储方案,支持PB级文档存储
- 实现文档断点续传和秒传功能,提升10倍传输效率
- 引入文档预览服务,支持100+种格式在线预览
- 复杂审批流程引擎优化
- 设计实现了基于Activity工作流引擎的自定义工作流程
- 通过状态机模式处理复杂审批流转,支持动态分支、会签、并行等场景
- 引入Redis分布式锁确保并发审批的数据一致性
- 审批处理效率提升200%,支持千级并发处理
- 系统性能优化
- 实现基于ElasticSearch的全文检索,检索响应时间优化至100ms以内
- 使用RabbitMQ消息队列实现系统解耦,提高系统可用性达99.99%
- 设计多级缓存架构(本地缓存+Redis集群),降低数据库压力80%
- 实现读写分离,通过分库分表处理海量数据,单表数据量控制在500万以内
- 安全性提升
- 实现基于RBAC的细粒度权限控制,支持动态权限调整
- 设计统一认证中心,实现SSO单点登录
- 所有敏感数据采用AES加密存储,确保数据安全
- 实现操作日志完整追踪机制,支持审计回溯
项目成果:
- 系统平均响应时间从3s优化至300ms,支持2000+并发用户
- 业务处理效率提升200%,用户满意度提升40%
- 系统运维成本降低60%,故障处理时间缩短80%
- 获得公司年度最佳技术创新奖
面试如何回答:
一、设计基于MinIO的分布式文件存储方案,支持PB级文档存储
- 业务背景分析
在我们的办公系统中,经常需要处理大量的文档上传下载需求,需要支持PB级的文档存储。考虑到性能、可靠性和成本等因素,我们选择了基于MinIO构建分布式存储方案。 - 整体架构设计
- 采用MinIO分布式集群架构,每个集群至少4个节点
- 使用Nginx做负载均衡
- 采用Redis缓存热点文件元数据
- MySQL存储文件索引信息
上面这里文件存储的整体架构很容易被问到,我通过redis和mysql的一些关键存储结构举例说明:
MySQL表结构设计
-- 文件信息主表:存储文件的基本信息 CREATE TABLE file_info ( file_id VARCHAR(32) COMMENT '文件ID,主键', file_name VARCHAR(255) COMMENT '文件名称', file_size BIGINT COMMENT '文件大小,单位字节', file_type VARCHAR(50) COMMENT '文件类型,如pdf、doc等', md5 VARCHAR(32) COMMENT '文件MD5值,用于秒传判断', bucket_name VARCHAR(100) COMMENT 'MinIO的存储桶名称', object_name VARCHAR(255) COMMENT 'MinIO中的对象名称,即存储路径', chunk_count INT COMMENT '文件分片总数', upload_status TINYINT COMMENT '上传状态:0-未上传,1-上传中,2-已完成,3-上传失败', create_time DATETIME COMMENT '创建时间', update_time DATETIME COMMENT '最后更新时间', creator VARCHAR(50) COMMENT '创建者用户ID', is_deleted TINYINT COMMENT '是否删除:0-未删除,1-已删除', PRIMARY KEY (file_id) ) COMMENT '文件信息主表';
-- 文件分片信息表:存储文件分片上传的详细信息
CREATE TABLE file_chunk (
chunk_id VARCHAR(32) COMMENT '分片ID,主键',
file_id VARCHAR(32) COMMENT '关联的文件ID',
chunk_index INT COMMENT '分片序号,从0开始',
chunk_size BIGINT COMMENT '分片大小,单位字节',
chunk_path VARCHAR(255) COMMENT '分片在MinIO中的存储路径',
upload_time DATETIME COMMENT '分片上传完成时间',
PRIMARY KEY (chunk_id),
INDEX idx_file_id (file_id) COMMENT '文件ID索引'
) COMMENT '文件分片信息表';
Redis存热点文件元数据设计
# 文件基本信息(Hash结构)
key: file:info:{fileId}
{
fileName: "测试文档.pdf",
fileSize: "1024000",
fileType: "pdf",
md5: "xxxxx",
uploadStatus: "1",
bucketName: "documents",
objectName: "2024/01/测试文档.pdf"
}文件分片上传进度(Hash结构)
key: upload:{fileId}
{
chunk:0: "1",
chunk:1: "1",
chunk:2: "1"
}热门文件访问计数(String结构)
key: file:access:{fileId}
value: 访问次数文件下载URL缓存(String结构)
key: file:url:{fileId}
value: 临时下载URL
代码实现示例
@Service
public class FileService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;@Autowired private FileInfoMapper fileInfoMapper; // 保存文件信息 public void saveFileInfo(FileInfo fileInfo) { // 1. 保存到MySQL fileInfoMapper.insert(fileInfo); // 2. 保存到Redis缓存 String redisKey = "file:info:" + fileInfo.getFileId(); Map<String, String> fileInfoMap = new HashMap<>(); fileInfoMap.put("fileName", fileInfo.getFileName()); fileInfoMap.put("fileSize", String.valueOf(fileInfo.getFileSize())); fileInfoMap.put("fileType", fileInfo.getFileType()); redisTemplate.opsForHash().putAll(redisKey, fileInfoMap); // 设置过期时间 redisTemplate.expire(redisKey, 24, TimeUnit.HOURS); } // 获取文件信息 public FileInfo getFileInfo(String fileId) { // 1. 先从Redis获取 String redisKey = "file:info:" + fileId; Map<Object, Object> fileInfoMap = redisTemplate.opsForHash().entries(redisKey); if (!fileInfoMap.isEmpty()) { // 更新访问计数 redisTemplate.opsForValue().increment("file:access:" + fileId); return convertMapToFileInfo(fileInfoMap); } // 2. Redis没有,从MySQL获取 FileInfo fileInfo = fileInfoMapper.selectById(fileId); if (fileInfo != null) { // 放入Redis缓存 saveFileInfoToRedis(fileInfo); } return fileInfo; } // 更新上传进度 public void updateUploadProgress(String fileId, int chunkIndex) { // 1. 更新Redis进度 String uploadKey = "upload:" + fileId; redisTemplate.opsForHash().put(uploadKey, "chunk:" + chunkIndex, "1"); // 2. 更新MySQL状态 FileChunk fileChunk = new FileChunk(); fileChunk.setFileId(fileId); fileChunk.setChunkIndex(chunkIndex); fileChunk.setUploadTime(new Date()); fileChunkMapper.insert(fileChunk); }
}
- 缓存策略说明:
- 热点文件判定:访问次数超过阈值的文件元数据会被缓存
- 缓存时间:一般文件信息缓存24小时,上传进度缓存12小时
- 更新机制:采用先更新数据库,再更新缓存的策略
- 缓存击穿防护:使用互斥锁防止缓存击穿
- 数据一致性保证:
- 采用Cache Aside Pattern模式
- 更新时先更新数据库,再删除缓存
- 定时任务对比数据库和缓存数据,确保一致性
3. 存储策略实现
- 文件目录采用分层设计:业务/年/月/日/文件
- 大文件采用分片上传,每片大小5MB
- 实现文件秒传功能,通过MD5判断
- 性能优化方案
- 实现了异步上传机制
java
@Async
public CompletableFuture<String> uploadAsync(MultipartFile file) {
String objectName = generateObjectName(file);
minioClient.putObject(bucketName, objectName, file.getInputStream());
return CompletableFuture.completedFuture(objectName);
}- 使用Redis缓存上传进度
- 采用分片并行上传提高效率
- 实现断点续传功能
- 高可用保障
- MinIO集群采用纠删码机制,配置N+4冗余
纠删码机制:
纠删码(Erasure Code)是一种数据保护机制,它的核心思想是将数据分片并生成校验数据,即使部分数据丢失也能通过剩余数据进行恢复。
举个简单的例子来说明:
- 传统的备份方式
- 比如存储1个100MB的文件
- 如果要做3个副本,需要300MB存储空间
- 只能防止整个副本丢失
- 纠删码的方式
- 将100MB文件分成10份,每份10MB
- 额外生成4份校验数据,每份也是10MB
- 总共占用140MB存储空间
- 这14份数据分散存储在不同节点
- 只要还剩下任意10份数据,就能完整恢复原始文件
优势:
- 存储效率高:比传统多副本节省30-60%空间
- 可靠性强:可以容忍多个节点同时故障
- 恢复能力强:丢失的数据可以通过剩余数据重建
在MinIO中的应用:
- 默认使用纠删码EC:4(即N+4配置)
- 数据分片分布在不同节点
- 支持最多4个节点同时故障
- 读写性能好,恢复速度快
这就像是一本书的内容被分成多页,即使丢失几页,通过目录和其他页面的信息也能推算出丢失页面的内容,这就是纠删码的基本原理。
java
// 分片上传实现
public void multipartUpload(String fileId, MultipartFile file) {
// 初始化分片上传
String uploadId = minioClient.initiateMultipartUpload(bucket, fileId);
// 分片上传
List<CompletableFuture<PartETag>> uploadFutures = new ArrayList<>();
int partNumber = 1;
for(byte[] bytes : splitFile(file)) {
uploadFutures.add(CompletableFuture.supplyAsync(() ->
uploadPart(bucket, fileId, uploadId, partNumber, bytes)
));
partNumber++;
}
// 合并分片
List<PartETag> partETags = uploadFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
minioClient.completeMultipartUpload(bucket, fileId, uploadId, partETags);
}- 监控和运维
- 使用Prometheus + Grafana监控系统运行状态
- 实现了完整的日志收集和分析系统
- 建立了容量预警机制
遇到的主要挑战和解决方案:
大文件上传性能问题
- 实现了分片上传
- 使用异步处理
- 优化网络配置
分布式文件系统上线后效果:
- 支持单文件最大100GB上传
- 上传速度提升300%
- 存储成本降低40%
- 系统可用性达到99.99%
二、通过状态机模式处理复杂审批流转,支持动态分支、会签、并行等场景
我们以一个简单的员工报销流程为例:
业务规则:
- 报销金额 < 1000元:直接主管审批
- 报销金额 1000-5000元:部门经理审批
- 报销金额 > 5000元:财务审批
- 允许申请人撤回和审批人驳回
- 传统实现方式(不使用状态机):
java
@Service
public class ExpenseService {
public void processExpense(String expenseId, String action, String operator) {
Expense expense = expenseRepository.findById(expenseId);
// 复杂的状态判断和处理逻辑
if ("DRAFT".equals(expense.getStatus())) {
if ("submit".equals(action)) {
// 提交处理
if (expense.getAmount() < 1000) {
expense.setStatus("SUPERVISOR_REVIEW");
expense.setCurrentApprover(getSupervisor(expense.getApplicant()));
} else if (expense.getAmount() <= 5000) {
expense.setStatus("MANAGER_REVIEW");
expense.setCurrentApprover(getManager(expense.getApplicant()));
} else {
expense.setStatus("FINANCE_REVIEW");
expense.setCurrentApprover(getFinanceManager());
}
}
} else if ("SUPERVISOR_REVIEW".equals(expense.getStatus())) {
if ("approve".equals(action)) {
expense.setStatus("APPROVED");
} else if ("reject".equals(action)) {
expense.setStatus("REJECTED");
}
} else if ("MANAGER_REVIEW".equals(expense.getStatus())) {
if ("approve".equals(action)) {
expense.setStatus("APPROVED");
} else if ("reject".equals(action)) {
expense.setStatus("REJECTED");
}
} else if ("FINANCE_REVIEW".equals(expense.getStatus())) {
if ("approve".equals(action)) {
expense.setStatus("APPROVED");
} else if ("reject".equals(action)) {
expense.setStatus("REJECTED");
}
}
// 更新数据
expenseRepository.save(expense);
}
}- 使用状态机的实现:
java
// 1. 状态定义
public enum ExpenseState {
DRAFT("草稿"),
SUBMITTED("已提交"),
SUPERVISOR_REVIEW("主管审批中"),
MANAGER_REVIEW("经理审批中"),
FINANCE_REVIEW("财务审批中"),
APPROVED("已通过"),
REJECTED("已拒绝"),
CANCELED("已取消");
private String description;
}
// 2. 报销上下文
@Data
@Builder
public class ExpenseContext {
private String expenseId;
private ExpenseState state;
private String applicant;
private BigDecimal amount;
private String description;
private String currentApprover;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
// 3. 状态机实现
@Service
@Slf4j
public class ExpenseStateMachine {
@Autowired
private ExpenseRepository expenseRepository;
@Autowired
private NotificationService notificationService;
@Transactional
public void processStateTransition(String expenseId, String action) {
ExpenseContext context = getExpenseContext(expenseId);
handleStateTransition(context, action);
saveContext(context);
notifyRelevantUsers(context);
}
private void handleStateTransition(ExpenseContext context, String action) {
switch (context.getState()) {
case DRAFT:
handleDraftState(context, action);
break;
case SUBMITTED:
handleSubmittedState(context);
break;
case SUPERVISOR_REVIEW:
case MANAGER_REVIEW:
case FINANCE_REVIEW:
handleReviewState(context, action);
break;
default:
throw new IllegalStateException("非法的状态转换");
}
}
private void handleDraftState(ExpenseContext context, String action) {
if ("submit".equals(action)) {
context.setState(ExpenseState.SUBMITTED);
} else if ("cancel".equals(action)) {
context.setState(ExpenseState.CANCELED);
}
}
private void handleSubmittedState(ExpenseContext context) {
// 根据金额确定下一个审批人
if (context.getAmount().compareTo(new BigDecimal(1000)) < 0) {
context.setState(ExpenseState.SUPERVISOR_REVIEW);
context.setCurrentApprover(getSupervisor(context.getApplicant()));
} else if (context.getAmount().compareTo(new BigDecimal(5000)) <= 0) {
context.setState(ExpenseState.MANAGER_REVIEW);
context.setCurrentApprover(getManager(context.getApplicant()));
} else {
context.setState(ExpenseState.FINANCE_REVIEW);
context.setCurrentApprover(getFinanceManager());
}
}
private void handleReviewState(ExpenseContext context, String action) {
if ("approve".equals(action)) {
context.setState(ExpenseState.APPROVED);
// 触发报销发放
triggerPayment(context);
} else if ("reject".equals(action)) {
context.setState(ExpenseState.REJECTED);
}
}
}使用状态机的优势:
- 代码更清晰易懂:
- 状态和转换规则一目了然
- 每个状态处理逻辑独立
- 避免了复杂的if-else嵌套
- 维护更简单:
- 修改状态流转规则容易
- 添加新状态方便
- 业务逻辑集中管理
- 功能扩展更容易:
- 添加新的审批环节简单
- 修改审批规则方便
- 添加新功能不影响现有代码
- 错误处理更完善:
- 状态转换更可控
- 异常处理更集中
- 便于问题排查
实际效果:
- 代码更容易理解
- 维护成本降低
- 开发效率提升
- 系统更稳定
这个简单的例子展示了即使在相对简单的业务场景中,使用状态机也能带来显著优势:
- 代码结构更清晰
- 业务逻辑更容易理解
- 维护和扩展更方便
- 代码质量更高
而且,随着业务复杂度增加(比如添加更多审批环节、特殊审批规则等),状态机的优势会更加明显。
PS:上面状态引擎详细代码讲解参考直播课《传统CRUD保险系统亮点与难点优化实战》
三、审批处理效率提升200%,支持千级并发处理
我们在实现审批系统时,通过多个层面的优化措施实现了高性能和高并发:
具体性能提升来自以下几个方面:
- 缓存策略:
- 对于一些变化不大的数据尽量提前加载到缓存,比如各种审批流程,审批规则等等
- 并发控制优化:
- 分布式锁:使用Redis实现,避免重复处理
- 乐观锁:数据库层面防止并发更新
- 队列缓冲:削峰填谷,比如月底会集中报销或处理各种事项,如果流程数量太多会影响整个OA系统的性能,这是可以借助队列削峰,后台用线程池异步处理流程
- 线程池隔离:不同业务使用独立线程池,这样可以减小业务之间的相互影响
- 异步处理机制:
- 状态变更通知异步化
- 审批通知异步发送
- 批量处理能力
- 失败重试机制
- 数据库优化:
- 分库分表:按租户ID水平分片
- 索引优化:状态、申请人等字段
- 分页查询:避免大结果集
- 读写分离:主从架构
性能数据:
- 平均响应时间:从500ms优化到150ms
- 并发处理能力:从300/秒提升到1000/秒
- CPU使用率:从平均85%降到45%
这些优化措施整体提升了审批处理效率:
- 通过引入缓存减少了70%的数据库访问
- 异步处理机制提升了并发处理能力
- 分布式锁保证了数据一致性
- 批量处理提高了系统吞吐量
最终实现了审批处理效率提升200%,支持千级并发处理的目标。这些优化不仅提升了性能,还保证了系统的可靠性和稳定性。
PS:上面这些优化方案如果不清楚具体实现的可以参考《图灵七天面试突击直播课》
四、服务器部署情况
参考保险分销平台的例子
五、相关阅读
拓展阅读 : 一文分清OA、CRM、ERP、MES、HRM、SCM、WMS、KMS等
后记:如有补充和纠错请在评论区指出,如有小伙伴有类似项目可以发在评论区。
更新: 2025-03-10 17:01:39
原文: https://www.yuque.com/tulingzhouyu/db22bv/na2rgdk111ggmagx