点狮OA-企业级 OA 办公自动化系统架构设计与实践

引言

办公自动化(Office Automation,OA)系统是企业数字化转型的核心基础设施,承载着审批流程、协同办公、信息管理等关键业务。本文以 PointLion Cloud OA 模块为例,深入探讨如何基于 Spring Boot + Flowable 构建企业级 OA 系统。

相关链接

技术架构概览

核心技术栈

  • Spring Boot 2.7.18: 微服务基础框架
  • Spring Security 5.8.16: 安全认证框架
  • Flowable 6.8.0: 工作流引擎
  • MyBatis Plus 3.5.10.1: ORM 框架
  • Redisson 3.41.0: 分布式缓存与锁
  • RocketMQ 2.3.1: 消息队列
  • EasyExcel 4.0.3: Excel 处理

OA 系统功能架构

复制代码
┌─────────────────────────────────────────────────────────┐
│                      应用展示层                            │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐  │
│  │ PC 端   │  │ 移动端   │  │ 待办提醒 │  │ 消息中心 │  │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘  │
├─────────────────────────────────────────────────────────┤
│                      业务功能层                            │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐  │
│  │ 审批管理 │  │ 请假管理 │  │ 报销管理 │  │ 用车申请 │  │
│  │ 会议室  │  │ 公文管理 │  │ 日程管理 │  │ 通讯录   │  │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘  │
├─────────────────────────────────────────────────────────┤
│                      流程引擎层                            │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐  │
│  │ 流程设计 │  │ 流程实例 │  │ 任务管理 │  │ 表单管理 │  │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘  │
├─────────────────────────────────────────────────────────┤
│                      平台支撑层                            │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐  │
│  │ 组织管理 │  │ 权限管理 │  │ 消息通知 │  │ 文件管理 │  │
│  │ 日志审计 │  │ 系统配置 │  │ 定时任务 │  │ 数据字典 │  │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘  │
├─────────────────────────────────────────────────────────┤
│                      数据存储层                            │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐  │
│  │ MySQL   │  │ Redis   │  │ MinIO   │  │ RocketMQ│  │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘  │
└─────────────────────────────────────────────────────────┘

审批流程引擎设计

Flowable 流程定义与部署

java 复制代码
@Service
public class WorkflowDefinitionService {
    
    private final ProcessEngine processEngine;
    private final ProcessDefinitionMapper definitionMapper;
    private final WorkflowFormService formService;
    
    /**
     * 部署流程定义
     */
    @Transactional(rollbackFor = Exception.class)
    public String deployProcessDefinition(ProcessDeployReq req) {
        // 1. 构建流程部署
        DeploymentBuilder deployment = processEngine.getRepositoryService()
            .createDeployment()
            .name(req.getProcessName())
            .category(req.getCategory())
            .key(req.getProcessKey());
        
        // 2. 添加流程资源
        if (req.getBpmnXml() != null) {
            deployment.addString(req.getProcessKey() + ".bpmn20.xml", req.getBpmnXml());
        }
        
        // 3. 添加流程表单
        if (req.getFormConfig() != null) {
            deployment.addString(req.getProcessKey() + ".form.json", req.getFormConfig());
        }
        
        // 4. 执行部署
        Deployment deploymentResult = deployment.deploy();
        
        // 5. 获取流程定义
        ProcessDefinition definition = processEngine.getRepositoryService()
            .createProcessDefinitionQuery()
            .deploymentId(deploymentResult.getId())
            .singleResult();
        
        // 6. 保存流程定义扩展信息
        saveProcessDefinitionExt(definition, req);
        
        // 7. 初始化表单配置
        formService.initFormConfig(definition.getId(), req.getFormConfig());
        
        return definition.getId();
    }
    
    /**
     * 启动流程实例
     */
    public String startProcessInstance(ProcessStartReq req) {
        // 1. 获取流程定义
        ProcessDefinition definition = processEngine.getRepositoryService()
            .createProcessDefinitionQuery()
            .processDefinitionId(req.getProcessDefinitionId())
            .singleResult();
        
        if (definition == null) {
            throw new BusinessException("流程定义不存在");
        }
        
        // 2. 构建流程变量
        Map<String, Object> variables = new HashMap<>();
        variables.put("applicant", SecurityUtils.getUserId());
        variables.put("applicantName", SecurityUtils.getUsername());
        variables.put("deptId", SecurityUtils.getDeptId());
        variables.put("applyTime", LocalDateTime.now());
        
        // 添加业务变量
        if (req.getVariables() != null) {
            variables.putAll(req.getVariables());
        }
        
        // 3. 启动流程实例
        ProcessInstance instance = processEngine.getRuntimeService()
            .startProcessInstanceById(
                req.getProcessDefinitionId(),
                req.getBusinessKey(),
                variables
            );
        
        // 4. 保存业务关联
        saveBusinessAssociation(instance.getId(), req.getBusinessKey(), 
            req.getBusinessType());
        
        // 5. 发送流程启动消息
        sendProcessStartMessage(instance);
        
        return instance.getId();
    }
    
