This commit is contained in:
2025-09-12 10:35:17 +08:00
parent 141299fcaa
commit 914c04c240
14 changed files with 388 additions and 1520 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -88,8 +88,21 @@ public class BizSubModuleController extends BaseController
{ {
return AjaxResult.error("父模块不存在"); return AjaxResult.error("父模块不存在");
} }
// 仅模块接取人可新增子模块 // 仅模块接取人可新增子模块兼容历史assignee 可能存为 用户ID/用户名/昵称)
if (!String.valueOf(getUserId()).equals(module.getAssignee())) Long currentUserId = getUserId();
boolean isAssignee = false;
if (module.getAssignee() != null)
{
if (String.valueOf(currentUserId).equals(module.getAssignee())) {
isAssignee = true;
} else {
String username = getUsername();
if (username != null && username.equals(module.getAssignee())) {
isAssignee = true;
}
}
}
if (!isAssignee)
{ {
return AjaxResult.error("仅接取人可在该模块下新增子模块"); return AjaxResult.error("仅接取人可在该模块下新增子模块");
} }
@@ -98,7 +111,8 @@ public class BizSubModuleController extends BaseController
{ {
return AjaxResult.error("父模块未处于进行中,无法新增子模块"); return AjaxResult.error("父模块未处于进行中,无法新增子模块");
} }
subModule.setStatus("0"); // 新增子模块默认进入进行中状态(与父模块接取人一致的执行中)
subModule.setStatus("1");
return toAjax(subService.insertBizSubModule(subModule)); return toAjax(subService.insertBizSubModule(subModule));
} }
@@ -114,7 +128,18 @@ public class BizSubModuleController extends BaseController
return AjaxResult.error("子模块不存在"); return AjaxResult.error("子模块不存在");
} }
BizModule module = moduleService.selectBizModuleByModuleId(current.getModuleId()); BizModule module = moduleService.selectBizModuleByModuleId(current.getModuleId());
if (!String.valueOf(getUserId()).equals(module.getAssignee())) Long currentUserId = getUserId();
String username = getUsername();
boolean isAssignee = false;
if (module != null && module.getAssignee() != null)
{
if (String.valueOf(currentUserId).equals(module.getAssignee())) {
isAssignee = true;
} else if (username != null && username.equals(module.getAssignee())) {
isAssignee = true;
}
}
if (!isAssignee)
{ {
return AjaxResult.error("仅接取人可修改子模块"); return AjaxResult.error("仅接取人可修改子模块");
} }
@@ -135,7 +160,18 @@ public class BizSubModuleController extends BaseController
return AjaxResult.error("子模块不存在"); return AjaxResult.error("子模块不存在");
} }
BizModule module = moduleService.selectBizModuleByModuleId(sub.getModuleId()); BizModule module = moduleService.selectBizModuleByModuleId(sub.getModuleId());
if (!String.valueOf(getUserId()).equals(module.getAssignee())) Long currentUserId = getUserId();
String username = getUsername();
boolean isAssignee = false;
if (module != null && module.getAssignee() != null)
{
if (String.valueOf(currentUserId).equals(module.getAssignee())) {
isAssignee = true;
} else if (username != null && username.equals(module.getAssignee())) {
isAssignee = true;
}
}
if (!isAssignee)
{ {
return AjaxResult.error("仅接取人可删除子模块"); return AjaxResult.error("仅接取人可删除子模块");
} }

View File

