Files
ccdi/openspec/changes/add-project-management/design.md
2026-02-09 14:34:27 +08:00

613 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```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列表
}
```
#### 审计字段自动填充配置
```java
/**
* 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";
}
}
}
```
#### 实体类审计字段注解
```java
@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
```java
@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
```java
@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
```java
@Data
public class CcdiProjectImportDTO {
/** 历史项目ID */
@NotNull(message = "请选择要导入的历史项目")
private Long historyProjectId;
/** 新项目名称 */
@NotBlank(message = "新项目名称不能为空")
@Size(max = 100, message = "项目名称不能超过100个字符")
private String projectName;
}
```
#### DTO 与 Entity 转换
```java
/**
* 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
```java
@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`
**响应**:
```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
- 表名使用 `ccdi_` 前缀(项目英文名首字母集合)
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. **菜单配置**:项目管理在系统菜单中的位置