    /**
     * 获取待办任务列表
     */
    public List<TaskInfo> getTodoTasks(TaskQueryReq req) {
        // 1. 构建任务查询
        TaskQuery query = processEngine.getTaskService()
            .createTaskQuery()
            .taskAssignee(SecurityUtils.getUserId().toString())
            .active();
        
        // 2. 添加查询条件
        if (StringUtils.isNotBlank(req.getProcessKey())) {
            query.processDefinitionKey(req.getProcessKey());
        }
        
        if (StringUtils.isNotBlank(req.getTaskName())) {
            query.taskNameLike("%" + req.getTaskName() + "%");
        }
        
        if (req.getStartTime() != null) {
            query.taskCreatedAfter(req.getStartTime());
        }
        
        // 3. 分页查询
        query.orderByTaskCreateTime().desc();
        
        int firstResult = (req.getPageNo() - 1) * req.getPageSize();
        List<Task> tasks = query.listPage(firstResult, req.getPageSize());
        
        // 4. 转换为业务对象
        List<TaskInfo> taskInfos = new ArrayList<>();
        for (Task task : tasks) {
            TaskInfo info = convertToTaskInfo(task);
            taskInfos.add(info);
        }
        
        return taskInfos;
    }
    
    /**
     * 完成任务
     */
    @Transactional(rollbackFor = Exception.class)
    public void completeTask(CompleteTaskReq req) {
        // 1. 获取任务信息
        Task task = processEngine.getTaskService()
            .createTaskQuery()
            .taskId(req.getTaskId())
            .singleResult();
        
        if (task == null) {
            throw new BusinessException("任务不存在");
        }
        
        // 2. 权限校验
        if (!task.getAssignee().equals(SecurityUtils.getUserId().toString())) {
            throw new BusinessException("无权限处理此任务");
        }
        
        // 3. 构建任务变量
        Map<String, Object> variables = new HashMap<>();
        variables.put("approver", SecurityUtils.getUserId());
        variables.put("approverName", SecurityUtils.getUsername());
        variables.put("approveTime", LocalDateTime.now());
        variables.put("approveResult", req.getApproveResult());
        variables.put("approveComment", req.getComment());
        
        // 添加业务变量
        if (req.getVariables() != null) {
            variables.putAll(req.getVariables());
        }
        
        // 4. 添加审批意见
        if (StringUtils.isNotBlank(req.getComment())) {
            addComment(req.getTaskId(), req.getComment());
        }
        
        // 5. 完成任务
        processEngine.getTaskService().complete(req.getTaskId(), variables);
        
        // 6. 发送任务完成消息
        sendTaskCompleteMessage(task, req);
        
        // 7. 检查流程是否结束
        checkProcessEnd(task.getProcessInstanceId());
    }
    
    /**
     * 任务转办
     */
    public void transferTask(TransferTaskReq req) {
        // 1. 校验任务权限
        Task task = validateTaskPermission(req.getTaskId());
        
        // 2. 校验接收人
        if (!isValidUser(req.getTargetUserId())) {
            throw new BusinessException("接收人不存在");
        }
        
        // 3. 执行转办
        processEngine.getTaskService().setAssignee(req.getTaskId(), 
            req.getTargetUserId().toString());
        
        // 4. 记录转办日志
        addTransferLog(req.getTaskId(), SecurityUtils.getUserId(), 
            req.getTargetUserId());
        
        // 5. 发送转办通知
        sendTransferNotification(task, req.getTargetUserId());
    }
    
    /**
     * 任务回退
     */
    public void rollbackTask(RollbackTaskReq req) {
        // 1. 校验任务权限
        Task task = validateTaskPermission(req.getTaskId());
        
        // 2. 获取流程实例
        ProcessInstance instance = processEngine.getRuntimeService()
            .createProcessInstanceQuery()
            .processInstanceId(task.getProcessInstanceId())
            .singleResult();
        
        // 3. 执行回退
        if (req.getRollbackToNodeId() != null) {
            // 回退到指定节点
            processEngine.getRuntimeService().createChangeActivityStateBuilder()
                .processInstanceId(task.getProcessInstanceId())
                .moveActivityIdTo(task.getTaskDefinitionKey(), req.getRollbackToNodeId())
                .changeState();
        } else {
            // 回退到上一节点
            rollbackToPreviousTask(task);
        }
        
        // 4. 记录回退日志
        addRollbackLog(task, req);
        
        // 5. 发送回退通知
        sendRollbackNotification(task);
    }
}

请假管理模块设计

请假申请与审批

java 复制代码
@Service
public class LeaveApplicationService {
    
    private final LeaveApplicationMapper leaveMapper;
    private final WorkflowDefinitionService workflowService;
    private final HolidayService holidayService;
    private final LeaveBalanceService balanceService;
    
