Skip to content

传统OA办公系统(亮点与难点)

附件: Java开发工程师.docx

上面这位学员简历的项目没什么亮点和难点,投出去面试机会可能不多,经过和老师沟通后,着重优化了第三个项目《OA办公系统》,优化如下:

项目三:企业级分布式协同办公系统

项目描述:
该项目是面向集团级企业的大型分布式协同办公平台,服务用户规模3000+,日均处理业务数据约10万+。平台采用微服务架构,实现了信息发布中心、流程审批中心、远程办公中心等核心功能模块,支持全面的信创环境适配。

技术架构:

  • 前端:Vue.js + Element UI
  • 后端:SpringBoot + SpringCloud Alibaba + Redis + Rabbitmq + MySQL + MinIO + ElasticSearch
  • 监控:Prometheus + Grafana + ELK

技术难点及解决方案:

  1. 文档处理性能优化:
  • 设计基于MinIO的分布式文件存储方案,支持PB级文档存储
  • 实现文档断点续传和秒传功能,提升10倍传输效率
  • 引入文档预览服务,支持100+种格式在线预览
  1. 复杂审批流程引擎优化
  • 设计实现了基于Activity工作流引擎的自定义工作流程
  • 通过状态机模式处理复杂审批流转,支持动态分支、会签、并行等场景
  • 引入Redis分布式锁确保并发审批的数据一致性
  • 审批处理效率提升200%,支持千级并发处理
  1. 系统性能优化
  • 实现基于ElasticSearch的全文检索,检索响应时间优化至100ms以内
  • 使用RabbitMQ消息队列实现系统解耦,提高系统可用性达99.99%
  • 设计多级缓存架构(本地缓存+Redis集群),降低数据库压力80%
  • 实现读写分离,通过分库分表处理海量数据,单表数据量控制在500万以内
  1. 安全性提升
  • 实现基于RBAC的细粒度权限控制,支持动态权限调整
  • 设计统一认证中心,实现SSO单点登录
  • 所有敏感数据采用AES加密存储,确保数据安全
  • 实现操作日志完整追踪机制,支持审计回溯

项目成果:

  1. 系统平均响应时间从3s优化至300ms,支持2000+并发用户
  2. 业务处理效率提升200%,用户满意度提升40%
  3. 系统运维成本降低60%,故障处理时间缩短80%
  4. 获得公司年度最佳技术创新奖

面试如何回答:

一、设计基于MinIO的分布式文件存储方案,支持PB级文档存储

  1. 业务背景分析
    在我们的办公系统中,经常需要处理大量的文档上传下载需求,需要支持PB级的文档存储。考虑到性能、可靠性和成本等因素,我们选择了基于MinIO构建分布式存储方案。
  2. 整体架构设计
  • 采用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 = &quot;file:info:&quot; + fileInfo.getFileId();  
    Map&lt;String, String&gt; fileInfoMap = new HashMap&lt;&gt;();  
    fileInfoMap.put(&quot;fileName&quot;, fileInfo.getFileName());  
    fileInfoMap.put(&quot;fileSize&quot;, String.valueOf(fileInfo.getFileSize()));  
    fileInfoMap.put(&quot;fileType&quot;, fileInfo.getFileType());  
    redisTemplate.opsForHash().putAll(redisKey, fileInfoMap);  
    // 设置过期时间  
    redisTemplate.expire(redisKey, 24, TimeUnit.HOURS);  
}  

// 获取文件信息  
public FileInfo getFileInfo(String fileId) {  
    // 1. 先从Redis获取  
    String redisKey = &quot;file:info:&quot; + fileId;  
    Map&lt;Object, Object&gt; fileInfoMap = redisTemplate.opsForHash().entries(redisKey);  

    if (!fileInfoMap.isEmpty()) {  
        // 更新访问计数  
        redisTemplate.opsForValue().increment(&quot;file:access:&quot; + 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 = &quot;upload:&quot; + fileId;  
    redisTemplate.opsForHash().put(uploadKey, &quot;chunk:&quot; + chunkIndex, &quot;1&quot;);  

    // 2. 更新MySQL状态  
    FileChunk fileChunk = new FileChunk();  
    fileChunk.setFileId(fileId);  
    fileChunk.setChunkIndex(chunkIndex);  
    fileChunk.setUploadTime(new Date());  
    fileChunkMapper.insert(fileChunk);  
}  

}

  1. 缓存策略说明:
  • 热点文件判定:访问次数超过阈值的文件元数据会被缓存
  • 缓存时间:一般文件信息缓存24小时,上传进度缓存12小时
  • 更新机制:采用先更新数据库,再更新缓存的策略
  • 缓存击穿防护:使用互斥锁防止缓存击穿
  1. 数据一致性保证:
  • 采用Cache Aside Pattern模式
  • 更新时先更新数据库,再删除缓存
  • 定时任务对比数据库和缓存数据,确保一致性

3. 存储策略实现

  • 文件目录采用分层设计:业务/年/月/日/文件
  • 大文件采用分片上传,每片大小5MB
  • 实现文件秒传功能,通过MD5判断
  1. 性能优化方案
  • 实现了异步上传机制
java
@Async  
public CompletableFuture<String> uploadAsync(MultipartFile file) {  
    String objectName = generateObjectName(file);  
    minioClient.putObject(bucketName, objectName, file.getInputStream());  
    return CompletableFuture.completedFuture(objectName);  
}
  • 使用Redis缓存上传进度
  • 采用分片并行上传提高效率
  • 实现断点续传功能
  1. 高可用保障
  • MinIO集群采用纠删码机制,配置N+4冗余
纠删码机制:

纠删码(Erasure Code)是一种数据保护机制,它的核心思想是将数据分片并生成校验数据,即使部分数据丢失也能通过剩余数据进行恢复。

举个简单的例子来说明:

  1. 传统的备份方式
  • 比如存储1个100MB的文件
  • 如果要做3个副本,需要300MB存储空间
  • 只能防止整个副本丢失
  1. 纠删码的方式
  • 将100MB文件分成10份,每份10MB
  • 额外生成4份校验数据,每份也是10MB
  • 总共占用140MB存储空间
  • 这14份数据分散存储在不同节点
  • 只要还剩下任意10份数据,就能完整恢复原始文件

优势:

  1. 存储效率高:比传统多副本节省30-60%空间
  2. 可靠性强:可以容忍多个节点同时故障
  3. 恢复能力强:丢失的数据可以通过剩余数据重建

在MinIO中的应用:

  • 默认使用纠删码EC:4(即N+4配置)
  • 数据分片分布在不同节点
  • 支持最多4个节点同时故障
  • 读写性能好,恢复速度快

这就像是一本书的内容被分成多页,即使丢失几页,通过目录和其他页面的信息也能推算出丢失页面的内容,这就是纠删码的基本原理。

+ 实现了完整的监控告警系统 + 定期数据备份策略 6. 关键技术实现
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);  
}
  1. 监控和运维
  • 使用Prometheus + Grafana监控系统运行状态
  • 实现了完整的日志收集和分析系统
  • 建立了容量预警机制

