引言
办公自动化(Office Automation,OA)系统是企业数字化转型的核心基础设施,承载着审批流程、协同办公、信息管理等关键业务。本文以 PointLion Cloud OA 模块为例,深入探讨如何基于 Spring Boot + Flowable 构建企业级 OA 系统。
相关链接:
- 🌐 官网:http://www.dianshixinxi.com/
- 📱 演示站:http://cloud.dianshixinxi.com:90/
- 🎨 Gitee:https://gitee.com/glorylion/JFinalOA
- 💻 GitCode:https://gitcode.com/Glory_Lion/pointlion-cloud
技术架构概览
核心技术栈
- 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 系统的设计与实现。通过审批流程引擎、请假管理、报销管理、会议室管理等核心模块的设计,展示了如何构建一个完整的办公自动化系统。
关键技术要点:
- 工作流引擎: 使用 Flowable 实现灵活的审批流程
- 分布式锁: 使用 Redisson 保证并发数据一致性
- 消息驱动: 使用 RocketMQ 实现异步消息处理
- 权限控制: 基于 Spring Security 实现细粒度权限管理
- 业务计算: 实现请假天数计算、预算控制等复杂业务逻辑
- 通知服务: 实现多渠道消息通知机制
- 数据统计: 提供审批效率分析和业务数据统计
该架构已在实际企业项目中得到验证,能够支撑大中型企业的办公自动化需求,提供高效、规范的流程管理,提升企业协同办公效率。