    /**
     * 创建请假申请
     */
    @Transactional(rollbackFor = Exception.class)
    public Long createLeaveApplication(LeaveApplicationCreateReq req) {
        // 1. 参数校验
        validateLeaveApplication(req);
        
        // 2. 检查请假时间冲突
        if (hasTimeConflict(SecurityUtils.getUserId(), req.getStartTime(), 
            req.getEndTime())) {
            throw new BusinessException("请假时间段存在冲突");
        }
        
        // 3. 检查假期余额
        if (!checkLeaveBalance(SecurityUtils.getUserId(), req.getLeaveType(), 
            req.getLeaveDays())) {
            throw new BusinessException("假期余额不足");
        }
        
        // 4. 计算请假天数
        BigDecimal leaveDays = calculateLeaveDays(req.getStartTime(), 
            req.getEndTime(), req.getLeaveType());
        req.setLeaveDays(leaveDays);
        
        // 5. 构建请假申请实体
        LeaveApplication application = buildLeaveApplication(req);
        
        // 6. 保存请假申请
        leaveMapper.insert(application);
        
        // 7. 启动审批流程
        ProcessStartReq processReq = new ProcessStartReq();
        processReq.setProcessDefinitionId(getProcessDefinitionByType(req.getLeaveType()));
        processReq.setBusinessKey(application.getId().toString());
        processReq.setBusinessType("LEAVE_APPLICATION");
        processReq.setVariables(buildProcessVariables(application));
        
        String processInstanceId = workflowService.startProcessInstance(processReq);
        
        // 8. 更新流程实例 ID
        application.setProcessInstanceId(processInstanceId);
        leaveMapper.updateById(application);
        
        // 9. 冻结假期余额
        balanceService.freezeBalance(SecurityUtils.getUserId(), req.getLeaveType(), 
            leaveDays);
        
        return application.getId();
    }
    
    /**
     * 计算请假天数
     */
    private BigDecimal calculateLeaveDays(LocalDateTime startTime, LocalDateTime endTime, 
                                          String leaveType) {
        // 1. 获取节假日配置
        List<Holiday> holidays = holidayService.getHolidaysInRange(
            startTime.toLocalDate(), endTime.toLocalDate());
        
        // 2. 根据请假类型计算天数
        switch (leaveType) {
            case "ANNUAL":
            case "PERSONAL":
                // 年假和事假按工作日计算
                return calculateWorkDays(startTime, endTime, holidays);
            case "SICK":
                // 病假按自然日计算
                return calculateNaturalDays(startTime, endTime);
            case "MATERNITY":
            case "PATERNITY":
                // 产假和陪产假按自然日计算
                return calculateNaturalDays(startTime, endTime);
            default:
                return calculateWorkDays(startTime, endTime, holidays);
        }
    }
    
    /**
     * 计算工作日天数
     */
    private BigDecimal calculateWorkDays(LocalDateTime startTime, LocalDateTime endTime,
                                        List<Holiday> holidays) {
        Set<LocalDate> holidaySet = holidays.stream()
            .map(Holiday::getHolidayDate)
            .collect(Collectors.toSet());
        
        int workDays = 0;
        LocalDate current = startTime.toLocalDate();
        LocalDate end = endTime.toLocalDate();
        
        while (!current.isAfter(end)) {
            // 判断是否为工作日
            DayOfWeek dayOfWeek = current.getDayOfWeek();
            if (dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY 
                && !holidaySet.contains(current)) {
                workDays++;
            }
            current = current.plusDays(1);
        }
        
        // 考虑小时数
        long hours = ChronoUnit.HOURS.between(startTime, endTime);
        BigDecimal decimalPart = BigDecimal.valueOf(hours % 24)
            .divide(BigDecimal.valueOf(8), 2, RoundingMode.HALF_UP);
        
        return BigDecimal.valueOf(workDays).add(decimalPart);
    }
    
    /**
     * 撤销请假申请
     */
    @Transactional(rollbackFor = Exception.class)
    public void cancelLeaveApplication(Long applicationId, String reason) {
        // 1. 获取请假申请
        LeaveApplication application = leaveMapper.selectById(applicationId);
        if (application == null) {
            throw new BusinessException("请假申请不存在");
        }
        
        // 2. 权限校验
        if (!application.getApplicantId().equals(SecurityUtils.getUserId())) {
            throw new BusinessException("无权限撤销此申请");
        }
        
        // 3. 状态检查
        if (!"PENDING".equals(application.getStatus())) {
            throw new BusinessException("当前状态不允许撤销");
        }
        
        // 4. 时间检查
        if (application.getStartTime().isBefore(LocalDateTime.now())) {
            throw new BusinessException("请假已开始,无法撤销");
        }
        
        // 5. 取消流程实例
        try {
            workflowService.cancelProcessInstance(application.getProcessInstanceId(), 
                "申请人撤销");
        } catch (Exception e) {
            log.error("取消流程实例失败", e);
        }
        
        // 6. 更新申请状态
        application.setStatus("CANCELLED");
        application.setCancelReason(reason);
        application.setCancelTime(LocalDateTime.now());
        leaveMapper.updateById(application);
        
        // 7. 释放假期余额
        balanceService.releaseBalance(application.getApplicantId(), 
            application.getLeaveType(), application.getLeaveDays());
        
        // 8. 发送撤销通知
        sendCancelNotification(application);
    }
    
