feat: 多处页面改造 + 新增劳务协议模板配置

主要改动:
- biz_meeting_attendee 中间表 + doctor/expert 角色数据隔离
- biz_meeting_attendee 加 handsign/labor_protocol 字段 (劳务协议改造)
- biz_labor_protocol_template 配置表 + admin 页面 (从 HeguiConstants 迁移)
- biz_expert.expertId 改 Long + IdGenerator 生成 (去自增)
- Login.vue / PublicityDetail.vue / Home.vue 等多处 bug 修复 + UI 改进
- 新增 page-tech-review 报告 3 篇 (_self/*.md)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
郭庆泰
2026-08-19 21:06:49 +08:00
co-authored by Claude
parent ec4a8aa8f0
commit e496f34393
44 changed files with 2143 additions and 130 deletions
@@ -36,7 +36,7 @@ public class BizExpertController extends BaseController
return getDataTable(list);
}
@GetMapping("/{expertId}")
public AjaxResult getInfo(@PathVariable("expertId") String expertId)
public AjaxResult getInfo(@PathVariable("expertId") Long expertId)
{
return success(bizExpertService.getById(expertId));
}
@@ -71,7 +71,7 @@ public class BizExpertController extends BaseController
*/
@Log(title = "专家启停", businessType = BusinessType.UPDATE)
@PutMapping("/{expertId}/status")
public AjaxResult updateStatus(@PathVariable String expertId, @RequestParam String status)
public AjaxResult updateStatus(@PathVariable Long expertId, @RequestParam String status)
{
return toAjax(bizExpertService.updateStatus(expertId, status));
}
@@ -89,7 +89,7 @@ public class BizExpertController extends BaseController
}
@Log(title = "专家", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(bizExpertService.deleteByPrimaryKeys(ids));
}
@@ -0,0 +1,85 @@
package com.ruoyi.business.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.business.domain.BizLaborProtocolTemplate;
import com.ruoyi.business.service.IBizLaborProtocolTemplateService;
/**
* 劳务协议模板配置 Controller (admin 网站管理)
* 全局共享, default_flag='Y' 同一时刻只有 1 条 (service.setDefault 保证)
*/
@RestController
@RequestMapping("/business/laborProtocolTemplate")
public class BizLaborProtocolTemplateController extends BaseController {
@Autowired
private IBizLaborProtocolTemplateService templateService;
/** 分页列表 (admin 后台用, 支持筛选) */
@GetMapping("/list")
public TableDataInfo list(BizLaborProtocolTemplate entity) {
startPage();
List<BizLaborProtocolTemplate> rows = templateService.selectList(entity);
return getDataTable(rows);
}
/** 全部启用模板 (前端下拉用, 不分页) */
@GetMapping("/allEnabled")
public AjaxResult allEnabled() {
return success(templateService.selectAllEnabled());
}
/** 取默认模板 */
@GetMapping("/default")
public AjaxResult getDefault() {
return success(templateService.selectDefault());
}
/** 详情 */
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
return success(templateService.getById(id));
}
@Log(title = "劳务协议模板", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody BizLaborProtocolTemplate entity) {
entity.setCreateBy(SecurityUtils.getUsername());
// 新建时若 default_flag='Y', 先清空其他默认 (保证只有 1 条 Y)
if ("Y".equals(entity.getDefaultFlag())) {
templateService.setDefault(0L); // id=0 不存在, 实际只清空其他行
}
return toAjax(templateService.insert(entity));
}
@Log(title = "劳务协议模板", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody BizLaborProtocolTemplate entity) {
entity.setUpdateBy(SecurityUtils.getUsername());
return toAjax(templateService.update(entity));
}
@Log(title = "劳务协议模板", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(templateService.deleteByPrimaryKeys(ids));
}
/**
* 设为默认 (app 层保证全局唯一)
* PUT /business/laborProtocolTemplate/{id}/default
*/
@Log(title = "劳务协议模板-设默认", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/default")
public AjaxResult setDefault(@PathVariable("id") Long id) {
return toAjax(templateService.setDefault(id));
}
}
@@ -0,0 +1,67 @@
package com.ruoyi.business.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
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.BizMeetingAttendee;
import com.ruoyi.business.service.IBizMeetingAttendeeService;
/**
* 会议参会人 Controller (劳务协议 / 手写签名)
*
* 公开端点 (任意登录用户可调):
* - 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)
*/
@RestController
@RequestMapping("/business/meetingAttendee")
public class BizMeetingAttendeeController extends BaseController {
@Autowired
private IBizMeetingAttendeeService attendeeService;
/**
* 当前登录用户的"待签署"列表 (handsign 或 labor_protocol 任一为空)
* 用于 /doctor/home 工作台
*/
@GetMapping("/unsigned")
public AjaxResult listUnsigned() {
Long userId = SecurityUtils.getUserId();
List<BizMeetingAttendee> rows = attendeeService.selectUnsignedByUserId(userId);
return success(rows);
}
/** 更新手写签名 (Base64 字符串, 直接存 DB longtext) */
@Log(title = "手写签名", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/handsign")
public AjaxResult updateHandsign(@PathVariable("id") Long id, @RequestBody BizMeetingAttendee body) {
BizMeetingAttendee entity = new BizMeetingAttendee();
entity.setId(id);
entity.setHandsign(body.getHandsign());
entity.setUpdateBy(SecurityUtils.getUsername());
return toAjax(attendeeService.updateHandsign(entity));
}
/** 更新劳务协议 URL (OSS 上传后调本接口) */
@Log(title = "劳务协议", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/laborProtocol")
public AjaxResult updateLaborProtocol(@PathVariable("id") Long id, @RequestBody BizMeetingAttendee body) {
BizMeetingAttendee entity = new BizMeetingAttendee();
entity.setId(id);
entity.setLaborProtocol(body.getLaborProtocol());
entity.setUpdateBy(SecurityUtils.getUsername());
return toAjax(attendeeService.updateLaborProtocol(entity));
}
/* ====== 管理端点 (给后续 BizMeetingController.add 调用) ====== */
// TODO: BizMeetingController.add 接受 attendeeUserIds: Long[], 批量 insert 中间表
}
@@ -8,6 +8,7 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.business.domain.BizMeeting;
import com.ruoyi.business.service.IBizMeetingService;
@@ -23,6 +24,11 @@ public class BizMeetingController extends BaseController
@GetMapping("/list")
public TableDataInfo list(BizMeeting bizMeeting)
{
// 医生/专家角色: 后端兜底只查"我参加的会议" (走 biz_meeting_attendee 中间表)
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
if ("doctor".equals(roleType) || "expert".equals(roleType)) {
bizMeeting.setUserId(SecurityUtils.getUserId());
}
startPage();
List<BizMeeting> list = bizMeetingService.selectList(bizMeeting);
return getDataTable(list);
@@ -13,7 +13,6 @@ import com.ruoyi.business.service.IBizExpertService;
import com.ruoyi.business.service.SysSmsService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.uuid.UUID;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.service.ISysUserService;
@@ -42,18 +41,26 @@ public class BizRegisterController extends BaseController {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/**
* 把任意类型安全转 String (兼容前端传 deptId/titileId 等 Number 也能跑通,
* 防止 Integer/Long 等数字类型 cast String 抛 ClassCastException)
*/
private static String toStr(Object o) {
return o == null ? null : o.toString();
}
@PostMapping("/registerExpert")
public AjaxResult registerExpert(@RequestBody Map<String, Object> body) {
String realName = (String) body.get("realName");
String workUnit = (String) body.get("workUnit");
String department = (String) body.get("department");
String doctorTitle = (String) body.get("doctorTitle");
String phone = (String) body.get("phone");
String code = (String) body.get("code");
String password = (String) body.get("password");
String uuid = (String) body.get("uuid");
String licenseCertUrl = (String) body.get("licenseCertUrl");
String titleCertUrl = (String) body.get("titleCertUrl");
String realName = toStr(body.get("realName"));
String workUnit = toStr(body.get("workUnit"));
String department = toStr(body.get("department"));
String doctorTitle = toStr(body.get("doctorTitle"));
String phone = toStr(body.get("phone"));
String code = toStr(body.get("code"));
String password = toStr(body.get("password"));
String uuid = toStr(body.get("uuid"));
String licenseCertUrl = toStr(body.get("licenseCertUrl"));
String titleCertUrl = toStr(body.get("titleCertUrl"));
if (realName == null || realName.isEmpty()) return error("姓名不能为空");
if (workUnit == null || workUnit.isEmpty()) return error("工作单位不能为空");
@@ -89,9 +96,9 @@ public class BizRegisterController extends BaseController {
// sys_user.role_type 表字段 (DB 默认 executor, 专家需 = doctor), 用 mapper 更新
userService.updateRoleType(userId, "doctor");
// 5. 插入 biz_expert
// 5. 插入 biz_expert (expertId 由 BizExpertServiceImpl.insert 里的 SnowflakeId.injectIfEmpty 自动填数字雪花 ID,
// 不要 controller 预生成 UUID — DB expert_id 是 bigint, UUID 字符串塞不进去)
BizExpert expert = new BizExpert();
expert.setExpertId(UUID.fastUUID().toString());
expert.setUserId(userId);
expert.setName(realName);
expert.setPhone(phone);
@@ -10,7 +10,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
public class BizExpert extends BaseEntity {
private static final long serialVersionUID = 1L;
/** expertId */
private String expertId;
private Long expertId;
/** name */
@Excel(name = "name")
private String name;
@@ -80,8 +80,8 @@ public class BizExpert extends BaseEntity {
private String auditOpinion;
/** 状态 0正常 1禁用 */
private String status;
public String getExpertId() { return expertId; }
public void setExpertId(String expertId) { this.expertId = expertId; }
public Long getExpertId() { return expertId; }
public void setExpertId(Long expertId) { this.expertId = expertId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPhone() { return phone; }
@@ -0,0 +1,54 @@
package com.ruoyi.business.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 劳务协议模板配置 (biz_labor_protocol_template)
* admin 在"网站管理"下维护, 全局共享
* default_flag='Y' 同一时刻只有1条 (service 层保证)
*/
public class BizLaborProtocolTemplate extends BaseEntity {
private static final long serialVersionUID = 1L;
/** id */
private Long id;
/** template_name */
@Excel(name = "template_name")
private String templateName;
/** template_content (HTML 含占位符 {name} {phone} 等) */
private String templateContent;
/** default_flag (Y=默认, N=非默认) */
@Excel(name = "default_flag")
private String defaultFlag;
/** sort_order (前端下拉顺序) */
private Integer sortOrder;
/** status (Y=启用, N=禁用) */
@Excel(name = "status")
private String status;
/** remark */
private String remark;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTemplateName() { return templateName; }
public void setTemplateName(String templateName) { this.templateName = templateName; }
public String getTemplateContent() { return templateContent; }
public void setTemplateContent(String templateContent) { this.templateContent = templateContent; }
public String getDefaultFlag() { return defaultFlag; }
public void setDefaultFlag(String defaultFlag) { this.defaultFlag = defaultFlag; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
public Date getUpdateTime() { return updateTime; }
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
}
@@ -76,6 +76,8 @@ public class BizMeeting extends BaseEntity {
private String scheduleUrl;
/** 签署劳务 0未签 1已签 */
private String laborSigned;
/** 参会人 user_id (非持久化字段, 仅用于 mapper WHERE 过滤; controller 注入, 配合 biz_meeting_attendee 中间表) */
private transient Long userId;
public Long getMeetingId() { return meetingId; }
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
public String getProjectNo() { return projectNo; }
@@ -122,4 +124,6 @@ public class BizMeeting extends BaseEntity {
public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; }
public String getLaborSigned() { return laborSigned; }
public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
}
@@ -0,0 +1,54 @@
package com.ruoyi.business.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 会议参会人 (biz_meeting_attendee 中间表)
* 用于按 user_id 过滤"我参加的会议", 替代在 biz_meeting 上加冗余字段
*/
public class BizMeetingAttendee extends BaseEntity {
private static final long serialVersionUID = 1L;
/** id */
private Long id;
/** meeting_id (FK biz_meeting.meeting_id) */
private Long meetingId;
/** user_id (FK sys_user.user_id) */
private Long userId;
/** 手写签名 Base64 (longtext) — 由前端手写板生成 */
private String handsign;
/** 劳务协议 URL (OSS) */
private String laborProtocol;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/* ====== 非持久化字段, 用于 selectUnsignedByUserId 联表查询 ====== */
private transient String meetingName;
private transient Date startTime;
private transient Date endTime;
private transient String projectName;
private transient String projectNo;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getMeetingId() { return meetingId; }
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getHandsign() { return handsign; }
public void setHandsign(String handsign) { this.handsign = handsign; }
public String getLaborProtocol() { return laborProtocol; }
public void setLaborProtocol(String laborProtocol) { this.laborProtocol = laborProtocol; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
public String getMeetingName() { return meetingName; }
public void setMeetingName(String meetingName) { this.meetingName = meetingName; }
public Date getStartTime() { return startTime; }
public void setStartTime(Date startTime) { this.startTime = startTime; }
public Date getEndTime() { return endTime; }
public void setEndTime(Date endTime) { this.endTime = endTime; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getProjectNo() { return projectNo; }
public void setProjectNo(String projectNo) { this.projectNo = projectNo; }
}
@@ -115,10 +115,6 @@ public class BizProject extends BaseEntity {
private String supportLetterUrl;
/** 已发布公告URL */
private String publishUrl;
/** 通知文件URL */
private String noticeUrl;
/** 日程文件URL */
private String scheduleUrl;
/** 是否已发布公示 0否 1是 */
private String isPublished;
/** 发布时间 */
@@ -215,10 +211,6 @@ public class BizProject extends BaseEntity {
public void setSupportLetterUrl(String supportLetterUrl) { this.supportLetterUrl = supportLetterUrl; }
public String getPublishUrl() { return publishUrl; }
public void setPublishUrl(String publishUrl) { this.publishUrl = publishUrl; }
public String getNoticeUrl() { return noticeUrl; }
public void setNoticeUrl(String noticeUrl) { this.noticeUrl = noticeUrl; }
public String getScheduleUrl() { return scheduleUrl; }
public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; }
public String getIsPublished() { return isPublished; }
public void setIsPublished(String isPublished) { this.isPublished = isPublished; }
public Date getPublishTime() { return publishTime; }
@@ -7,13 +7,13 @@ import com.ruoyi.business.domain.BizExpert;
*/
public interface BizExpertMapper
{
BizExpert selectByPrimaryKey(String expertId);
BizExpert selectByPrimaryKey(Long expertId);
BizExpert selectByUserId(Long userId);
List<BizExpert> selectList(BizExpert entity);
int insert(BizExpert entity);
int insertWithUserId(BizExpert entity);
int updateByPrimaryKey(BizExpert entity);
int updateByUserId(BizExpert entity);
int deleteByPrimaryKey(String expertId);
int deleteByPrimaryKeys(String[] expertIds);
int deleteByPrimaryKey(Long expertId);
int deleteByPrimaryKeys(Long[] expertIds);
}
@@ -0,0 +1,19 @@
package com.ruoyi.business.mapper;
import java.util.List;
import com.ruoyi.business.domain.BizLaborProtocolTemplate;
public interface BizLaborProtocolTemplateMapper {
BizLaborProtocolTemplate selectByPrimaryKey(Long id);
List<BizLaborProtocolTemplate> selectList(BizLaborProtocolTemplate entity);
/** 取默认模板 (default_flag='Y' AND status='Y'), 0 或 1 条 */
BizLaborProtocolTemplate selectDefault();
/** 取所有启用模板 (status='Y'), 按 sort_order 排序 */
List<BizLaborProtocolTemplate> selectAllEnabled();
int insert(BizLaborProtocolTemplate entity);
int updateByPrimaryKey(BizLaborProtocolTemplate entity);
/** 把所有行的 default_flag 设为 'N' (service.setDefault 调用) */
int clearAllDefault();
int deleteByPrimaryKey(Long id);
int deleteByPrimaryKeys(Long[] ids);
}
@@ -0,0 +1,15 @@
package com.ruoyi.business.mapper;
import java.util.List;
import com.ruoyi.business.domain.BizMeetingAttendee;
public interface BizMeetingAttendeeMapper {
int insert(BizMeetingAttendee entity);
int updateHandsign(BizMeetingAttendee entity);
int updateLaborProtocol(BizMeetingAttendee entity);
int deleteByMeetingId(Long meetingId);
int deleteByMeetingIdAndUserId(BizMeetingAttendee entity);
List<BizMeetingAttendee> selectByMeetingId(Long meetingId);
List<BizMeetingAttendee> selectByUserId(Long userId);
List<BizMeetingAttendee> selectUnsignedByUserId(Long userId);
}
@@ -10,7 +10,7 @@ import com.ruoyi.common.core.domain.entity.SysUser;
*/
public interface IBizExpertService
{
BizExpert getById(String expertId);
BizExpert getById(Long expertId);
BizExpert getByUserId(Long userId);
List<BizExpert> selectList(BizExpert entity);
/**
@@ -24,11 +24,11 @@ public interface IBizExpertService
* 启用/禁用专家: 同步更新 biz_expert.status + sys_user.status
* status='Y' 正常, status='N' 禁用
*/
int updateStatus(String expertId, String status);
int updateStatus(Long expertId, String status);
/** 按 userId 更新或新建 (upsert) */
int updateProfileByUserId(BizExpert entity);
int deleteByPrimaryKey(String expertId);
int deleteByPrimaryKeys(String[] expertId);
int deleteByPrimaryKey(Long expertId);
int deleteByPrimaryKeys(Long[] expertId);
/**
* 批量导入专家: 每行调用 insert, updateSupport=true 时跳过已存在手机号(视为成功)
@@ -0,0 +1,20 @@
package com.ruoyi.business.service;
import java.util.List;
import com.ruoyi.business.domain.BizLaborProtocolTemplate;
public interface IBizLaborProtocolTemplateService {
BizLaborProtocolTemplate getById(Long id);
List<BizLaborProtocolTemplate> selectList(BizLaborProtocolTemplate entity);
BizLaborProtocolTemplate selectDefault();
List<BizLaborProtocolTemplate> selectAllEnabled();
int insert(BizLaborProtocolTemplate entity);
int update(BizLaborProtocolTemplate entity);
/**
* 设为默认: 先清空所有行的 default_flag='N', 再把目标行设为 'Y'
* 保证全局只有 1 条 default_flag='Y'
*/
int setDefault(Long id);
int deleteByPrimaryKey(Long id);
int deleteByPrimaryKeys(Long[] ids);
}
@@ -0,0 +1,16 @@
package com.ruoyi.business.service;
import java.util.List;
import com.ruoyi.business.domain.BizMeetingAttendee;
public interface IBizMeetingAttendeeService {
int insert(BizMeetingAttendee entity);
int updateHandsign(BizMeetingAttendee entity);
int updateLaborProtocol(BizMeetingAttendee entity);
int deleteByMeetingId(Long meetingId);
int deleteByMeetingIdAndUserId(BizMeetingAttendee entity);
List<BizMeetingAttendee> selectByMeetingId(Long meetingId);
List<BizMeetingAttendee> selectByUserId(Long userId);
/** 当前用户的"待签署"会议列表 (handsign 或 labor_protocol 任一为空) */
List<BizMeetingAttendee> selectUnsignedByUserId(Long userId);
}
@@ -10,6 +10,7 @@ import com.ruoyi.business.service.IBizExpertService;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.id.IdGenerator;
import com.ruoyi.system.service.ISysUserService;
@Service
@@ -22,7 +23,7 @@ public class BizExpertServiceImpl implements IBizExpertService
private ISysUserService sysUserService;
@Override
public BizExpert getById(String expertId)
public BizExpert getById(Long expertId)
{ return bizExpertMapper.selectByPrimaryKey(expertId); }
@Override
public BizExpert getByUserId(Long userId)
@@ -32,12 +33,10 @@ public class BizExpertServiceImpl implements IBizExpertService
{ return bizExpertMapper.selectList(entity); }
/**
* admin 创建专家: 同时创建 sys_user (用户名=手机号, 密码=手机号, role_type=doctor)
* 1. 校验 phone 没注册过 (抛 ServiceException)
* 2. 创建 sys_user + bcrypt 加密密码
* 3. 设置 role_type=doctor (与公开注册一致)
* 4. 创建 biz_expert 绑定 user_id
* 5. 返回 SysUser 含明文 password (前端 toast 用完即丢)
* 创建专家 + 绑定 sys_user。双职责:
* A. admin 创建 / 批量导入: entity.userId == null → 全流程 (校验 phone + 建 sys_user + 建 biz_expert)
* B. 公开注册 (BizRegisterController): entity.userId 已设 → 控制器已建 sys_user, 本方法只做 biz_expert 绑定
* 区分标志: entity.getUserId() 是否已设
*/
@Override
public SysUser insert(BizExpert entity) {
@@ -45,34 +44,47 @@ public class BizExpertServiceImpl implements IBizExpertService
if (phone == null || phone.isEmpty()) {
throw new ServiceException("手机号不能为空");
}
// 0. 校验 phone 唯一 (查 sys_user, 若 username=phone 已存在即重复)
if (sysUserService.isPhoneRegistered(phone)) {
throw new ServiceException("该手机号已注册,请直接登录");
Long userId = entity.getUserId();
SysUser result = new SysUser();
if (userId == null) {
// ===== A. admin / 批量导入路径: 全流程 =====
// 0. 校验 phone 唯一
if (sysUserService.isPhoneRegistered(phone)) {
throw new ServiceException("该手机号已注册,请直接登录");
}
// 1. 创建 sys_user (用户名=phone, 密码=phone)
SysUser newUser = new SysUser();
newUser.setUserName(phone);
newUser.setNickName(entity.getName());
newUser.setPhonenumber(phone);
newUser.setPassword(SecurityUtils.encryptPassword(phone));
newUser.setStatus("0");
newUser.setDelFlag("0");
newUser.setCreateBy(SecurityUtils.getUsername());
sysUserService.insertUser(newUser);
userId = newUser.getUserId();
// 2. role_type = doctor (DB 默认 executor, 专家需 doctor)
sysUserService.updateRoleType(userId, "doctor");
// admin 路径前端需要明文密码做 toast 提示
result.setUserId(userId);
result.setPassword(phone);
}
// 1. 创建 sys_user (用户名=phone, 密码=phone)
SysUser newUser = new SysUser();
newUser.setUserName(phone);
newUser.setNickName(entity.getName());
newUser.setPhonenumber(phone);
newUser.setPassword(SecurityUtils.encryptPassword(phone));
newUser.setStatus("0");
newUser.setDelFlag("0");
newUser.setCreateBy(SecurityUtils.getUsername());
sysUserService.insertUser(newUser);
Long userId = newUser.getUserId();
// 2. role_type = doctor (跟公开注册一致,DB 默认 executor, 专家需 doctor)
sysUserService.updateRoleType(userId, "doctor");
// 3. 创建 biz_expert 绑定 user_id
// ===== A + B 都走: 创建 biz_expert 绑定 user_id =====
// expertId 用雪花 ID (53位, JS Number 安全, 不用 DB 自增)
entity.setUserId(userId);
com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "expertId");
entity.setExpertId(IdGenerator.generateId());
bizExpertMapper.insert(entity);
// 4. 把明文密码回填 SysUser (仅本次返回,前端 toast 显示)
newUser.setPassword(phone);
return newUser;
if (userId != null && result.getUserId() == null) {
// B 路径: 控制器已知 userId, 不需返回明文密码
result.setUserId(userId);
}
return result;
}
@Override
@@ -85,7 +97,7 @@ public class BizExpertServiceImpl implements IBizExpertService
* sys_user.status: '0'=正常 '1'=停用 (RuoYi 框架约定, 同步时转换)
*/
@Override
public int updateStatus(String expertId, String status) {
public int updateStatus(Long expertId, String status) {
if (status == null || (!"Y".equals(status) && !"N".equals(status))) {
throw new ServiceException("status 必须是 'Y'(正常) 或 'N'(禁用)");
}
@@ -120,10 +132,10 @@ public class BizExpertServiceImpl implements IBizExpertService
return bizExpertMapper.updateByUserId(entity);
}
@Override
public int deleteByPrimaryKey(String expertId)
public int deleteByPrimaryKey(Long expertId)
{ return bizExpertMapper.deleteByPrimaryKey(expertId); }
@Override
public int deleteByPrimaryKeys(String[] expertId)
public int deleteByPrimaryKeys(Long[] expertId)
{ return bizExpertMapper.deleteByPrimaryKeys(expertId); }
/**
@@ -0,0 +1,70 @@
package com.ruoyi.business.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.business.domain.BizLaborProtocolTemplate;
import com.ruoyi.business.mapper.BizLaborProtocolTemplateMapper;
import com.ruoyi.business.service.IBizLaborProtocolTemplateService;
@Service
public class BizLaborProtocolTemplateServiceImpl implements IBizLaborProtocolTemplateService {
@Autowired
private BizLaborProtocolTemplateMapper mapper;
@Override
public BizLaborProtocolTemplate getById(Long id) {
return mapper.selectByPrimaryKey(id);
}
@Override
public List<BizLaborProtocolTemplate> selectList(BizLaborProtocolTemplate entity) {
return mapper.selectList(entity);
}
@Override
public BizLaborProtocolTemplate selectDefault() {
return mapper.selectDefault();
}
@Override
public List<BizLaborProtocolTemplate> selectAllEnabled() {
return mapper.selectAllEnabled();
}
@Override
public int insert(BizLaborProtocolTemplate entity) {
return mapper.insert(entity);
}
@Override
public int update(BizLaborProtocolTemplate entity) {
return mapper.updateByPrimaryKey(entity);
}
/**
* 设为默认: app 层保证全局只有 1 条 default_flag='Y'
* 事务保护: 先 clearAllDefault, 再把目标行 default_flag='Y'
*/
@Override
@Transactional
public int setDefault(Long id) {
mapper.clearAllDefault();
BizLaborProtocolTemplate target = new BizLaborProtocolTemplate();
target.setId(id);
target.setDefaultFlag("Y");
return mapper.updateByPrimaryKey(target);
}
@Override
public int deleteByPrimaryKey(Long id) {
return mapper.deleteByPrimaryKey(id);
}
@Override
public int deleteByPrimaryKeys(Long[] ids) {
return mapper.deleteByPrimaryKeys(ids);
}
}
@@ -0,0 +1,55 @@
package com.ruoyi.business.service.impl;
import java.util.List;
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;
@Service
public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService {
@Autowired
private BizMeetingAttendeeMapper bizMeetingAttendeeMapper;
@Override
public int insert(BizMeetingAttendee entity) {
return bizMeetingAttendeeMapper.insert(entity);
}
@Override
public int updateHandsign(BizMeetingAttendee entity) {
return bizMeetingAttendeeMapper.updateHandsign(entity);
}
@Override
public int updateLaborProtocol(BizMeetingAttendee entity) {
return bizMeetingAttendeeMapper.updateLaborProtocol(entity);
}
@Override
public int deleteByMeetingId(Long meetingId) {
return bizMeetingAttendeeMapper.deleteByMeetingId(meetingId);
}
@Override
public int deleteByMeetingIdAndUserId(BizMeetingAttendee entity) {
return bizMeetingAttendeeMapper.deleteByMeetingIdAndUserId(entity);
}
@Override
public List<BizMeetingAttendee> selectByMeetingId(Long meetingId) {
return bizMeetingAttendeeMapper.selectByMeetingId(meetingId);
}
@Override
public List<BizMeetingAttendee> selectByUserId(Long userId) {
return bizMeetingAttendeeMapper.selectByUserId(userId);
}
@Override
public List<BizMeetingAttendee> selectUnsignedByUserId(Long userId) {
return bizMeetingAttendeeMapper.selectUnsignedByUserId(userId);
}
}
@@ -36,7 +36,7 @@
<include refid="selectFields"/>
where user_id = #{userId} limit 1
</select>
<select id="selectByPrimaryKey" resultMap="BizExpertResult" parameterType="String">
<select id="selectByPrimaryKey" resultMap="BizExpertResult" parameterType="Long">
<include refid="selectFields"/>
where expert_id = #{expertId}
</select>
@@ -53,7 +53,7 @@
<insert id="insert" parameterType="BizExpert">
insert into biz_expert
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="expertId != null and expertId != ''">expert_id,</if>
<if test="expertId != null">expert_id,</if>
<if test="userId != null">user_id,</if>
<if test="name != null">name,</if>
<if test="phone != null">phone,</if>
@@ -81,7 +81,7 @@
<if test="updateTime != null">update_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="expertId != null and expertId != ''">#{expertId},</if>
<if test="expertId != null">#{expertId},</if>
<if test="userId != null">#{userId},</if>
<if test="name != null">#{name},</if>
<if test="phone != null">#{phone},</if>
@@ -206,13 +206,13 @@
</trim>
where expert_id = #{expertId}
</update>
<delete id="deleteByPrimaryKey" parameterType="String">
<delete id="deleteByPrimaryKey" parameterType="Long">
delete from biz_expert where expert_id = #{expertId}
</delete>
<delete id="deleteByPrimaryKeys" parameterType="String">
<delete id="deleteByPrimaryKeys" parameterType="Long">
delete from biz_expert where expert_id in
<foreach collection="expertIds" item="expertId" open="(" separator="," close=")">
#{expertId}
<foreach collection="expertId" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.business.mapper.BizLaborProtocolTemplateMapper">
<resultMap type="BizLaborProtocolTemplate" id="BizLaborProtocolTemplateResult">
<id property="id" column="id" />
<result property="templateName" column="template_name" />
<result property="templateContent" column="template_content" />
<result property="defaultFlag" column="default_flag" />
<result property="sortOrder" column="sort_order" />
<result property="status" column="status" />
<result property="remark" column="remark" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectFields">
select id, template_name, template_content, default_flag, sort_order, status, remark,
create_by, create_time, update_by, update_time
from biz_labor_protocol_template
</sql>
<select id="selectByPrimaryKey" resultMap="BizLaborProtocolTemplateResult" parameterType="Long">
<include refid="selectFields"/>
where id = #{id}
</select>
<select id="selectList" resultMap="BizLaborProtocolTemplateResult" parameterType="BizLaborProtocolTemplate">
<include refid="selectFields"/>
<where>
<if test="templateName != null and templateName != ''">and template_name like concat('%', #{templateName}, '%')</if>
<if test="status != null and status != ''">and status = #{status}</if>
<if test="defaultFlag != null and defaultFlag != ''">and default_flag = #{defaultFlag}</if>
</where>
order by sort_order asc, id asc
</select>
<select id="selectDefault" resultMap="BizLaborProtocolTemplateResult">
<include refid="selectFields"/>
where default_flag = 'Y' and status = 'Y'
limit 1
</select>
<select id="selectAllEnabled" resultMap="BizLaborProtocolTemplateResult">
<include refid="selectFields"/>
where status = 'Y'
order by sort_order asc, id asc
</select>
<insert id="insert" parameterType="BizLaborProtocolTemplate">
insert into biz_labor_protocol_template(template_name, template_content, default_flag, sort_order, status, remark, create_by, create_time)
values(#{templateName}, #{templateContent}, #{defaultFlag}, #{sortOrder}, #{status}, #{remark}, #{createBy}, sysdate())
</insert>
<update id="updateByPrimaryKey" parameterType="BizLaborProtocolTemplate">
update biz_labor_protocol_template
<set>
<if test="templateName != null and templateName != ''">template_name = #{templateName},</if>
template_content = #{templateContent},
<if test="defaultFlag != null and defaultFlag != ''">default_flag = #{defaultFlag},</if>
<if test="sortOrder != null">sort_order = #{sortOrder},</if>
<if test="status != null and status != ''">status = #{status},</if>
<if test="remark != null">remark = #{remark},</if>
update_by = #{updateBy},
update_time = sysdate()
</set>
where id = #{id}
</update>
<update id="clearAllDefault">
update biz_labor_protocol_template set default_flag = 'N', update_time = sysdate()
</update>
<delete id="deleteByPrimaryKey" parameterType="Long">
delete from biz_labor_protocol_template where id = #{id}
</delete>
<delete id="deleteByPrimaryKeys" parameterType="Long">
delete from biz_labor_protocol_template where id in
<foreach collection="array" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.business.mapper.BizMeetingAttendeeMapper">
<resultMap type="BizMeetingAttendee" id="BizMeetingAttendeeResult">
<id property="id" column="id" />
<result property="meetingId" column="meeting_id" />
<result property="userId" column="user_id" />
<result property="handsign" column="handsign" />
<result property="laborProtocol" column="labor_protocol" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<!-- 联表字段 (非持久化, entity transient 字段接收) -->
<result property="meetingName" column="meeting_name" />
<result property="startTime" column="start_time" />
<result property="endTime" column="end_time" />
<result property="projectName" column="project_name" />
<result property="projectNo" column="project_no" />
</resultMap>
<insert id="insert" parameterType="BizMeetingAttendee">
insert into biz_meeting_attendee(meeting_id, user_id, create_by, create_time)
values(#{meetingId}, #{userId}, #{createBy}, sysdate())
</insert>
<update id="updateHandsign" parameterType="BizMeetingAttendee">
update biz_meeting_attendee
set handsign = #{handsign},
update_by = #{updateBy},
update_time = sysdate()
where id = #{id}
</update>
<update id="updateLaborProtocol" parameterType="BizMeetingAttendee">
update biz_meeting_attendee
set labor_protocol = #{laborProtocol},
update_by = #{updateBy},
update_time = sysdate()
where id = #{id}
</update>
<delete id="deleteByMeetingId" parameterType="Long">
delete from biz_meeting_attendee where meeting_id = #{meetingId}
</delete>
<delete id="deleteByMeetingIdAndUserId" parameterType="BizMeetingAttendee">
delete from biz_meeting_attendee where meeting_id = #{meetingId} and user_id = #{userId}
</delete>
<select id="selectByMeetingId" resultMap="BizMeetingAttendeeResult" parameterType="Long">
select id, meeting_id, user_id, handsign, labor_protocol, create_by, create_time
from biz_meeting_attendee where meeting_id = #{meetingId}
</select>
<select id="selectByUserId" resultMap="BizMeetingAttendeeResult" parameterType="Long">
select id, meeting_id, user_id, handsign, labor_protocol, create_by, create_time
from biz_meeting_attendee where user_id = #{userId}
</select>
<!--
当前用户的"待签署"会议列表 (任一未签: handsign 或 labor_protocol 为 NULL)
INNER JOIN biz_meeting 取会议名/时间/项目名, 用于 /doctor/home 工作台
字段别名 + resultMap 上面的 transient property 接收
-->
<select id="selectUnsignedByUserId" resultType="BizMeetingAttendee" parameterType="Long">
select a.id, a.meeting_id, a.user_id, a.handsign, a.labor_protocol, a.create_by, a.create_time,
m.meeting_name as meetingName, m.start_time as startTime,
m.end_time as endTime, m.project_name as projectName, m.project_no as projectNo
from biz_meeting_attendee a
inner join biz_meeting m on m.meeting_id = a.meeting_id
where a.user_id = #{userId}
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>
</mapper>
@@ -45,6 +45,8 @@
<if test="currentStage != null and currentStage != ''">and current_stage = #{currentStage}</if>
<if test="startTime != null">and start_time &gt;= #{startTime}</if>
<if test="endTime != null">and end_time &lt;= #{endTime}</if>
<!-- doctor 角色按 user_id 过滤 (走 biz_meeting_attendee 中间表) -->
<if test="userId != null">and exists (select 1 from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId})</if>
</where>
order by meeting_id desc
</select>
@@ -57,6 +57,7 @@
<if test="role != null and role != ''"> and p.role = #{role}</if>
<if test="status != null and status != ''"> and u.status = #{status}</if>
<if test="parentUserId != null"> and u.parent_user_id = #{parentUserId}</if>
<if test="userId != null"> and p.user_id = #{userId}</if>
<!-- 业务主账号隔离: biz_person.user_id IN (我的子账号 user_ids) -->
<if test="params.subUserIds != null and params.subUserIds.size() > 0">
and p.user_id in
@@ -60,6 +60,9 @@ public class SecurityConfig
requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
// OSS 文件代理 (PDF/图片内嵌预览, 重写 Content-Disposition 为 inline)
.requestMatchers(HttpMethod.GET, "/common/oss/proxy").permitAll()
// OSS 直传签名 (注册场景需匿名访问: 专家/执行方/支持方上传证书时还没 token)
// 安全性: OssController 已用 policy 限定 dir 前缀 + 文件大小, key 含时间戳+随机串防覆盖
.requestMatchers(HttpMethod.GET, "/common/oss/sign").permitAll()
// 业务公开门户与注册入口可匿名访问; 字典 active/单查公开, 写操作需登录+权限
.requestMatchers("/business/public/**", "/business/publicity/**", "/business/auth/**", "/business/sms/**").permitAll()
// 字典 active (只读) + 按 ID 查 公开给前端拉下拉数据