基于 SpringBoot + Vue3 开发的完整OA系统,代码开源、运行稳定,涵盖动态工作流审批、文档管理、博客社区、在线网盘等丰富功能,适合企业自用、二次开发或全栈学习。
基于 SpringBoot Vue3 开发的完整OA系统代码开源、运行稳定涵盖动态工作流审批、文档管理、博客社区、在线网盘等丰富功能适合企业自用、二次开发或全栈学习。✅ 核心功能亮点· 智能工作流支持动态审批、加签、会签可像钉钉/飞书一样自由选择审批人· 协同办公内置文档/图片预览、评论讨论、任务管理、日程安排等OA常用模块· 内容社区集成博客撰写、问答社区、文章搜索增强内部知识共享· 文件管理支持在线网盘实现文件上传、分类、共享与预览· 流程可视化审批过程可跟踪、可评论提升协同透明度技术栈简析前端Vue3 Ant Design Vue Vite 数据可视化图表后端SpringBoot 2 MyBatis Plus Redis JWT Shiro数据库MySQL推荐部署支持Docker提供基础部署说明资源清单前后端完整源码开源协议无加密数据库初始化脚本基础部署指南含环境配置步骤系统功能说明文档代码包含了你描述中最核心、最复杂的动态工作流引擎审批流的后端实现逻辑以及前后端的基础框架搭建指南。你可以基于此骨架扩展文档、博客和网盘功能。项目名称OpenOA (基于 SpringBoot Vue3)一、数据库设计 (MySQL) - 核心工作流部分这是OA系统的灵魂支持动态审批、加签、会签。– 1. 流程定义表 (存储审批模板如请假流程、报销流程)CREATE TABLE oa_process_def (id bigint(20) NOT NULL AUTO_INCREMENT,name varchar(100) NOT NULL COMMENT ‘流程名称’,key varchar(50) NOT NULL COMMENT ‘流程标识’,version int(11) DEFAULT 1,content json NOT NULL COMMENT ‘流程节点JSON配置(包含节点ID、类型、审批人规则)’,status tinyint(4) DEFAULT 1 COMMENT ‘1:启用 0:停用’,create_time datetime DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (id)) ENGINEInnoDB DEFAULT CHARSETutf8mb4;– 2. 流程实例表 (每一次具体的审批请求)CREATE TABLE oa_process_instance (id bigint(20) NOT NULL AUTO_INCREMENT,process_def_id bigint(20) NOT NULL,title varchar(200) NOT NULL COMMENT ‘申请标题’,applicant_id bigint(20) NOT NULL COMMENT ‘申请人ID’,current_node_id varchar(50) DEFAULT NULL COMMENT ‘当前停留节点ID’,status tinyint(4) DEFAULT 0 COMMENT ‘0:审批中 1:通过 2:驳回 3:撤销’,business_data json DEFAULT NULL COMMENT ‘业务数据快照(如请假天数、金额)’,create_time datetime DEFAULT CURRENT_TIMESTAMP,finish_time datetime DEFAULT NULL,PRIMARY KEY (id)) ENGINEInnoDB DEFAULT CHARSETutf8mb4;– 3. 审批任务表 (具体到某个人需要做的操作)CREATE TABLE oa_task (id bigint(20) NOT NULL AUTO_INCREMENT,instance_id bigint(20) NOT NULL,node_id varchar(50) NOT NULL,task_name varchar(100) NOT NULL,assignee_id bigint(20) DEFAULT NULL COMMENT ‘指定办理人ID (为空表示待领取或会签组)’,candidate_ids varchar(500) DEFAULT NULL COMMENT ‘候选人群体ID集合 (逗号分隔用于会签)’,task_type tinyint(4) DEFAULT 1 COMMENT ‘1:串行 2:并行(会签) 3:或签’,status tinyint(4) DEFAULT 0 COMMENT ‘0:待处理 1:已同意 2:已驳回 3:已转办’,comment varchar(500) DEFAULT NULL COMMENT ‘审批意见’,handle_time datetime DEFAULT NULL,PRIMARY KEY (id)) ENGINEInnoDB DEFAULT CHARSETutf8mb4;– 4. 审批历史记录表CREATE TABLE oa_task_history (id bigint(20) NOT NULL AUTO_INCREMENT,task_id bigint(20) NOT NULL,operator_id bigint(20) NOT NULL,action varchar(20) NOT NULL COMMENT ‘AGREE/REJECT/TRANSFER’,comment varchar(500),operate_time datetime DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (id)) ENGINEInnoDB DEFAULT CHARSETutf8mb4;二、后端核心代码 (SpringBoot MyBatis Plus)实体类 (Entity) - 流程定义DataTableName(“oa_process_def”)public class ProcessDef {TableId(type IdType.AUTO)private Long id;private String name;private String key;private Integer version;// 使用 Jackson 处理 JSON 字段存储节点流转逻辑TableField(typeHandler JacksonTypeHandler.class)private ProcessConfig content;private Integer status;private Date createTime;}// 嵌套类定义流程节点结构Datapublic class ProcessConfig {private List nodes;private List connections;Data public static class Node { private String id; private String name; private String type; // START, USER_TASK, END private String assigneeRule; // FIXED(指定人), ROLE(角色), MANAGER(主管) private List userIds; // 指定用户ID列表 private Long roleId; }}Service 层 - 核心工作流引擎逻辑这是实现“动态审批、加签、会签”的关键逻辑。Servicepublic class WorkflowService {Autowired private ProcessInstanceMapper instanceMapper; Autowired private TaskMapper taskMapper; Autowired private UserMapper userMapper; /** 发起流程 */ Transactional(rollbackFor Exception.class) public Long startProcess(Long userId, Long processDefId, String title, JsonNode businessData) { // 1. 创建实例 ProcessInstance instance new ProcessInstance(); instance.setProcessDefId(processDefId); instance.setTitle(title); instance.setApplicantId(userId); instance.setStatus(0); // 审批中 instance.setBusinessData(businessData); instanceMapper.insert(instance); // 2. 解析流程定义找到第一个节点 ProcessDef def processDefMapper.selectById(processDefId); ProcessConfig config def.getContent(); Node startNode config.getNodes().stream() .filter(n - START.equals(n.getType())).findFirst().orElseThrow(); // 获取下一个节点 Node firstTaskNode getNextNode(config, startNode.getId()); // 3. 创建第一个任务 createTask(instance.getId(), firstTaskNode); return instance.getId(); } /** 审批任务 (同意/驳回) */ Transactional(rollbackFor Exception.class) public void approveTask(Long taskId, Long operatorId, String action, String comment) { Task task taskMapper.selectById(taskId); if (!task.getAssigneeId().equals(operatorId)) { throw new RuntimeException(无权操作此任务); } // 记录历史 TaskHistory history new TaskHistory(); history.setTaskId(taskId); history.setOperatorId(operatorId); history.setAction(action); history.setComment(comment); history.setOperateTime(new Date()); taskHistoryMapper.insert(history); // 更新当前任务状态 task.setStatus(AGREE.equals(action) ? 1 : 2); task.setHandleTime(new Date()); task.setComment(comment); taskMapper.updateById(task); if (REJECT.equals(action)) { // 驳回结束整个实例 ProcessInstance instance instanceMapper.selectById(task.getInstanceId()); instance.setStatus(2); // 驳回 instanceMapper.updateById(instance); return; } // 同意查找下一个节点并创建新任务 ProcessInstance instance instanceMapper.selectById(task.getInstanceId()); ProcessDef def processDefMapper.selectById(instance.getProcessDefId()); Node currentNode def.getContent().getNodes().stream() .filter(n - n.getId().equals(task.getNodeId())).findFirst().orElseThrow(); Node nextNode getNextNode(def.getContent(), currentNode.getId()); if (nextNode null || END.equals(nextNode.getType())) { // 流程结束 instance.setStatus(1); // 通过 instance.setFinishTime(new Date()); instanceMapper.updateById(instance); } else { // 创建下一个任务 instance.setCurrentNodeId(nextNode.getId()); instanceMapper.updateById(instance); createTask(instance.getId(), nextNode); } } /** 根据规则创建任务 (支持会签、指定人、角色) */ private void createTask(Long instanceId, Node node) { Task task new Task(); task.setInstanceId(instanceId); task.setNodeId(node.getId()); task.setTaskName(node.getName()); if (FIXED.equals(node.getAssigneeRule())) { // 指定多人 - 会签逻辑 (这里简化为创建多个任务或存入candidate_ids) // 实际生产中会签通常创建一个主任务关联多个子任务或者利用 candidate_ids task.setCandidateIds(String.join(,, node.getUserIds().stream().map(String::valueOf).toArray(String[]::new))); task.setTaskType(2); // 并行会签 // 注意这里需要根据具体业务逻辑拆分任务给每个人或者在前端展示为“待会签” // 简化版直接指派给第一个人其他人作为抄送或后续加签 if (!node.getUserIds().isEmpty()) { task.setAssigneeId(node.getUserIds().get(0)); } } else if (ROLE.equals(node.getAssigneeRule())) { // 根据角色查找用户逻辑略... } taskMapper.insert(task); } // 辅助方法根据连线找下一个节点 private Node getNextNode(ProcessConfig config, String currentId) { Connection conn config.getConnections().stream() .filter(c - c.getSource().equals(currentId)).findFirst().orElse(null); if (conn null) return null; return config.getNodes().stream() .filter(n - n.getId().equals(conn.getTarget())).findFirst().orElse(null); }}Controller 层RestControllerRequestMapping(“/api/workflow”)public class WorkflowController {Autowired private WorkflowService workflowService; PostMapping(/start) public Result start(RequestBody StartProcessDTO dto, HttpServletRequest request) { Long userId getCurrentUserId(request); // 从JWT/Shiro获取 Long instanceId workflowService.startProcess(userId, dto.getProcessDefId(), dto.getTitle(), dto.getData()); return Result.success(instanceId); } PostMapping(/task/approve) public Result approve(RequestBody ApproveTaskDTO dto, HttpServletRequest request) { Long userId getCurrentUserId(request); workflowService.approveTask(dto.getTaskId(), userId, dto.getAction(), dto.getComment()); return Result.success(); } GetMapping(/my-tasks) public Result getMyTasks(HttpServletRequest request) { Long userId getCurrentUserId(request); // 查询分配给该用户的所有待办任务 List tasks taskMapper.selectList(new QueryWrapper() .eq(assignee_id, userId) .eq(status, 0)); return Result.success(tasks); }}三、前端核心代码 (Vue3 Ant Design Vue)项目初始化npm create vitelatest openoa-web – --template vuecd openoa-webnpm install ant-design-vue axios pinia vue-router echarts审批发起页面 (StartProcess.vue){{ p.name }} 提交申请import { ref, onMounted } from ‘vue’;import { message } from ‘ant-design-vue’;import axios from ‘axios’;const loading ref(false);const processes ref([]);const form ref({processDefId: undefined,title: ‘’,content: ‘’});onMounted(async () {const res await axios.get(‘/api/workflow/definitions’);processes.value res.data.data;});const handleSubmit async () {if (!form.value.processDefId) return message.error(‘请选择流程’);loading.value true;try {await axios.post(‘/api/workflow/start’, {processDefId: form.value.processDefId,title: form.value.title,data: { content: form.value.content }});message.success(‘发起成功’);// 跳转列表页} catch (e) {message.error(e.response?.data?.message || ‘发起失败’);} finally {loading.value false;}};待办任务处理页面 (TaskHandle.vue)办理 详情 同意 驳回import { ref, onMounted } from ‘vue’;import { message } from ‘ant-design-vue’;import axios from ‘axios’;const tasks ref([]);const visible ref(false);const currentTask ref(null);const action ref(‘AGREE’);const comment ref(‘’);const columns [{ title: ‘任务名称’, dataIndex: ‘taskName’ },{ title: ‘流程标题’, dataIndex: ‘instanceTitle’ },{ title: ‘发起人’, dataIndex: ‘applicantName’ },{ title: ‘时间’, dataIndex: ‘createTime’ },{ key: ‘action’, title: ‘操作’ }];onMounted(async () {const res await axios.get(‘/api/workflow/my-tasks’);tasks.value res.data.data;});const showModal (record) {currentTask.value record;visible.value true;action.value ‘AGREE’;comment.value ‘’;};const handleApprove async () {await axios.post(‘/api/workflow/task/approve’, {taskId: currentTask.value.id,action: action.value,comment: comment.value});message.success(‘办理成功’);visible.value false;// 刷新列表const res await axios.get(‘/api/workflow/my-tasks’);tasks.value res.data.data;};四、资源清单与部署指南目录结构建议openoa-system├── openoa-backend (SpringBoot)│ ├── src/main/java/com/openoa│ │ ├── controller (API接口)│ │ ├── service (业务逻辑含工作流引擎)│ │ ├── mapper (MyBatis Plus)│ │ ├── entity (实体类)│ │ └── config (Security, CORS, MyBatisConfig)│ └── src/main/resources│ ├── application.yml│ └── mapper (XML文件)├── openoa-web (Vue3)│ ├── src/views│ │ ├── workflow (工作流相关页面)│ │ ├── docs (文档管理)│ │ └── blog (博客社区)│ ├── src/api (Axios封装)│ └── package.json└── docker-compose.yml (一键部署)Docker 部署脚本 (docker-compose.yml)version: ‘3.8’services:mysql:image: mysql:8.0environment:MYSQL_ROOT_PASSWORD: root123MYSQL_DATABASE: openoa_dbports:“3306:3306”volumes:./sql/init.sql:/docker-entrypoint-initdb.d/init.sqlredis:image: redis:alpineports:“6379:6379”backend:build: ./openoa-backendports:“8080:8080”depends_on:mysqlredisenvironment:SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/openoa_dbSPRING_REDIS_HOST: redisfrontend:build: ./openoa-webports:“80:80”depends_on:backend功能扩展建议文档管理/网盘利用 SpringBoot 的 MultipartFile 接收文件存储到本地磁盘或 MinIO/OSS数据库只存路径和元数据。前端使用 antd-upload 组件。博客社区复用通用的 CRUD 架构增加富文本编辑器如 WangEditor 或 TinyMCE。数据可视化在后台首页引入 ECharts统计“待办任务数”、“流程平均耗时”、“部门活跃度”等。SpringBoot Vue3 Ant Design Vue 技术栈我将为你实现这个动态可配置的工作台首页。核心实现思路后端提供一个接口返回当前用户可见的“应用卡片”配置名称、图标、链接、排序。这样不同角色的用户如普通员工 vs 财务看到的内容可以不同。前端使用 Vue3 的组件化思想将每个板块财务管理、差旅服务等封装成独立组件或者直接通过数据驱动渲染网格布局。样式完全复刻截图中的卡片式布局、阴影效果和配色。一、后端代码 (SpringBoot)实体类 (DTO) - 应用卡片配置Datapublic class AppCardDTO {private String category; // 分类名称如 “财务管理”private List items; // 该分类下的应用列表Data public static class AppItem { private String name; // 应用名称如 工资单 private String icon; // 图标类名或URL private String color; // 图标背景色 private String url; // 跳转链接 private String desc; // 简短描述 }}Controller 层 - 获取工作台数据RestControllerRequestMapping(“/api/dashboard”)public class DashboardController {GetMapping(/config) public Result getDashboardConfig(HttpServletRequest request) { Long userId getCurrentUserId(request); // 模拟数据实际项目中应根据用户角色从数据库查询 List config generateMockData(); return Result.success(config); } private List generateMockData() { List list new ArrayList(); // 1. 财务管理 AppCardDTO finance new AppCardDTO(); finance.setCategory(财务管理); finance.setItems(Arrays.asList( createItem(工资单, icon-pay, #1890ff, /finance/salary, 查看个人工资信息), createItem(付款申请, icon-payment, #1890ff, /finance/payment, 对外付款申请流程), createItem(备用金申请, icon-reserve, #1890ff, /finance/reserve, 备用金申请与核销), createItem(采购申请, icon-purchase, #1890ff, /finance/purchase, 采购申请流程办理), createItem(用章申请, icon-seal, #1890ff, /finance/seal, 印章使用申请流程), createItem(费用报销, icon-reimburse, #52c41a, /finance/reimburse, 费用报销流程办理) )); list.add(finance); // 2. 差旅服务 AppCardDTO travel new AppCardDTO(); travel.setCategory(差旅服务); travel.setItems(Arrays.asList( createItem(出差申请, icon-travel, #52c41a, /travel/apply, 国内出差申请), createItem(外出申请, icon-out, #52c41a, /travel/out, 短期外出申请) )); list.add(travel); // 3. 沟通协作 AppCardDTO collab new AppCardDTO(); collab.setCategory(沟通协作); collab.setItems(Arrays.asList( createItem(日报, icon-day, #fa8c16, /collab/daily, 填写每日工作汇报), createItem(周报, icon-week, #52c41a, /collab/weekly, 填写每周工作总结), createItem(月报, icon-month, #722ed1, /collab/monthly, 填写每月工作汇报), createItem汇报, icon-report, #1890ff, /collab/report, 临时汇报提交), createItem(公告, icon-notice, #fa8c16, /collab/notice, 最新信息发布), createItem(审批, icon-approve, #fa8c16, /collab/approve, 待办、已办的审批) )); list.add(collab); return list; } private AppCardDTO.AppItem createItem(String name, String icon, String color, String url, String desc) { AppCardDTO.AppItem item new AppCardDTO.AppItem(); item.setName(name); item.setIcon(icon); item.setColor(color); item.setUrl(url); item.setDesc(desc); return item; }}二、前端代码 (Vue3 Ant Design Vue)这是核心部分我们将创建一个响应式的网格布局来展示这些卡片。主页面组件 (Dashboard.vue)OpenOA 开发管理 问题反馈 流程统计 shangjia.club / dashboard/workplace 管理员 {{ group.category }} {{ item.name.charAt(0) }} {{ item.name }} {{ item.desc }} 动态 {{ item.user }} 发起了 {{ item.process }} 的流程申请 查看全部 热门博客 知名博主 博文排行 博客中心 检索资料 专业资料 实用文档 资格考试 精品文档 个人中心 学习课程 全部课程 最新课程 热门课程 我的课程 全部视频 推荐视频import { ref, onMounted } from ‘vue’;import { useRouter } from ‘vue-router’;import axios from ‘axios’;import {FireOutlined, UserOutlined, LineChartOutlined, StarOutlined,BookOutlined, FileTextOutlined, EditOutlined, FolderOutlined,VideoCameraOutlined, ClockCircleOutlined, PlayCircleOutlined} from ‘ant-design/icons-vue’;const router useRouter();const appGroups ref([]);const activities ref([{ user: ‘陈经理’, process: ‘用章申请(非合同类)’, time: ‘2020-09-12 10:49:47’ },{ user: ‘李主管’, process: ‘采购申请’, time: ‘2020-09-12 09:30:00’ }]);onMounted(async () {// 调用后端接口获取配置try {const res await axios.get(‘/api/dashboard/config’);appGroups.value res.data.data;} catch (error) {console.error(‘加载工作台失败’, error);}});const navigateTo (url) {// 简单路由跳转实际可能需要新窗口打开router.push(url);};.dashboard-container {display: flex;height: 100vh;background-color: #f0f2f5;font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;}/* 侧边栏 */.sidebar {width: 200px;background: #001529;color: white;flex-shrink: 0;}.logo {height: 64px;line-height: 64px;text-align: center;font-size: 20px;font-weight: bold;background: #002140;}/* 主内容区 */.main-content {flex: 1;display: flex;flex-direction: column;overflow: hidden;}.top-bar {height: 64px;background: white;display: flex;align-items: center;justify-content: space-between;padding: 0 24px;box-shadow: 0 1px 4px rgba(0,21,41,.08);z-index: 10;}.user-info {display: flex;align-items: center;gap: 8px;cursor: pointer;}/* 工作台网格布局/.workplace-grid {flex: 1;padding: 24px;overflow-y: auto;display: grid;grid-template-columns: 2fr 1fr; /左侧占2份右侧占1份 */gap: 24px;align-items: start;}/* 左侧应用区 */.main-apps {display: flex;flex-direction: column;gap: 24px;}.app-section {background: white;padding: 24px;border-radius: 4px;box-shadow: 0 1px 2px -2px rgba(0,0,0,.16), 0 3px 6px 0 rgba(0,0,0,.12), 0 5px 12px 4px rgba(0,0,0,.09);}.section-title {margin: 0 0 16px 0;font-size: 16px;color: #333;font-weight: 500;}.app-cards {display: grid;grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));gap: 16px;}.app-card {border: none;box-shadow: 0 1px 2px -2px rgba(0,0,0,.16), 0 3px 6px 0 rgba(0,0,0,.12);transition: all 0.3s;cursor: pointer;}.app-card:hover {transform: translateY(-2px);box-shadow: 0 4px 12px rgba(0,0,0,0.15);}.card-inner {display: flex;align-items: center;gap: 12px;}.icon-box {width: 40px;height: 40px;border-radius: 8px;display: flex;align-items: center;justify-content: center;font-size: 20px;font-weight: bold;}.card-info {flex: 1;}.card-name {font-size: 15px;font-weight: 500;color: #333;margin-bottom: 4px;}.card-desc {font-size: 12px;color: #999;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;}/* 右侧小部件 */.side-widgets {display: flex;flex-direction: column;gap: 24px;}.widget-card {box-shadow: 0 1px 2px -2px rgba(0,0,0,.16), 0 3px 6px 0 rgba(0,0,0,.12);}.widget-list {display: grid;grid-template-columns: 1fr 1fr;gap: 12px;}.widget-item {display: flex;align-items: center;gap: 6px;font-size: 13px;color: #666;cursor: pointer;padding: 4px 0;}.widget-item:hover {color: #1890ff;}/* 响应式调整/media (max-width: 1200px) {.workplace-grid {grid-template-columns: 1fr; /小屏幕下单列显示 */}}三、关键点解析动态配置后端 generateMockData 方法模拟了从数据库读取配置的过程。你可以轻松地在数据库中增加一张 app_config 表存储这些 JSON 数据从而实现管理员在后台拖拽配置首页功能。布局技巧使用了 CSS Grid (grid-template-columns: 2fr 1fr) 完美还原了截图中“左宽右窄”的布局。repeat(auto-fill, minmax(200px, 1fr)) 让应用卡片在不同屏幕宽度下自动换行排列无需媒体查询即可实现响应式。视觉还原使用了 Ant Design 的 Card 组件和自定义阴影 (box-shadow)还原了截图中的悬浮感和层次感。图标颜色采用了内联样式动态绑定方便为不同模块设置不同的主题色。扩展性“动态”列表可以直接对接后端的消息通知接口。右侧的“博客”、“文库”等模块点击后可直接跳转到对应的子系统设计页面。SpringBoot Vue3 Ant Design Vue 来实现这个功能。核心思路是后端提供一个接口返回分页的、按时间排序的动态消息列表。前端使用 a-list 组件来渲染这个列表并处理滚动加载如果需要。一、后端代码 (SpringBoot)实体类 (Entity DTO)// Activity.java - 数据库实体DataTableName(“sys_activity_log”)public class ActivityLog {TableId(type IdType.AUTO)private Long id;private Long userId; // 操作用户IDprivate String username; // 用户名 (冗余字段方便查询)private String avatar; // 用户头像private String actionType; // 动作类型: START_PROCESS, APPROVE_TASK, POST_ARTICLE, etc.private String actionDesc; // 动作描述模板: “发起了 {processName} 的流程申请”private String targetId; // 关联的目标ID (如流程实例ID、文章ID)private String targetType; // 目标类型: PROCESS, ARTICLE, etc.private Date createTime;}// ActivityDTO.java - 返回给前端的DTODatapublic class ActivityDTO {private Long id;private String username;private String avatar;private String content; // 最终拼接好的完整内容如 “陈经理 发起了 用章申请 的流程申请”private Date time;// 用于前端判断是否显示“查看详情”等按钮 private String linkUrl;}Mapper 层Mapperpublic interface ActivityLogMapper extends BaseMapper {// 可以使用 MyBatis Plus 的分页插件进行分页查询}Service 层Servicepublic class ActivityService {Autowired private ActivityLogMapper activityLogMapper; /** 获取动态消息列表 */ public IPage getActivityList(Page page) { // 1. 分页查询数据库 IPage logPage activityLogMapper.selectPage(page, new QueryWrapper().orderByDesc(create_time)); // 2. 转换为 DTO并拼接完整的消息内容 List dtoList logPage.getRecords().stream().map(log - { ActivityDTO dto new ActivityDTO(); dto.setId(log.getId()); dto.setUsername(log.getUsername()); dto.setAvatar(log.getAvatar()); dto.setTime(log.getCreateTime()); // 根据动作类型和目标信息拼接最终显示的文字 dto.setContent(formatContent(log)); // 生成跳转链接 dto.setLinkUrl(generateLink(log)); return dto; }).collect(Collectors.toList()); // 3. 构建返回的分页对象 Page dtoPage new Page(logPage.getCurrent(), logPage.getSize(), logPage.getTotal()); dtoPage.setRecords(dtoList); return dtoPage; } // 辅助方法格式化内容 private String formatContent(ActivityLog log) { // 这里可以根据 actionType 做更复杂的逻辑比如从数据库查流程名称 // 简化版直接使用预设的描述 return log.getActionDesc(); } // 辅助方法生成链接 private String generateLink(ActivityLog log) { if (PROCESS.equals(log.getTargetType())) { return /workflow/detail/ log.getTargetId(); } else if (ARTICLE.equals(log.getTargetType())) { return /blog/detail/ log.getTargetId(); } return null; }}Controller 层RestControllerRequestMapping(“/api/activity”)public class ActivityController {Autowired private ActivityService activityService; GetMapping(/list) public Result getList( RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size) { Page page new Page(current, size); IPage result activityService.getActivityList(page); return Result.success(result); }}二、前端代码 (Vue3 Ant Design Vue)动态 {{ item.content }} {{ item.content }} 0 0 -- 加载更多import { ref, onMounted } from ‘vue’;import { useRouter } from ‘vue-router’;import axios from ‘axios’;import dayjs from ‘dayjs’;import relativeTime from ‘dayjs/plugin/relativeTime’;import ‘dayjs/locale/zh-cn’;// 初始化 dayjs 插件用于显示 “刚刚”, “5分钟前”dayjs.extend(relativeTime);dayjs.locale(‘zh-cn’);const router useRouter();const activities ref([]);const loading ref(false);const loadingMore ref(false);const hasMore ref(true);const currentPage ref(1);const pageSize 10;onMounted(() {loadActivities();});const loadActivities async () {if (loading.value || (!loadingMore.value activities.value.length 0)) return;loading.value true;try {const res await axios.get(‘/api/activity/list’, {params: {current: currentPage.value,size: pageSize}});const data res.data.data; if (currentPage.value 1) { activities.value data.records; } else { activities.value [...activities.value, ...data.records]; } // 判断是否还有更多数据 hasMore.value activities.value.length {loadingMore.value true;loadActivities();};const formatTime (time) {// 使用 dayjs 显示相对时间例如 “2小时前”return dayjs(time).fromNow();// 或者显示绝对时间: return dayjs(time).format(‘YYYY-MM-DD HH:mm:ss’);};const navigateTo (url) {router.push(url);};.activity-feed {background: white;padding: 24px;border-radius: 4px;box-shadow: 0 1px 2px -2px rgba(0,0,0,.16), 0 3px 6px 0 rgba(0,0,0,.12);}.section-title {margin: 0 0 16px 0;font-size: 16px;color: #333;font-weight: 500;}/* 自定义列表项样式使其更接近截图 */:deep(.ant-list-item) {padding-top: 16px;padding-bottom: 16px;border-bottom-color: #f0f0f0;}:deep(.ant-list-item:last-child) {border-bottom: none;}:deep(.ant-list-item-meta-title) {margin-bottom: 4px;color: #333;font-size: 14px;line-height: 1.5;}:deep(.ant-list-item-meta-description) {color: #999;font-size: 12px;}:deep(.ant-list-item-meta-avatar) {margin-right: 12px;}.load-more {text-align: center;margin-top: 16px;}三、如何集成到你的项目中创建数据库表执行第一部分的 SQL 语句创建 sys_activity_log 表。埋点记录日志在你的业务代码中每当有重要操作发生时如提交审批、发布文章就向 sys_activity_log 表中插入一条记录。// 示例在审批服务中Autowiredprivate ActivityLogMapper activityLogMapper;public void approveTask(...) { // ... 审批逻辑 ... // 记录动态 ActivityLog log new ActivityLog(); log.setUserId(currentUser.getId()); log.setUsername(currentUser.getName()); log.setAvatar(currentUser.getAvatar()); log.setActionType(APPROVE_TASK); log.setActionDesc(currentUser.getName() 同意了 task.getInstanceTitle() 的申请); log.setTargetId(instanceId); log.setTargetType(PROCESS); log.setCreateTime(new Date()); activityLogMapper.insert(log); } 