    /**
     * 请假审批处理
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleLeaveApproval(String taskId, ApproveReq req) {
        // 1. 获取任务信息
        Task task = workflowService.getTask(taskId);
        
        // 2. 获取业务数据
        String businessKey = workflowService.getProcessBusinessKey(
            task.getProcessInstanceId());
        LeaveApplication application = leaveMapper.selectById(Long.parseLong(businessKey));
        
        // 3. 审批通过处理
        if ("APPROVED".equals(req.getApproveResult())) {
            application.setStatus("APPROVED");
            application.setApproverId(SecurityUtils.getUserId());
            application.setApproveTime(LocalDateTime.now());
            application.setApproveComment(req.getComment());
            leaveMapper.updateById(application);
            
            // 扣减假期余额
            balanceService.deductBalance(application.getApplicantId(), 
                application.getLeaveType(), application.getLeaveDays());
        } 
        // 审批拒绝处理
        else if ("REJECTED".equals(req.getApproveResult())) {
            application.setStatus("REJECTED");
            application.setApproverId(SecurityUtils.getUserId());
            application.setApproveTime(LocalDateTime.now());
            application.setApproveComment(req.getComment());
            leaveMapper.updateById(application);
            
            // 释放假期余额
            balanceService.releaseBalance(application.getApplicantId(), 
                application.getLeaveType(), application.getLeaveDays());
        }
        
        // 4. 完成任务
        CompleteTaskReq completeReq = new CompleteTaskReq();
        completeReq.setTaskId(taskId);
        completeReq.setApproveResult(req.getApproveResult());
        completeReq.setComment(req.getComment());
        
        workflowService.completeTask(completeReq);
    }
}

报销管理模块设计

报销申请与审批流程

java 复制代码
@Service
public class ExpenseReimbursementService {
    
    private final ExpenseReimbursementMapper reimbursementMapper;
    private final ExpenseDetailMapper detailMapper;
    private final WorkflowDefinitionService workflowService;
    private final BudgetService budgetService;
    private final PaymentService paymentService;
    
    /**
     * 创建报销申请
     */
    @Transactional(rollbackFor = Exception.class)
    public Long createExpenseReimbursement(ExpenseReimbursementCreateReq req) {
        // 1. 参数校验
        validateReimbursementReq(req);
        
        // 2. 构建报销申请主表
        ExpenseReimbursement reimbursement = new ExpenseReimbursement();
        reimbursement.setReimbursementNo(generateReimbursementNo());
        reimbursement.setApplicantId(SecurityUtils.getUserId());
        reimbursement.setApplicantName(SecurityUtils.getUsername());
        reimbursement.setDeptId(SecurityUtils.getDeptId());
        reimbursement.setReimbursementType(req.getReimbursementType());
        reimbursement.setTotalAmount(req.getTotalAmount());
        reimbursement.setApplyTime(LocalDateTime.now());
        reimbursement.setStatus("PENDING");
        reimbursement.setRemark(req.getRemark());
        
        reimbursementMapper.insert(reimbursement);
        
        // 3. 保存报销明细
        List<ExpenseDetail> details = new ArrayList<>();
        for (ExpenseDetailReq detailReq : req.getDetails()) {
            ExpenseDetail detail = new ExpenseDetail();
            detail.setReimbursementId(reimbursement.getId());
            detail.setExpenseType(detailReq.getExpenseType());
            detail.setExpenseDate(detailReq.getExpenseDate());
            detail.setAmount(detailReq.getAmount());
            detail.setDescription(detailReq.getDescription());
            detail.setInvoiceNo(detailReq.getInvoiceNo());
            
            // 处理附件
            if (detailReq.getAttachments() != null) {
                detail.setAttachments(JSON.toJSONString(detailReq.getAttachments()));
            }
            
            details.add(detail);
        }
        
        detailMapper.batchInsert(details);
        
        // 4. 预算检查
        if (!budgetService.checkBudget(SecurityUtils.getDeptId(), 
            req.getReimbursementType(), req.getTotalAmount())) {
            throw new BusinessException("部门预算不足");
        }
        
        // 5. 启动审批流程
        ProcessStartReq processReq = new ProcessStartReq();
        processReq.setProcessDefinitionId(getProcessDefinitionByAmount(req.getTotalAmount()));
        processReq.setBusinessKey(reimbursement.getId().toString());
        processReq.setBusinessType("EXPENSE_REIMBURSEMENT");
        processReq.setVariables(buildProcessVariables(reimbursement));
        
        String processInstanceId = workflowService.startProcessInstance(processReq);
        
        // 6. 更新流程实例 ID
        reimbursement.setProcessInstanceId(processInstanceId);
        reimbursementMapper.updateById(reimbursement);
        
        // 7. 冻结预算
        budgetService.freezeBudget(SecurityUtils.getDeptId(), 
            req.getReimbursementType(), req.getTotalAmount());
        
        return reimbursement.getId();
    }
    
