Files
ccdi/openspec/changes/add-project-management/design.md

613 lines
17 KiB
Markdown
Raw Normal View History

2026-01-27 17:09:32 +08:00
# Design: 项目管理模块
## 概述
本文档描述项目管理模块的技术设计方案包括系统架构、数据模型、API设计等。
## 系统架构
### 模块划分
遵循若依框架的分层架构,新建 `ruoyi-dpc` 模块与若依框架代码分离Controller 层也放在新建模块中:
```
ruoyi-dpc/ (新建模块)
├── pom.xml # 模块依赖配置
├── src/main/java/com/ruoyi/dpc/
│ ├── controller/
2026-01-30 14:15:21 +08:00
│ │ └── CcdiProjectController.java # 项目控制器
2026-01-27 17:09:32 +08:00
│ ├── domain/
2026-01-30 14:15:21 +08:00
│ │ ├── CcdiProject.java # 项目实体
│ │ ├── CcdiProjectPerson.java # 项目人员关联
2026-01-27 17:09:32 +08:00
│ │ ├── dto/
2026-01-30 14:15:21 +08:00
│ │ │ ├── CcdiProjectDTO.java # 项目数据传输对象
│ │ │ ├── CcdiProjectQueryDTO.java # 项目查询DTO
│ │ │ └── CcdiProjectImportDTO.java # 项目导入DTO
2026-01-27 17:09:32 +08:00
│ │ └── vo/
2026-01-30 14:15:21 +08:00
│ │ ├── CcdiProjectVO.java # 项目视图对象
│ │ └── CcdiProjectQueryVO.java # 查询视图对象
2026-01-27 17:09:32 +08:00
│ ├── mapper/
2026-01-30 14:15:21 +08:00
│ │ ├── CcdiProjectMapper.java
│ │ └── CcdiProjectPersonMapper.java
2026-01-27 17:09:32 +08:00
│ └── service/
2026-01-30 14:15:21 +08:00
│ ├── ICcdiProjectService.java
2026-01-27 17:09:32 +08:00
│ └── impl/
2026-01-30 14:15:21 +08:00
│ └── CcdiProjectServiceImpl.java
2026-01-27 17:09:32 +08:00
└── src/main/resources/mapper/dpc/
2026-01-30 14:15:21 +08:00
├── CcdiProjectMapper.xml
└── CcdiProjectPersonMapper.xml
2026-01-27 17:09:32 +08:00
ruoyi-ui/src/
├── api/
│ └── dpcProject.js # API请求定义
└── views/
└── dpcProject/
├── index.vue # 项目列表页
├── add-or-edit.vue # 新增/编辑弹窗
└── import-history.vue # 导入历史项目弹窗
```
## 数据模型
### 数据库表设计
2026-01-30 14:15:21 +08:00
#### ccdi_project (项目主表)
2026-01-27 17:09:32 +08:00
| 字段名 | 类型 | 说明 | 约束 |
|-------|------|------|-----|
| 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`: 已归档
2026-01-30 14:15:21 +08:00
#### ccdi_project_person (项目人员关联表)
2026-01-27 17:09:32 +08:00
| 字段名 | 类型 | 说明 | 约束 |
|-------|------|------|-----|
| id | BIGINT | 主键ID | PK, AUTO_INCREMENT |
2026-01-30 14:15:21 +08:00
| project_id | BIGINT | 项目ID | FK -> ccdi_project.project_id |
2026-01-27 17:09:32 +08:00
| 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) 唯一索引
### 实体类设计
2026-01-30 14:15:21 +08:00
#### CcdiProject.java
2026-01-27 17:09:32 +08:00
```java
@Data
2026-01-30 14:15:21 +08:00
@TableName("ccdi_project")
public class CcdiProject {
2026-01-27 17:09:32 +08:00
/** 项目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列表
}
```
#### 审计字段自动填充配置
```java
/**
* MyBatis Plus 审计字段自动填充处理器
*/
@Component
2026-01-30 14:15:21 +08:00
public class CcdiMetaObjectHandler implements MetaObjectHandler {
2026-01-27 17:09:32 +08:00
@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";
}
}
}
```
#### 实体类审计字段注解
```java
@Data
2026-01-30 14:15:21 +08:00
@TableName("ccdi_project")
public class CcdiProject {
2026-01-27 17:09:32 +08:00
// ... 其他字段
/** 创建者 */
@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 混用。
2026-01-30 14:15:21 +08:00
#### CcdiProjectDTO.java新增/修改项目DTO
2026-01-27 17:09:32 +08:00
```java
@Data
2026-01-30 14:15:21 +08:00
public class CcdiProjectDTO {
2026-01-27 17:09:32 +08:00
/** 项目名称 */
@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;
}
```
2026-01-30 14:15:21 +08:00
#### CcdiProjectQueryDTO.java查询项目DTO
2026-01-27 17:09:32 +08:00
```java
@Data
2026-01-30 14:15:21 +08:00
public class CcdiProjectQueryDTO {
2026-01-27 17:09:32 +08:00
/** 项目名称(模糊搜索) */
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;
}
```
2026-01-30 14:15:21 +08:00
#### CcdiProjectImportDTO.java导入历史项目DTO
2026-01-27 17:09:32 +08:00
```java
@Data
2026-01-30 14:15:21 +08:00
public class CcdiProjectImportDTO {
2026-01-27 17:09:32 +08:00
/** 历史项目ID */
@NotNull(message = "请选择要导入的历史项目")
private Long historyProjectId;
/** 新项目名称 */
@NotBlank(message = "新项目名称不能为空")
@Size(max = 100, message = "项目名称不能超过100个字符")
private String projectName;
}
```
#### DTO 与 Entity 转换
```java
/**
* DTO 与 Entity 转换工具类
*/
2026-01-30 14:15:21 +08:00
public class CcdiProjectConverter {
2026-01-27 17:09:32 +08:00
/**
* DTO 转 Entity新增/修改)
*/
2026-01-30 14:15:21 +08:00
public static CcdiProject toEntity(CcdiProjectDTO dto) {
2026-01-27 17:09:32 +08:00
if (dto == null) {
return null;
}
2026-01-30 14:15:21 +08:00
CcdiProject entity = new CcdiProject();
2026-01-27 17:09:32 +08:00
entity.setProjectName(dto.getProjectName());
entity.setDescription(dto.getDescription());
entity.setStartDate(dto.getStartDate());
entity.setEndDate(dto.getEndDate());
return entity;
}
/**
* Entity 转 VO
*/
2026-01-30 14:15:21 +08:00
public static CcdiProjectVO toVO(CcdiProject entity) {
2026-01-27 17:09:32 +08:00
if (entity == null) {
return null;
}
2026-01-30 14:15:21 +08:00
CcdiProjectVO vo = new CcdiProjectVO();
2026-01-27 17:09:32 +08:00
BeanUtils.copyProperties(entity, vo);
return vo;
}
/**
* Entity 列表转 VO 列表
*/
2026-01-30 14:15:21 +08:00
public static List<CcdiProjectVO> toVOList(List<CcdiProject> entityList) {
2026-01-27 17:09:32 +08:00
if (entityList == null || entityList.isEmpty()) {
return new ArrayList<>();
}
return entityList.stream()
2026-01-30 14:15:21 +08:00
.map(CcdiProjectConverter::toVO)
2026-01-27 17:09:32 +08:00
.collect(Collectors.toList());
}
}
```
#### Controller 使用 DTO
```java
@RestController
@RequestMapping("/dpc/project")
2026-01-30 14:15:21 +08:00
public class CcdiProjectController {
2026-01-27 17:09:32 +08:00
@Resource
2026-01-30 14:15:21 +08:00
private ICcdiProjectService projectService;
2026-01-27 17:09:32 +08:00
@PreAuthorize("@ss.hasPermi('dpc:project:add')")
@PostMapping
2026-01-30 14:15:21 +08:00
public AjaxResult add(@Validated @RequestBody CcdiProjectDTO dto) {
CcdiProject project = CcdiProjectConverter.toEntity(dto);
2026-01-27 17:09:32 +08:00
return AjaxResult.success(projectService.insertProject(project));
}
@PreAuthorize("@ss.hasPermi('dpc:project:edit')")
@PutMapping
2026-01-30 14:15:21 +08:00
public AjaxResult edit(@Validated @RequestBody CcdiProjectDTO dto) {
CcdiProject project = CcdiProjectConverter.toEntity(dto);
2026-01-27 17:09:32 +08:00
return AjaxResult.success(projectService.updateProject(project));
}
@PreAuthorize("@ss.hasPermi('dpc:project:list')")
@GetMapping("/list")
2026-01-30 14:15:21 +08:00
public TableDataInfo list(CcdiProjectQueryDTO queryDTO) {
CcdiProject project = new CcdiProject();
2026-01-27 17:09:32 +08:00
// 将查询条件转换到实体
BeanUtils.copyProperties(queryDTO, project);
startPage();
2026-01-30 14:15:21 +08:00
List<CcdiProject> list = projectService.selectProjectList(project);
List<CcdiProjectVO> voList = CcdiProjectConverter.toVOList(list);
2026-01-27 17:09:32 +08:00
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`
**响应**:
```json
{
"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`
```json
{
"projectName": "2026年Q1初核排查",
"description": "季度常规排查",
"startDate": "2026-01-01",
"endDate": "2026-03-31",
"personIds": [1, 2, 3, 4, 5]
}
```
**响应**:
```json
{
"code": 200,
"msg": "新增成功"
}
```
## 业务逻辑设计
### 项目状态流转
```
[新建] → [进行中] → [已完成] → [已归档]
|---- [重新分析]
```
### 核心业务规则
1. **新建项目**
- 项目名称必填
- 至少选择一名人员
- 默认状态为"进行中"
2. **导入历史项目**
- 选择已完成的历史项目
- 复制人员配置
- 生成新项目(状态为"进行中"
3. **归档项目**
- 只能归档"已完成"状态的项目
- 生成PDF归档文件
- 更新项目状态为"已归档"
4. **重新分析**
- 只能对"已完成"项目执行
- 异步执行风险模型
- 更新预警人数
## 前端设计
### 页面组件结构
#### 项目列表页 (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)
```javascript
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
})
}
```
## 技术约束
1. **后端**
- Spring Boot 3.5.8
- 使用 MyBatis Plus 3.5.10 进行数据访问Spring Boot 3 适配版)
- 使用 `@Resource` 注入依赖(替代 `@Autowired`
- 使用 `@Data` 注解简化实体类
- 实体类不继承 BaseEntity基础字段直接定义在实体类中
- 新建 `ruoyi-dpc` 模块,与若依框架代码分离
- Controller 层也要放在新建模块中(不在 `ruoyi-admin` 中)
- 接口传参使用单独的 DTO不与 entity 混用
2. **前端**
- Vue 2.6.12
- 使用 Element UI 2.15.14 组件库
- 遵循现有视图组件风格
- 在添加页面和组件后,注意与数据库中菜单表进行联动修改
3. **数据库**
- MySQL 8.2.0
2026-01-30 14:15:21 +08:00
- 表名使用 `ccdi_` 前缀(项目英文名首字母集合)
2026-01-27 17:09:32 +08:00
4. **安全**
- 所有API需要 `@PreAuthorize` 权限注解
- 敏感操作记录操作日志
5. **文档**
- 完成后端代码 controller 层代码生成测试后,在项目文件目录下生成 API 文档
6. **测试**
- 测试方式为生成可执行的测试脚本
- 在测试中启动的进程,在测试完成后立刻结束
- `/login/test` 接口可传入 `username``password` 获取 token测试账号admin/admin123
- swagger-ui 地址:`/swagger-ui/index.html`
7. **审计字段自动填充**
- 配置 MyBatis Plus `MetaObjectHandler` 实现审计字段自动填充
- 插入操作自动填充:`create_by``create_time`
- 更新操作自动填充:`update_by``update_time`
## 待确认事项
1. **PDF生成方案**:需确认使用 iText 或其他方案
2. **异步任务实现**:重新分析是否需要集成若依的定时任务模块
3. **菜单配置**:项目管理在系统菜单中的位置