# 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. **菜单配置**：项目管理在系统菜单中的位置