    /**
     * 报销审批处理
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleReimbursementApproval(String taskId, ApproveReq req) {
        // 1. 获取任务信息
        Task task = workflowService.getTask(taskId);
        
        // 2. 获取业务数据
        String businessKey = workflowService.getProcessBusinessKey(
            task.getProcessInstanceId());
        ExpenseReimbursement reimbursement = reimbursementMapper.selectById(
            Long.parseLong(businessKey));
        
        // 3. 审批通过处理
        if ("APPROVED".equals(req.getApproveResult())) {
            reimbursement.setStatus("APPROVED");
            reimbursement.setFinalApproverId(SecurityUtils.getUserId());
            reimbursement.setFinalApproveTime(LocalDateTime.now());
            reimbursement.setFinalApproveComment(req.getComment());
            reimbursementMapper.updateById(reimbursement);
            
            // 扣减预算
            budgetService.deductBudget(reimbursement.getDeptId(), 
                reimbursement.getReimbursementType(), reimbursement.getTotalAmount());
            
            // 创建付款单
            createPaymentOrder(reimbursement);
        } 
        // 审批拒绝处理
        else if ("REJECTED".equals(req.getApproveResult())) {
            reimbursement.setStatus("REJECTED");
            reimbursement.setFinalApproverId(SecurityUtils.getUserId());
            reimbursement.setFinalApproveTime(LocalDateTime.now());
            reimbursement.setFinalApproveComment(req.getComment());
            reimbursementMapper.updateById(reimbursement);
            
            // 释放预算
            budgetService.releaseBudget(reimbursement.getDeptId(), 
                reimbursement.getReimbursementType(), reimbursement.getTotalAmount());
        }
        
        // 4. 完成任务
        CompleteTaskReq completeReq = new CompleteTaskReq();
        completeReq.setTaskId(taskId);
        completeReq.setApproveResult(req.getApproveResult());
        completeReq.setComment(req.getComment());
        
        workflowService.completeTask(completeReq);
    }
    
    /**
     * 创建付款单
     */
    private void createPaymentOrder(ExpenseReimbursement reimbursement) {
        PaymentOrder payment = new PaymentOrder();
        payment.setPaymentNo(generatePaymentNo());
        payment.setBusinessType("EXPENSE_REIMBURSEMENT");
        payment.setBusinessId(reimbursement.getId());
        payment.setPayeeId(reimbursement.getApplicantId());
        payment.setPayeeName(reimbursement.getApplicantName());
        payment.setPaymentAmount(reimbursement.getTotalAmount());
        payment.setPaymentAccount(getDefaultAccount(reimbursement.getApplicantId()));
        payment.setStatus("PENDING");
        payment.setCreateTime(LocalDateTime.now());
        
        paymentService.createPayment(payment);
    }
}

会议室管理模块设计

会议室预订与管理

java 复制代码
@Service
public class MeetingRoomService {
    
    private final MeetingRoomMapper roomMapper;
    private final MeetingBookingMapper bookingMapper;
    private final RedissonClient redissonClient;
    
    /**
     * 查询可用会议室
     */
    public List<MeetingRoom> getAvailableRooms(RoomQueryReq req) {
        // 1. 构建查询条件
        LambdaQueryWrapper<MeetingRoom> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(MeetingRoom::getStatus, "ACTIVE");
        
        if (req.getMinCapacity() != null) {
            wrapper.ge(MeetingRoom::getCapacity, req.getMinCapacity());
        }
        
        if (req.hasProjector()) {
            wrapper.eq(MeetingRoom::getHasProjector, true);
        }
        
        if (req.hasWhiteboard()) {
            wrapper.eq(MeetingRoom::getHasWhiteboard, true);
        }
        
        // 2. 查询会议室列表
        List<MeetingRoom> rooms = roomMapper.selectList(wrapper);
        
        // 3. 过滤已被预订的会议室
        List<MeetingRoom> availableRooms = rooms.stream()
            .filter(room -> isRoomAvailable(room.getId(), req.getStartTime(), 
                req.getEndTime()))
            .collect(Collectors.toList());
        
        return availableRooms;
    }
    
