feat(meeting): 会议详情页参会人 CRUD + BizProjectPlan audit 字段

- 后端: BizMeetingAttendee 增 list/add/update/delete 4 端点
  按手机号定位 sys_user (无则新建, 用户名=密码=phone, role_type='doctor')
  Mapper: insertWithProfile (一次写完整档案) + deleteByPrimaryKey
  Service: insertByPhoneWithProfile (重复参会人抛友好异常)
- #4 配套: BizProjectPlan.auditOpinion/auditBy/auditTime + 关联项目名
  BizProjectPlan.projectName (LEFT JOIN biz_project) 用于 #4 通知 content
- 前端 MeetingDetail.vue: 劳务材料 tab 内, 劳务明细表 file-row 下方
  直接出表 (无表题), 含姓名/手机/单位/科室/职称/劳务费/签字状态/操作
  新增/编辑 dialog 共用, 删除走 ElMessageBox.confirm 二次确认
- 触发 #5: 新参会人 POST 时也发会议邀请通知 (与 BizMeetingController.add 一致)
This commit is contained in:
郭庆泰
2026-08-22 15:53:06 +08:00
parent 86e85d02cb
commit 2cef296b1e
10 changed files with 522 additions and 13 deletions
@@ -8,19 +8,25 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.business.domain.BizMeeting;
import com.ruoyi.business.domain.BizMeetingAttendee;
import com.ruoyi.business.notify.BizNotifyService;
import com.ruoyi.business.service.IBizMeetingAttendeeService;
import com.ruoyi.business.service.IBizMeetingService;
/**
* 会议参会人 Controller (劳务协议 / 手写签名)
* 会议参会人 Controller (劳务协议 / 手写签名 + 管理端 CRUD)
*
* 公开端点 (任意登录用户可调):
* - GET /business/meetingAttendee/unsigned 当前用户的"待签署"会议 (handsign 或 labor_protocol 为空)
* - PUT /business/meetingAttendee/{id}/handsign 更新手写签名 (Base64, 直接存 DB)
* - PUT /business/meetingAttendee/{id}/laborProtocol 更新劳务协议 URL (OSS)
*
* 管理端点 (后续 BizMeetingController.add/edit 调用):
* - /business/meetingAttendee (CRUD)
* 管理端点 (MeetingDetail 参会人 CRUD 用, admin/manager):
* - GET /business/meetingAttendee/list/{meetingId} 会议下所有参会人
* - POST /business/meetingAttendee 按手机号新增参会人 (定位/新建 sys_user)
* - PUT /business/meetingAttendee 更新参会人档案
* - DELETE /business/meetingAttendee/{id} 删除参会人 (单条)
*/
@RestController
@RequestMapping("/business/meetingAttendee")
@@ -28,6 +34,10 @@ public class BizMeetingAttendeeController extends BaseController {
@Autowired
private IBizMeetingAttendeeService attendeeService;
@Autowired
private IBizMeetingService bizMeetingService;
@Autowired
private BizNotifyService bizNotifyService;
/**
* 当前登录用户的"待签署"列表 (handsign 或 labor_protocol 任一为空)
@@ -40,6 +50,64 @@ public class BizMeetingAttendeeController extends BaseController {
return success(rows);
}
/**
* 管理端: 某会议的全部参会人 (MeetingDetail 参会人 CRUD 用).
* 返回全字段, 前端按需展示.
*/
@GetMapping("/list/{meetingId}")
public AjaxResult listByMeetingId(@PathVariable("meetingId") Long meetingId) {
return success(attendeeService.selectByMeetingId(meetingId));
}
/**
* 管理端: 按手机号新增参会人.
*
* <p>后端流程: 按 body.phone 查 sys_user → 查到用之, 查不到新建 (用户名=密码=phone, role_type='doctor')
* → 写完整档案行 (含 name/work_unit/fee...) → 触发 #5 会议邀请通知.
*
* <p>请求体示例:
* <pre>
* { "meetingId": 123, "phone": "13800138000", "name": "张三",
* "workUnit": "协和医院", "department": "心内科", "title": "副主任医师",
* "laborForm": "授课", "feePreTax": 1000, "tax": 100, "fee": 900 }
* </pre>
*/
@Log(title = "参会人", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody BizMeetingAttendee body) {
Long attendeeId = attendeeService.insertByPhoneWithProfile(body);
// #5 触发: 新参会人发邀请 (走 BizMeetingController.edit 同一路径, 不需要 dedup — 这是新行)
BizMeeting m = bizMeetingService.getById(body.getMeetingId());
bizNotifyService.meetingInvitation(body.getUserId(), body.getMeetingId(),
m != null ? m.getMeetingName() : null,
m != null ? m.getStartTime() : null);
return success(attendeeId);
}
/**
* 管理端: 更新参会人档案 (管理员在 MeetingDetail 改了姓名/单位/劳务费等).
* 不允许改 phone/userId (用 delete + add 来改手机号, 避免破坏 sys_user 关联).
*/
@Log(title = "参会人", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody BizMeetingAttendee body) {
if (body.getId() == null) {
return error("id 不能为空");
}
body.setUpdateBy(SecurityUtils.getUsername());
return toAjax(attendeeService.updateProfile(body));
}
/**
* 管理端: 按 id 删除参会人 (单条).
* 注意: 仅删 biz_meeting_attendee 行, 不删 sys_user (参会人可能是真实用户, 不能因参会关系删号).
*/
@Log(title = "参会人", businessType = BusinessType.DELETE)
@DeleteMapping("/{id}")
public AjaxResult remove(@PathVariable("id") Long id) {
return toAjax(attendeeService.deleteByPrimaryKey(id));
}
/** 更新手写签名 (Base64 字符串, 直接存 DB longtext) */
@Log(title = "手写签名", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/handsign")
@@ -51,17 +119,35 @@ public class BizMeetingAttendeeController extends BaseController {
return toAjax(attendeeService.updateHandsign(entity));
}
/** 更新劳务协议 URL (OSS 上传后调本接口) */
/**
* 更新劳务协议 URL (OSS 上传后调本接口).
*
* <p>同时承担 #6 通知触发: 当 labor_protocol 从空 → 非空 (或从无 → 有新 URL) 时,
* 给参会人 (a.user_id) 发一条"劳务协议待签"通知. 协议被替换 (URL 改了) 不再重发,
* 避免重复打扰 — 医生在 /doctor/home 列表里能直接看到 URL.
*/
@Log(title = "劳务协议", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/laborProtocol")
public AjaxResult updateLaborProtocol(@PathVariable("id") Long id, @RequestBody BizMeetingAttendee body) {
// #6 dedup: 先查旧值, 仅在"从空变有"时通知
BizMeetingAttendee before = attendeeService.selectById(id);
String newUrl = body.getLaborProtocol();
boolean wasEmpty = before == null
|| before.getLaborProtocol() == null
|| before.getLaborProtocol().isEmpty();
BizMeetingAttendee entity = new BizMeetingAttendee();
entity.setId(id);
entity.setLaborProtocol(body.getLaborProtocol());
entity.setLaborProtocol(newUrl);
entity.setUpdateBy(SecurityUtils.getUsername());
return toAjax(attendeeService.updateLaborProtocol(entity));
}
int rows = attendeeService.updateLaborProtocol(entity);
/* ====== 管理端点 (给后续 BizMeetingController.add 调用) ====== */
// TODO: BizMeetingController.add 接受 attendeeUserIds: Long[], 批量 insert 中间表
// 仅在原 URL 为空 + 新 URL 非空时推通知, 避免协议替换产生噪音
if (rows > 0 && wasEmpty && newUrl != null && !newUrl.isEmpty() && before != null) {
BizMeeting m = bizMeetingService.getById(before.getMeetingId());
String meetingName = m != null ? m.getMeetingName() : null;
bizNotifyService.agreementAwaitingSign(before.getUserId(), id, before.getMeetingId(), meetingName);
}
return toAjax(rows);
}
}
@@ -20,6 +20,7 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.business.domain.BizMeeting;
import com.ruoyi.business.notify.BizNotifyService;
import com.ruoyi.business.service.IBizMeetingService;
import com.ruoyi.business.service.IBizMeetingAttendeeService;
@@ -42,6 +43,8 @@ public class BizMeetingController extends BaseController {
private IBizMeetingSupervisorService bizMeetingSupervisorService;
@Autowired
private IBizMeetingExecutorService bizMeetingExecutorService;
@Autowired
private BizNotifyService bizNotifyService;
@GetMapping("/list")
public TableDataInfo list(BizMeeting bizMeeting) {
@@ -67,6 +70,13 @@ public class BizMeetingController extends BaseController {
Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds();
if (attendeeUserIds != null && attendeeUserIds.length > 0) {
attendeeService.insertBatch(bizMeeting.getMeetingId(), attendeeUserIds);
// #5 会议邀请通知 (新增会议, 全部新参会人都要通知)
String meetingName = bizMeeting.getMeetingName();
Date startTime = bizMeeting.getStartTime();
for (Long uid : attendeeUserIds) {
if (uid == null) continue;
bizNotifyService.meetingInvitation(uid, bizMeeting.getMeetingId(), meetingName, startTime);
}
}
return toAjax(rows);
}
@@ -77,7 +87,16 @@ public class BizMeetingController extends BaseController {
int rows = bizMeetingService.updateByPrimaryKey(bizMeeting);
Long[] attendeeUserIds = bizMeeting.getAttendeeUserIds();
if (attendeeUserIds != null && attendeeUserIds.length > 0) {
attendeeService.insertBatch(bizMeeting.getMeetingId(), attendeeUserIds);
Long meetingId = bizMeeting.getMeetingId();
// #5 dedup: 先拿已有的 userId 集合, 仅给"新增"的 userId 发通知, 避免重复打扰已参会医生
java.util.Set<Long> existingUids = new java.util.HashSet<>(attendeeService.selectUserIdsByMeetingId(meetingId));
attendeeService.insertBatch(meetingId, attendeeUserIds);
String meetingName = bizMeeting.getMeetingName();
Date startTime = bizMeeting.getStartTime();
for (Long uid : attendeeUserIds) {
if (uid == null || existingUids.contains(uid)) continue;
bizNotifyService.meetingInvitation(uid, meetingId, meetingName, startTime);
}
}
return toAjax(rows);
}
@@ -39,6 +39,8 @@ public class BizProjectPlan extends BaseEntity {
/** project_no */
@Excel(name = "project_no")
private String projectNo;
/** 关联项目名 (LEFT JOIN biz_project.project_name, 通知/列表展示用, 不持久化) */
private String projectName;
/** remark */
@Excel(name = "remark")
private String remark;
@@ -91,6 +93,8 @@ public class BizProjectPlan extends BaseEntity {
public void setIsSettled(String isSettled) { this.isSettled = isSettled; }
public String getProjectNo() { return projectNo; }
public void setProjectNo(String projectNo) { this.projectNo = projectNo; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public String getIsFinished() { return isFinished; }
@@ -103,6 +107,12 @@ public class BizProjectPlan extends BaseEntity {
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public Date getUpdateTime() { return updateTime; }
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
public String getAuditOpinion() { return auditOpinion; }
public void setAuditOpinion(String auditOpinion) { this.auditOpinion = auditOpinion; }
public String getAuditBy() { return auditBy; }
public void setAuditBy(String auditBy) { this.auditBy = auditBy; }
public String getAuditTime() { return auditTime; }
public void setAuditTime(String auditTime) { this.auditTime = auditTime; }
public Long getSubmitterId() { return submitterId; }
public void setSubmitterId(Long submitterId) { this.submitterId = submitterId; }
public String getSubmitterName() { return submitterName; }
@@ -8,6 +8,12 @@ public interface BizMeetingAttendeeMapper {
int insert(BizMeetingAttendee entity);
/** 批量插入参会人 (BizMeetingController.add 调用) */
int insertBatch(@Param("meetingId") Long meetingId, @Param("userIds") Long[] userIds, @Param("createBy") String createBy);
/**
* 管理端"新增参会人"用: 一次性插入完整档案 (含 name/phone/workUnit 等).
* 与 {@link #insert} 区别: insert 只写 meeting_id+user_id+create_by (医生端"刚被加入"零信息行),
* 本方法带档案用于 admin 在会议详情页手动加参会人.
*/
int insertWithProfile(BizMeetingAttendee entity);
/** 医生填写信息保存草稿: 更新签字快照字段 + 劳务信息 (不含 handsign/PDF) */
int updateProfile(BizMeetingAttendee entity);
int updateHandsign(BizMeetingAttendee entity);
@@ -15,9 +21,16 @@ public interface BizMeetingAttendeeMapper {
int updateSign(BizMeetingAttendee entity);
int updateLaborProtocol(BizMeetingAttendee entity);
int deleteByMeetingId(Long meetingId);
/** 管理端按 attendee.id 单删 (MeetingDetail 参会人 CRUD 用) */
int deleteByPrimaryKey(Long id);
int deleteByMeetingIdAndUserId(BizMeetingAttendee entity);
List<BizMeetingAttendee> selectByMeetingId(Long meetingId);
List<BizMeetingAttendee> selectByUserId(Long userId);
BizMeetingAttendee selectById(Long id);
List<BizMeetingAttendee> selectUnsignedByUserId(Long userId);
/**
* 拿某会议已存在的参会人 userId 列表 (用于 add/edit 时 diff 新加入的人, 仅通知增量)
* 性能: 只查 user_id 一列, 走 meeting_id 索引; meeting 参会人通常 < 100, 无压力.
*/
List<Long> selectUserIdsByMeetingId(Long meetingId);
}
@@ -14,9 +14,35 @@ public interface IBizMeetingAttendeeService {
int updateSign(BizMeetingAttendee entity);
int updateLaborProtocol(BizMeetingAttendee entity);
int deleteByMeetingId(Long meetingId);
/** MeetingDetail 参会人 CRUD 用: 按 id 单删 (管理端视角) */
int deleteByPrimaryKey(Long id);
int deleteByMeetingIdAndUserId(BizMeetingAttendee entity);
/**
* 管理端新增参会人 (MeetingDetail 参会人 CRUD 用).
*
* <p>逻辑:
* <ol>
* <li>按 body.phone 查 sys_user (del_flag='0')</li>
* <li>查不到 → 创建 sys_user (userName=phone, nickName=body.name, password=phone, roleType='doctor', status='0')</li>
* <li>查到 → 复用其 userId</li>
* <li>检查 (meetingId, userId) 是否已存在 → 抛"该参会人已在会议中"</li>
* <li>insertWithProfile 写完整档案行</li>
* <li>返回新 attendee.id (用于 #5 会议邀请触发 — 由 controller 决定是否推, 这里不推, 业务解耦)</li>
* </ol>
*
* @param body 必填 meetingId/phone; 可选 name/workUnit/department/title/laborForm/feePreTax/tax/fee
* @return 新 attendee.id
* @throws ServiceException phone 为空 / 用户已在会议中
*/
Long insertByPhoneWithProfile(BizMeetingAttendee body);
List<BizMeetingAttendee> selectByMeetingId(Long meetingId);
List<BizMeetingAttendee> selectByUserId(Long userId);
BizMeetingAttendee selectById(Long id);
List<BizMeetingAttendee> selectUnsignedByUserId(Long userId);
/**
* 拿某会议已存在的参会人 userId 列表 (#5 会议邀请 dedup 用).
* 列表实现层直接返 mapper 结果; 业务方通常用 {@code new HashSet<>(service.selectUserIdsByMeetingId(mid))} 做 contains 判断.
*/
List<Long> selectUserIdsByMeetingId(Long meetingId);
}
@@ -1,18 +1,31 @@
package com.ruoyi.business.service.impl;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.business.domain.BizMeetingAttendee;
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
import com.ruoyi.business.service.IBizMeetingAttendeeService;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.ISysUserService;
@Service
public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService {
private static final Logger log = LoggerFactory.getLogger(BizMeetingAttendeeServiceImpl.class);
@Autowired
private BizMeetingAttendeeMapper mapper;
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private ISysUserService sysUserService;
@Override
public int insert(BizMeetingAttendee entity) {
@@ -25,6 +38,69 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
return mapper.insertBatch(meetingId, userIds, SecurityUtils.getUsername());
}
/**
* 管理端"新增参会人"全流程: 按 phone 定位/新建 sys_user + 写入完整档案.
*
* <p>参照 {@link com.ruoyi.business.service.impl.BizExpertServiceImpl#insert} 的"phone → userId"
* 模式 (用户名=phone, 密码=phone, roleType='doctor'), 但本方法**只做 phone 查重+建账号**,
* 不做"是否已注册专家"等业务校验 (参会人 ≠ 专家, 只要有手机号就能加入会议).
*
* <p>异常: phone 为空 → 抛; (meetingId, userId) 已存在 → 抛 (用 mapper 的 insertWithProfile
* 在 PK 冲突时报 DuplicateEntry 兜底, 这里先 SELECT 给出友好提示).
*/
@Override
public Long insertByPhoneWithProfile(BizMeetingAttendee body) {
String phone = body.getPhone();
if (phone == null || phone.trim().isEmpty()) {
throw new ServiceException("手机号不能为空");
}
phone = phone.trim();
// 1. 按 phone 查 sys_user (单条 IN 查, selectByPhoneList 接受 List<String>)
List<SysUser> hits = sysUserMapper.selectByPhoneList(Collections.singletonList(phone));
Long userId;
SysUser existed = (hits != null && !hits.isEmpty()) ? hits.get(0) : null;
if (existed != null) {
userId = existed.getUserId();
} else {
// 2. 查不到 → 建 sys_user (用户名=phone, 密码=phone, role_type='doctor')
SysUser newUser = new SysUser();
newUser.setUserName(phone);
newUser.setNickName(body.getName() != null && !body.getName().isEmpty() ? body.getName() : phone);
newUser.setPhonenumber(phone);
newUser.setPassword(SecurityUtils.encryptPassword(phone));
newUser.setStatus("0");
newUser.setDelFlag("0");
newUser.setCreateBy(SecurityUtils.getUsername());
sysUserService.insertUser(newUser);
userId = newUser.getUserId();
if (userId == null) {
// 兜底: insertUser 用了 useGeneratedKeys, 正常能拿到; 拿不到时按 phone 再查一次
SysUser re = sysUserMapper.selectUserByUserName(phone);
if (re == null) throw new ServiceException("建账号失败, 请重试");
userId = re.getUserId();
}
// 显式设 role_type='doctor' (DB 默认 'executor', 参会人应为 doctor 视角)
sysUserService.updateRoleType(userId, "doctor");
log.info("[attendee] 新建 sys_user (phone={}, userId={}, roleType=doctor)", phone, userId);
}
// 3. 检查 (meetingId, userId) 是否已存在 → 友好提示
// 仅 SELECT, 不 DELETE (deleteByMeetingIdAndUserId 是破坏性的, 不能误用做探测)
java.util.Set<Long> existing = new java.util.HashSet<>(mapper.selectUserIdsByMeetingId(body.getMeetingId()));
if (existing.contains(userId)) {
throw new ServiceException("该手机号参会人已在会议中, 无需重复添加");
}
// 4. 写完整档案行
body.setUserId(userId);
body.setCreateBy(SecurityUtils.getUsername());
mapper.insertWithProfile(body);
Long newId = body.getId();
log.info("[attendee] 新增参会人 meetingId={} userId={} attendeeId={}", body.getMeetingId(), userId, newId);
return newId;
}
@Override
public int updateProfile(BizMeetingAttendee entity) {
return mapper.updateProfile(entity);
@@ -50,6 +126,11 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
return mapper.deleteByMeetingId(meetingId);
}
@Override
public int deleteByPrimaryKey(Long id) {
return mapper.deleteByPrimaryKey(id);
}
@Override
public int deleteByMeetingIdAndUserId(BizMeetingAttendee entity) {
return mapper.deleteByMeetingIdAndUserId(entity);
@@ -74,4 +155,9 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
public List<BizMeetingAttendee> selectUnsignedByUserId(Long userId) {
return mapper.selectUnsignedByUserId(userId);
}
@Override
public List<Long> selectUserIdsByMeetingId(Long meetingId) {
return mapper.selectUserIdsByMeetingId(meetingId);
}
}
@@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.business.domain.BizProjectPlan;
import com.ruoyi.business.mapper.BizProjectPlanMapper;
import com.ruoyi.business.notify.BizNotifyService;
import com.ruoyi.business.service.IBizProjectPlanService;
@Service
@@ -12,6 +13,8 @@ public class BizProjectPlanServiceImpl implements IBizProjectPlanService
{
@Autowired
private BizProjectPlanMapper bizProjectPlanMapper;
@Autowired
private BizNotifyService bizNotifyService;
@Override
public BizProjectPlan getById(String planId)
@@ -21,9 +24,34 @@ public class BizProjectPlanServiceImpl implements IBizProjectPlanService
{ return bizProjectPlanMapper.selectList(entity); }
@Override
public int insert(BizProjectPlan entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "planId"); return bizProjectPlanMapper.insert(entity); }
/**
* 通用 update. #4 触发点 (方案审核结果通知) 在这里插桩:
* - 先读旧 status
* - 执行 update
* - 仅当 status 真的变化 (1→2 通过 / 1→3 拒绝) 才发通知
* - 其它字段编辑 (plan_name/file/...) 不发, 避免噪音
*/
@Override
public int updateByPrimaryKey(BizProjectPlan entity)
{ return bizProjectPlanMapper.updateByPrimaryKey(entity); }
{
String oldStatus = null;
if (entity.getPlanId() != null) {
BizProjectPlan existed = bizProjectPlanMapper.selectByPrimaryKey(entity.getPlanId());
if (existed != null) {
oldStatus = existed.getStatus();
}
}
int n = bizProjectPlanMapper.updateByPrimaryKey(entity);
if (n > 0 && entity.getStatus() != null && !entity.getStatus().equals(oldStatus)) {
// 重新读一次拿 submitterId/planName/auditOpinion (entity 可能只传了 planId+status+opinion)
BizProjectPlan after = bizProjectPlanMapper.selectByPrimaryKey(entity.getPlanId());
if (after != null) {
bizNotifyService.planAuditResult(after, oldStatus);
}
}
return n;
}
@Override
public int deleteByPrimaryKey(String planId)
{ return bizProjectPlanMapper.deleteByPrimaryKey(planId); }
@@ -40,6 +40,20 @@
insert into biz_meeting_attendee(meeting_id, user_id, create_by, create_time)
values(#{meetingId}, #{userId}, #{createBy}, sysdate())
</insert>
<!--
管理端新增参会人 (MeetingDetail 参会人 CRUD 用):
一次性写入 meeting_id+user_id+档案字段 (name/phone/work_unit/...).
若档案字段为 NULL 则不写 (COALESCE 在调用方给空字符串兜底).
useGeneratedKeys 让调用方能拿到新 attendee.id (用于 #5 触发邀请通知).
-->
<insert id="insertWithProfile" parameterType="BizMeetingAttendee" useGeneratedKeys="true" keyProperty="id">
insert into biz_meeting_attendee(meeting_id, user_id, name, phone, work_unit, department, title,
id_card, bank_card, bank_name, bank_branch, bank_region, bank_address, account_name,
id_card_attachments, labor_form, fee_pre_tax, tax, fee, create_by, create_time)
values(#{meetingId}, #{userId}, #{name}, #{phone}, #{workUnit}, #{department}, #{title},
#{idCard}, #{bankCard}, #{bankName}, #{bankBranch}, #{bankRegion}, #{bankAddress}, #{accountName},
#{idCardAttachments}, #{laborForm}, #{feePreTax}, #{tax}, #{fee}, #{createBy}, sysdate())
</insert>
<!-- 批量插入参会人 (BizMeetingController.add 调用) -->
<insert id="insertBatch">
insert into biz_meeting_attendee(meeting_id, user_id, create_by, create_time)
@@ -103,6 +117,10 @@
<delete id="deleteByMeetingId" parameterType="Long">
delete from biz_meeting_attendee where meeting_id = #{meetingId}
</delete>
<!-- MeetingDetail 参会人 CRUD 用: 按 id 单删 -->
<delete id="deleteByPrimaryKey" parameterType="Long">
delete from biz_meeting_attendee where id = #{id}
</delete>
<delete id="deleteByMeetingIdAndUserId" parameterType="BizMeetingAttendee">
delete from biz_meeting_attendee where meeting_id = #{meetingId} and user_id = #{userId}
</delete>
@@ -133,4 +151,11 @@
and (a.handsign is null or a.handsign = '' or a.labor_protocol is null or a.labor_protocol = '')
order by m.start_time asc
</select>
<!--
给 BizMeetingController.edit 做差集用: 查该会议已存在的参会人 userId 列表.
用于 #5 会议邀请: 仅给"新加入"的 userId 发通知, 已存在的用户不重发.
-->
<select id="selectUserIdsByMeetingId" resultType="java.lang.Long" parameterType="Long">
select user_id from biz_meeting_attendee where meeting_id = #{meetingId}
</select>
</mapper>
@@ -13,7 +13,11 @@
<result property="status" column="status" />
<result property="isSettled" column="is_settled" />
<result property="projectNo" column="project_no" />
<result property="projectName" column="project_name" />
<result property="remark" column="remark" />
<result property="auditOpinion" column="audit_opinion" />
<result property="auditBy" column="audit_by" />
<result property="auditTime" column="audit_time" />
<result property="submitterId" column="submitter_id" />
<result property="submitterName" column="submitter_name" />
<result property="createBy" column="create_by" />
@@ -25,13 +29,15 @@
select p.plan_id, p.plan_name, p.plan_direction, p.plan_direction_id,
s.title as plan_direction_title,
p.plan_category, p.project_form, p.design_file_url, p.status, p.is_settled,
p.project_no, p.remark, p.submitter_id,
p.project_no, p.remark, p.audit_opinion, p.audit_by, p.audit_time, p.submitter_id,
COALESCE(bp.name, u.user_name) as submitter_name,
proj.project_name as project_name,
p.create_by, p.create_time, p.update_by, p.update_time
from biz_project_plan p
left join biz_special_plan s on s.id = p.plan_direction_id
left join sys_user u on u.user_id = p.submitter_id
left join biz_person bp on bp.user_id = u.user_id
left join biz_project proj on proj.project_no = p.project_no
</sql>
<select id="selectByPrimaryKey" resultMap="BizProjectPlanResult" parameterType="String">
<include refid="selectFields"/>