会议纪要功能修复与改进
后端: - 待办同步改走 ISysOaTaskService.insertByBo/updateByBo,新任务带操作日志和IM通知 - 任务状态映射修正:done→2执行完成,其余→0执行中(原 progress→1 会显示成"等待验收") - 无负责人/无内容的待办仅作纪要记录,不再生成无主任务 - 更新时可清空字段改用显式 set(原来解绑项目、清空内容不生效) - 新增接口返回纪要ID,前端据此进入编辑态,避免重复保存生成多条 - 会议编号加3位随机数防同秒撞唯一键;异常改 ServiceException;同步失败记日志 - enrich 为待办条目注入 assigneeName,列表/详情/导出可显示负责人姓名 - SysOaTaskServiceImpl.insertByBo 回填 taskId 供调用方关联 前端: - 主持人/待办负责人改用人员单选弹窗(原多选组件取首位的方式易误操作) - 会议类型、待办状态接入 sys_dict 字典(oa_meeting_type / oa_meeting_task_status) - 新建保存后切换为编辑态;默认日期用本地时区(原 UTC 凌晨会差一天) - 导出/打印带主持人、参会人、待办负责人姓名(原来只有用户ID) - 删除已同步待办时提示任务不会被删除 SQL(已直接应用到生产库): - 字典数据补全并修复 dict_id=0 脏数据(sys_dict_* 主键为雪花ID须显式指定) - 菜单 2063809716454174722 icon 修为 documentation,授权10个角色 - 脚本改为幂等,去掉 DROP TABLE,del_flag 注释修正为逻辑删除值2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,11 +39,17 @@ public class OaMeetingMinutesController extends BaseController {
|
||||
return R.ok(service.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增,返回新纪要ID(前端据此切换为编辑态,避免重复保存生成多条)
|
||||
*/
|
||||
@Log(title = "会议纪要", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit
|
||||
@PostMapping
|
||||
public R<Void> add(@RequestBody OaMeetingMinutesBo bo) {
|
||||
return toAjax(service.insertByBo(bo));
|
||||
public R<Long> add(@RequestBody OaMeetingMinutesBo bo) {
|
||||
if (!service.insertByBo(bo)) {
|
||||
return R.fail("保存失败");
|
||||
}
|
||||
return R.ok(bo.getId());
|
||||
}
|
||||
|
||||
@Log(title = "会议纪要", businessType = BusinessType.UPDATE)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.ruoyi.oa.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -12,19 +14,22 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.oa.domain.OaMeetingMinutes;
|
||||
import com.ruoyi.oa.domain.SysOaProject;
|
||||
import com.ruoyi.oa.domain.SysOaTask;
|
||||
import com.ruoyi.oa.domain.bo.OaMeetingMinutesBo;
|
||||
import com.ruoyi.oa.domain.bo.SysOaTaskBo;
|
||||
import com.ruoyi.oa.domain.vo.OaMeetingMinutesVo;
|
||||
import com.ruoyi.oa.mapper.OaMeetingMinutesMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaProjectMapper;
|
||||
import com.ruoyi.oa.mapper.SysOaTaskMapper;
|
||||
import com.ruoyi.oa.service.IOaMeetingMinutesService;
|
||||
import com.ruoyi.oa.service.ISysOaTaskService;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -32,6 +37,13 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 会议纪要
|
||||
*
|
||||
* 待办同步说明:tasks_json 中"有负责人且有内容"的条目会通过 ISysOaTaskService
|
||||
* 生成/更新 sys_oa_task(复用任务模块的操作日志、IM 通知逻辑),任务ID回写进 JSON。
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
@@ -39,12 +51,20 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
private final OaMeetingMinutesMapper baseMapper;
|
||||
private final SysOaProjectMapper projectMapper;
|
||||
private final SysOaTaskMapper taskMapper;
|
||||
private final ISysOaTaskService taskService;
|
||||
private final SysUserMapper userMapper;
|
||||
private final ObjectMapper json = new ObjectMapper();
|
||||
|
||||
/** sys_oa_task.state:0执行中 2执行完成(1等待验收/15延期申请由任务模块流转,会议页不使用) */
|
||||
private static final Long TASK_STATE_DOING = 0L;
|
||||
private static final Long TASK_STATE_DONE = 2L;
|
||||
|
||||
@Override
|
||||
public OaMeetingMinutesVo queryById(Long id) {
|
||||
OaMeetingMinutesVo vo = baseMapper.selectVoById(id);
|
||||
if (vo == null) {
|
||||
throw new ServiceException("会议纪要不存在或已删除");
|
||||
}
|
||||
enrich(Collections.singletonList(vo));
|
||||
return vo;
|
||||
}
|
||||
@@ -72,7 +92,8 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
if (StringUtils.isNotBlank(bo.getKeyword())) {
|
||||
String kw = bo.getKeyword().trim();
|
||||
lqw.and(w -> w.like(OaMeetingMinutes::getSubject, kw)
|
||||
.or().like(OaMeetingMinutes::getLocation, kw));
|
||||
.or().like(OaMeetingMinutes::getLocation, kw)
|
||||
.or().like(OaMeetingMinutes::getMeetingCode, kw));
|
||||
}
|
||||
lqw.ge(bo.getDateFrom() != null, OaMeetingMinutes::getMeetingDate, bo.getDateFrom());
|
||||
lqw.le(bo.getDateTo() != null, OaMeetingMinutes::getMeetingDate, bo.getDateTo());
|
||||
@@ -82,16 +103,25 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/** 给列表填上 项目名/编号、主持人/参会人员 昵称 */
|
||||
/** 填充 项目名/编号、主持人/参会人/待办负责人 昵称 */
|
||||
private void enrich(List<OaMeetingMinutesVo> list) {
|
||||
if (list == null || list.isEmpty()) return;
|
||||
Set<Long> projectIds = new HashSet<>();
|
||||
Set<Long> userIds = new HashSet<>();
|
||||
Map<Long, ArrayNode> taskNodes = new HashMap<>();
|
||||
for (OaMeetingMinutesVo v : list) {
|
||||
if (v == null) continue;
|
||||
if (v.getProjectId() != null) projectIds.add(v.getProjectId());
|
||||
if (v.getHostUserId() != null) userIds.add(v.getHostUserId());
|
||||
for (Long uid : parseLongCsv(v.getAttendeeUserIds())) userIds.add(uid);
|
||||
userIds.addAll(parseLongCsv(v.getAttendeeUserIds()));
|
||||
ArrayNode arr = parseTaskArray(v.getTasksJson());
|
||||
if (arr != null) {
|
||||
taskNodes.put(v.getId(), arr);
|
||||
for (JsonNode n : arr) {
|
||||
Long uid = longOf(n, "assigneeUserId");
|
||||
if (uid != null) userIds.add(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<Long, SysOaProject> pMap = projectIds.isEmpty() ? Collections.emptyMap()
|
||||
: projectMapper.selectList(new QueryWrapper<SysOaProject>().in("project_id", projectIds))
|
||||
@@ -113,14 +143,26 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
SysUser u = uMap.get(v.getHostUserId());
|
||||
if (u != null) v.setHostUserName(u.getNickName());
|
||||
}
|
||||
List<Long> attIds = parseLongCsv(v.getAttendeeUserIds());
|
||||
if (!attIds.isEmpty()) {
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Long uid : attIds) {
|
||||
SysUser u = uMap.get(uid);
|
||||
if (u != null) names.add(u.getNickName());
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Long uid : parseLongCsv(v.getAttendeeUserIds())) {
|
||||
SysUser u = uMap.get(uid);
|
||||
if (u != null) names.add(u.getNickName());
|
||||
}
|
||||
if (!names.isEmpty()) v.setAttendeeUserNames(String.join(",", names));
|
||||
// 待办条目写入 assigneeName,前端展示/导出用
|
||||
ArrayNode arr = taskNodes.get(v.getId());
|
||||
if (arr != null) {
|
||||
for (JsonNode n : arr) {
|
||||
if (!n.isObject()) continue;
|
||||
Long uid = longOf(n, "assigneeUserId");
|
||||
SysUser u = uid == null ? null : uMap.get(uid);
|
||||
((ObjectNode) n).put("assigneeName", u == null ? null : u.getNickName());
|
||||
}
|
||||
try {
|
||||
v.setTasksJson(json.writeValueAsString(arr));
|
||||
} catch (Exception e) {
|
||||
log.warn("会议纪要[{}] tasksJson 序列化失败", v.getId(), e);
|
||||
}
|
||||
v.setAttendeeUserNames(String.join(",", names));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,9 +171,10 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean insertByBo(OaMeetingMinutesBo bo) {
|
||||
OaMeetingMinutes add = BeanUtil.toBean(bo, OaMeetingMinutes.class);
|
||||
if (StringUtils.isBlank(add.getMeetingCode())) {
|
||||
add.setMeetingCode("MT-" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
|
||||
}
|
||||
add.setId(null);
|
||||
// 时间戳+3位随机数,避免同一秒并发保存撞唯一键
|
||||
add.setMeetingCode("MT-" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
|
||||
+ RandomUtil.randomNumbers(3));
|
||||
if (StringUtils.isBlank(add.getMeetingType())) add.setMeetingType("other");
|
||||
if (add.getSyncTask() == null) add.setSyncTask(1);
|
||||
validBeforeSave(add);
|
||||
@@ -141,8 +184,7 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
if (Integer.valueOf(1).equals(add.getSyncTask())) {
|
||||
String updated = syncTasks(add);
|
||||
if (updated != null && !updated.equals(add.getTasksJson())) {
|
||||
add.setTasksJson(updated);
|
||||
baseMapper.updateById(add);
|
||||
patchTasksJson(add.getId(), updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,28 +194,50 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateByBo(OaMeetingMinutesBo bo) {
|
||||
if (bo.getId() == null) throw new ServiceException("缺少纪要ID");
|
||||
OaMeetingMinutes old = baseMapper.selectById(bo.getId());
|
||||
if (old == null) throw new ServiceException("会议纪要不存在或已删除");
|
||||
OaMeetingMinutes upd = BeanUtil.toBean(bo, OaMeetingMinutes.class);
|
||||
if (StringUtils.isBlank(upd.getMeetingType())) upd.setMeetingType("other");
|
||||
validBeforeSave(upd);
|
||||
boolean ok = baseMapper.updateById(upd) > 0;
|
||||
if (ok && Integer.valueOf(1).equals(upd.getSyncTask())) {
|
||||
String updated = syncTasks(upd);
|
||||
if (updated != null && !updated.equals(upd.getTasksJson())) {
|
||||
upd.setTasksJson(updated);
|
||||
baseMapper.updateById(upd);
|
||||
}
|
||||
if (Integer.valueOf(1).equals(upd.getSyncTask())) {
|
||||
upd.setTasksJson(syncTasks(upd));
|
||||
}
|
||||
return ok;
|
||||
// 可清空字段(解绑项目、清空人员/内容等)显式 set,避免 MyBatis-Plus 忽略 null 导致清空不生效
|
||||
LambdaUpdateWrapper<OaMeetingMinutes> luw = Wrappers.<OaMeetingMinutes>lambdaUpdate()
|
||||
.set(OaMeetingMinutes::getProjectId, upd.getProjectId())
|
||||
.set(OaMeetingMinutes::getHostUserId, upd.getHostUserId())
|
||||
.set(OaMeetingMinutes::getAttendeeUserIds, upd.getAttendeeUserIds())
|
||||
.set(OaMeetingMinutes::getLocation, upd.getLocation())
|
||||
.set(OaMeetingMinutes::getTopic, upd.getTopic())
|
||||
.set(OaMeetingMinutes::getDiscussion, upd.getDiscussion())
|
||||
.set(OaMeetingMinutes::getDecision, upd.getDecision())
|
||||
.set(OaMeetingMinutes::getTasksJson, upd.getTasksJson())
|
||||
.eq(OaMeetingMinutes::getId, upd.getId());
|
||||
OaMeetingMinutes entity = new OaMeetingMinutes();
|
||||
entity.setMeetingDate(upd.getMeetingDate());
|
||||
entity.setMeetingType(upd.getMeetingType());
|
||||
entity.setSubject(upd.getSubject());
|
||||
entity.setSyncTask(upd.getSyncTask());
|
||||
return baseMapper.update(entity, luw) > 0;
|
||||
}
|
||||
|
||||
private void patchTasksJson(Long id, String tasksJson) {
|
||||
baseMapper.update(null, Wrappers.<OaMeetingMinutes>lambdaUpdate()
|
||||
.set(OaMeetingMinutes::getTasksJson, tasksJson)
|
||||
.eq(OaMeetingMinutes::getId, id));
|
||||
}
|
||||
|
||||
private void validBeforeSave(OaMeetingMinutes e) {
|
||||
if (e.getMeetingDate() == null) throw new IllegalArgumentException("请选择会议日期");
|
||||
if (StringUtils.isBlank(e.getSubject())) throw new IllegalArgumentException("请输入会议主题");
|
||||
if (e.getMeetingDate() == null) throw new ServiceException("请选择会议日期");
|
||||
if (StringUtils.isBlank(e.getSubject())) throw new ServiceException("请输入会议主题");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 tasks_json 中的每个待办同步生成 sys_oa_task:
|
||||
* - 若条目已有 taskId 且仍存在 → 更新内容/截止/完成状态
|
||||
* - 否则新建并把 taskId 写回 JSON
|
||||
* 将待办同步到 sys_oa_task(通过任务服务,带操作日志和 IM 通知):
|
||||
* - 无内容或无负责人的条目仅作纪要记录,不生成任务
|
||||
* - 已有 taskId 且任务仍存在 → 更新;否则新建并把 taskId 写回 JSON
|
||||
* - 同步失败不阻塞纪要保存,仅记录日志
|
||||
*/
|
||||
private String syncTasks(OaMeetingMinutes meeting) {
|
||||
if (StringUtils.isBlank(meeting.getTasksJson())) return meeting.getTasksJson();
|
||||
@@ -181,61 +245,64 @@ public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
|
||||
JsonNode root = json.readTree(meeting.getTasksJson());
|
||||
if (!root.isArray()) return meeting.getTasksJson();
|
||||
ArrayNode arr = (ArrayNode) root;
|
||||
Long currentUser = LoginHelper.getUserId();
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
JsonNode n = arr.get(i);
|
||||
for (JsonNode n : arr) {
|
||||
if (!n.isObject()) continue;
|
||||
ObjectNode o = (ObjectNode) n;
|
||||
String content = textOf(o, "content");
|
||||
if (StringUtils.isBlank(content)) continue;
|
||||
|
||||
Long assignee = longOf(o, "assigneeUserId");
|
||||
String deadline = textOf(o, "deadline");
|
||||
if (StringUtils.isBlank(content) || assignee == null) continue;
|
||||
|
||||
String status = textOf(o, "status");
|
||||
boolean done = "done".equals(status);
|
||||
Long existTaskId = longOf(o, "taskId");
|
||||
SysOaTask exist = existTaskId == null ? null : taskMapper.selectById(existTaskId);
|
||||
|
||||
SysOaTask t = null;
|
||||
if (existTaskId != null) {
|
||||
t = taskMapper.selectById(existTaskId);
|
||||
}
|
||||
boolean isNew = (t == null);
|
||||
if (isNew) {
|
||||
t = new SysOaTask();
|
||||
t.setCreateUserId(currentUser);
|
||||
t.setBeginTime(meeting.getMeetingDate());
|
||||
}
|
||||
SysOaTaskBo t = new SysOaTaskBo();
|
||||
t.setProjectId(meeting.getProjectId());
|
||||
t.setTaskTitle(content.length() > 200 ? content.substring(0, 200) : content);
|
||||
t.setContent("来自会议纪要:" + meeting.getSubject());
|
||||
t.setWorkerId(assignee);
|
||||
if (StringUtils.isNotBlank(deadline)) {
|
||||
try { t.setFinishTime(new SimpleDateFormat("yyyy-MM-dd").parse(deadline)); } catch (Exception ignored) {}
|
||||
}
|
||||
// 任务状态:done=2 已完成;progress=1 进行中;pending=0 待办
|
||||
if ("done".equals(status)) t.setState(2L);
|
||||
else if ("progress".equals(status)) t.setState(1L);
|
||||
else t.setState(0L);
|
||||
|
||||
if (isNew) {
|
||||
taskMapper.insert(t);
|
||||
o.put("taskId", t.getTaskId());
|
||||
t.setTaskTitle(StringUtils.substring(content, 0, 200));
|
||||
t.setContent("来自会议纪要「" + meeting.getSubject() + "」");
|
||||
t.setFinishTime(parseDay(textOf(o, "deadline")));
|
||||
t.setState(done ? TASK_STATE_DONE : TASK_STATE_DOING);
|
||||
if (done) t.setCompletedTime(new Date());
|
||||
if (exist == null) {
|
||||
t.setBeginTime(meeting.getMeetingDate());
|
||||
t.setWorkerIds(String.valueOf(assignee));
|
||||
t.setStatus(0L);
|
||||
taskService.insertByBo(t);
|
||||
if (t.getTaskId() != null) o.put("taskId", t.getTaskId());
|
||||
} else {
|
||||
taskMapper.updateById(t);
|
||||
t.setTaskId(existTaskId);
|
||||
t.setWorkerId(assignee);
|
||||
taskService.updateByBo(t);
|
||||
}
|
||||
}
|
||||
return json.writeValueAsString(arr);
|
||||
} catch (Exception e) {
|
||||
// 同步失败不影响主流程,仅日志
|
||||
log.warn("会议纪要[{}]待办同步OA任务失败", meeting.getMeetingCode(), e);
|
||||
return meeting.getTasksJson();
|
||||
}
|
||||
}
|
||||
|
||||
private String textOf(ObjectNode n, String k) {
|
||||
private Date parseDay(String s) {
|
||||
if (StringUtils.isBlank(s)) return null;
|
||||
try { return new SimpleDateFormat("yyyy-MM-dd").parse(s); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
private ArrayNode parseTaskArray(String s) {
|
||||
if (StringUtils.isBlank(s)) return null;
|
||||
try {
|
||||
JsonNode root = json.readTree(s);
|
||||
return root.isArray() ? (ArrayNode) root : null;
|
||||
} catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
private String textOf(JsonNode n, String k) {
|
||||
JsonNode v = n.get(k);
|
||||
return v == null || v.isNull() ? null : v.asText();
|
||||
}
|
||||
|
||||
private Long longOf(ObjectNode n, String k) {
|
||||
private Long longOf(JsonNode n, String k) {
|
||||
JsonNode v = n.get(k);
|
||||
if (v == null || v.isNull()) return null;
|
||||
try { return v.isNumber() ? v.asLong() : Long.parseLong(v.asText()); }
|
||||
|
||||
@@ -217,6 +217,8 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
|
||||
add.setOriginFinishTime(add.getFinishTime());
|
||||
add.setWorkerId(workerId);
|
||||
flag = baseMapper.insert(add) > 0;
|
||||
// 回填任务ID,供调用方(如会议纪要待办同步)建立关联
|
||||
bo.setTaskId(add.getTaskId());
|
||||
if (flag) {
|
||||
operationLogService.recordLog(add.getProjectId(), 3, add.getTaskId(),
|
||||
add.getTaskTitle(), 1, "新增任务: " + add.getTaskTitle(), null, null);
|
||||
|
||||
Reference in New Issue
Block a user