@@ -7,6 +7,8 @@ import com.ruoyi.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.models.mapper.BizModuleMapper; import com.ruoyi.models.mapper.BizModuleMapper;
import com.ruoyi.models.mapper.BizSubModuleMapper;
import com.ruoyi.models.domain.BizSubModule;
import com.ruoyi.models.domain.BizModule; import com.ruoyi.models.domain.BizModule;
import com.ruoyi.models.service.IBizModuleService; import com.ruoyi.models.service.IBizModuleService;
import com.ruoyi.project.domain.BizProject; import com.ruoyi.project.domain.BizProject;
@@ -31,6 +33,9 @@ public class BizModuleServiceImpl implements IBizModuleService {
@Autowired @Autowired
private ISysUserService sysUserService; private ISysUserService sysUserService;
@Autowired
private BizSubModuleMapper bizSubModuleMapper;
/** /**
* 查询模块 * 查询模块
* *
@@ -157,6 +162,7 @@ public class BizModuleServiceImpl implements IBizModuleService {
module.setAssignee(String.valueOf(userId)); module.setAssignee(String.valueOf(userId));
} }
module.setAssignTime(DateUtils.getNowDate()); module.setAssignTime(DateUtils.getNowDate());
// 避免重复接取:若已有相同 moduleId 的进行中记录,这里只更新而不新增
return updateBizModule(module); return updateBizModule(module);
} }
@@ -173,7 +179,7 @@ public class BizModuleServiceImpl implements IBizModuleService {
throw new RuntimeException("模块不存在或状态异常"); throw new RuntimeException("模块不存在或状态异常");
} }
if (!String.valueOf(userId).equals(module.getAssignee())) { if (!isCurrentUserAssignee(module, userId)) {
throw new RuntimeException("您不是此模块的接取人"); throw new RuntimeException("您不是此模块的接取人");
} }
@@ -206,10 +212,18 @@ public class BizModuleServiceImpl implements IBizModuleService {
throw new RuntimeException("模块不存在或状态异常"); throw new RuntimeException("模块不存在或状态异常");
} }
if (!String.valueOf(userId).equals(module.getAssignee())) { if (!isCurrentUserAssignee(module, userId)) {
throw new RuntimeException("您不是此模块的接取人"); throw new RuntimeException("您不是此模块的接取人");
} }
// 约束父模块完成前需所有子模块均为已完成status = '2',且未被软删除)
List<BizSubModule> subModules = bizSubModuleMapper.selectByModuleId(moduleId);
boolean hasUnfinished = subModules.stream()
.anyMatch(sm -> sm != null && !"2".equals(sm.getStatus()));
if (hasUnfinished) {
throw new RuntimeException("仍有未完成的子模块,无法完成父模块");
}
module.setStatus("2"); // 已完成 module.setStatus("2"); // 已完成
module.setFinishTime(DateUtils.getNowDate()); module.setFinishTime(DateUtils.getNowDate());
int result = updateBizModule(module); int result = updateBizModule(module);
@@ -220,6 +234,35 @@ public class BizModuleServiceImpl implements IBizModuleService {
return result; return result;
} }
/**
* 判断当前用户是否为该模块的接取人兼容历史assignee 可能保存为 用户ID/用户名/昵称)
*/
private boolean isCurrentUserAssignee(BizModule module, Long userId) {
if (module == null) {
return false;
}
String assignee = module.getAssignee();
if (assignee == null) {
return false;
}
// 1) 与用户ID字符串相等
if (String.valueOf(userId).equals(assignee)) {
return true;
}
try {
SysUser u = sysUserService.selectUserById(userId);
if (u != null) {
if (u.getUserName() != null && u.getUserName().equals(assignee)) {
return true;
}
if (u.getNickName() != null && u.getNickName().equals(assignee)) {
return true;
}
}
} catch (Exception ignored) {}
return false;
}
/** /**
* 检查项目是否完成 * 检查项目是否完成
* @param projectId 项目ID * @param projectId 项目ID

View File

@@ -3,6 +3,7 @@ package com.ruoyi.models.service.impl;
import java.util.List; import java.util.List;
import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.models.mapper.BizSubModuleMapper; import com.ruoyi.models.mapper.BizSubModuleMapper;
@@ -82,7 +83,9 @@ public class BizSubModuleServiceImpl implements IBizSubModuleService
{ {
throw new RuntimeException("父模块不存在"); throw new RuntimeException("父模块不存在");
} }
if (!String.valueOf(userId).equals(module.getAssignee())) String currentUsername = SecurityUtils.getUsername();
if (!(String.valueOf(userId).equals(module.getAssignee())
|| (module.getAssignee() != null && module.getAssignee().equals(currentUsername))))
{ {
throw new RuntimeException("仅父模块接取人可接取子模块"); throw new RuntimeException("仅父模块接取人可接取子模块");
} }
@@ -107,7 +110,9 @@ public class BizSubModuleServiceImpl implements IBizSubModuleService
{ {
throw new RuntimeException("父模块不存在"); throw new RuntimeException("父模块不存在");
} }
if (!String.valueOf(userId).equals(module.getAssignee())) String currentUsername2 = SecurityUtils.getUsername();
if (!(String.valueOf(userId).equals(module.getAssignee())
|| (module.getAssignee() != null && module.getAssignee().equals(currentUsername2))))
{ {
throw new RuntimeException("仅父模块接取人可完成子模块"); throw new RuntimeException("仅父模块接取人可完成子模块");
} }
@@ -117,6 +122,8 @@ public class BizSubModuleServiceImpl implements IBizSubModuleService
sub.setUpdateTime(DateUtils.getNowDate()); sub.setUpdateTime(DateUtils.getNowDate());
return updateBizSubModule(sub); return updateBizSubModule(sub);
} }
} }

View File

@@ -164,24 +164,9 @@ public class BizDocController extends BaseController
Long currentUserId = getUserId(); Long currentUserId = getUserId();
if ("0".equals(kindType)) { if ("0".equals(kindType)) {
// 项目文档:项目管理员 或 项目参与者(该项目下任一模块的接取人/被指派人)可上传 // 项目文档:仅项目创建者可上传
BizProject project = bizProjectService.selectBizProjectByProjectId(projectId); BizProject project = bizProjectService.selectBizProjectByProjectId(projectId);
boolean isOwner = project != null && project.getOwnerId() != null && project.getOwnerId().equals(currentUserId); if (project == null || project.getOwnerId() == null || !project.getOwnerId().equals(currentUserId)) {
boolean isParticipant = false;
if (!isOwner) {
// 参与者判断:当前用户是否在该项目下拥有被指派的模块
com.ruoyi.models.domain.BizModule query = new com.ruoyi.models.domain.BizModule();
query.setProjectId(projectId);
java.util.List<com.ruoyi.models.domain.BizModule> modules = bizModuleService.selectBizModuleList(query);
if (modules != null) {
for (com.ruoyi.models.domain.BizModule m : modules) {
if (m.getAssignee() != null && (m.getAssignee().equals(String.valueOf(currentUserId)) || m.getAssignee().equals(getUsername()))) {
isParticipant = true; break;
}
}
}
}
if (!isOwner && !isParticipant) {
return AjaxResult.error("无权上传项目文档"); return AjaxResult.error("无权上传项目文档");
} }
} else if ("1".equals(kindType)) { } else if ("1".equals(kindType)) {

View File

@@ -46,7 +46,9 @@ public class BizDocServiceImpl implements IBizDocService
// - 项目文档:当前用户为项目所有者 // - 项目文档:当前用户为项目所有者
// - 模块文档:当前用户为该模块被指派人 // - 模块文档:当前用户为该模块被指派人
Long currentUserId = SecurityUtils.getUserId(); Long currentUserId = SecurityUtils.getUserId();
String currentUsername = SecurityUtils.getUsername();
bizDoc.getParams().put("currentUserId", currentUserId); bizDoc.getParams().put("currentUserId", currentUserId);
bizDoc.getParams().put("currentUsername", currentUsername);
return bizDocMapper.selectBizDocList(bizDoc); return bizDocMapper.selectBizDocList(bizDoc);
} }

View File

@@ -56,12 +56,12 @@ public class SysUserController extends BaseController {
/** /**
* 获取用户列表 * 获取用户列表
*/ */
// 平台管理员独占用户管理权限 // 用户查询:平台管理员与项目管理员均可用于业务指派选择(不暴露敏感信息)
@PreAuthorize("@ss.hasRole('platform_admin') and @ss.hasPermi('system:user:list')") @PreAuthorize("(@ss.hasRole('platform_admin') or @ss.hasRole('project_admin')) and @ss.hasPermi('system:user:list')")
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo list(SysUser user) { public TableDataInfo list(SysUser user) {
startPage(); startPage();
// 平台管理员不受数据范围限制;其他无权访问已由 @PreAuthorize 拦截 // 项目管理员仅用于指派下拉选择:读取基本字段
List<SysUser> list = userService.selectUserListAll(user); List<SysUser> list = userService.selectUserListAll(user);
return getDataTable(list); return getDataTable(list);
} }
@@ -195,7 +195,7 @@ public class SysUserController extends BaseController {
/** /**
* 根据用户编号获取授权角色 * 根据用户编号获取授权角色
*/ */
@PreAuthorize("@ss.hasPermi('system:user:query')") @PreAuthorize("@ss.hasPermi('system:user:query') or @ss.hasRole('project_admin')")
@GetMapping("/authRole/{userId}") @GetMapping("/authRole/{userId}")
public AjaxResult authRole(@PathVariable("userId") Long userId) { public AjaxResult authRole(@PathVariable("userId") Long userId) {
AjaxResult ajax = AjaxResult.success(); AjaxResult ajax = AjaxResult.success();

View File

@@ -154,7 +154,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</update> </update>
<select id="selectAssignedModules" parameterType="Long" resultMap="BizModuleResult"> <select id="selectAssignedModules" parameterType="Long" resultMap="BizModuleResult">
select m.module_id, select DISTINCT m.module_id,
m.project_id, m.project_id,
m.module_name, m.module_name,
m.status, m.status,

View File

@@ -53,23 +53,42 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="docName != null and docName != ''"> and d.file_name like concat('%', #{docName}, '%')</if> <if test="docName != null and docName != ''"> and d.file_name like concat('%', #{docName}, '%')</if>
<if test="docPath != null and docPath != ''"> and d.file_url = #{docPath}</if> <if test="docPath != null and docPath != ''"> and d.file_url = #{docPath}</if>
and d.del_flag = '0' and d.del_flag = '0'
<!-- 权限控制: <!-- 权限控制(最终口径)
- 项目文档(kind_type='0')项目创建者可见 - 项目文档(kind_type='0'):项目创建者可见全部;普通用户仅能看到自己上传的项目文档
- 模块文档(kind_type='1')仅该模块被指派用户可见 - 模块文档(kind_type='1')项目创建者可见全部;普通用户可见
a) 自己上传的模块文档
b) 自己是该模块接取人assignee = 当前用户名 或 = 当前用户ID
c) 自己被指派designated_user = 当前用户ID
--> -->
<if test="params != null and params.currentUserId != null"> <if test="params != null and params.currentUserId != null">
and ( and (
-- 项目文档:仅项目创建者 -- 项目文档:仅项目创建者可见 或 自己上传的项目文档
(kind_type = '0' and exists ( (kind_type = '0' and (
exists (
select 1 from biz_project p select 1 from biz_project p
where p.project_id = d.project_id where p.project_id = d.project_id
and p.owner_id = #{params.currentUserId} and p.owner_id = #{params.currentUserId}
)
or d.upload_by = #{params.currentUsername}
)) ))
-- 模块文档:仅该模块指派用户 -- 模块文档:项目创建者可见全部;普通用户可见与自己强相关的
or (kind_type = '1' and exists ( or (kind_type = '1' and (
exists (
select 1 from biz_project p
where p.project_id = d.project_id
and p.owner_id = #{params.currentUserId}
)
or d.upload_by = #{params.currentUsername}
or exists (
select 1 from biz_module m select 1 from biz_module m
where m.module_id = d.module_id where m.module_id = d.module_id
and m.designated_user = #{params.currentUserId} and m.del_flag = '0'
and (
m.designated_user = #{params.currentUserId}
or CAST(m.assignee AS CHAR) = CAST(#{params.currentUserId} AS CHAR)
or m.assignee = #{params.currentUsername}
)
)
)) ))
) )
</if> </if>

View File

@@ -146,10 +146,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="checkEmailUnique" parameterType="String" resultMap="SysUserResult"> <select id="checkEmailUnique" parameterType="String" resultMap="SysUserResult">
select user_id, email from sys_user where email = #{email} and del_flag = '0' limit 1 select user_id, email from sys_user where email = #{email} and del_flag = '0' limit 1
</select> </select>
<select id="selectUserIdByUserCount" resultType="java.lang.Long"> <select id="selectUserIdByUserCount" parameterType="String" resultType="java.lang.Long">
select user_id from sys_user <where> select user_id from sys_user where user_name = #{userCount} and del_flag = '0' limit 1
<if test="userName != null">user_name = #{userCount}</if>
</where>
</select> </select>
<insert id="insertUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId"> <insert id="insertUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">

View File

@@ -174,6 +174,11 @@ export default {
const roles = this.$store.state.user.roles || [] const roles = this.$store.state.user.roles || []
const hasNormal = roles.some(r => (typeof r === 'string' ? r : r.roleKey) === 'normal_user') const hasNormal = roles.some(r => (typeof r === 'string' ? r : r.roleKey) === 'normal_user')
return hasNormal && !this.isProjectAdmin return hasNormal && !this.isProjectAdmin
},
// 当前用户ID用于前端过滤项目管理员仅看见自己创建的项目
currentUserId() {
const u = this.$store.state.user || {}
return u.userId || u.user_id || u.id || null
} }
}, },
data() { data() {
@@ -248,27 +253,17 @@ export default {
methods: { methods: {
async initProjects() { async initProjects() {
try { try {
// 并行获取:管理员可见项目 + 我参与的模块聚合项目,取并集,避免有时显示有时不显示
const tasks = []
if (this.isProjectAdmin) { if (this.isProjectAdmin) {
tasks.push(listProject({ pageNum: 1, pageSize: 9999 })) // 项目管理员:仅显示自己创建的项目
const projRes = await listProject({ pageNum: 1, pageSize: 9999 })
const owned = (projRes && Array.isArray(projRes.rows))
? projRes.rows.filter(p => String(p.ownerId) === String(this.currentUserId))
: []
this.projectOptions = owned.map(p => ({ projectId: p.projectId, projectName: p.projectName || (`项目#${p.projectId}`) }))
} else { } else {
// 也许用户具备项目管理员角色但权限范围有限,这里仍保留 getMyModules 以保证兜底 // 普通用户:根据“我的模块”推导可见项目
tasks.push(Promise.resolve({ rows: [] })) const myModRes = await getMyModules({ pageNum: 1, pageSize: 9999 })
}
tasks.push(getMyModules({ pageNum: 1, pageSize: 9999 }))
const [projRes, myModRes] = await Promise.all(tasks)
const map = new Map() const map = new Map()
// 管理员项目
if (projRes && Array.isArray(projRes.rows)) {
projRes.rows.forEach(p => {
if (p.projectId && !map.has(p.projectId)) {
map.set(p.projectId, { projectId: p.projectId, projectName: p.projectName || (`项目#${p.projectId}`) })
}
})
}
// 我参与的模块所在项目
if (myModRes && Array.isArray(myModRes.rows)) { if (myModRes && Array.isArray(myModRes.rows)) {
myModRes.rows.forEach(m => { myModRes.rows.forEach(m => {
if (m.projectId && !map.has(m.projectId)) { if (m.projectId && !map.has(m.projectId)) {
@@ -276,15 +271,17 @@ export default {
} }
}) })
} }
// 路由上下文一定补入 // 注:普通用户可保留路由上下文补入,管理员不补入非本人项目
if (this.currentContext.projectId && !map.has(this.currentContext.projectId)) { if (this.currentContext.projectId && !map.has(this.currentContext.projectId)) {
map.set(this.currentContext.projectId, { projectId: this.currentContext.projectId, projectName: this.currentContext.projectName || (`项目#${this.currentContext.projectId}`) }) map.set(this.currentContext.projectId, { projectId: this.currentContext.projectId, projectName: this.currentContext.projectName || (`项目#${this.currentContext.projectId}`) })
} }
this.projectOptions = Array.from(map.values()) this.projectOptions = Array.from(map.values())
}
// 如果之前没选过,按上下文预选 // 如果之前没选过,按上下文预选(仅当选项中存在)
if (!this.upload.meta.projectId && this.currentContext.projectId) { if (!this.upload.meta.projectId && this.currentContext.projectId) {
this.upload.meta.projectId = this.currentContext.projectId const exists = this.projectOptions.some(p => String(p.projectId) === String(this.currentContext.projectId))
this.upload.meta.projectId = exists ? this.currentContext.projectId : null
} }
if (this.upload.meta.projectId) { if (this.upload.meta.projectId) {
await this.handleProjectChange(this.upload.meta.projectId) await this.handleProjectChange(this.upload.meta.projectId)
@@ -402,6 +399,10 @@ export default {
} }
// 每次打开均刷新一次项目集合,避免角色/指派变更导致的偶发不显示 // 每次打开均刷新一次项目集合,避免角色/指派变更导致的偶发不显示
await this.initProjects() await this.initProjects()
// 普通用户默认选择“模块文档”,并禁用“项目文档”的单选
if (this.isNormalUser && !this.isProjectAdmin) {
this.upload.meta.kindType = 1
}
// 预选:优先使用路由上下文;否则选第一项 // 预选:优先使用路由上下文;否则选第一项
if (this.currentContext.projectId) { if (this.currentContext.projectId) {
this.upload.meta.projectId = this.currentContext.projectId this.upload.meta.projectId = this.currentContext.projectId

View File

@@ -156,11 +156,11 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="描述" prop="description" /> <el-table-column label="描述" prop="description" />
<el-table-column label="操作" align="center" width="220"> <el-table-column label="操作" align="center" width="260">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditSub(scope.row)" v-if="canOperateSub()">修改</el-button> <el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditSub(scope.row)" v-if="canOperateSub()">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDeleteSub(scope.row)" v-if="canOperateSub()">删除</el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDeleteSub(scope.row)" v-if="canOperateSub()">删除</el-button>
<el-button size="mini" type="text" icon="el-icon-check" @click="handleCompleteSub(scope.row)" v-if="canOperateSub() && scope.row.status==='1'">完成</el-button> <el-button size="mini" type="text" icon="el-icon-check" @click="handleCompleteSub(scope.row)" v-if="canOperateSub() && scope.row.status!=='2'">完成</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>

View File

@@ -582,6 +582,11 @@ export default {
// 添加/修改模块(根据是否存在 moduleId 判断) // 添加/修改模块(根据是否存在 moduleId 判断)
subMitModel() { subMitModel() {
this.modelsForm.projectId = this.projectId; this.modelsForm.projectId = this.projectId;
// 表单校验(模块名称、截止日期等必填)
this.$refs["form"].validate(valid => {
if (!valid) {
return;
}
const isEdit = !!this.modelsForm.moduleId; const isEdit = !!this.modelsForm.moduleId;
if (!isEdit) { if (!isEdit) {
// 新增:默认待接取 // 新增:默认待接取
@@ -609,6 +614,7 @@ export default {
} }
}) })
} }
})
}, },
//接取模块 //接取模块
@@ -635,7 +641,19 @@ export default {
//添加模块 //添加模块
ModelsAdd() { ModelsAdd() {
// 打开模块添加对话框并传递projectId // 打开模块添加对话框并传递projectId;清空之前编辑残留,确保走新增
this.modelsForm = {
moduleId: null,
projectId: this.projectId,
moduleName: null,
status: 0,
assignee: null,
assignTime: null,
del_flag: null,
create_by: null,
finishTime: null,
deadline: null
}
this.Models = true; this.Models = true;
this.title = "添加模块"; this.title = "添加模块";
}, },

File diff suppressed because one or more lines are too long