    /**
     * 检查会议室是否可用
     */
    private boolean isRoomAvailable(Long roomId, LocalDateTime startTime, 
                                   LocalDateTime endTime) {
        // 1. 查询时间范围内的预订
        List<MeetingBooking> bookings = bookingMapper.selectList(
            new LambdaQueryWrapper<MeetingBooking>()
                .eq(MeetingBooking::getRoomId, roomId)
                .in(MeetingBooking::getStatus, "CONFIRMED", "PENDING")
                .and(wrapper -> wrapper
                    .ge(MeetingBooking::getStartTime, startTime)
                    .le(MeetingBooking::getStartTime, endTime)
                    .or()
                    .ge(MeetingBooking::getEndTime, startTime)
                    .le(MeetingBooking::getEndTime, endTime)
                    .or()
                    .le(MeetingBooking::getStartTime, startTime)
                    .ge(MeetingBooking::getEndTime, endTime)
                )
        );
        
        return bookings.isEmpty();
    }
    
    /**
     * 预订会议室
     */
    @Transactional(rollbackFor = Exception.class)
    public Long bookMeetingRoom(MeetingBookingCreateReq req) {
        // 1. 参数校验
        validateBookingReq(req);
        
        // 2. 检查会议室是否可用
        if (!isRoomAvailable(req.getRoomId(), req.getStartTime(), req.getEndTime())) {
            throw new BusinessException("该时间段会议室已被预订");
        }
        
        // 3. 使用分布式锁防止并发预订
        String lockKey = "meeting:room:lock:" + req.getRoomId();
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                try {
                    // 4. 再次检查会议室可用性
                    if (!isRoomAvailable(req.getRoomId(), req.getStartTime(), 
                        req.getEndTime())) {
                        throw new BusinessException("该时间段会议室已被预订");
                    }
                    
                    // 5. 构建预订信息
                    MeetingBooking booking = new MeetingBooking();
                    booking.setBookingNo(generateBookingNo());
                    booking.setRoomId(req.getRoomId());
                    booking.setTopic(req.getTopic());
                    booking.setBookerId(SecurityUtils.getUserId());
                    booking.setBookerName(SecurityUtils.getUsername());
                    booking.setStartTime(req.getStartTime());
                    booking.setEndTime(req.getEndTime());
                    booking.setAttendees(req.getAttendees());
                    booking.setStatus("CONFIRMED");
                    booking.setRemark(req.getRemark());
                    
                    bookingMapper.insert(booking);
                    
                    // 6. 发送预订通知
                    sendBookingNotification(booking);
                    
                    return booking.getId();
                    
                } finally {
                    lock.unlock();
                }
            } else {
                throw new BusinessException("系统繁忙,请稍后重试");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BusinessException("系统异常");
        }
    }
    
    /**
     * 取消预订
     */
    @Transactional(rollbackFor = Exception.class)
    public void cancelBooking(Long bookingId, String reason) {
        // 1. 获取预订信息
        MeetingBooking booking = bookingMapper.selectById(bookingId);
        if (booking == null) {
            throw new BusinessException("预订信息不存在");
        }
        
        // 2. 权限校验
        if (!booking.getBookerId().equals(SecurityUtils.getUserId())) {
            throw new BusinessException("无权限取消此预订");
        }
        
        // 3. 状态检查
        if ("CANCELLED".equals(booking.getStatus())) {
            throw new BusinessException("预订已取消");
        }
        
        // 4. 时间检查(会议开始前 30 分钟不能取消)
        if (booking.getStartTime().minusMinutes(30).isBefore(LocalDateTime.now())) {
            throw new BusinessException("会议即将开始,无法取消");
        }
        
        // 5. 更新状态
        booking.setStatus("CANCELLED");
        booking.setCancelReason(reason);
        booking.setCancelTime(LocalDateTime.now());
        bookingMapper.updateById(booking);
        
        // 6. 发送取消通知
        sendCancelNotification(booking);
    }
}

消息通知模块设计

多渠道消息通知

java 复制代码
@Service
public class NotificationService {
    
    private final NotificationTemplateService templateService;
    private final RocketMQTemplate rocketMQTemplate;
    private final RedissonClient redissonClient;
    
    /**
     * 发送流程审批通知
     */
    public void sendApprovalNotification(String processInstanceId, Long assigneeId, 
                                        Task task) {
        // 1. 获取消息模板
        NotificationTemplate template = templateService.getTemplate("PROCESS_APPROVAL");
        
        // 2. 构建消息参数
        Map<String, Object> params = new HashMap<>();
        params.put("processName", getProcessName(task.getProcessDefinitionId()));
        params.put("taskName", task.getName());
        params.put("applicant", getApplicantName(processInstanceId));
        params.put("applyTime", task.getCreateTime());
        params.put("url", buildApprovalUrl(task.getId()));
        
        // 3. 替换模板内容
        String title = replaceTemplate(template.getTitle(), params);
        String content = replaceTemplate(template.getContent(), params);
        
        // 4. 构建通知消息
        NotificationMessage message = new NotificationMessage();
        message.setUserId(assigneeId);
        message.setTitle(title);
        message.setContent(content);
        message.setType("PROCESS_APPROVAL");
        message.setBusinessId(task.getId());
        message.setCreateTime(LocalDateTime.now());
        
        // 5. 发送到消息队列
        rocketMQTemplate.syncSend("NOTIFICATION:SEND", message);
    }
    
