Files
ccdi/openspec/changes/add-project-management/design.md
2026-01-30 14:15:21 +08:00

17 KiB
Raw Blame History

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": "新增成功"
}

业务逻辑设计

项目状态流转

[新建] → [进行中] → [已完成] → [已归档]
                      ↑
                      |---- [重新分析]

核心业务规则

  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)

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 接口可传入 usernamepassword 获取 token测试账号admin/admin123
    • swagger-ui 地址:/swagger-ui/index.html
  7. 审计字段自动填充

    • 配置 MyBatis Plus MetaObjectHandler 实现审计字段自动填充
    • 插入操作自动填充:create_bycreate_time
    • 更新操作自动填充:update_byupdate_time

待确认事项

  1. PDF生成方案:需确认使用 iText 或其他方案
  2. 异步任务实现:重新分析是否需要集成若依的定时任务模块
  3. 菜单配置:项目管理在系统菜单中的位置