Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
2026-01-27 17:32:15 +08:00
4 changed files with 276 additions and 75 deletions

View File

@@ -1,16 +1,14 @@
package com.openhis.web.doctorstation.appservice;
import com.core.common.core.domain.R;
import com.openhis.template.domain.DoctorPhrase;
import java.util.List;
public interface IDoctorPhraseAppService {
R<?> getDoctorPhraseList();
List<DoctorPhrase> getDoctorPhraseList();
List<DoctorPhrase> searchDoctorPhraseList(String phraseName, Integer phraseType);
Boolean addDoctorPhrase(DoctorPhrase doctorPhrase);
Boolean updateDoctorPhrase(DoctorPhrase doctorPhrase);
Boolean deleteDoctorPhrase(Integer doctorPhraseId);
R<?> searchDoctorPhraseList(String phraseName ,Integer phraseType);
R<?> addDoctorPhrase(DoctorPhrase doctorPhrase);
R<?> updateDoctorPhrase(DoctorPhrase doctorPhrase);
R<?> deleteDoctorPhrase(Integer doctorPhraseId);
}

View File

@@ -2,30 +2,71 @@ package com.openhis.web.doctorstation.appservice.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.core.common.core.domain.R;
import com.core.common.utils.SecurityUtils;
import com.openhis.common.enums.BindingType;
import com.openhis.template.domain.DoctorPhrase;
import com.openhis.template.service.IDoctorPhraseService;
import com.openhis.web.doctorstation.appservice.IDoctorPhraseAppService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Slf4j
@Service
public class DoctorPhraseAppServiceImpl implements IDoctorPhraseAppService {
@Resource
private IDoctorPhraseService doctorPhraseService;
@Override
public R<?> getDoctorPhraseList() {
List<DoctorPhrase> list = doctorPhraseService.list();
return R.ok(list);
@Override
public List<DoctorPhrase> getDoctorPhraseList() {
Long orgId = SecurityUtils.getLoginUser().getOrgId();
if (log.isDebugEnabled()) {
log.debug("orgId: {}", orgId);
}
LambdaQueryWrapper<DoctorPhrase> wrapper = new LambdaQueryWrapper<>();
// 1. 获取当前登录用户信息
Long userId = SecurityUtils.getUserId();
// 2. 权限判定:非管理员才需要过滤
// 如果是超级管理员,默认可以看到所有
if (!SecurityUtils.isAdmin(userId)) {
// 3. 获取当前医生的科室编码
String deptCode = "";
if (orgId != null) {
deptCode = String.valueOf(orgId);
}
// final 变量用于 Lambda 表达式
String finalDeptCode = deptCode;
// 4. 核心逻辑:三级数据共享
wrapper.and(w -> w
.eq(DoctorPhrase::getStaffId, userId.intValue()) // 1. 个人的:只看 staffId 是我的
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.HOSPITAL.getValue())) // 2. 全院的:类型为 3
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.ORGANIZATION.getValue()) // 3. 科室的:类型为 2 且 科室编码匹配
.eq(DoctorPhrase::getDeptCode, finalDeptCode))
);
}
// 5. 按排序号排序(可选优化,让常用语显示更整齐)
List<DoctorPhrase> list = doctorPhraseService.list(wrapper);
return list;
}
@Override
public R<?> searchDoctorPhraseList(String phraseName,Integer phraseType) {
public List<DoctorPhrase> searchDoctorPhraseList(String phraseName,Integer phraseType) {
// 1. 获取当前登录用户信息
Long userId = SecurityUtils.getUserId();
//2.获取到当前医生当前科室的id
Long orgId = SecurityUtils.getLoginUser().getOrgId();
if (log.isDebugEnabled()) {
log.debug("Search phrase - orgId: {}, phraseName: {}, phraseType: {}", orgId, phraseName, phraseType);
}
String deptCode = "";
if (orgId != null) {
deptCode = String.valueOf(orgId);
}
String finalDeptCode = deptCode;
LambdaQueryWrapper<DoctorPhrase> wrapper = new LambdaQueryWrapper<>();
if (phraseName !=null && ObjectUtil.isNotEmpty(phraseName)) {
wrapper.like(DoctorPhrase::getPhraseName, phraseName);
@@ -33,50 +74,135 @@ public class DoctorPhraseAppServiceImpl implements IDoctorPhraseAppService {
if (phraseType !=null && ObjectUtil.isNotEmpty(phraseType)) {
wrapper.eq(DoctorPhrase::getPhraseType, phraseType);
}
Long currentUserId = SecurityUtils.getUserId();
if (!SecurityUtils.isAdmin(currentUserId)) {
// 建议统一使用 staffId 进行业务隔离
wrapper.and(w -> w
.eq(DoctorPhrase::getStaffId, userId.intValue()) // 1. 个人的:只看 staffId 是我的
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.HOSPITAL.getValue())) // 2. 全院的:类型为 3
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.ORGANIZATION.getValue()) // 3. 科室的:类型为 2 且 科室编码匹配
.eq(DoctorPhrase::getDeptCode, finalDeptCode))
);
}
//2.查询
List<DoctorPhrase> list = doctorPhraseService.list(wrapper);
return R.ok(list);
return list;
}
@Override
public R<?> addDoctorPhrase(DoctorPhrase doctorPhrase) {
//1.数据校验
if(ObjectUtil.isEmpty(doctorPhrase)){
return R.fail("医生常用语不能为空");
public Boolean addDoctorPhrase(DoctorPhrase doctorPhrase) {
// 1. 基础校验
if (ObjectUtil.isEmpty(doctorPhrase) || ObjectUtil.isEmpty(doctorPhrase.getPhraseName())) {
throw new IllegalArgumentException("新增失败:常用语名称不能为空");
}
//2.名称唯一性校验
LambdaUpdateWrapper<DoctorPhrase> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DoctorPhrase::getPhraseName, doctorPhrase.getPhraseName());
DoctorPhrase one = doctorPhraseService.getOne(wrapper);
if(ObjectUtil.isNotEmpty(one)){
return R.fail("该名称已经存在");
Long currentUserId = SecurityUtils.getUserId();
/*
* 如果前端没传类型,必须给个默认值(比如 1-个人)
* 否则存成 NULL查询列表时会被过滤掉导致"新增了却看不见"
*/
if (doctorPhrase.getPhraseType() == null) {
doctorPhrase.setPhraseType(BindingType.PERSONAL.getValue());
}
//3.新增
boolean save = doctorPhraseService.save(doctorPhrase);
System.out.println(save);
return R.ok(save);
// 2. 注入归属信息
doctorPhrase.setStaffId(currentUserId.intValue());
doctorPhrase.setCreatorId(currentUserId.intValue());
// 注入科室 (处理 null 情况)
Long orgId = SecurityUtils.getLoginUser().getOrgId();
if (orgId != null) {
// 检查dept_code字段长度避免数据库错误
String deptCode = String.valueOf(orgId);
if (deptCode.length() > 50) { // 假设字段长度为50根据实际情况调整
// 如果超过字段长度限制,可以考虑截断或抛出有意义的错误
throw new IllegalArgumentException("科室ID过长无法保存");
}
doctorPhrase.setDeptCode(deptCode);
}
// =========== 【修复点 2】查重范围限制在"个人" ===========
// 使用 QueryWrapper 而不是 UpdateWrapper
LambdaQueryWrapper<DoctorPhrase> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DoctorPhrase::getPhraseName, doctorPhrase.getPhraseName())
.eq(DoctorPhrase::getStaffId, currentUserId.intValue()); // 重点:只查自己名下的!
if (doctorPhraseService.count(queryWrapper) > 0) {
throw new IllegalArgumentException("新增失败:您已存在同名的常用语");
}
return doctorPhraseService.save(doctorPhrase);
}
@Override
public R<?> updateDoctorPhrase(DoctorPhrase doctorPhrase) {
//1.数据校验
if(ObjectUtil.isEmpty(doctorPhrase)){
return R.fail("医生常用语不能为空");
public Boolean updateDoctorPhrase(DoctorPhrase doctorPhrase) {
// 1. 基础校验
if (ObjectUtil.isEmpty(doctorPhrase) || doctorPhrase.getId() == null) {
throw new IllegalArgumentException("修改失败ID不能为空");
}
//2.更新
boolean updateById = doctorPhraseService.updateById(doctorPhrase);
return R.ok(updateById);
// 2. 查旧数据
DoctorPhrase original = doctorPhraseService.getById(doctorPhrase.getId());
if (original == null) {
throw new IllegalArgumentException("修改失败:该常用语不存在或已被删除");
}
Long currentUserId = SecurityUtils.getUserId();
// 3. 【权限校验优化】
if (!SecurityUtils.isAdmin(currentUserId)) {
// 规则 A严禁修改全院公共模板 (Type=3)
// 这一步是关键!之前你的代码漏了这里,所以普通医生可能改动全院模板
if (BindingType.HOSPITAL.getValue().equals(original.getPhraseType())) {
throw new SecurityException("无权操作:全院公共常用语仅限管理员修改");
}
// 规则 B严禁修改他人的模板
if (!original.getStaffId().equals(currentUserId.intValue())) {
throw new SecurityException("无权操作:您只能修改自己创建的常用语");
}
}
// 4. 数据保护:防止篡改归属
doctorPhrase.setStaffId(original.getStaffId());
doctorPhrase.setCreatorId(original.getCreatorId());
Long orgId = SecurityUtils.getLoginUser().getOrgId();
if (orgId != null) {
// 检查dept_code字段长度避免数据库错误
String deptCode = String.valueOf(orgId);
if (deptCode.length() > 50) { // 假设字段长度为50根据实际情况调整
throw new IllegalArgumentException("科室ID过长无法保存");
}
doctorPhrase.setDeptCode(deptCode);
}
return doctorPhraseService.updateById(doctorPhrase);
}
@Override
public R<?> deleteDoctorPhrase(Integer doctorPhraseId) {
//1.数据校验
if(doctorPhraseId == null){
return R.fail("ID不能为空");
public Boolean deleteDoctorPhrase(Integer doctorPhraseId) {
if (doctorPhraseId == null) {
throw new IllegalArgumentException("删除失败ID不能为空");
}
//2.删除
boolean removeById = doctorPhraseService.removeById(doctorPhraseId);
return R.ok(removeById);
DoctorPhrase original = doctorPhraseService.getById(doctorPhraseId);
if (original == null) {
throw new IllegalArgumentException("删除失败:数据不存在");
}
Long currentUserId = SecurityUtils.getUserId();
// 权限校验
if (!SecurityUtils.isAdmin(currentUserId)) {
if (BindingType.HOSPITAL.getValue().equals(original.getPhraseType())) {
throw new SecurityException("无权操作:全院公共常用语仅限管理员删除");
}
if (!original.getStaffId().equals(currentUserId.intValue())) {
throw new SecurityException("无权操作:您只能删除自己创建的常用语");
}
}
return doctorPhraseService.removeById(doctorPhraseId);
}

View File

@@ -1,6 +1,7 @@
package com.openhis.web.doctorstation.controller;
import com.core.common.core.domain.R;
import com.openhis.common.enums.BindingType;
import com.openhis.template.domain.DoctorPhrase;
import com.openhis.web.doctorstation.appservice.IDoctorPhraseAppService;
import lombok.extern.slf4j.Slf4j;
@@ -23,7 +24,13 @@ public class DoctorPhraseController {
*/
@GetMapping("/list")
public R<?> getDoctorPhraseList(){
return R.ok(doctorPhraseAppService.getDoctorPhraseList());
try {
return R.ok(doctorPhraseAppService.getDoctorPhraseList());
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("获取医生常用语列表系统异常", e);
return R.fail("获取医生常用语列表失败: " + e.getMessage());
}
}
/**
@@ -37,7 +44,13 @@ public class DoctorPhraseController {
@RequestParam(required = false) Integer phraseType,
@RequestParam(required = false) String phraseName
){
return R.ok(doctorPhraseAppService.searchDoctorPhraseList(phraseName,phraseType));
try {
return R.ok(doctorPhraseAppService.searchDoctorPhraseList(phraseName, phraseType));
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("查询医生常用语系统异常", e);
return R.fail("查询医生常用语失败: " + e.getMessage());
}
}
/**
@@ -48,7 +61,22 @@ public class DoctorPhraseController {
*/
@PostMapping("/add")
public R<?> addDoctorPhrase(@RequestBody DoctorPhrase doctorPhrase){
return R.ok(doctorPhraseAppService.addDoctorPhrase(doctorPhrase));
try {
Boolean result = doctorPhraseAppService.addDoctorPhrase(doctorPhrase);
if (result != null && result) {
return R.ok("新增成功");
} else {
return R.fail("新增失败");
}
} catch (IllegalArgumentException e) {
// 参数错误异常使用warn级别日志
log.warn("新增医生常用语参数错误: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("新增医生常用语系统异常", e);
return R.fail("新增失败: " + e.getMessage());
}
}
/**
@@ -59,7 +87,26 @@ public class DoctorPhraseController {
*/
@PutMapping("/update")
public R<?> updateDoctorPhrase(@RequestBody DoctorPhrase doctorPhrase){
return R.ok(doctorPhraseAppService.updateDoctorPhrase(doctorPhrase));
try {
Boolean result = doctorPhraseAppService.updateDoctorPhrase(doctorPhrase);
if (result != null && result) {
return R.ok("更新成功");
} else {
return R.fail("更新失败");
}
} catch (IllegalArgumentException e) {
// 参数错误异常使用warn级别日志
log.warn("更新医生常用语参数错误: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (SecurityException e) {
// 权限相关异常使用warn级别日志
log.warn("更新医生常用语权限异常: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("更新医生常用语系统异常", e);
return R.fail("更新失败: " + e.getMessage());
}
}
/**
@@ -70,7 +117,26 @@ public class DoctorPhraseController {
*/
@DeleteMapping("/delete/{DoctorPhraseId}")
public R<?> deleteDoctorPhrase(@PathVariable Integer DoctorPhraseId){
return R.ok(doctorPhraseAppService.deleteDoctorPhrase(DoctorPhraseId));
try {
Boolean result = doctorPhraseAppService.deleteDoctorPhrase(DoctorPhraseId);
if (result != null && result) {
return R.ok("删除成功");
} else {
return R.fail("删除失败");
}
} catch (IllegalArgumentException e) {
// 参数错误异常使用warn级别日志
log.warn("删除医生常用语参数错误: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (SecurityException e) {
// 权限相关异常使用warn级别日志
log.warn("删除医生常用语权限异常: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("删除医生常用语系统异常", e);
return R.fail("删除失败: " + e.getMessage());
}
}
}

View File

@@ -268,14 +268,15 @@ const validatePhraseName = (phraseName, excludeId = null) => {
// 所有数据(用于客户端分页处理)
const allData = ref([])
// 获取医生常用语列表数据
// 获取医生常用语列表数据
const fetchDoctorPhraseList = async () => {
try {
const response = await getDoctorPhraseList()
// 处理后端返回的数据结构data.data
if (response.code === 200 && response.data && response.data.data) {
// 【关键修改】去掉 response.data.data直接取 response.data
if (response.code === 200 && response.data) {
// 按照sortNo由小到大排序保证列表顺序正确
allData.value = response.data.data.sort((a, b) => a.sortNo - b.sortNo)
allData.value = response.data.sort((a, b) => a.sortNo - b.sortNo)
total.value = allData.value.length
// 执行客户端分页逻辑
applyPagination()
@@ -285,7 +286,7 @@ const fetchDoctorPhraseList = async () => {
total.value = 0
}
} catch (error) {
console.error('获取列表失败:', error) // 增加控制台日志便于调试
console.error('获取列表失败:', error)
ElMessage.error('获取数据失败: 网络请求错误')
allData.value = []
total.value = 0
@@ -322,19 +323,18 @@ const handleCurrentChange = (val) => {
applyPagination()
}
// 搜索功能核心方法
// 搜索功能核心方法
const handleSearch = async () => {
try {
// searchScope可能是null未选择、1=个人2=科室3=全院
const phraseType = searchScope.value === null ? undefined : searchScope.value
// 调用搜索接口phraseName, phraseType
const response = await searchDoctorPhraseList(searchKeyword.value, phraseType)
if (response.code === 200 && response.data && response.data.data) {
// 按照sortNo由小到大排序
allData.value = response.data.data.sort((a, b) => a.sortNo - b.sortNo)
// 【关键修改】去掉 response.data.data直接取 response.data
if (response.code === 200 && response.data) {
allData.value = response.data.sort((a, b) => a.sortNo - b.sortNo)
total.value = allData.value.length
currentPage.value = 1 // 搜索后重置到第一页
applyPagination() // 应用分页
currentPage.value = 1
applyPagination()
} else {
ElMessage.error('搜索失败: ' + (response.msg || '未知错误'))
allData.value = []
@@ -349,20 +349,30 @@ const handleSearch = async () => {
}
// 打开新增模态框方法
// index.vue
const showAddDialog = () => {
// 重置表单数据
// 1. 算出当前最大的排序号
// 如果列表是空的,就从 1 开始;如果不空,取第一条(因为我们排过序了)或遍历找最大值
let maxSortNo = 0
if (allData.value && allData.value.length > 0) {
// 既然 allData 已经按 sortNo 排序了,那最后一个就是最大的?
// 或者保险起见,用 Math.max 算一下
maxSortNo = Math.max(...allData.value.map(item => item.sortNo || 0))
}
// 2. 重置表单,并将排序号设为 最大值 + 1
addForm.value = {
phraseName: '',
phraseContent: '',
sortNo: 1,
sortNo: maxSortNo + 1, // <--- 这样每次打开就是 2, 3, 4...
phraseType: 1,
phraseCategory: ''
}
// 重置表单验证状态
if (addFormRef.value) {
addFormRef.value.clearValidate()
}
// 打开模态框
addDialogVisible.value = true
}
@@ -434,7 +444,6 @@ const handleDelete = async (row) => {
// 用户取消删除时不提示错误
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error('删除操作失败: 网络异常或权限不足')
}
}
}
@@ -455,39 +464,41 @@ const showEditDialog = (row) => {
}
// 编辑表单提交保存方法
// 修改 index.vue 中的 handleEditSave 方法
const handleEditSave = async () => {
try {
// 先执行表单验证
// 1. 表单校验
const validateResult = await editFormRef.value.validate()
if (!validateResult) return
// 名称唯一性校验排除当前编辑的这条记录ID
// 2. 名称唯一性校验
const nameValidation = validatePhraseName(editForm.value.phraseName, editForm.value.id)
if (!nameValidation.valid) {
ElMessage.error(nameValidation.message)
return
}
// 准备更新数据修复时间格式为ISO字符串适配后端LocalDateTime
// 3. 准备数据
const updateData = {
...editForm.value,
enableFlag: 1,
updateTime: new Date().toISOString() // 前端临时赋值,后端最终以自己的为准
updateTime: new Date().toISOString()
}
// 调用更新接口
// 4. 调用接口
const response = await updateDoctorPhrase(updateData)
// 【核心修改】直接判断 code === 200 即可
// 因为后端现在失败会返回 R.fail (code!=200),所以只要是 200 就是成功
if (response.code === 200) {
ElMessage.success('更新成功')
ElMessage.success(response.msg || '更新成功') // 优先显示后端返回的消息
editDialogVisible.value = false
// 重新拉取数据,保证列表数据最新
fetchDoctorPhraseList()
} else {
ElMessage.error('更新失败: ' + (response.msg || '未知错误'))
ElMessage.error(response.msg || '更新失败')
}
} catch (error) {
console.error('更新失败:', error)
ElMessage.error('更新操作失败: 网络请求错误')
}
}