# 项目与补贴 — 项目 — 技术方案

> 依据：同目录 `项目与补贴-项目功能需求.md`。台账 CRUD + **执行状态自动推算** + **发布/下架**。

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **后端** | RuoYi **v3.9.2**（**springboot2** 分支）：JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| **数据库** | MySQL **5.7.39**，InnoDB，`utf8mb4` |
| **前端** | 若依 Vue2；封面上传复用 `/common/upload` |

**分层**：`Controller` → `Service`（校验、**状态推算**、删封面文件）→ `Mapper`/XML → `domain`。

**状态推算（Service 摘要）**

| 场景 | 行为 |
| --- | --- |
| 新增/修改保存 | 校验通过后调用 `resolveExecStatus` 写库 |
| 列表/详情查询 | 对 `manual_complete_flag=0` 的记录按当前日期**计算展示态**（推荐同步写回 `exec_status`，避免列表过期） |
| `manual_complete_flag=1` | 不自动改 `exec_status`（已完成锁定，见 **§2.1**） |

**推算规则（T=今天，S=开始日，E=预计完成日，优先级序）**：

1. `T < S` → `exec_status=1`（未开始）  
2. `T = E` → `exec_status=3`（已完成）；若 `actual_end_date` 为空则默认 `T`  
3. `T < E` 且 `T ≥ S` → `exec_status=2`（进行中）  
4. `T > E` → `exec_status=2`（进行中，超期不自动完成）

用户**手工**提交 `execStatus=3` 时须带 `actualEndDate`，并置 `manual_complete_flag=1`。

**发布状态（摘要）**

| 操作 | 前置 `publish_status` | 结果 |
| --- | --- | --- |
| 新增/修改/删除/发布 | `0`（未发布） | 发布 → `1`，写 `publish_time` |
| 下架 | `1`（已发布） | `0`，写 `offline_time`；**不删**封面 |
| 修改/删除/发布 | `1` | **拒绝** |

---

## 2. 数据库设计

### 2.1 表 `biz_subsidy_project`（补贴类投资项目）

| 字段 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `project_name` | `varchar(64)` | Y | — | 项目名称（**1～20** 字） |
| `introduction` | `varchar(200)` | N | NULL | 项目简介，选填 |
| `investment_amount` | `decimal(14,2)` | Y | — | 投资金额（&gt; 0） |
| `project_start_date` | `date` | Y | — | 项目开始日期 |
| `planned_end_date` | `date` | Y | — | 预计完成日期（≥ 开始日） |
| `cover_file_url` | `varchar(512)` | N | NULL | 封面 URL |
| `cover_file_path` | `varchar(512)` | N | NULL | 封面存储路径 |
| `exec_status` | `tinyint(4)` | Y | `1` | `1` 未开始 `2` 进行中 `3` 已完成 |
| `actual_end_date` | `date` | N | NULL | 实际完成日期（已完成时） |
| `manual_complete_flag` | `tinyint(4)` | Y | `0` | `0` 可自动推算；`1` 手工已完成锁定 |
| `publish_status` | `tinyint(4)` | Y | `0` | `0` 未发布 `1` 已发布 |
| `publish_time` | `datetime` | N | NULL | 发布时间 |
| `offline_time` | `datetime` | N | NULL | 下架时间 |
| `create_by` / `create_time` / `update_by` / `update_time` | 若依惯例 | — | — | 审计 |
| `remark` | `varchar(500)` | N | NULL | 备注 |

**索引**：`PRIMARY KEY (id)`；`KEY idx_start_date (project_start_date)`；`KEY idx_exec_status (exec_status)`；`KEY idx_planned_end (planned_end_date)`；`KEY idx_publish (publish_status)`。

**DDL（MySQL 5.7）**

```sql
CREATE TABLE `biz_subsidy_project` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `project_name` varchar(64) NOT NULL COMMENT '项目名称',
  `introduction` varchar(200) DEFAULT NULL COMMENT '项目简介',
  `investment_amount` decimal(14,2) NOT NULL COMMENT '投资金额',
  `project_start_date` date NOT NULL COMMENT '项目开始日期',
  `planned_end_date` date NOT NULL COMMENT '预计完成日期',
  `cover_file_url` varchar(512) DEFAULT NULL COMMENT '封面URL',
  `cover_file_path` varchar(512) DEFAULT NULL COMMENT '封面存储路径',
  `exec_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1未开始 2进行中 3已完成',
  `actual_end_date` date DEFAULT NULL COMMENT '实际完成日期',
  `manual_complete_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0可自动推算 1手工已完成锁定',
  `publish_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0未发布 1已发布',
  `publish_time` datetime DEFAULT NULL COMMENT '发布时间',
  `offline_time` datetime DEFAULT NULL COMMENT '下架时间',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_start_date` (`project_start_date`),
  KEY `idx_exec_status` (`exec_status`),
  KEY `idx_planned_end` (`planned_end_date`),
  KEY `idx_publish` (`publish_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目与补贴-项目';
