会议纪要:原始WIP版本留底

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 10:12:12 +08:00
parent 79e536aeca
commit e5bfa0c78c
10 changed files with 1275 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
package com.ruoyi.oa.controller;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.oa.domain.bo.OaMeetingMinutesBo;
import com.ruoyi.oa.domain.vo.OaMeetingMinutesVo;
import com.ruoyi.oa.service.IOaMeetingMinutesService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
/**
* 会议纪要
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/meetingMinutes")
public class OaMeetingMinutesController extends BaseController {
private final IOaMeetingMinutesService service;
@GetMapping("/list")
public TableDataInfo<OaMeetingMinutesVo> list(OaMeetingMinutesBo bo, PageQuery pageQuery) {
return service.queryPageList(bo, pageQuery);
}
@GetMapping("/{id}")
public R<OaMeetingMinutesVo> getInfo(@NotNull @PathVariable Long id) {
return R.ok(service.queryById(id));
}
@Log(title = "会议纪要", businessType = BusinessType.INSERT)
@RepeatSubmit
@PostMapping
public R<Void> add(@RequestBody OaMeetingMinutesBo bo) {
return toAjax(service.insertByBo(bo));
}
@Log(title = "会议纪要", businessType = BusinessType.UPDATE)
@RepeatSubmit
@PutMapping
public R<Void> edit(@RequestBody OaMeetingMinutesBo bo) {
return toAjax(service.updateByBo(bo));
}
@Log(title = "会议纪要", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty @PathVariable Long[] ids) {
return toAjax(service.deleteWithValidByIds(Arrays.asList(ids), true));
}
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.oa.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* 会议纪要
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("oa_meeting_minutes")
public class OaMeetingMinutes extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "id")
private Long id;
private String meetingCode;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date meetingDate;
/** sys_oa_project.project_id */
private Long projectId;
private String meetingType;
private String subject;
private String location;
/** sys_user.user_id */
private Long hostUserId;
/** 参会人员 user_id CSV */
private String attendeeUserIds;
private String topic;
private String discussion;
private String decision;
/** 待办 JSON[{assigneeUserId, content, deadline, status, taskId}] */
private String tasksJson;
/** 1=保存时自动同步生成 sys_oa_task */
private Integer syncTask;
@TableLogic
private String delFlag;
}

View File

@@ -0,0 +1,47 @@
package com.ruoyi.oa.domain.bo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* 会议纪要 BO
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OaMeetingMinutesBo extends BaseEntity {
private Long id;
private String meetingCode;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date meetingDate;
private Long projectId;
private String meetingType;
private String subject;
private String location;
private Long hostUserId;
private String attendeeUserIds;
private String topic;
private String discussion;
private String decision;
private String tasksJson;
private Integer syncTask;
/** 关键字模糊(主题/地点/项目名) */
private String keyword;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateFrom;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateTo;
}

View File

@@ -0,0 +1,51 @@
package com.ruoyi.oa.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 会议纪要 VO
*/
@Data
public class OaMeetingMinutesVo implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String meetingCode;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date meetingDate;
private Long projectId;
/** 冗余:项目编号 / 名称(列表联表带出) */
private String projectNum;
private String projectName;
private String meetingType;
private String subject;
private String location;
private Long hostUserId;
/** 冗余:主持人昵称 */
private String hostUserName;
private String attendeeUserIds;
/** 冗余:参会人员昵称(逗号分隔) */
private String attendeeUserNames;
private String topic;
private String discussion;
private String decision;
private String tasksJson;
private Integer syncTask;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
private String updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
}

View File