    /**
     * 发送待办提醒
     */
    @Scheduled(cron = "0 0/30 * * * ?")  // 每 30 分钟执行一次
    public void sendPendingTaskReminder() {
        // 1. 获取所有待办任务
        List<Task> pendingTasks = workflowService.getPendingTasks();
        
        // 2. 按用户分组
        Map<Long, List<Task>> userTasks = pendingTasks.stream()
            .collect(Collectors.groupingBy(task -> 
                Long.parseLong(task.getAssignee())));
        
        // 3. 发送提醒
        userTasks.forEach((userId, tasks) -> {
            // 检查是否已发送过提醒
            String reminderKey = "task:reminder:" + userId + ":" + 
                LocalDate.now().toString();
            RBucket<String> bucket = redissonClient.getBucket(reminderKey);
            
            if (!bucket.isExists()) {
                // 发送提醒
                sendTaskReminder(userId, tasks);
                
                // 设置已发送标记
                bucket.set("SENT", 24, TimeUnit.HOURS);
            }
        });
    }
    
    /**
     * 发送系统公告
     */
    @Transactional(rollbackFor = Exception.class)
    public void sendSystemAnnouncement(AnnouncementCreateReq req) {
        // 1. 创建公告
        SystemAnnouncement announcement = new SystemAnnouncement();
        announcement.setTitle(req.getTitle());
        announcement.setContent(req.getContent());
        announcement.setType(req.getType());
        announcement.setPublisherId(SecurityUtils.getUserId());
        announcement.setPublisherName(SecurityUtils.getUsername());
        announcement.setPublishTime(LocalDateTime.now());
        announcement.setStatus("PUBLISHED");
        
        announcementMapper.insert(announcement);
        
        // 2. 确定接收人
        List<Long> receiverIds = determineReceivers(req);
        
        // 3. 批量发送通知
        List<NotificationMessage> messages = new ArrayList<>();
        for (Long receiverId : receiverIds) {
            NotificationMessage message = new NotificationMessage();
            message.setUserId(receiverId);
            message.setTitle("系统公告:" + req.getTitle());
            message.setContent(buildAnnouncementContent(req));
            message.setType("SYSTEM_ANNOUNCEMENT");
            message.setBusinessId(announcement.getId());
            message.setCreateTime(LocalDateTime.now());
            
            messages.add(message);
        }
        
        // 4. 批量发送到消息队列
        if (!messages.isEmpty()) {
            rocketMQTemplate.syncSend("NOTIFICATION:BATCH_SEND", messages);
        }
    }
}

数据统计与报表

审批效率分析

java 复制代码
@Service
public class ApprovalAnalysisService {
    
    private final ProcessEngine processEngine;
    private final ApprovalStatisticsMapper statisticsMapper;
    
    /**
     * 获取部门审批效率统计
     */
    public DeptApprovalStatistics getDeptApprovalStatistics(Long deptId, 
                                                          LocalDate startDate, 
                                                          LocalDate endDate) {
        // 1. 查询部门所有已完成的流程实例
        List<HistoricProcessInstance> instances = processEngine.getHistoryService()
            .createHistoricProcessInstanceQuery()
            .finishedAfter(startDate.atStartOfDay())
            .finishedBefore(endDate.plusDays(1).atStartOfDay())
            .variableValueEquals("deptId", deptId.toString())
            .finished()
            .list();
        
        // 2. 统计分析
        DeptApprovalStatistics statistics = new DeptApprovalStatistics();
        statistics.setDeptId(deptId);
        statistics.setStartDate(startDate);
        statistics.setEndDate(endDate);
        
        if (instances.isEmpty()) {
            return statistics;
        }
        
        // 3. 计算平均审批时长
        List<Long> durations = instances.stream()
            .map(instance -> {
                long start = instance.getStartTime().toEpochSecond(ZoneOffset.UTC);
                long end = instance.getEndTime().toEpochSecond(ZoneOffset.UTC);
                return end - start;
            })
            .collect(Collectors.toList());
        
        double avgDuration = durations.stream()
            .mapToLong(Long::longValue)
            .average()
            .orElse(0.0);
        
        statistics.setAvgApprovalDuration((long) avgDuration);
        
        // 4. 统计审批通过率
        long approvedCount = instances.stream()
            .filter(instance -> {
                Map<String, Object> variables = processEngine.getHistoryService()
                    .createHistoricVariableInstanceQuery()
                    .processInstanceId(instance.getId())
                    .list()
                    .stream()
                    .collect(Collectors.toMap(
                        HistoricVariableInstance::getVariableName,
                        HistoricVariableInstance::getValue
                    ));
                
                return "APPROVED".equals(variables.get("approveResult"));
            })
            .count();
        
        statistics.setApprovalRate((double) approvedCount / instances.size());
        
        // 5. 统计各节点平均耗时
        Map<String, Long> nodeDurations = calculateNodeDurations(instances);
        statistics.setNodeDurations(nodeDurations);
        
        return statistics;
    }
    