```

### 2.2 枚举常量

| 字段 | 值 | 含义 |
| --- | --- | --- |
| `exec_status` | `1` | 未开始 |
| | `2` | 进行中 |
| | `3` | 已完成 |
| `manual_complete_flag` | `0` | 允许自动推算 |
| | `1` | 手工已完成，不自动降级 |
| `publish_status` | `0` | 未发布 |
| | `1` | 已发布 |

---

## 3. 接口设计

**统一响应**：`AjaxResult` / 分页 `TableDataInfo`。

**权限标识（示例）**：`industryPolicy:subsidyProject:list|query|add|edit|remove|publish|offline|export`

**Base Path（示例）**：`/industryPolicy/subsidyProject`

| # | 说明 | Method | URI | 权限 | 要点 |
| --- | --- | --- | --- | --- | --- |
| 3.1 | 分页列表 | GET | `/industryPolicy/subsidyProject/list` | `industryPolicy:subsidyProject:list` | Query **3.1.1**；排序 `project_start_date DESC, create_time DESC, id DESC`；返回前执行状态刷新（**§1**） |
| 3.2 | 详情 | GET | `/industryPolicy/subsidyProject/{id}` | `industryPolicy:subsidyProject:query` | 全字段；状态刷新后返回 |
| 3.3 | 新增 | POST | `/industryPolicy/subsidyProject` | `industryPolicy:subsidyProject:add` | Body **3.3.1**；`publish_status=0`；保存前 `resolveExecStatus` |
| 3.4 | 修改 | PUT | `/industryPolicy/subsidyProject` | `industryPolicy:subsidyProject:edit` | **仅** `publish_status=0`；替换封面删旧 |
| 3.5 | 删除 | DELETE | `/industryPolicy/subsidyProject/{ids}` | `industryPolicy:subsidyProject:remove` | **仅** `publish_status=0`；删封面文件 |
| 3.6 | 发布 | POST | `/industryPolicy/subsidyProject/publish/{id}` | `industryPolicy:subsidyProject:publish` | 前置 `0`；`publish_status=1`，`publish_time=now()` |
| 3.7 | 下架 | POST | `/industryPolicy/subsidyProject/offline/{id}` | `industryPolicy:subsidyProject:offline` | 前置 `1`；`publish_status=0`，`offline_time=now()` |

#### 3.1.1 列表 Query

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pageNum` / `pageSize` | int | N | 默认 `1` / `20` |
| `keyword` | string | N | 项目名称 / 简介 **OR** 模糊 |
| `execStatus` | int | N | `1`～`3` 精确 |
| `publishStatus` | int | N | `0` / `1` 精确 |
| `startDate` / `endDate` | string | N | 针对 **项目开始日期** 闭区间；`startDate`≤`endDate` |

#### 3.3.1 新增 / 修改 Body（驼峰）

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `id` | 修改 Y | 主键 |
| `projectName` | Y | 1～20 字符 |
| `introduction` | N | ≤50 字 |
| `investmentAmount` | Y | &gt; 0，≤2 位小数 |
| `projectStartDate` | Y | `yyyy-MM-dd` |
| `plannedEndDate` | Y | ≥ `projectStartDate` |
| `coverFileUrl` / `coverFilePath` | N | 封面对 |
| `execStatus` | Y | 用户选择值；保存时与推算协同 |
| `actualEndDate` | 条件 | `execStatus=3` 时必填；≤ 当天 |
| `remark` | N | 备注 |

**保存逻辑（后端）**

1. 校验字段；`plannedEndDate` ≥ `projectStartDate`。  
2. 若请求 `execStatus=3`：校验 `actualEndDate`，设 `manual_complete_flag=1`。  
3. 若 `manual_complete_flag=0`：按 **§1** 优先级覆盖 `exec_status`；仅当 `T=E` 时为 `3`；自动为 `3` 且未传 `actualEndDate` 时默认 `actualEndDate=T`。  
4. 若用户将 `execStatus` 从 `3` 改为非 `3`：清 `manual_complete_flag=0`、`actual_end_date=null`（可选规则）。

### 3.4 服务端校验（摘要）

| 项 | 规则 |
| --- | --- |
| 项目名称 | 非空，1～20 |
| 投资金额 | &gt; 0 |
| 日期 | `plannedEndDate` ≥ `projectStartDate` |
| 封面 | 可空；jpg/jpeg/png，≤10MB |
| 已完成 | `actualEndDate` 必填 |
| 列表区间 | `startDate` ≤ `endDate` |

---

## 4. 菜单与权限 SQL（示例）

| 类型 | 名称 | 权限标识 |
| --- | --- | --- |
| 菜单 | 项目与补贴-项目 | `industryPolicy:subsidyProject:list` |
| 按钮 | 查询/新增/修改/删除/发布/下架/导出 | `…:query` `…:add` `…:edit` `…:remove` `…:publish` `…:offline` `…:export` |

组件路径建议：`policy/subsidyProject/index`。

---

## 5. 交付清单

- [ ] `biz_subsidy_project` DDL、菜单权限 SQL  
- [ ] `BizSubsidyProject` + `SubsidyProjectStatusResolver`（状态推算）  
- [ ] 前端：关键字/状态/开始日期筛选、封面上传、已完成必填实际完成日  
- [ ] 用例：自动推算、手工锁定、改期、删封面  

---

## 6. 修订记录

| 日期 | 说明 |
| --- | --- |
| — | 初稿：对齐功能需求；`exec_status` 1/2/3；`manual_complete_flag` 支持已完成不降级 |
| 1.1 | 同步草稿 §1.1：已完成仅 `T=E`；`T>E` 仍为进行中；自动完成默认 `actual_end_date=T` |
| 1.2 | 草稿 §6：新增 `publish_status`/`publish_time`/`offline_time`；发布/下架接口 |