@@ -0,0 +1,8 @@
package com.ruoyi.oa.mapper;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import com.ruoyi.oa.domain.OaMeetingMinutes;
import com.ruoyi.oa.domain.vo.OaMeetingMinutesVo;
public interface OaMeetingMinutesMapper extends BaseMapperPlus<OaMeetingMinutesMapper, OaMeetingMinutes, OaMeetingMinutesVo> {
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.oa.service;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.oa.domain.bo.OaMeetingMinutesBo;
import com.ruoyi.oa.domain.vo.OaMeetingMinutesVo;
import java.util.Collection;
import java.util.List;
public interface IOaMeetingMinutesService {
OaMeetingMinutesVo queryById(Long id);
TableDataInfo<OaMeetingMinutesVo> queryPageList(OaMeetingMinutesBo bo, PageQuery pageQuery);
List<OaMeetingMinutesVo> queryList(OaMeetingMinutesBo bo);
Boolean insertByBo(OaMeetingMinutesBo bo);
Boolean updateByBo(OaMeetingMinutesBo bo);
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}

View File

@@ -0,0 +1,260 @@
package com.ruoyi.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
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.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.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.system.mapper.SysUserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class OaMeetingMinutesServiceImpl implements IOaMeetingMinutesService {
private final OaMeetingMinutesMapper baseMapper;
private final SysOaProjectMapper projectMapper;
private final SysOaTaskMapper taskMapper;
private final SysUserMapper userMapper;
private final ObjectMapper json = new ObjectMapper();
@Override
public OaMeetingMinutesVo queryById(Long id) {
OaMeetingMinutesVo vo = baseMapper.selectVoById(id);
enrich(Collections.singletonList(vo));
return vo;
}
@Override
public TableDataInfo<OaMeetingMinutesVo> queryPageList(OaMeetingMinutesBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<OaMeetingMinutes> lqw = buildQueryWrapper(bo);
Page<OaMeetingMinutesVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
enrich(result.getRecords());
return TableDataInfo.build(result);
}
@Override
public List<OaMeetingMinutesVo> queryList(OaMeetingMinutesBo bo) {
List<OaMeetingMinutesVo> list = baseMapper.selectVoList(buildQueryWrapper(bo));
enrich(list);
return list;
}
private LambdaQueryWrapper<OaMeetingMinutes> buildQueryWrapper(OaMeetingMinutesBo bo) {
LambdaQueryWrapper<OaMeetingMinutes> lqw = Wrappers.lambdaQuery();
if (bo != null) {
lqw.eq(bo.getProjectId() != null, OaMeetingMinutes::getProjectId, bo.getProjectId());
lqw.eq(StringUtils.isNotBlank(bo.getMeetingType()), OaMeetingMinutes::getMeetingType, bo.getMeetingType());
if (StringUtils.isNotBlank(bo.getKeyword())) {
String kw = bo.getKeyword().trim();
lqw.and(w -> w.like(OaMeetingMinutes::getSubject, kw)
.or().like(OaMeetingMinutes::getLocation, kw));
}
lqw.ge(bo.getDateFrom() != null, OaMeetingMinutes::getMeetingDate, bo.getDateFrom());
lqw.le(bo.getDateTo() != null, OaMeetingMinutes::getMeetingDate, bo.getDateTo());
}
lqw.orderByDesc(OaMeetingMinutes::getMeetingDate)
.orderByDesc(OaMeetingMinutes::getCreateTime);
return lqw;
}
/** 给列表填上 项目名/编号、主持人/参会人员 昵称 */
private void enrich(List<OaMeetingMinutesVo> list) {
if (list == null || list.isEmpty()) return;
Set<Long> projectIds = new HashSet<>();
Set<Long> userIds = new HashSet<>();
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);
}
Map<Long, SysOaProject> pMap = projectIds.isEmpty() ? Collections.emptyMap()
: projectMapper.selectList(new QueryWrapper<SysOaProject>().in("project_id", projectIds))
.stream().collect(Collectors.toMap(SysOaProject::getProjectId, p -> p, (a, b) -> a));
Map<Long, SysUser> uMap = userIds.isEmpty() ? Collections.emptyMap()
: userMapper.selectList(new QueryWrapper<SysUser>().in("user_id", userIds))
.stream().collect(Collectors.toMap(SysUser::getUserId, u -> u, (a, b) -> a));
for (OaMeetingMinutesVo v : list) {
if (v == null) continue;
if (v.getProjectId() != null) {
SysOaProject p = pMap.get(v.getProjectId());
if (p != null) {
v.setProjectName(p.getProjectName());
v.setProjectNum(p.getProjectNum());
}
}
if (v.getHostUserId() != null) {
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());
}
v.setAttendeeUserNames(String.join(",", names));
}
}
}
@Override
@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()));
}
if (StringUtils.isBlank(add.getMeetingType())) add.setMeetingType("other");
if (add.getSyncTask() == null) add.setSyncTask(1);
validBeforeSave(add);
boolean ok = baseMapper.insert(add) > 0;
if (ok) {
bo.setId(add.getId());
if (Integer.valueOf(1).equals(add.getSyncTask())) {
String updated = syncTasks(add);
if (updated != null && !updated.equals(add.getTasksJson())) {
add.setTasksJson(updated);
baseMapper.updateById(add);
}
}
}
return ok;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(OaMeetingMinutesBo bo) {
OaMeetingMinutes upd = BeanUtil.toBean(bo, OaMeetingMinutes.class);
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);
}
}
return ok;
}
private void validBeforeSave(OaMeetingMinutes e) {
if (e.getMeetingDate() == null) throw new IllegalArgumentException("请选择会议日期");
if (StringUtils.isBlank(e.getSubject())) throw new IllegalArgumentException("请输入会议主题");
}
/**
* 将 tasks_json 中的每个待办同步生成 sys_oa_task
* - 若条目已有 taskId 且仍存在 → 更新内容/截止/完成状态
* - 否则新建并把 taskId 写回 JSON
*/
private String syncTasks(OaMeetingMinutes meeting) {
if (StringUtils.isBlank(meeting.getTasksJson())) return meeting.getTasksJson();
try {
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);
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");
String status = textOf(o, "status");
Long existTaskId = longOf(o, "taskId");
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());
}
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());
} else {
taskMapper.updateById(t);
}
}
return json.writeValueAsString(arr);
} catch (Exception e) {
// 同步失败不影响主流程,仅日志
return meeting.getTasksJson();
}
}
private String textOf(ObjectNode n, String k) {
JsonNode v = n.get(k);
return v == null || v.isNull() ? null : v.asText();
}
private Long longOf(ObjectNode 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()); }
catch (NumberFormatException e) { return null; }
}
private List<Long> parseLongCsv(String csv) {
if (StringUtils.isBlank(csv)) return Collections.emptyList();
List<Long> r = new ArrayList<>();
for (String s : csv.split(",")) {
String t = s.trim();
if (t.isEmpty()) continue;
try { r.add(Long.parseLong(t)); } catch (NumberFormatException ignored) {}
}
return r;
}
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
return baseMapper.deleteBatchIds(ids) > 0;
}
}