# 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 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 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 toVOList(List 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 list = projectService.selectProjectList(project); List 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) ``` ``` ### 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. **菜单配置**:项目管理在系统菜单中的位置