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("父模块不存在");
}
// 仅模块接取人可新增子模块
if (!String.valueOf(getUserId()).equals(module.getAssignee()))
// 仅模块接取人可新增子模块兼容历史assignee 可能存为 用户ID/用户名/昵称)
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("仅接取人可在该模块下新增子模块");
}
@@ -98,7 +111,8 @@ public class BizSubModuleController extends BaseController
{
return AjaxResult.error("父模块未处于进行中,无法新增子模块");
}
subModule.setStatus("0");
// 新增子模块默认进入进行中状态(与父模块接取人一致的执行中)
subModule.setStatus("1");
return toAjax(subService.insertBizSubModule(subModule));
}
@@ -114,7 +128,18 @@ public class BizSubModuleController extends BaseController
return AjaxResult.error("子模块不存在");
}
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("仅接取人可修改子模块");
}
@@ -135,7 +160,18 @@ public class BizSubModuleController extends BaseController
return AjaxResult.error("子模块不存在");
}
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("仅接取人可删除子模块");
}

View File

@@ -7,6 +7,8 @@ import com.ruoyi.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
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.service.IBizModuleService;
import com.ruoyi.project.domain.BizProject;
@@ -31,6 +33,9 @@ public class BizModuleServiceImpl implements IBizModuleService {
@Autowired
private ISysUserService sysUserService;
@Autowired
private BizSubModuleMapper bizSubModuleMapper;
/**
* 查询模块
*
@@ -157,6 +162,7 @@ public class BizModuleServiceImpl implements IBizModuleService {
module.setAssignee(String.valueOf(userId));
}
module.setAssignTime(DateUtils.getNowDate());
// 避免重复接取:若已有相同 moduleId 的进行中记录,这里只更新而不新增
return updateBizModule(module);
}
@@ -173,7 +179,7 @@ public class BizModuleServiceImpl implements IBizModuleService {
throw new RuntimeException("模块不存在或状态异常");
}
if (!String.valueOf(userId).equals(module.getAssignee())) {
if (!isCurrentUserAssignee(module, userId)) {
throw new RuntimeException("您不是此模块的接取人");
}
@@ -206,10 +212,18 @@ public class BizModuleServiceImpl implements IBizModuleService {
throw new RuntimeException("模块不存在或状态异常");
}
if (!String.valueOf(userId).equals(module.getAssignee())) {
if (!isCurrentUserAssignee(module, userId)) {
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.setFinishTime(DateUtils.getNowDate());
int result = updateBizModule(module);
@@ -220,6 +234,35 @@ public class BizModuleServiceImpl implements IBizModuleService {
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

View File

@@ -3,6 +3,7 @@ package com.ruoyi.models.service.impl;
import java.util.List;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.models.mapper.BizSubModuleMapper;
@@ -82,7 +83,9 @@ public class BizSubModuleServiceImpl implements IBizSubModuleService
{
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("仅父模块接取人可接取子模块");
}
@@ -107,7 +110,9 @@ public class BizSubModuleServiceImpl implements IBizSubModuleService
{
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("仅父模块接取人可完成子模块");
}
@@ -117,6 +122,8 @@ public class BizSubModuleServiceImpl implements IBizSubModuleService
sub.setUpdateTime(DateUtils.getNowDate());
return updateBizSubModule(sub);
}
}

View File

@@ -164,24 +164,9 @@ public class BizDocController extends BaseController
Long currentUserId = getUserId();
if ("0".equals(kindType)) {
// 项目文档:项目管理员 或 项目参与者(该项目下任一模块的接取人/被指派人)可上传
// 项目文档:仅项目创建者可上传
BizProject project = bizProjectService.selectBizProjectByProjectId(projectId);
boolean isOwner = 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) {
if (project == null || project.getOwnerId() == null || !project.getOwnerId().equals(currentUserId)) {
return AjaxResult.error("无权上传项目文档");
}
} else if ("1".equals(kindType)) {

View File

@@ -46,7 +46,9 @@ public class BizDocServiceImpl implements IBizDocService
// - 项目文档:当前用户为项目所有者
// - 模块文档:当前用户为该模块被指派人
Long currentUserId = SecurityUtils.getUserId();
String currentUsername = SecurityUtils.getUsername();
bizDoc.getParams().put("currentUserId", currentUserId);
bizDoc.getParams().put("currentUsername", currentUsername);
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")
public TableDataInfo list(SysUser user) {
startPage();
// 平台管理员不受数据范围限制;其他无权访问已由 @PreAuthorize 拦截
// 项目管理员仅用于指派下拉选择:读取基本字段
List<SysUser> list = userService.selectUserListAll(user);
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}")
public AjaxResult authRole(@PathVariable("userId") Long userId) {
AjaxResult ajax = AjaxResult.success();

View File

@@ -154,7 +154,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</update>
<select id="selectAssignedModules" parameterType="Long" resultMap="BizModuleResult">
select m.module_id,
select DISTINCT m.module_id,
m.project_id,
m.module_name,
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="docPath != null and docPath != ''"> and d.file_url = #{docPath}</if>
and d.del_flag = '0'
<!-- 权限控制:
- 项目文档(kind_type='0')项目创建者可见
- 模块文档(kind_type='1')仅该模块被指派用户可见
<!-- 权限控制(最终口径)
- 项目文档(kind_type='0'):项目创建者可见全部;普通用户仅能看到自己上传的项目文档
- 模块文档(kind_type='1')项目创建者可见全部;普通用户可见
a) 自己上传的模块文档
b) 自己是该模块接取人assignee = 当前用户名 或 = 当前用户ID
c) 自己被指派designated_user = 当前用户ID
-->
<if test="params != null and params.currentUserId != null">
and (
-- 项目文档:仅项目创建者
(kind_type = '0' and exists (
select 1 from biz_project p
where p.project_id = d.project_id
and p.owner_id = #{params.currentUserId}
-- 项目文档:仅项目创建者可见 或 自己上传的项目文档
(kind_type = '0' 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 (kind_type = '1' and exists (
select 1 from biz_module m
where m.module_id = d.module_id
and m.designated_user = #{params.currentUserId}
-- 模块文档:项目创建者可见全部;普通用户可见与自己强相关的
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
where m.module_id = d.module_id
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>

View File

@@ -146,10 +146,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="checkEmailUnique" parameterType="String" resultMap="SysUserResult">
select user_id, email from sys_user where email = #{email} and del_flag = '0' limit 1
</select>
<select id="selectUserIdByUserCount" resultType="java.lang.Long">
select user_id from sys_user <where>
<if test="userName != null">user_name = #{userCount}</if>
</where>
<select id="selectUserIdByUserCount" parameterType="String" resultType="java.lang.Long">
select user_id from sys_user where user_name = #{userCount} and del_flag = '0' limit 1
</select>
<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 hasNormal = roles.some(r => (typeof r === 'string' ? r : r.roleKey) === 'normal_user')
return hasNormal && !this.isProjectAdmin
},
// 当前用户ID用于前端过滤项目管理员仅看见自己创建的项目
currentUserId() {
const u = this.$store.state.user || {}
return u.userId || u.user_id || u.id || null
}
},
data() {
@@ -248,43 +253,35 @@ export default {
methods: {
async initProjects() {
try {
// 并行获取:管理员可见项目 + 我参与的模块聚合项目,取并集,避免有时显示有时不显示
const tasks = []
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 {
// 也许用户具备项目管理员角色但权限范围有限,这里仍保留 getMyModules 以保证兜底
tasks.push(Promise.resolve({ rows: [] }))
// 普通用户:根据“我的模块”推导可见项目
const myModRes = await getMyModules({ pageNum: 1, pageSize: 9999 })
const map = new Map()
if (myModRes && Array.isArray(myModRes.rows)) {
myModRes.rows.forEach(m => {
if (m.projectId && !map.has(m.projectId)) {
map.set(m.projectId, { projectId: m.projectId, projectName: m.projectName || (`项目#${m.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}`) })
}
this.projectOptions = Array.from(map.values())
}
tasks.push(getMyModules({ pageNum: 1, pageSize: 9999 }))
const [projRes, myModRes] = await Promise.all(tasks)
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)) {
myModRes.rows.forEach(m => {
if (m.projectId && !map.has(m.projectId)) {
map.set(m.projectId, { projectId: m.projectId, projectName: m.projectName || (`项目#${m.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}`) })
}
this.projectOptions = Array.from(map.values())
// 如果之前没选过,按上下文预选
// 如果之前没选过,按上下文预选(仅当选项中存在)
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) {
await this.handleProjectChange(this.upload.meta.projectId)
@@ -402,6 +399,10 @@ export default {
}
// 每次打开均刷新一次项目集合,避免角色/指派变更导致的偶发不显示
await this.initProjects()
// 普通用户默认选择“模块文档”,并禁用“项目文档”的单选
if (this.isNormalUser && !this.isProjectAdmin) {
this.upload.meta.kindType = 1
}
// 预选:优先使用路由上下文;否则选第一项
if (this.currentContext.projectId) {
this.upload.meta.projectId = this.currentContext.projectId

View File

@@ -156,11 +156,11 @@
</template>
</el-table-column>
<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">
<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-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>
</el-table-column>
</el-table>

View File

@@ -582,33 +582,39 @@ export default {
// 添加/修改模块(根据是否存在 moduleId 判断)
subMitModel() {
this.modelsForm.projectId = this.projectId;
const isEdit = !!this.modelsForm.moduleId;
if (!isEdit) {
// 新增:默认待接取
this.modelsForm.status = 0;
// 使用当前登录用户名作为创建人
this.modelsForm.create_by = (this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.name) || undefined;
addModels(this.modelsForm).then(res => {
if (res.code === 200) {
this.$message.success("添加完成");
this.Models = false;
this.getModuleList();
} else {
this.$message.error("添加失败,请重试")
}
})
} else {
// 修改:保留原状态/接取信息,仅更新可编辑字段
updateModels(this.modelsForm).then(res => {
if (res.code === 200) {
this.$message.success("修改完成");
this.Models = false;
this.getModuleList();
} else {
this.$message.error("修改失败,请重试")
}
})
}
// 表单校验(模块名称、截止日期等必填)
this.$refs["form"].validate(valid => {
if (!valid) {
return;
}
const isEdit = !!this.modelsForm.moduleId;
if (!isEdit) {
// 新增:默认待接取
this.modelsForm.status = 0;
// 使用当前登录用户名作为创建人
this.modelsForm.create_by = (this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.name) || undefined;
addModels(this.modelsForm).then(res => {
if (res.code === 200) {
this.$message.success("添加完成");
this.Models = false;
this.getModuleList();
} else {
this.$message.error("添加失败,请重试")
}
})
} else {
// 修改:保留原状态/接取信息,仅更新可编辑字段
updateModels(this.modelsForm).then(res => {
if (res.code === 200) {
this.$message.success("修改完成");
this.Models = false;
this.getModuleList();
} else {
this.$message.error("修改失败,请重试")
}
})
}
})
},
//接取模块
@@ -635,7 +641,19 @@ export default {
//添加模块
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.title = "添加模块";
},

File diff suppressed because one or more lines are too long