17 KiB
17 KiB
Design: 项目管理模块
概述
本文档描述项目管理模块的技术设计方案,包括系统架构、数据模型、API设计等。
系统架构
模块划分
遵循若依框架的分层架构,新建 ruoyi-dpc 模块与若依框架代码分离,Controller 层也放在新建模块中:
ruoyi-dpc/ (新建模块)
├── pom.xml # 模块依赖配置
├── src/main/java/com/ruoyi/dpc/
│ ├── controller/
│ │ └── CcdiProjectController.java # 项目控制器
│ ├── domain/
│ │ ├── CcdiProject.java # 项目实体
│ │ ├── CcdiProjectPerson.java # 项目人员关联
│ │ ├── dto/
│ │ │ ├── CcdiProjectDTO.java # 项目数据传输对象
│ │ │ ├── CcdiProjectQueryDTO.java # 项目查询DTO
│ │ │ └── CcdiProjectImportDTO.java # 项目导入DTO
│ │ └── vo/
│ │ ├── CcdiProjectVO.java # 项目视图对象
│ │ └── CcdiProjectQueryVO.java # 查询视图对象
│ ├── mapper/
│ │ ├── CcdiProjectMapper.java
│ │ └── CcdiProjectPersonMapper.java
│ └── service/
│ ├── ICcdiProjectService.java
│ └── impl/
│ └── CcdiProjectServiceImpl.java
└── src/main/resources/mapper/dpc/
├── CcdiProjectMapper.xml
└── CcdiProjectPersonMapper.xml
ruoyi-ui/src/
├── api/
│ └── dpcProject.js # API请求定义
└── views/
└── dpcProject/
├── index.vue # 项目列表页
├── add-or-edit.vue # 新增/编辑弹窗
└── import-history.vue # 导入历史项目弹窗
数据模型
数据库表设计
ccdi_project (项目主表)
| 字段名 | 类型 | 说明 | 约束 |
|---|---|---|---|
| project_id | BIGINT | 项目ID | PK, AUTO_INCREMENT |
| project_name | VARCHAR(100) | 项目名称 | NOT NULL |
| description | VARCHAR(500) | 项目描述 | |
| status | CHAR(1) | 状态 | NOT NULL, DEFAULT '0' |
| target_count | INT | 目标人数 | NOT NULL, DEFAULT 0 |
| warning_count | INT | 预警人数 | NOT NULL, DEFAULT 0 |
| start_date | DATE | 开始日期 | |
| end_date | DATE | 结束日期 | |
| is_archived | CHAR(1) | 是否归档 | DEFAULT '0' |
| archive_file_path | VARCHAR(255) | 归档文件路径 | |
| create_by | VARCHAR(64) | 创建者 | |
| create_time | DATETIME | 创建时间 | |
| update_by | VARCHAR(64) | 更新者 | |
| update_time | DATETIME | 更新时间 | |
| remark | VARCHAR(500) | 备注 |
状态枚举值:
0: 进行中1: 已完成2: 已归档
ccdi_project_person (项目人员关联表)
| 字段名 | 类型 | 说明 | 约束 |
|---|---|---|---|
| id | BIGINT | 主键ID | PK, AUTO_INCREMENT |
| project_id | BIGINT | 项目ID | FK -> ccdi_project.project_id |
| person_id | BIGINT | 人员ID | FK -> sys_user.user_id |
| person_name | VARCHAR(30) | 人员姓名 | 冗余字段 |
| person_dept_id | BIGINT | 部门ID | 冗余字段 |
索引:
idx_project_id: (project_id)uk_project_person: (project_id, person_id) 唯一索引
实体类设计
CcdiProject.java
@Data
@TableName("ccdi_project")
public class CcdiProject {
/** 项目ID */
@TableId(type = IdType.AUTO)
private Long projectId;
/** 项目名称 */
@NotBlank(message = "项目名称不能为空")
@Size(max = 100, message = "项目名称不能超过100个字符")
private String projectName;
/** 项目描述 */
@Size(max = 500, message = "项目描述不能超过500个字符")
private String description;
/** 状态(0进行中 1已完成 2已归档) */
private String status;
/** 目标人数 */
private Integer targetCount;
/** 预警人数 */
private Integer warningCount;
/** 开始日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date startDate;
/** 结束日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date endDate;
/** 是否归档 */
private String isArchived;
/** 归档文件路径 */
private String archiveFilePath;
/** 创建者 */
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新者 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/** 备注 */
private String remark;
@Transient
private List<Long> personIds; // 关联人员ID列表
}
审计字段自动填充配置
/**
* MyBatis Plus 审计字段自动填充处理器
*/
@Component
public class CcdiMetaObjectHandler implements MetaObjectHandler {
@Resource
private TokenService tokenService;
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "createBy", String.class, getUsername());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
this.strictUpdateFill(metaObject, "updateBy", String.class, getUsername());
}
/**
* 获取当前登录用户名
*/
private String getUsername() {
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
return loginUser != null ? loginUser.getUsername() : "system";
} catch (Exception e) {
return "system";
}
}
}
实体类审计字段注解
@Data
@TableName("ccdi_project")
public class CcdiProject {
// ... 其他字段
/** 创建者 */
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新者 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/** 备注 */
private String remark;
}
配置说明:
@TableField(fill = FieldFill.INSERT)- 仅在插入时自动填充@TableField(fill = FieldFill.INSERT_UPDATE)- 插入和更新时都自动填充MetaObjectHandler处理器会自动从 Spring Security 上下文中获取当前登录用户
DTO 设计
按照全局配置要求,接口传参使用单独的 DTO,不与 entity 混用。
CcdiProjectDTO.java(新增/修改项目DTO)
@Data
public class CcdiProjectDTO {
/** 项目名称 */
@NotBlank(message = "项目名称不能为空")
@Size(max = 100, message = "项目名称不能超过100个字符")
private String projectName;
/** 项目描述 */
@Size(max = 500, message = "项目描述不能超过500个字符")
private String description;
/** 开始日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date startDate;
/** 结束日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date endDate;
/** 关联人员ID列表 */
@NotEmpty(message = "请至少选择一名参与人员")
private List<Long> personIds;
}
CcdiProjectQueryDTO.java(查询项目DTO)
@Data
public class CcdiProjectQueryDTO {
/** 项目名称(模糊搜索) */
private String projectName;
/** 状态 */
private String status;
/** 创建时间范围开始 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTimeStart;
/** 创建时间范围结束 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTimeEnd;
}
CcdiProjectImportDTO.java(导入历史项目DTO)
@Data
public class CcdiProjectImportDTO {
/** 历史项目ID */
@NotNull(message = "请选择要导入的历史项目")
private Long historyProjectId;
/** 新项目名称 */
@NotBlank(message = "新项目名称不能为空")
@Size(max = 100, message = "项目名称不能超过100个字符")
private String projectName;
}
DTO 与 Entity 转换
/**
* DTO 与 Entity 转换工具类
*/
public class CcdiProjectConverter {
/**
* DTO 转 Entity(新增/修改)
*/
public static CcdiProject toEntity(CcdiProjectDTO dto) {
if (dto == null) {
return null;
}
CcdiProject entity = new CcdiProject();
entity.setProjectName(dto.getProjectName());
entity.setDescription(dto.getDescription());
entity.setStartDate(dto.getStartDate());
entity.setEndDate(dto.getEndDate());
return entity;
}
/**
* Entity 转 VO
*/
public static CcdiProjectVO toVO(CcdiProject entity) {
if (entity == null) {
return null;
}
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(entity, vo);
return vo;
}
/**
* Entity 列表转 VO 列表
*/
public static List<CcdiProjectVO> toVOList(List<CcdiProject> entityList) {
if (entityList == null || entityList.isEmpty()) {
return new ArrayList<>();
}
return entityList.stream()
.map(CcdiProjectConverter::toVO)
.collect(Collectors.toList());
}
}
Controller 使用 DTO
@RestController
@RequestMapping("/dpc/project")
public class CcdiProjectController {
@Resource
private ICcdiProjectService projectService;
@PreAuthorize("@ss.hasPermi('dpc:project:add')")
@PostMapping
public AjaxResult add(@Validated @RequestBody CcdiProjectDTO dto) {
CcdiProject project = CcdiProjectConverter.toEntity(dto);
return AjaxResult.success(projectService.insertProject(project));
}
@PreAuthorize("@ss.hasPermi('dpc:project:edit')")
@PutMapping
public AjaxResult edit(@Validated @RequestBody CcdiProjectDTO dto) {
CcdiProject project = CcdiProjectConverter.toEntity(dto);
return AjaxResult.success(projectService.updateProject(project));
}
@PreAuthorize("@ss.hasPermi('dpc:project:list')")
@GetMapping("/list")
public TableDataInfo list(CcdiProjectQueryDTO queryDTO) {
CcdiProject project = new CcdiProject();
// 将查询条件转换到实体
BeanUtils.copyProperties(queryDTO, project);
startPage();
List<CcdiProject> list = projectService.selectProjectList(project);
List<CcdiProjectVO> voList = CcdiProjectConverter.toVOList(list);
return getDataTable(voList);
}
}
API设计
RESTful API规范
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /project/list | 查询项目列表 | project:list |
| GET | /project/{id} | 查询项目详情 | project:query |
| POST | /project | 新增项目 | project:add |
| PUT | /project | 修改项目 | project:edit |
| DELETE | /project/{ids} | 删除项目 | project:remove |
| GET | /project/history/{id} | 获取历史项目配置 | project:query |
| POST | /project/import | 导入历史项目 | project:add |
| POST | /project/archive/{id} | 归档项目 | project:archive |
| POST | /project/reanalyze/{id} | 重新分析 | project:reanalyze |
请求/响应格式
查询项目列表
请求: GET /project/list?projectName=xxx&status=0&pageNum=1&pageSize=10
响应:
{
"total": 100,
"rows": [
{
"projectId": 1,
"projectName": "2026年Q1初核排查",
"description": "季度常规排查",
"status": "0",
"targetCount": 100,
"warningCount": 5,
"createTime": "2026-01-01 10:00:00"
}
],
"code": 200,
"msg": "查询成功"
}
新增项目
请求: POST /project
{
"projectName": "2026年Q1初核排查",
"description": "季度常规排查",
"startDate": "2026-01-01",
"endDate": "2026-03-31",
"personIds": [1, 2, 3, 4, 5]
}
响应:
{
"code": 200,
"msg": "新增成功"
}
业务逻辑设计
项目状态流转
[新建] → [进行中] → [已完成] → [已归档]
↑
|---- [重新分析]
核心业务规则
-
新建项目
- 项目名称必填
- 至少选择一名人员
- 默认状态为"进行中"
-
导入历史项目
- 选择已完成的历史项目
- 复制人员配置
- 生成新项目(状态为"进行中")
-
归档项目
- 只能归档"已完成"状态的项目
- 生成PDF归档文件
- 更新项目状态为"已归档"
-
重新分析
- 只能对"已完成"项目执行
- 异步执行风险模型
- 更新预警人数
前端设计
页面组件结构
项目列表页 (index.vue)
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-form :inline="true">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" />
<el-button @click="handleQuery">搜索</el-button>
<el-button @click="handleAdd">新建项目</el-button>
<el-button @click="handleImport">导入历史项目</el-button>
</el-form>
<!-- 数据表格 -->
<el-table :data="projectList">
<el-table-column prop="projectName" label="项目名称" />
<el-table-column prop="description" label="项目描述" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag v-if="scope.row.status === '0'">进行中</el-tag>
<el-tag v-else-if="scope.row.status === '1'" type="success">已完成</el-tag>
<el-tag v-else type="info">已归档</el-tag>
</template>
</el-table-column>
<el-table-column prop="targetCount" label="目标人数" />
<el-table-column prop="warningCount" label="预警人数" />
<el-table-column label="操作" fixed="right">
<template #default="scope">
<el-button v-if="scope.row.status === '0'" @click="handleEnter(scope.row)">进入项目</el-button>
<el-button v-if="scope.row.status === '1'" @click="handleViewResult(scope.row)">查看结果</el-button>
<el-button v-if="scope.row.status === '1'" @click="handleReanalyze(scope.row)">重新分析</el-button>
<el-button v-if="scope.row.status === '1'" @click="handleArchive(scope.row)">归档</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
API封装 (project.js)
import request from '@/utils/request'
// 查询项目列表
export function listProject(query) {
return request({
url: '/dpc/project/list',
method: 'get',
params: query
})
}
// 新增项目
export function addProject(data) {
return request({
url: '/dpc/project',
method: 'post',
data: data
})
}
// 归档项目
export function archiveProject(projectId) {
return request({
url: '/dpc/project/archive/' + projectId,
method: 'post'
})
}
// 重新分析
export function reanalyzeProject(projectId) {
return request({
url: '/dpc/project/reanalyze/' + projectId,
method: 'post'
})
}
// 导入历史项目
export function importProject(data) {
return request({
url: '/dpc/project/import',
method: 'post',
data: data
})
}
技术约束
-
后端
- Spring Boot 3.5.8
- 使用 MyBatis Plus 3.5.10 进行数据访问(Spring Boot 3 适配版)
- 使用
@Resource注入依赖(替代@Autowired) - 使用
@Data注解简化实体类 - 实体类不继承 BaseEntity,基础字段直接定义在实体类中
- 新建
ruoyi-dpc模块,与若依框架代码分离 - Controller 层也要放在新建模块中(不在
ruoyi-admin中) - 接口传参使用单独的 DTO,不与 entity 混用
-
前端
- Vue 2.6.12
- 使用 Element UI 2.15.14 组件库
- 遵循现有视图组件风格
- 在添加页面和组件后,注意与数据库中菜单表进行联动修改
-
数据库
- MySQL 8.2.0
- 表名使用
ccdi_前缀(项目英文名首字母集合)
-
安全
- 所有API需要
@PreAuthorize权限注解 - 敏感操作记录操作日志
- 所有API需要
-
文档
- 完成后端代码 controller 层代码生成测试后,在项目文件目录下生成 API 文档
-
测试
- 测试方式为生成可执行的测试脚本
- 在测试中启动的进程,在测试完成后立刻结束
/login/test接口可传入username和password获取 token(测试账号:admin/admin123)- swagger-ui 地址:
/swagger-ui/index.html
-
审计字段自动填充
- 配置 MyBatis Plus
MetaObjectHandler实现审计字段自动填充 - 插入操作自动填充:
create_by、create_time - 更新操作自动填充:
update_by、update_time
- 配置 MyBatis Plus
待确认事项
- PDF生成方案:需确认使用 iText 或其他方案
- 异步任务实现:重新分析是否需要集成若依的定时任务模块
- 菜单配置:项目管理在系统菜单中的位置