    /**
     * 获取员工请假统计
     */
    public List<EmployeeLeaveStatistics> getEmployeeLeaveStatistics(Long deptId, 
                                                                    int year, 
                                                                    int month) {
        // 1. 构建查询条件
        LocalDate startDate = LocalDate.of(year, month, 1);
        LocalDate endDate = startDate.plusMonths(1).minusDays(1);
        
        // 2. 查询请假记录
        List<LeaveApplication> leaveList = leaveMapper.selectList(
            new LambdaQueryWrapper<LeaveApplication>()
                .eq(LeaveApplication::getDeptId, deptId)
                .between(LeaveApplication::getStartTime, 
                    startDate.atStartOfDay(), endDate.atTime(23, 59, 59))
                .in(LeaveApplication::getStatus, "APPROVED", "IN_PROGRESS")
        );
        
        // 3. 按员工分组统计
        Map<Long, List<LeaveApplication>> employeeLeaves = leaveList.stream()
            .collect(Collectors.groupingBy(LeaveApplication::getApplicantId));
        
        // 4. 构建统计结果
        List<EmployeeLeaveStatistics> statisticsList = new ArrayList<>();
        employeeLeaves.forEach((employeeId, leaves) -> {
            EmployeeLeaveStatistics statistics = new EmployeeLeaveStatistics();
            statistics.setEmployeeId(employeeId);
            statistics.setYear(year);
            statistics.setMonth(month);
            
            // 统计各类假期天数
            Map<String, BigDecimal> leaveTypeDays = leaves.stream()
                .collect(Collectors.groupingBy(
                    LeaveApplication::getLeaveType,
                    Collectors.reducing(BigDecimal.ZERO, 
                        LeaveApplication::getLeaveDays, 
                        BigDecimal::add)
                ));
            
            statistics.setAnnualDays(leaveTypeDays.getOrDefault("ANNUAL", BigDecimal.ZERO));
            statistics.setSickDays(leaveTypeDays.getOrDefault("SICK", BigDecimal.ZERO));
            statistics.setPersonalDays(leaveTypeDays.getOrDefault("PERSONAL", BigDecimal.ZERO));
            
            statisticsList.add(statistics);
        });
        
        return statisticsList;
    }
}

总结

本文介绍了基于 Spring Boot + Flowable 构建企业级 OA 系统的设计与实现。通过审批流程引擎、请假管理、报销管理、会议室管理等核心模块的设计,展示了如何构建一个完整的办公自动化系统。

关键技术要点:

  1. 工作流引擎: 使用 Flowable 实现灵活的审批流程
  2. 分布式锁: 使用 Redisson 保证并发数据一致性
  3. 消息驱动: 使用 RocketMQ 实现异步消息处理
  4. 权限控制: 基于 Spring Security 实现细粒度权限管理
  5. 业务计算: 实现请假天数计算、预算控制等复杂业务逻辑
  6. 通知服务: 实现多渠道消息通知机制
  7. 数据统计: 提供审批效率分析和业务数据统计

该架构已在实际企业项目中得到验证,能够支撑大中型企业的办公自动化需求,提供高效、规范的流程管理,提升企业协同办公效率。

相关推荐
taocarts_bidfans1 小时前
反向海淘系统架构设计与 taocarts 分层实践
系统架构·反向海淘·taocarts
swordbob1 小时前
Nacos vs Eureka
spring cloud·云原生·eureka
生成论实验室1 小时前
六十四卦态势操作系统技术白皮书
人工智能·语言模型·系统架构·机器人·自动驾驶·agi·安全架构
爱看科技1 小时前
微美全息(NASDAQ:WIMI)研究基于强化学习的量子编码电路适配优化架构
架构·量子计算
by————组态2 小时前
Ricon组态可视化编辑器 - 所见即所得的工业画布
前端·javascript·物联网·架构·编辑器·组态
Jul1en_2 小时前
【SpringCloud】SkyWalking 链路追踪知识详解及部署教程
java·后端·spring·spring cloud·skywalking
「、皓子~2 小时前
海狸IM 2.0 开放能力说明:OAuth2 接入与群推送机器人
人工智能·架构·electron·机器人·开源·交友·im
逻极2 小时前
Spring Boot 微服务开发提速:我们如何将接口响应时间降低60%
java·spring boot·微服务·性能优化·自动配置
Swift社区2 小时前
鸿蒙 PC 正在诞生“第二操作系统”:Agent Runtime 架构揭秘
华为·架构·harmonyos