会议纪要功能修复与改进

后端:
- 待办同步改走 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:
2026-06-11 10:12:48 +08:00
parent e5bfa0c78c
commit a9c9b8a5ea
5 changed files with 330 additions and 174 deletions

View File

@@ -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)

View File

@@ -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.state0执行中 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()); }

View File

@@ -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);

View File

@@ -18,7 +18,8 @@
<el-button size="small" :type="isRecording ? 'danger' : 'warning'"
:icon="isRecording ? 'el-icon-video-pause' : 'el-icon-microphone'"
@click="cmdRecord">{{ isRecording ? '停止录音' : '语音录入' }}</el-button>
<el-button size="small" type="primary" icon="el-icon-document-checked" @click="cmdSave">保存</el-button>
<el-button size="small" type="primary" icon="el-icon-document-checked"
:loading="saving" @click="cmdSave">保存</el-button>
<el-button size="small" icon="el-icon-download" @click="cmdExport">导出</el-button>
<el-button size="small" icon="el-icon-printer" @click="cmdPrint">打印</el-button>
</div>
@@ -37,22 +38,22 @@
<el-button type="text" icon="el-icon-plus" @click="cmdNew">新建</el-button>
</div>
<div class="filter-row">
<el-input v-model="historyQuery.keyword" size="mini" placeholder="搜索主题 / 地点"
<el-input v-model="historyQuery.keyword" size="mini" placeholder="搜索编号 / 主题 / 地点"
clearable prefix-icon="el-icon-search"
@keyup.enter.native="loadHistory" @clear="loadHistory" />
@keyup.enter.native="searchHistory" @clear="searchHistory" />
</div>
<div class="filter-row">
<project-select v-model="historyQuery.projectId" placeholder="项目筛选" clearable
size="mini" style="flex:1" @input="loadHistory" />
size="mini" style="flex:1" @input="searchHistory" />
<el-select v-model="historyQuery.meetingType" size="mini" clearable placeholder="类型"
style="width:90px;margin-left:4px" @change="loadHistory">
<el-option v-for="t in meetingTypes" :key="t.value" :value="t.value" :label="t.label" />
style="width:90px;margin-left:4px" @change="searchHistory">
<el-option v-for="t in dict.type.oa_meeting_type" :key="t.value" :value="t.value" :label="t.label" />
</el-select>
</div>
<div class="filter-row">
<el-date-picker v-model="historyQuery.dateRange" type="daterange" size="mini" range-separator="~"
start-placeholder="开始" end-placeholder="结束" value-format="yyyy-MM-dd"
style="width:100%" @change="loadHistory" />
style="width:100%" @change="searchHistory" />
</div>
<div v-loading="historyLoading" class="hist-list">
@@ -64,15 +65,13 @@
@click="loadMinutes(m.id)">
<div class="hc-top">
<span class="hc-code">{{ m.meetingCode }}</span>
<el-tag size="mini" :type="typeTag(m.meetingType)" effect="plain">
{{ typeLabel(m.meetingType) }}
</el-tag>
<dict-tag :options="dict.type.oa_meeting_type" :value="m.meetingType" />
<el-button type="text" icon="el-icon-delete" class="hc-del"
@click.stop="removeMinutes(m)" />
</div>
<div class="hc-subject">{{ m.subject }}</div>
<div class="hc-line"><i class="el-icon-date" /> {{ m.meetingDate }}
<span v-if="m.projectNum" class="hc-proj">· {{ m.projectNum }}</span>
<span v-if="m.projectName" class="hc-proj">· {{ m.projectName }}</span>
</div>
<div v-if="m.hostUserName" class="hc-line"><i class="el-icon-s-custom" /> 主持{{ m.hostUserName }}</div>
<div v-if="m.location" class="hc-line"><i class="el-icon-location-outline" /> {{ m.location }}</div>
@@ -106,33 +105,35 @@
</el-col>
<el-col :span="8">
<el-form-item label="项目">
<project-select v-model="form.projectId" placeholder="选择项目" clearable style="width:100%" />
<project-select v-model="form.projectId" placeholder="不选则为非项目会议" clearable style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="会议类型">
<el-select v-model="form.meetingType" style="width:100%">
<el-option v-for="t in meetingTypes" :key="t.value" :value="t.value" :label="t.label" />
<el-option v-for="t in dict.type.oa_meeting_type" :key="t.value" :value="t.value" :label="t.label" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="会议主题" required>
<el-input v-model="form.subject" placeholder="输入会议主题" />
<el-input v-model="form.subject" maxlength="200" placeholder="输入会议主题" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="会议地点">
<el-input v-model="form.location" placeholder="会议室 / 线上" />
<el-input v-model="form.location" maxlength="100" placeholder="会议室 / 线上" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-col :span="8">
<el-form-item label="主持人">
<user-select :value="hostCsv" @input="onHostInput" />
<div class="help-tip">仅取首位为主持人如要换人请先点 ×</div>
<el-tag v-if="form.hostUserId" closable @close="clearHost">
{{ form.hostUserName || ('#' + form.hostUserId) }}
</el-tag>
<el-button type="text" @click="pickHost">{{ form.hostUserId ? '更换' : '点击选择' }}</el-button>
</el-form-item>
</el-col>
<el-col :span="24">
<el-col :span="16">
<el-form-item label="参会人员">
<user-select v-model="form.attendeeUserIds" />
</el-form-item>
@@ -172,7 +173,7 @@
<div class="sec-block">
<div class="sec-hd">
<span class="sec-num"></span> 待办事项
<span class="sec-tip">保存时按上方开关自动同步 OA 任务</span>
<span class="sec-tip">填了负责人和内容的待办保存时按上方开关同步 OA 任务并通知负责人</span>
<el-button type="text" icon="el-icon-plus" class="add-task" @click="addTask">添加待办</el-button>
</div>
@@ -181,11 +182,18 @@
<div class="task-line">
<div class="tf tf-assignee">
<label>负责人</label>
<user-select :value="t._assigneeCsv" @input="onTaskAssigneeChange(t, $event)" />
<div>
<el-tag v-if="t.assigneeUserId" size="small" closable @close="clearAssignee(t)">
{{ t.assigneeName || ('#' + t.assigneeUserId) }}
</el-tag>
<el-button type="text" size="mini" @click="pickAssignee(i)">
{{ t.assigneeUserId ? '更换' : '选择' }}
</el-button>
</div>
</div>
<div class="tf tf-content">
<label>任务内容</label>
<el-input v-model="t.content" size="mini" placeholder="任务描述..." />
<el-input v-model="t.content" size="mini" maxlength="200" placeholder="任务描述..." />
</div>
<div class="tf tf-deadline">
<label>截止日期</label>
@@ -195,7 +203,8 @@
<div class="tf tf-status">
<label>状态</label>
<el-select v-model="t.status" size="mini" style="width:100%">
<el-option v-for="o in taskStatusOpts" :key="o.value" :value="o.value" :label="o.label" />
<el-option v-for="o in dict.type.oa_meeting_task_status" :key="o.value"
:value="o.value" :label="o.label" />
</el-select>
</div>
<div class="tf tf-act">
@@ -209,6 +218,9 @@
</el-card>
</el-col>
</el-row>
<!-- 人员单选弹窗主持人 / 待办负责人共用 -->
<user-single-select ref="userPicker" v-model="userPickerVisible" @onSelected="onUserPicked" />
</div>
</template>
@@ -218,33 +230,28 @@ import {
updateMeetingMinutes, delMeetingMinutes
} from '@/api/oa/meetingMinutes'
import UserSelect from '@/components/UserSelect'
import UserSingleSelect from '@/components/UserSelect/single'
import ProjectSelect from '@/components/fad-service/ProjectSelect'
const MEETING_TYPES = [
{ value: 'tech', label: '技术评审' },
{ value: 'project', label: '项目推进' },
{ value: 'client', label: '客户沟通' },
{ value: 'weekly', label: '周例会' },
{ value: 'other', label: '其他' }
]
const TASK_STATUS = [
{ value: 'pending', label: '待办' },
{ value: 'progress', label: '进行中' },
{ value: 'done', label: '已完成' }
]
const TYPE_TAG = { tech: 'info', project: 'primary', client: 'warning', weekly: 'success', other: '' }
function localToday () {
const d = new Date()
const p = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
}
function emptyForm () {
return {
id: null,
meetingCode: '',
meetingDate: new Date().toISOString().slice(0, 10),
meetingDate: localToday(),
projectId: null,
meetingType: 'tech',
meetingType: 'other',
subject: '',
location: '',
hostUserId: null,
hostUserName: '',
attendeeUserIds: '',
attendeeUserNames: '',
topic: '',
discussion: '',
decision: '',
@@ -255,15 +262,14 @@ function emptyForm () {
export default {
name: 'OaMeeting',
components: { UserSelect, ProjectSelect },
components: { UserSelect, UserSingleSelect, ProjectSelect },
dicts: ['oa_meeting_type', 'oa_meeting_task_status'],
data () {
return {
meetingTypes: MEETING_TYPES,
taskStatusOpts: TASK_STATUS,
statusType: 'success',
statusText: '就绪',
protoWarn: false,
saving: false,
form: emptyForm(),
@@ -280,8 +286,9 @@ export default {
voiceFinalText: '',
voiceInterim: '',
/** 主持人 CSV — 直接绑 UserSelect。@input 单向同步到 form.hostUserId取首位 */
hostCsv: ''
// 人员单选弹窗当前服务对象:'host' 或待办行下标
userPickerVisible: false,
userPickerTarget: 'host'
}
},
created () {
@@ -296,29 +303,58 @@ export default {
const local = host === 'localhost' || host === '127.0.0.1'
this.protoWarn = !(location.protocol === 'https:' || local)
},
typeLabel (v) { return (MEETING_TYPES.find(t => t.value === v) || {}).label || '其他' },
typeTag (v) { return TYPE_TAG[v] || '' },
dictLabel (dictKey, v) {
const hit = (this.dict.type[dictKey] || []).find(t => t.value === v)
return hit ? hit.label : (v || '-')
},
/** UserSelect 是多选;主持人只保留首位 */
onHostInput (val) {
const arr = typeof val === 'string' ? val.split(',').filter(Boolean) : (val || [])
this.hostCsv = arr.length ? String(arr[0]) : ''
this.form.hostUserId = arr.length ? Number(arr[0]) : null
// ============ 人员选择 ============
pickHost () {
this.userPickerTarget = 'host'
this.userPickerVisible = true
},
clearHost () {
this.form.hostUserId = null
this.form.hostUserName = ''
},
pickAssignee (i) {
this.userPickerTarget = i
this.userPickerVisible = true
},
clearAssignee (t) {
t.assigneeUserId = null
t.assigneeName = ''
},
onUserPicked (row) {
if (!row) return
if (this.userPickerTarget === 'host') {
this.form.hostUserId = row.userId
this.form.hostUserName = row.nickName
} else {
const t = this.form.tasks[this.userPickerTarget]
if (t) {
t.assigneeUserId = row.userId
t.assigneeName = row.nickName
}
}
},
// ============ 待办 ============
addTask () {
this.form.tasks.push({
assigneeUserId: null, _assigneeCsv: '',
assigneeUserId: null, assigneeName: '',
content: '', deadline: '', status: 'pending', taskId: null
})
},
removeTask (i) { this.form.tasks.splice(i, 1) },
/** 待办负责人 UserSelect 多选 → 取首位 */
onTaskAssigneeChange (task, val) {
const arr = typeof val === 'string' ? val.split(',').filter(Boolean) : (val || [])
task.assigneeUserId = arr.length ? Number(arr[0]) : null
task._assigneeCsv = arr.length ? String(arr[0]) : ''
removeTask (i) {
const t = this.form.tasks[i]
if (t.taskId) {
this.$modal.confirm('该待办已同步为 OA 任务,移除后任务本身不会删除,仅与纪要解除关联。继续?')
.then(() => this.form.tasks.splice(i, 1))
.catch(() => {})
} else {
this.form.tasks.splice(i, 1)
}
},
// ============ 新建 / 保存 ============
@@ -326,16 +362,14 @@ export default {
const keepProject = this.form.projectId
this.form = emptyForm()
if (keepProject) this.form.projectId = keepProject
this.hostCsv = ''
this.clearVoice()
this.$modal.msgSuccess('已新建纪要')
},
async cmdSave () {
if (!this.form.meetingDate) return this.$modal.msgError('请选择会议日期')
if (!this.form.subject) return this.$modal.msgError('请输入会议主题')
// 序列化时清掉 _assigneeCsv 临时字段
const cleanTasks = (this.form.tasks || []).map(t => ({
assigneeUserId: t.assigneeUserId,
assigneeName: t.assigneeName,
content: t.content,
deadline: t.deadline,
status: t.status,
@@ -343,20 +377,32 @@ export default {
}))
const payload = { ...this.form, tasksJson: JSON.stringify(cleanTasks) }
delete payload.tasks
this.saving = true
try {
if (this.form.id) await updateMeetingMinutes(payload)
else await addMeetingMinutes(payload)
let id = this.form.id
if (id) {
await updateMeetingMinutes(payload)
} else {
const res = await addMeetingMinutes(payload)
id = res.data
}
this.setStatus('已保存', 'success')
setTimeout(() => this.setStatus('就绪', 'success'), 2000)
this.$modal.msgSuccess('纪要已保存' + (this.form.syncTask ? ',待办已同步到 OA 任务' : ''))
await this.loadHistory()
if (this.form.id) await this.loadMinutes(this.form.id, true)
if (id) await this.loadMinutes(id, true)
} catch (err) {
this.$modal.msgError(err.msg || '保存失败')
this.setStatus('保存失败', 'warning')
} finally {
this.saving = false
}
},
// ============ 历史 ============
searchHistory () {
this.historyQuery.pageNum = 1
this.loadHistory()
},
async loadHistory () {
this.historyLoading = true
try {
@@ -387,14 +433,15 @@ export default {
subject: m.subject || '',
location: m.location || '',
hostUserId: m.hostUserId || null,
hostUserName: m.hostUserName || '',
attendeeUserIds: m.attendeeUserIds || '',
attendeeUserNames: m.attendeeUserNames || '',
topic: m.topic || '',
discussion: m.discussion || '',
decision: m.decision || '',
tasks: this.parseTasks(m.tasksJson),
syncTask: m.syncTask == null ? 1 : m.syncTask
}
this.hostCsv = m.hostUserId ? String(m.hostUserId) : ''
if (!silent) this.$modal.msgSuccess('已加载:' + m.subject)
},
parseTasks (s) {
@@ -404,7 +451,7 @@ export default {
if (!Array.isArray(a)) return []
return a.map(t => ({
assigneeUserId: t.assigneeUserId || null,
_assigneeCsv: t.assigneeUserId ? String(t.assigneeUserId) : '',
assigneeName: t.assigneeName || '',
content: t.content || '',
deadline: t.deadline || '',
status: t.status || 'pending',
@@ -413,10 +460,10 @@ export default {
} catch (e) { return [] }
},
removeMinutes (m) {
this.$modal.confirm(`确认删除「${m.subject}」?此操作不可恢复`).then(async () => {
this.$modal.confirm(`确认删除「${m.subject}」?已同步的 OA 任务不受影响`).then(async () => {
await delMeetingMinutes(m.id)
this.$modal.msgSuccess('已删除')
if (this.form.id === m.id) this.form = emptyForm()
if (this.form.id === m.id) this.cmdNew()
await this.loadHistory()
}).catch(() => {})
},
@@ -494,14 +541,15 @@ export default {
cmdExport () {
if (!this.form.subject) return this.$modal.msgError('无内容可导出')
const d = this.form
const typeLabel = this.typeLabel(d.meetingType)
const statusMap = { pending: '待办', progress: '进行中', done: '已完成' }
const lines = []
lines.push('德睿福成套设备有限公司 · 会议纪要')
lines.push('='.repeat(50))
lines.push('日期: ' + d.meetingDate + ' 类型: ' + typeLabel)
lines.push('编号: ' + (d.meetingCode || '-'))
lines.push('日期: ' + d.meetingDate + ' 类型: ' + this.dictLabel('oa_meeting_type', d.meetingType))
lines.push('主题: ' + d.subject)
lines.push('地点: ' + (d.location || '-'))
lines.push('主持: ' + (d.hostUserName || '-'))
lines.push('参会: ' + (d.attendeeUserNames || '-'))
lines.push('='.repeat(50))
lines.push('')
lines.push('一、会议议题'); lines.push('-'.repeat(30)); lines.push(d.topic || '(无)'); lines.push('')
@@ -510,8 +558,8 @@ export default {
lines.push('四、待办事项'); lines.push('-'.repeat(30))
if (d.tasks && d.tasks.length) {
d.tasks.forEach(t => {
lines.push(' • [#' + (t.assigneeUserId || '-') + '] ' + (t.content || '') +
' | 截止:' + (t.deadline || '-') + ' | 状态:' + (statusMap[t.status] || t.status))
lines.push(' • [' + (t.assigneeName || '未指派') + '] ' + (t.content || '') +
' | 截止:' + (t.deadline || '-') + ' | 状态:' + this.dictLabel('oa_meeting_task_status', t.status))
})
} else { lines.push('(无)') }
lines.push(''); lines.push('='.repeat(50))
@@ -528,12 +576,11 @@ export default {
const d = this.form
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]))
const statusMap = { pending: '待办', progress: '进行中', done: '已完成' }
let taskHtml = '(无)'
if (d.tasks && d.tasks.length) {
const rows = d.tasks.map(t =>
`<tr><td>#${esc(t.assigneeUserId || '-')}</td><td>${esc(t.content)}</td>` +
`<td>${esc(t.deadline || '-')}</td><td>${esc(statusMap[t.status] || t.status)}</td></tr>`
`<tr><td>${esc(t.assigneeName || '未指派')}</td><td>${esc(t.content)}</td>` +
`<td>${esc(t.deadline || '-')}</td><td>${esc(this.dictLabel('oa_meeting_task_status', t.status))}</td></tr>`
).join('')
taskHtml = `<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<tr style="background:#eee"><th>负责人</th><th>任务内容</th><th>截止</th><th>状态</th></tr>${rows}</table>`
@@ -547,10 +594,13 @@ export default {
.meta span{margin-right:18px}.sect{font-size:14px;margin:14px 0 6px;font-weight:700}
.body{white-space:pre-wrap;font-size:13px;margin-bottom:16px}
@media print{body{padding:20px}}</style></head><body>
<h1>德睿福成套设备有限公司</h1><div class="sub">会 议 纪 要</div>
<h1>德睿福成套设备有限公司</h1><div class="sub">会 议 纪 要 ${esc(d.meetingCode || '')}</div>
<div class="meta"><span>📅 ${esc(d.meetingDate)}</span>
<span>📝 ${esc(d.subject)}</span></div>
<div class="meta"><span>📍 ${esc(d.location || '-')}</span></div>
<span>📝 ${esc(d.subject)}</span>
<span>🏷 ${esc(this.dictLabel('oa_meeting_type', d.meetingType))}</span></div>
<div class="meta"><span>📍 ${esc(d.location || '-')}</span>
<span>🎤 主持:${esc(d.hostUserName || '-')}</span></div>
<div class="meta"><span>👥 参会:${esc(d.attendeeUserNames || '-')}</span></div>
<div class="sect">一、会议议题</div><div class="body">${esc(d.topic || '(无)')}</div>
<div class="sect">二、讨论内容</div><div class="body">${esc(d.discussion || '(无)')}</div>
<div class="sect">三、决议事项</div><div class="body">${esc(d.decision || '(无)')}</div>
@@ -627,7 +677,6 @@ export default {
.meta-form {
::v-deep .el-form-item { margin-bottom: 8px; }
.help-tip { color: #909399; font-size: 11px; line-height: 1.4; }
}
.voice-card {
@@ -670,7 +719,7 @@ export default {
}
.task-line {
display: grid;
grid-template-columns: 200px 1fr 140px 110px 100px;
grid-template-columns: 170px 1fr 140px 110px 100px;
gap: 8px; align-items: start;
.tf {
label { display: block; font-size: 11px; color: #909399; margin-bottom: 2px; }

View File

@@ -1,21 +1,19 @@
-- =====================================================
-- 智能会议纪要 (Smart Meeting Minutes)
-- - 弃用 oa_meeting_project改为绑定 sys_oa_project.project_id
-- - 会议可绑定 sys_oa_project.project_id,也可不绑定(非项目会议)
-- - 主持人/参会/待办负责人 统一存 user_id
-- - 待办在保存时可同步生成 sys_oa_task
-- - 待办在保存时可同步生成 sys_oa_task(带操作日志和 IM 通知)
-- 本脚本可重复执行(幂等)。
-- 注意sys_dict_type/sys_dict_data/sys_menu 主键为雪花ID非自增必须显式指定。
-- =====================================================
-- 清理上一版(如有)
DROP TABLE IF EXISTS `oa_meeting_project`;
-- ---------------- 会议纪要 ----------------
DROP TABLE IF EXISTS `oa_meeting_minutes`;
CREATE TABLE `oa_meeting_minutes` (
-- ---------------- 会议纪要表 ----------------
CREATE TABLE IF NOT EXISTS `oa_meeting_minutes` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`meeting_code` varchar(32) NOT NULL COMMENT '会议编号 MT-yyyyMMddHHmmss',
`meeting_code` varchar(32) NOT NULL COMMENT '会议编号 MT-yyyyMMddHHmmss+3位随机',
`meeting_date` date NOT NULL COMMENT '会议日期',
`project_id` bigint(20) DEFAULT NULL COMMENT '关联 sys_oa_project.project_id',
`meeting_type` varchar(20) DEFAULT 'other' COMMENT '类型 tech/project/client/weekly/other',
`project_id` bigint(20) DEFAULT NULL COMMENT '关联 sys_oa_project.project_id(可空=非项目会议)',
`meeting_type` varchar(20) DEFAULT 'other' COMMENT '类型 字典 oa_meeting_type',
`subject` varchar(500) NOT NULL COMMENT '会议主题',
`location` varchar(255) DEFAULT NULL COMMENT '会议地点',
`host_user_id` bigint(20) DEFAULT NULL COMMENT '主持人 sys_user.user_id',
@@ -23,13 +21,13 @@ CREATE TABLE `oa_meeting_minutes` (
`topic` text COMMENT '会议议题',
`discussion` text COMMENT '讨论内容',
`decision` text COMMENT '决议事项',
`tasks_json` text COMMENT '待办 JSON[{assigneeUserId,content,deadline,status,taskId}]',
`tasks_json` text COMMENT '待办 JSON[{assigneeUserId,assigneeName,content,deadline,status,taskId}]',
`sync_task` tinyint(1) DEFAULT 1 COMMENT '是否将待办同步为 OA 任务',
`create_by` varchar(64) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_by` varchar(64) DEFAULT NULL,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志0正常 1删除',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志0正常 2删除mybatis-plus logicDeleteValue=2',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_meeting_code` (`meeting_code`, `del_flag`),
KEY `idx_date` (`meeting_date`),
@@ -37,26 +35,60 @@ CREATE TABLE `oa_meeting_minutes` (
KEY `idx_type` (`meeting_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会议纪要';
-- ---------------- 清理早期错误数据dict_id/dict_code 误插为 0 ----------------
DELETE FROM `sys_dict_type` WHERE `dict_id` = 0 AND `dict_type` IN ('oa_meeting_type', 'oa_meeting_task_status');
DELETE FROM `sys_dict_data` WHERE `dict_code` = 0 AND `dict_type` IN ('oa_meeting_type', 'oa_meeting_task_status');
-- ---------------- 字典:会议类型 ----------------
INSERT IGNORE INTO `sys_dict_type` (`dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
VALUES ('会议类型', 'oa_meeting_type', '0', 'admin', NOW(), '智能会议纪要-会议类型');
INSERT IGNORE INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
VALUES (2063900000000000001, '会议类型', 'oa_meeting_type', '0', 'admin', NOW(), '智能会议纪要-会议类型');
INSERT IGNORE INTO `sys_dict_data`
(`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
(`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
VALUES
(1, '技术评审', 'tech', 'oa_meeting_type', '', 'info', 'N', '0', 'admin', NOW(), ''),
(2, '项目推进', 'project', 'oa_meeting_type', '', 'primary', 'N', '0', 'admin', NOW(), ''),
(3, '客户沟通', 'client', 'oa_meeting_type', '', 'warning', 'N', '0', 'admin', NOW(), ''),
(4, '周例会', 'weekly', 'oa_meeting_type', '', 'success', 'N', '0', 'admin', NOW(), ''),
(5, '其他', 'other', 'oa_meeting_type', '', '', 'Y', '0', 'admin', NOW(), '');
(2063900000000000011, 1, '技术评审', 'tech', 'oa_meeting_type', '', 'info', 'N', '0', 'admin', NOW(), ''),
(2063900000000000012, 2, '项目推进', 'project', 'oa_meeting_type', '', 'primary', 'N', '0', 'admin', NOW(), ''),
(2063900000000000013, 3, '客户沟通', 'client', 'oa_meeting_type', '', 'warning', 'N', '0', 'admin', NOW(), ''),
(2063900000000000014, 4, '周例会', 'weekly', 'oa_meeting_type', '', 'success', 'N', '0', 'admin', NOW(), ''),
(2063900000000000015, 5, '其他', 'other', 'oa_meeting_type', '', 'default', 'Y', '0', 'admin', NOW(), '');
-- ---------------- 字典:待办状态 ----------------
INSERT IGNORE INTO `sys_dict_type` (`dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
VALUES ('会议待办状态', 'oa_meeting_task_status', '0', 'admin', NOW(), '智能会议纪要-待办状态');
INSERT IGNORE INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
VALUES (2063900000000000002, '会议待办状态', 'oa_meeting_task_status', '0', 'admin', NOW(), '智能会议纪要-待办状态');
INSERT IGNORE INTO `sys_dict_data`
(`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
(`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
VALUES
(1, '待办', 'pending', 'oa_meeting_task_status', '', 'info', 'Y', '0', 'admin', NOW(), ''),
(2, '进行中', 'progress', 'oa_meeting_task_status', '', 'primary', 'N', '0', 'admin', NOW(), ''),
(3, '已完成', 'done', 'oa_meeting_task_status', '', 'success', 'N', '0', 'admin', NOW(), '');
(2063900000000000021, 1, '待办', 'pending', 'oa_meeting_task_status', '', 'info', 'Y', '0', 'admin', NOW(), ''),
(2063900000000000022, 2, '进行中', 'progress', 'oa_meeting_task_status', '', 'primary', 'N', '0', 'admin', NOW(), ''),
(2063900000000000023, 3, '已完成', 'done', 'oa_meeting_task_status', '', 'success', 'N', '0', 'admin', NOW(), '');
-- ---------------- 菜单:信息 > 会议纪要 ----------------
-- 父菜单 1774989374680858626 = 「信息」
INSERT IGNORE INTO `sys_menu`
(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`)
VALUES
(2063809716454174722, '会议纪要', 1774989374680858626, 3,
'meeting', 'oa/meeting/index', 'C', '0', '0',
NULL, 'documentation', 'admin', NOW());
UPDATE `sys_menu` SET `icon` = 'documentation' WHERE `menu_id` = 2063809716454174722 AND (`icon` = '#' OR `icon` IS NULL);
-- ---------------- 角色授权(与「信息」下兄弟菜单一致的角色集) ----------------
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
VALUES
(1743186990678077442, 2063809716454174722), -- 总经理
(1743204526291349506, 2063809716454174722), -- 技术总监
(1743205028123045890, 2063809716454174722), -- 信息化部
(1852970465740505090, 2063809716454174722), -- 普通员工
(1859257980152692738, 2063809716454174722), -- 职工
(1859548445766717441, 2063809716454174722), -- 后勤
(1893987128812761089, 2063809716454174722), -- 新员工临时身份
(1914212623781187585, 2063809716454174722), -- 技术总工
(1914213026883162113, 2063809716454174722), -- 设计主任
(1925062159919448065, 2063809716454174722); -- 外贸专责
-- ---------------- 校验 ----------------
SELECT menu_id, menu_name, path, component, icon FROM sys_menu WHERE menu_id = 2063809716454174722;
SELECT dict_type, dict_label, dict_value FROM sys_dict_data
WHERE dict_type IN ('oa_meeting_type', 'oa_meeting_task_status') ORDER BY dict_type, dict_sort;