遇到的主要挑战和解决方案:

大文件上传性能问题

- 实现了分片上传
- 使用异步处理
- 优化网络配置

分布式文件系统上线后效果:

  • 支持单文件最大100GB上传
  • 上传速度提升300%
  • 存储成本降低40%
  • 系统可用性达到99.99%

二、通过状态机模式处理复杂审批流转,支持动态分支、会签、并行等场景

我们以一个简单的员工报销流程为例:

业务规则:

  1. 报销金额 < 1000元:直接主管审批
  2. 报销金额 1000-5000元:部门经理审批
  3. 报销金额 > 5000元:财务审批
  4. 允许申请人撤回和审批人驳回
  5. 传统实现方式(不使用状态机):
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);  
    }  
}
  1. 使用状态机的实现:
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);  
        }  
    } 
}

使用状态机的优势:

  1. 代码更清晰易懂:
  • 状态和转换规则一目了然
  • 每个状态处理逻辑独立
  • 避免了复杂的if-else嵌套
  1. 维护更简单:
  • 修改状态流转规则容易
  • 添加新状态方便
  • 业务逻辑集中管理
  1. 功能扩展更容易:
  • 添加新的审批环节简单
  • 修改审批规则方便
  • 添加新功能不影响现有代码
  1. 错误处理更完善:
  • 状态转换更可控
  • 异常处理更集中
  • 便于问题排查

实际效果:

  • 代码更容易理解
  • 维护成本降低
  • 开发效率提升
  • 系统更稳定

这个简单的例子展示了即使在相对简单的业务场景中,使用状态机也能带来显著优势:

  1. 代码结构更清晰
  2. 业务逻辑更容易理解
  3. 维护和扩展更方便
  4. 代码质量更高

而且,随着业务复杂度增加(比如添加更多审批环节、特殊审批规则等),状态机的优势会更加明显。

PS:上面状态引擎详细代码讲解参考直播课《传统CRUD保险系统亮点与难点优化实战》

三、审批处理效率提升200%,支持千级并发处理

我们在实现审批系统时,通过多个层面的优化措施实现了高性能和高并发:

具体性能提升来自以下几个方面:

  1. 缓存策略:
  • 对于一些变化不大的数据尽量提前加载到缓存,比如各种审批流程,审批规则等等
  1. 并发控制优化:
  • 分布式锁:使用Redis实现,避免重复处理
  • 乐观锁:数据库层面防止并发更新
  • 队列缓冲:削峰填谷,比如月底会集中报销或处理各种事项,如果流程数量太多会影响整个OA系统的性能,这是可以借助队列削峰,后台用线程池异步处理流程
  • 线程池隔离:不同业务使用独立线程池,这样可以减小业务之间的相互影响
  1. 异步处理机制:
  • 状态变更通知异步化
  • 审批通知异步发送
  • 批量处理能力
  • 失败重试机制
  1. 数据库优化:
  • 分库分表:按租户ID水平分片
  • 索引优化:状态、申请人等字段
  • 分页查询:避免大结果集
  • 读写分离:主从架构

性能数据:

  • 平均响应时间:从500ms优化到150ms
  • 并发处理能力:从300/秒提升到1000/秒
  • CPU使用率:从平均85%降到45%

这些优化措施整体提升了审批处理效率:

  1. 通过引入缓存减少了70%的数据库访问
  2. 异步处理机制提升了并发处理能力
  3. 分布式锁保证了数据一致性
  4. 批量处理提高了系统吞吐量

最终实现了审批处理效率提升200%,支持千级并发处理的目标。这些优化不仅提升了性能,还保证了系统的可靠性和稳定性。

PS:上面这些优化方案如果不清楚具体实现的可以参考《图灵七天面试突击直播课》

四、服务器部署情况

参考保险分销平台的例子

五、相关阅读

拓展阅读 : 一文分清OA、CRM、ERP、MES、HRM、SCM、WMS、KMS等

后记:如有补充和纠错请在评论区指出,如有小伙伴有类似项目可以发在评论区。

更新: 2025-03-10 17:01:39
原文: https://www.yuque.com/tulingzhouyu/db22bv/na2rgdk111ggmagx