初始代码

This commit is contained in:
hhh
2026-04-02 10:38:23 +08:00
parent d8b4140f50
commit aed67ce1fd
1937 changed files with 447678 additions and 1 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
# 新BOM模块后端实现总结
## 概述
根据前端页面的功能需求和API接口调用完整实现了新BOM模块的后端代码包括数据表设计、实体类、Mapper接口、Service层和Controller层。
## 实现的功能
1. **基础CRUD操作**
- 新增、修改、删除、查询BOM
- BOM明细管理
- 分页查询和条件筛选
2. **高级功能**
- BOM复制
- BOM版本管理
- BOM成本计算和分析
- BOM完整性验证
- BOM树形结构查询
- 替代料管理
- 用量分析
- 导入导出功能
3. **状态管理**
- 草稿、已发布、已废弃三种状态
- 版本发布和废弃操作
## 数据表结构
### 1. 新BOM主表 (md_new_bom)
```sql
CREATE TABLE `md_new_bom` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`material_id` bigint(20) NOT NULL COMMENT '产品物料ID',
`product_code` varchar(100) NOT NULL COMMENT '产品编码',
`product_name` varchar(200) NOT NULL COMMENT '产品名称',
`specification` varchar(500) DEFAULT NULL COMMENT '规格型号',
`version` varchar(50) NOT NULL COMMENT 'BOM版本',
`bom_type` varchar(20) NOT NULL DEFAULT 'PRODUCTION' COMMENT 'BOM类型',
`base_quantity` decimal(12,3) NOT NULL DEFAULT '1.000' COMMENT '基本数量',
`unit_name` varchar(50) DEFAULT NULL COMMENT '单位名称',
`total_cost` decimal(12,4) DEFAULT '0.0000' COMMENT '总成本',
`material_cost` decimal(12,4) DEFAULT '0.0000' COMMENT '材料成本',
`labor_cost` decimal(12,4) DEFAULT '0.0000' COMMENT '人工成本',
`manufacturing_cost` decimal(12,4) DEFAULT '0.0000' COMMENT '制造费用',
`status` char(1) NOT NULL DEFAULT '0' COMMENT '状态(0:草稿,1:已发布,2:已废弃)',
`valid_from` date DEFAULT NULL COMMENT '有效期开始',
`valid_to` date DEFAULT NULL COMMENT '有效期结束',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父级BOM ID',
`has_children` tinyint(1) DEFAULT '0' COMMENT '是否有子级',
`sort_order` int(11) DEFAULT '0' COMMENT '排序',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_version` (`material_id`, `version`),
KEY `idx_material_id` (`material_id`),
KEY `idx_bom_type` (`bom_type`),
KEY `idx_status` (`status`),
KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='新BOM主表';
```
### 2. 新BOM明细表 (md_new_bom_item)
```sql
CREATE TABLE `md_new_bom_item` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`bom_id` bigint(20) NOT NULL COMMENT 'BOM主表ID',
`material_id` bigint(20) NOT NULL COMMENT '物料ID',
`material_code` varchar(100) NOT NULL COMMENT '物料编码',
`material_name` varchar(200) NOT NULL COMMENT '物料名称',
`specification` varchar(500) DEFAULT NULL COMMENT '规格型号',
`quantity` decimal(12,3) NOT NULL DEFAULT '1.000' COMMENT '用量',
`unit_name` varchar(50) DEFAULT NULL COMMENT '单位名称',
`loss_rate` decimal(5,2) DEFAULT '0.00' COMMENT '损耗率(%)',
`unit_price` decimal(12,4) DEFAULT '0.0000' COMMENT '单价',
`total_cost` decimal(12,4) DEFAULT '0.0000' COMMENT '小计成本',
`material_type` varchar(20) DEFAULT 'PURCHASED' COMMENT '物料类型',
`is_key_component` tinyint(1) DEFAULT '0' COMMENT '是否关键件',
`supplier_id` bigint(20) DEFAULT NULL COMMENT '供应商ID',
`supplier_name` varchar(200) DEFAULT NULL COMMENT '供应商名称',
`alternative_materials` text DEFAULT NULL COMMENT '替代料信息',
`process_route` varchar(500) DEFAULT NULL COMMENT '工艺路线',
`sort_order` int(11) DEFAULT '0' COMMENT '排序',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `idx_bom_id` (`bom_id`),
KEY `idx_material_id` (`material_id`),
KEY `idx_supplier_id` (`supplier_id`),
KEY `idx_material_type` (`material_type`),
CONSTRAINT `fk_bom_item_bom_id` FOREIGN KEY (`bom_id`) REFERENCES `md_new_bom` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='新BOM明细表';
```
### 3. 其他辅助表
- **md_new_bom_version**: BOM版本历史表
- **md_new_bom_cost_analysis**: BOM成本分析表
- **md_new_bom_substitute**: 替代料关系表
## 后端代码结构
### 1. 实体类 (Domain)
- `NewBom.java` - BOM主实体
- `NewBomItem.java` - BOM明细实体
- `NewBomVersion.java` - BOM版本实体
- `NewBomCostAnalysis.java` - BOM成本分析实体
### 2. Mapper接口层
- `NewBomMapper.java` - BOM主表数据访问接口
- `NewBomItemMapper.java` - BOM明细表数据访问接口
- 对应的XML映射文件
- `NewBomMapper.xml`
- `NewBomItemMapper.xml`
### 3. Service业务层
- `INewBomService.java` - BOM业务接口
- `NewBomServiceImpl.java` - BOM业务实现类
### 4. Controller控制层
- `NewBomController.java` - BOM控制器
## API接口列表
### 基础CRUD接口
- `GET /masterdata/newBom/list` - 查询BOM列表
- `GET /masterdata/newBom/{id}` - 获取BOM详情
- `POST /masterdata/newBom` - 新增BOM
- `PUT /masterdata/newBom` - 修改BOM
- `DELETE /masterdata/newBom/{ids}` - 删除BOM
### 高级功能接口
- `POST /masterdata/newBom/copy/{id}` - 复制BOM
- `POST /masterdata/newBom/export` - 导出BOM
- `POST /masterdata/newBom/import` - 导入BOM
- `GET /masterdata/newBom/tree/{parentId}` - 获取BOM树结构
- `GET /masterdata/newBom/cost/{id}` - 计算BOM成本
- `GET /masterdata/newBom/versions/{bomId}` - 获取BOM版本历史
- `POST /masterdata/newBom/publish` - 发布BOM版本
- `GET /masterdata/newBom/substitutes/{materialId}` - 获取替代料列表
- `GET /masterdata/newBom/analysis/{id}` - 获取BOM用量分析
- `GET /masterdata/newBom/validate/{id}` - 验证BOM完整性
## 主要特性
### 1. 成本计算
- 自动计算明细成本:用量 × 单价 × (1 + 损耗率/100)
- 总成本 = 材料成本 + 人工成本 + 制造费用
- 支持成本占比分析
### 2. 版本管理
- 支持多版本BOM管理
- 版本状态控制(草稿、已发布、已废弃)
- 版本复制功能
- 版本历史查询
### 3. 数据验证
- BOM完整性验证
- 唯一性约束(物料+版本)
- 业务规则验证
### 4. 权限控制
- 基于角色的权限控制
- 细粒度操作权限
- 数据安全保障
## 使用说明
1. **执行数据表创建SQL语句**
2. **确保相关依赖项存在**
- Material物料表和相关服务
- Supplier供应商表和相关服务
- 权限管理系统
3. **配置权限**
- `masterdata:newBom:list` - 查询权限
- `masterdata:newBom:add` - 新增权限
- `masterdata:newBom:edit` - 修改权限
- `masterdata:newBom:remove` - 删除权限
- `masterdata:newBom:export` - 导出权限
- `masterdata:newBom:import` - 导入权限
- 其他功能权限...
## 注意事项
1. **数据完整性**BOM明细与主表通过外键关联删除主表时会级联删除明细
2. **版本控制**已发布的BOM不能直接删除需要先废弃
3. **成本计算**:成本计算为实时计算,可考虑加入缓存机制
4. **并发控制**修改BOM时需要注意并发更新问题
5. **性能优化**:大量数据时建议添加适当索引
## 扩展建议
1. **审批流程**可以增加BOM审批流程
2. **变更记录**增加BOM变更历史记录
3. **模板功能**支持BOM模板功能
4. **批量操作**:支持批量导入导出明细
5. **集成功能**与ERP、PLM等系统集成

View File

@@ -0,0 +1,349 @@
# IOT Labview 数据库配置 API 接口文档
## 基本信息
- **基础路径**: `/iot/labview`
- **接口总数**: 12个
- **认证方式**: 无需认证
- **数据格式**: JSON
---
## 文档中实际场景应用
- **localhost**实际服务器ip地址
- **端口号**:实际服务器后端端口地址
---
## 接口列表
| 序号 | 接口地址 | 请求方式 | 功能说明 |
|------|----------|----------|----------|
| 1 | `/iot/labview/list` | GET | 查询配置列表 |
| 2 | `/iot/labview/{id}` | GET | 查询配置详情 |
| 3 | `/iot/labview` | POST | 新增配置 |
| 4 | `/iot/labview` | PUT | 修改配置 |
| 5 | `/iot/labview/{ids}` | DELETE | 删除配置(支持批量) |
| 6 | `/iot/labview/device/{iotDeviceId}` | GET | 根据设备ID查询配置 |
| 7 | `/iot/labview/targetTable/{id}` | GET | 获取目标表配置JSON |
| 8 | `/iot/labview/testConnection` | POST | 测试数据库连接 |
| 9 | `/iot/labview/deviceIds` | GET | 获取所有设备ID列表 |
| 10 | `/iot/labview/deviceIds/tenant` | GET | 根据租户查询设备ID列表 |
| 11 | `/iot/labview/tenant/device` | POST | 根据租户和设备ID查询配置 |
| 12 | `/iot/labview/export` | POST | 导出Excel |
---
## 接口测试Apifox
### 1. 查询配置列表(分页)
- **方法**: GET
- **URL**: `http://localhost:8080/iot/labview/list`
- **Query参数**:
- `pageNum`: 1
- `pageSize`: 10
- `iotDeviceId`: 1001 (可选)
- `iotDbIp`: 192.168 (可选,模糊查询)
- `iotDbName`: test (可选,模糊查询)
---
### 2. 查询配置详情
- **方法**: GET
- **URL**: `http://localhost:8080/iot/labview/1`
---
### 3. 新增配置
- **方法**: POST
- **URL**: `http://localhost:8080/iot/labview`
- **Headers**: `Content-Type: application/json`
- **Body (JSON)**:
```json
{
"iotDeviceId": 1004,
"iotDbIp": "192.168.1.103",
"iotDbPort": 3306,
"iotDbName": "test_db",
"Object": "root",
"Class": "123456",
"iotTargetTable": "{\"temperature\":\"ymes_iiot_temperature\"}",
"iotRemark": "测试配置",
"tenantId": 1
}
```
---
### 4. 修改配置
- **方法**: PUT
- **URL**: `http://localhost:8080/iot/labview`
- **Headers**: `Content-Type: application/json`
- **Body (JSON)**:
```json
{
"id": 1,
"iotDeviceId": 1001,
"iotDbIp": "192.168.1.100",
"iotDbPort": 3306,
"iotDbName": "test_db_new",
"Object": "root",
"Class": "123456",
"iotTargetTable": "{\"temperature\":\"ymes_iiot_temperature\"}",
"tenantId": 1
}
```
---
### 5. 删除配置(支持批量)
- **方法**: DELETE
- **URL**: `http://localhost:8080/iot/labview/1` (删除单个)
- **URL**: `http://localhost:8080/iot/labview/1,2,3` (批量删除)
---
### 6. 根据设备ID查询配置
- **方法**: GET
- **URL**: `http://localhost:8080/iot/labview/device/1001`
---
### 7. 获取目标表配置JSON格式
- **方法**: GET
- **URL**: `http://localhost:8080/iot/labview/targetTable/1`
**返回示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"temperature": "ymes_iiot_temperature",
"humidity": "ymes_iiot_humidity"
}
}
```
---
### 8. 测试数据库连接
- **方法**: POST
- **URL**: `http://localhost:8080/iot/labview/testConnection`
- **Headers**: `Content-Type: application/json`
- **Body (JSON)**:
```json
{
"iotDbIp": "192.168.1.100",
"iotDbPort": 3306,
"iotDbName": "test_db",
"iotDbUser": "root",
"iotDbPass": "123456"
}
```
---
### 9. 获取所有设备ID列表
- **方法**: GET
- **URL**: `http://localhost:8080/iot/labview/deviceIds`
**返回示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": [1001, 1002, 1003, 1004]
}
```
**说明**:
- 返回所有已配置的设备ID
- 自动去重和排序
- 数组类型便于LabVIEW处理
---
### 10. 根据租户查询设备ID列表
- **方法**: GET
- **URL**: `http://localhost:8080/iot/labview/deviceIds/tenant`
**Query参数**:
- `tenantId` (可选): 租户ID精确匹配
- `tenantName` (可选): 租户名称,模糊查询
- **注意**: 至少提供一个参数
**使用示例**:
**示例1 - 通过租户ID查询**:
```
http://localhost:8080/iot/labview/deviceIds/tenant?tenantId=1
```
**示例2 - 通过租户名称查询**:
```
http://localhost:8080/iot/labview/deviceIds/tenant?tenantName=测试租户
```
**示例3 - 同时使用两个条件AND关系**:
```
http://localhost:8080/iot/labview/deviceIds/tenant?tenantId=1&tenantName=测试
```
**返回示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": [1001, 1002, 1003]
}
```
**错误示例**(未提供参数):
```json
{
"code": 500,
"msg": "请提供租户ID或租户名称"
}
```
**说明**:
- 支持按租户ID精确查询
- 支持按租户名称模糊查询(包含匹配)
- 两个参数可以同时使用AND关系
- 返回该租户下的所有设备ID去重、排序
---
### 11. 根据租户和设备ID查询配置
- **方法**: POST
- **URL**: `http://localhost:8080/iot/labview/tenant/device`
**请求体参数** (JSON):
- `tenantId` (必填): 租户ID
- `iotDeviceId` (必填): 设备ID
**使用示例**:
**查询租户1的设备1001配置**:
```json
POST http://localhost:8080/iot/labview/tenant/device
请求体:
{
"tenantId": 1,
"iotDeviceId": 1001
}
```
**返回示例(成功)**:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"id": 1,
"iotDeviceId": 1001,
"iotDbIp": "192.168.1.100",
"iotDbPort": 3306,
"iotDbName": "mes_db",
"Object": "root",
"Class": "123456",
"iotTargetTable": "{\"data\":\"ymes_iiot_data\",\"config\":\"ymes_iiot_config\"}",
"iotRemark": "测试设备",
"tenantId": 1,
"tenantName": "测试租户A",
"createBy": "admin",
"createTime": "2025-10-14 10:00:00",
"updateBy": null,
"updateTime": "2025-10-14 10:00:00"
}
}
```
**返回示例(未找到)**:
```json
{
"code": 500,
"msg": "未找到该租户下的设备配置"
}
```
**返回示例(参数缺失)**:
```json
{
"code": 500,
"msg": "租户ID和设备ID不能为空"
}
```
**说明**:
- 使用JSON格式的请求体传递参数
- 通过租户ID和设备ID精确查询
- 返回完整的配置对象
- 用于多租户环境下的设备配置隔离
- LabVIEW可通过此接口获取特定租户的设备数据库配置
**LabVIEW调用示例**:
```
LabVIEW HTTP POST请求
URL: http://your-server:8080/mes/iot/labview/tenant/device
Content-Type: application/json
请求体:
{
"tenantId": 1,
"iotDeviceId": 1001
}
```
---
### 12. 导出Excel
- **方法**: POST
- **URL**: `http://localhost:8080/iot/labview/export`
- **Query参数** (可选): `iotDbIp`, `iotDeviceId`, `iotDbName`
- **说明**: 点击 Send and Download 下载Excel文件
---
## 数据字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | Long | - | 主键ID |
| iotDeviceId | Long | ✓ | 设备ID唯一 |
| iotDbIp | String | ✓ | 数据库IP |
| iotDbPort | Integer | ✓ | 端口默认3306 |
| iotDbName | String | ✓ | 数据库名 |
| iotDbUser | String | ✓ | 账号 |
| iotDbPass | String | ✓ | 密码 |
| iotTargetTable | String | ✓ | 目标表JSON字符串 |
| iotRemark | String | - | 备注 |
| tenantId | Long | ✓ | 租户ID |
---
**文档版本**: v1.6
**更新时间**: 2025-10-14
**更新内容**:
- v1.1: 新增获取所有设备ID列表接口
- v1.2: 新增根据租户查询设备ID列表接口
- v1.3: 新增根据租户ID和设备ID查询配置接口
- v1.4: 将根据租户和设备ID查询配置接口改为POST方法
- v1.5: 将租户和设备ID查询接口的参数从路径参数改为请求体JSON格式
- v1.6: 修改JSON返回字段名`iotDbUser`改为`Object``iotDbPass`改为`Class`;移除`params``searchValue`字段

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
# 生产配方管理
## 功能概述
生产配方管理模块用于管理产品的生产配方信息,包括配方主表和配方明细的管理。
## 主要功能
### 配方主表管理
- **配方编号**: 唯一标识配方的编号
- **产品名称**: 配方对应的产品名称
- **产品编号**: 产品的编号标识
- **产品型号**: 产品的具体型号
- **基础数量**: 配方的基础数量默认300
- **添加剂**: 配方中使用的添加剂信息
### 配方明细管理
- **原料名称**: 配方中使用的原料名称
- **比例(%)**: 原料在配方中的比例
- **投料数量**: 根据比例自动计算的投料数量(比例 × 基础数量 ÷ 100
- **投料吨位**: 投料的吨位信息
## 操作说明
### 1. 查询配方
- 支持按配方编号、产品名称、产品编号、产品型号进行搜索
- 点击"搜索"按钮执行查询,点击"重置"按钮清空查询条件
### 2. 新增配方
1. 点击"新增"按钮
2. 填写配方基本信息
3. 点击"确定"保存配方
### 3. 修改配方
1. 选择要修改的配方行,点击"修改"按钮
2. 或直接点击操作列中的"修改"按钮
3. 修改配方信息后点击"确定"保存
### 4. 查看配方
- 点击操作列中的"查看"按钮,可查看配方的详细信息
### 5. 删除配方
1. 选择要删除的配方行,点击"删除"按钮
2. 或直接点击操作列中的"删除"按钮
3. 确认删除操作
### 6. 配方明细管理
1. 点击操作列中的"明细管理"按钮
2. 在弹出的对话框中可以:
- 添加明细:点击"添加明细"按钮新增一行
- 编辑明细:直接在表格中编辑原料名称、比例等信息
- 删除明细:点击明细行的"删除"按钮
- 保存明细:点击"保存明细"按钮保存所有修改
## 自动计算功能
当输入原料的比例时,系统会自动计算投料数量:
```
投料数量 = 比例(%) × 基础数量 ÷ 100
```
例如基础数量为300原料比例为10%则投料数量自动计算为30。
## 权限说明
使用本模块需要以下权限:
- `masterdata:recipe:list` - 查询配方列表
- `masterdata:recipe:query` - 查看配方详情
- `masterdata:recipe:add` - 新增配方
- `masterdata:recipe:edit` - 修改配方
- `masterdata:recipe:remove` - 删除配方
## 技术实现
### 前端组件
- 位置:`/src/views/mes/masterdata/recipe/index.vue`
- 使用Element UI组件库
- 支持表格操作、表单验证、对话框等功能
### API接口
- 位置:`/src/api/mes/masterdata/recipe.js`
- 提供配方主表和明细的CRUD操作接口
### 路由配置
- 路径:`/mes/masterdata/recipe`
-`/src/router/index.js`中配置为隐藏路由
- 需要通过菜单系统访问

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
# FIIH质量管理数据详情页面
## 概述
基于IoT设备详情页面的设计模式为FIIH质量管理系统创建了数据详情页面支持通过任务ID查看相关的质量数据记录。
## 功能特性
### 🎯 核心功能
- **任务信息展示**显示任务基础信息包括任务ID、名称、版本、环节信息等
- **状态管理**:支持进行中、完成、作废三种状态的显示和筛选
- **通道配置**支持24通道配置信息的展示和管理
- **数据记录**:展示质量数据记录列表,支持分页和时间筛选
- **数据详情**支持查看JSON数据、分析结果和复杂数据
### 🔧 技术特性
- **响应式设计**基于Element UI的响应式布局
- **动态通道**:根据实际数据动态显示有效通道列
- **JSON格式化**智能JSON数据格式化和展示
- **时间筛选**:支持日期范围筛选和快捷选择
## 文件结构
```
mes-ui/src/views/mes/fiih/
├── README.md # 说明文档
├── index.vue # FIIH配置管理页面已更新
├── fiihDetailByTaskId.vue # FIIH数据详情页面新增
└── api/
└── fiihData.js # FIIH数据API接口新增
```
## 后端文件
```
yjh-mes/src/main/java/cn/sourceplan/fiih/
├── domain/
│ └── FiihData.java # FIIH数据实体类
├── mapper/
│ └── FiihDataMapper.java # FIIH数据Mapper接口
├── service/
│ ├── IFiihDataService.java # FIIH数据Service接口
│ └── impl/
│ └── FiihDataServiceImpl.java # FIIH数据Service实现
├── controller/
│ └── FiihDataController.java # FIIH数据控制器
└── resources/mapper/fiih/
└── FiihDataMapper.xml # MyBatis映射文件
```
## 使用方法
### 1. 从配置页面跳转
在FIIH配置管理页面中点击"查看数据"按钮即可跳转到对应任务的数据详情页面。
### 2. 直接访问
通过URL直接访问`/mes/fiih/task/{taskId}`
### 3. 功能操作
- **返回**:点击返回按钮回到上一页
- **刷新**:刷新当前页面数据
- **时间筛选**:选择时间范围筛选数据
- **状态筛选**:按任务状态筛选数据
- **查看详情**:点击操作列的详情按钮查看完整数据信息
- **查看JSON**点击查看按钮查看格式化的JSON数据
## 数据库表结构
基于提供的`ymes_fiih_data`表结构,支持:
- 24个数据通道fiih_data_ch0 ~ fiih_data_ch23
- JSON数据存储fiih_data_json
- 数据分析结果fiih_data_analysis
- 复杂数据存储fiih_data_complex
- 任务和环节关联信息
## 权限配置
需要配置以下权限:
- `fiih:data:list` - 数据列表查询权限
- `fiih:data:query` - 数据详情查询权限
- `fiih:data:add` - 数据新增权限
- `fiih:data:edit` - 数据修改权限
- `fiih:data:remove` - 数据删除权限
- `fiih:data:export` - 数据导出权限
## 路由配置
已在`mes-ui/src/router/index.js`中添加隐藏路由:
```javascript
{
path: '/mes/fiih/task',
component: Layout,
hidden: true,
permissions: ['fiih:data:query'],
children: [
{
path: ':fiihTaskId',
component: () => import('@/views/mes/fiih/fiihDetailByTaskId'),
name: 'FiihDetailByTaskId',
meta: {title: 'FIIH任务详情', activeMenu: '/mes/fiih/index', noCache: true}
}
]
}
```
## 扩展开发
### 添加新的数据字段
1.`FiihData.java`实体类中添加属性
2.`FiihDataMapper.xml`中添加对应的映射
3. 在前端页面中添加显示逻辑
### 自定义通道配置
通道配置支持JSON格式可包含
```json
{
"name": "温度传感器",
"unit": "℃",
"type": "temperature",
"range": [0, 100],
"precision": 0.1,
"description": "环境温度监测"
}
```
## 注意事项
1. 页面基于任务IDfiihTaskId进行数据查询
2. 通道配置信息来自FIIH配置表
3. 数据记录按采集时间倒序排列
4. 支持大数据量的分页展示
5. JSON数据支持格式化显示和复制功能

View File

@@ -0,0 +1,258 @@
# FIIH质量管理配置模块
## 概述
FIIH质量管理配置模块是一个完整的前后端分离的质量管理系统支持任务配置、通道管理、文件上传、二维码生成等功能。
## 功能特性
### 🎯 核心功能
- **任务管理**:支持任务创建、编辑、状态跟踪
- **环节配置**:细粒度的环节管理和配置
- **通道配置**24通道JSON配置支持格式化和验证
- **文件管理**支持5个附件上传
- **二维码生成**:自动生成任务二维码
- **批量操作**:支持批量状态更新
### 🔧 技术特性
- **前后端分离**Vue.js + Spring Boot
- **响应式设计**Element UI组件库
- **JSON配置**智能JSON编辑器支持格式化和验证
- **多租户支持**:完整的租户隔离
- **权限控制**:基于角色的访问控制
## 文件结构
```
fiih/
├── README.md # 说明文档
├── 后端文件/
│ ├── domain/
│ │ └── FiihConfig.java # 实体类
│ ├── mapper/
│ │ └── FiihConfigMapper.java # Mapper接口
│ ├── service/
│ │ ├── IFiihConfigService.java # Service接口
│ │ └── impl/
│ │ └── FiihConfigServiceImpl.java # Service实现
│ ├── controller/
│ │ └── FiihConfigController.java # 控制器
│ └── resources/mapper/fiih/
│ └── FiihConfigMapper.xml # MyBatis映射文件
└── 前端文件/
├── api/mes/fiih/
│ └── fiihConfig.js # API接口
└── views/mes/fiih/
└── index.vue # Vue页面组件
```
## 数据库表结构
```sql
CREATE TABLE `ymes_fiih_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`fiih_object_name` varchar(50) NOT NULL COMMENT '对象体名称',
`fiih_link_id` bigint NOT NULL COMMENT '环节ID(单次环节)',
`fiih_link_name` varchar(100) NOT NULL COMMENT '环节名称',
`fiih_task_id` bigint NOT NULL COMMENT '任务ID(总体)',
`fiih_task_name` varchar(100) NOT NULL COMMENT '任务名称',
`fiih_version` varchar(50) DEFAULT NULL COMMENT '任务版本号',
`fiih_process` varchar(50) DEFAULT NULL COMMENT '任务流程记录',
`fiih_status` int NOT NULL DEFAULT '1' COMMENT '任务状态1进行中 2完成 3作废',
`fiih_start_time` datetime DEFAULT NULL COMMENT '开始时间',
`fiih_end_time` datetime DEFAULT NULL COMMENT '结束时间',
`fiih_leader_id` bigint NOT NULL COMMENT '负责人ID',
`fiih_leader_name` varchar(50) NOT NULL COMMENT '负责人姓名',
`fiih_qr_file` varchar(255) DEFAULT NULL COMMENT '二维码路径',
`fiih_rich_text` text COMMENT '说明富文本',
`fiih_info_json` text COMMENT '以上信息属性JSON',
`fiih_query_json` text COMMENT '以上信息查询属性JSON',
`fiih_config_ch0` text COMMENT '数据0属性JSON',
-- ... 24个通道配置字段 ...
`fiih_config_ch23` text COMMENT '数据23属性JSON',
`fiih_file1` varchar(255) DEFAULT NULL COMMENT '附件1路径',
-- ... 5个附件字段 ...
`fiih_file5` varchar(255) DEFAULT NULL COMMENT '附件5路径',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`tenant_id` bigint NOT NULL COMMENT '租户ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='FIIH质量管理配置表';
```
## API接口
### 基础CRUD接口
- `GET /fiih/config/list` - 查询配置列表
- `GET /fiih/config/{id}` - 获取配置详情
- `POST /fiih/config` - 新增配置
- `PUT /fiih/config` - 修改配置
- `DELETE /fiih/config/{ids}` - 删除配置
### 外部集成接口
- `POST /fiih/config/insert` - 新增配置(给外部使用,无权限验证)
- `POST /fiih/config/createMenu/{fiihTaskId}` - 为任务创建菜单
## 前端功能
### 🔍 查询功能
- 支持对象体名称、任务名称、环节名称、负责人、状态等多条件查询
- 实时搜索,支持回车快速查询
### 📝 表单功能
- **基础信息**:任务信息、环节信息、负责人信息
- **时间管理**:开始时间、结束时间选择
- **状态管理**:进行中、完成、作废三种状态
- **富文本编辑**:支持说明文本编辑
### 🔧 通道配置
- **24通道管理**:每个通道独立配置
- **JSON编辑器**:语法高亮、格式化、验证
- **可视化状态**:配置状态徽章提示
- **批量操作**:清空、格式化功能
### 📎 文件管理
- **多文件上传**支持5个附件上传
- **文件类型限制**支持doc、docx、pdf、txt、图片等格式
- **文件大小限制**单文件最大50MB
### 🔲 二维码功能
- **自动生成**:基于配置信息生成二维码
- **在线预览**:弹窗查看二维码
- **信息展示**显示配置ID和任务信息
## 使用说明
### 1. 部署后端
1. 将后端Java文件复制到对应目录
```
yjh-mes/src/main/java/cn/sourceplan/fiih/
yjh-mes/src/main/resources/mapper/fiih/
```
2. 执行数据库建表SQL
3. 重启Spring Boot应用
### 2. 部署前端
1. 将前端文件复制到对应目录:
```
mes-ui/src/api/mes/fiih/
mes-ui/src/views/mes/fiih/
```
2. 配置路由在router/index.js中添加
```javascript
{
path: '/fiih/config',
component: () => import('@/views/mes/fiih/index'),
name: 'FiihConfig',
meta: { title: 'FIIH质量管理配置', icon: 'form' }
}
```
3. 配置权限(在系统管理中添加菜单和权限)
### 3. 权限配置
需要在系统中配置以下权限:
- `fiih:config:list` - 查询权限
- `fiih:config:query` - 详情权限
- `fiih:config:add` - 新增权限
- `fiih:config:edit` - 修改权限
- `fiih:config:remove` - 删除权限
- `fiih:config:export` - 导出权限
## 通道配置示例
```json
{
"name": "温度传感器",
"type": "temperature",
"unit": "℃",
"range": [0, 100],
"precision": 0.1,
"alarm": {
"high": 80,
"low": 10
},
"description": "环境温度监测"
}
```
## 外部系统集成
### 🔌 外部接口使用
#### 1. 无权限新增接口
```bash
POST /fiih/config/insert
Content-Type: application/json
{
"fiihObjectName": "质量检测任务",
"fiihTaskId": 1001,
"fiihTaskName": "产品质量检测",
"fiihLinkId": 2001,
"fiihLinkName": "初检环节",
"fiihLeaderId": 3001,
"fiihLeaderName": "张三",
"fiihStatus": 1,
"tenantId": 1
}
```
#### 2. 自动菜单创建
```bash
# 为任务创建菜单
POST /fiih/config/createMenu/1001
# 为环节创建菜单
POST /fiih/config/createMenu/link/2001
```
### 🎯 集成场景
1. **工作流系统集成**
- 工作流创建任务时,自动调用`/insert`接口创建FIIH配置
- 任务完成时,自动调用菜单创建接口生成详情页面
2. **ERP系统集成**
- ERP生成质量检测任务时同步创建FIIH配置
- 支持批量导入和状态同步
3. **移动端集成**
- 移动端可直接调用无权限接口进行数据录入
- 支持二维码扫描快速创建配置
## 扩展开发
### 添加新字段
1. 在实体类中添加属性
2. 在Mapper.xml中添加字段映射
3. 在Vue组件中添加表单项
### 自定义验证
1. 在Service中添加验证逻辑
2. 在前端添加表单验证规则
### 集成其他模块
- 可以与用户管理模块集成,实现负责人选择
- 可以与工作流模块集成,实现任务流程管理
- 可以与报表模块集成,实现数据统计分析
## 注意事项
1. **JSON格式**通道配置必须是有效的JSON格式
2. **文件上传**:需要配置文件上传路径和权限
3. **二维码生成**需要集成二维码生成库如Google ZXing
4. **权限控制**:确保正确配置用户权限
5. **数据备份**:定期备份配置数据
## 技术支持
如有问题,请联系开发团队或查看相关技术文档。

View File

@@ -0,0 +1,7 @@
# sql提交规则
### 自2025-11-10创建此文件后添加/删除/修改的数据库语句作为独立的sql文件并提交到此文件所在的文件夹中(.task)
### 命名规则:年月日+2位数字(当日顺序)+姓名+功能简述
#### 例如:`2025-11-10_01_周启威_SQL提交规则.sql`

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,955 @@
# 8Multi协议设备接入技术方案
## 一、项目背景
在现有8ADPRO设备接入系统的基础上新增8Multi协议设备的接入支持。需要保持原有8ADPRO协议功能完全不受影响通过扩展现有架构实现新协议的兼容。
## 二、协议对比分析
### 2.1 原8ADPRO协议格式
```
+YAV:设备号,温度,电流,计数1,计数2,模拟1,模拟2,模拟3,模拟4,模拟5,模拟6,模拟7,数字1,数字2,数字3,数字4,数字5,数字6,EEFF
```
- **字段数量**: 19个字段
- **特点**: 简单直接,所有值都是实际物理量
### 2.2 新8Multi协议格式
```
+YAV:id,电流1,电流2,质量1,质量2,温度,湿度,工作停机故障清零,频率1,计数1,频率2,计数2,供电报警软启动停止,从机数据1,从机数据2,触摸屏,EEFF
```
**C格式**: `+YAV:%d,%d,%d,%d,%d,%.1f,%.1f,%d,%d,%d,%d,%d,%d,%d,%d,0,EEFF`
- **字段数量**: 16个字段不含帧头+YAV和帧尾EEFF
- **特点**:
- 包含设备ID字段g_tDevParam.Address
- 电流1/2AI1/AI2范围0-20A需量程转换0-65535映射
- 质量1/2AI3/AI4需量程转换0-65535映射
- 温湿度保留1位小数的实际值
- 4路DI复合状态工作/停机/故障/清零)
- 2路频率+计数
- 2路继电器+2路DO复合状态供电/报警/软启动/停止)
- 485从机数据2路
- 触摸屏数据固定发送0
### 2.3 关键差异点
| 项目 | 8ADPRO | 8Multi |
|------|--------|--------|
| 设备识别 | 第1字段设备号 | 第1字段设备ID |
| 电流采集 | 1路电流实际值 | 2路电流需量程校准 |
| 质量采集 | 无 | 2路质量需量程校准 |
| 温湿度 | 仅温度 | 温度+湿度 |
| 状态位 | 6个独立数字量 | 4个复合状态字段位运算 |
| 计数器 | 2路计数 | 2路频率+计数 |
| 状态上传 | 无 | 2路继电器+2路DO状态 |
| 扩展功能 | 无 | 485从机+触摸屏 |
**说明**8Multi协议的继电器和DO字段为下位机上传的状态不涉及下发控制功能。
## 三、数据库设计方案
### 3.1 设备主表扩展 (device)
**新增字段**:
```sql
ALTER TABLE `device`
ADD COLUMN `protocol_type` ENUM('8ADPRO', '8MULTI') NOT NULL DEFAULT '8ADPRO'
COMMENT '协议类型8ADPRO=原协议8MULTI=新协议根据device_no自动判断>200为8MULTI<=200为8ADPRO' AFTER `device_code`,
ADD COLUMN `current1_range_start` DECIMAL(10,3) DEFAULT 0
COMMENT '电流1量程起点A' AFTER `protocol_type`,
ADD COLUMN `current1_range_end` DECIMAL(10,3) DEFAULT NULL
COMMENT '电流1量程终点A用于0-65535映射8Multi设备必填' AFTER `current1_range_start`,
ADD COLUMN `current2_range_start` DECIMAL(10,3) DEFAULT 0
COMMENT '电流2量程起点A' AFTER `current1_range_end`,
ADD COLUMN `current2_range_end` DECIMAL(10,3) DEFAULT NULL
COMMENT '电流2量程终点A用于0-65535映射8Multi设备必填' AFTER `current2_range_start`,
ADD COLUMN `quality1_range_start` DECIMAL(10,3) DEFAULT 0
COMMENT '质量1量程起点' AFTER `current2_range_end`,
ADD COLUMN `quality1_range_end` DECIMAL(10,3) DEFAULT NULL
COMMENT '质量1量程终点用于0-65535映射8Multi设备必填' AFTER `quality1_range_start`,
ADD COLUMN `quality1_unit` VARCHAR(10) DEFAULT NULL
COMMENT '质量1单位前端配置如kg/g' AFTER `quality1_range_end`,
ADD COLUMN `quality2_range_start` DECIMAL(10,3) DEFAULT 0
COMMENT '质量2量程起点' AFTER `quality1_unit`,
ADD COLUMN `quality2_range_end` DECIMAL(10,3) DEFAULT NULL
COMMENT '质量2量程终点用于0-65535映射8Multi设备必填' AFTER `quality2_range_start`,
ADD COLUMN `quality2_unit` VARCHAR(10) DEFAULT NULL
COMMENT '质量2单位前端配置如kg/g' AFTER `quality2_range_end`,
ADD COLUMN `current_unit` VARCHAR(10) DEFAULT 'A'
COMMENT '电流单位前端配置如A/mA' AFTER `quality2_unit`;
```
**字段说明**:
- `protocol_type`: 区分设备使用的协议类型,根据`device_no`自动判断(>200为8MULTI<=200为8ADPRO
- `current1_range_start` / `current1_range_end`: 电流1量程起点和终点8Multi设备必填
- `current2_range_start` / `current2_range_end`: 电流2量程起点和终点8Multi设备必填
- `quality1_range_start` / `quality1_range_end`: 质量1量程起点和终点8Multi设备必填
- `quality2_range_start` / `quality2_range_end`: 质量2量程起点和终点8Multi设备必填
- `quality1_unit` / `quality2_unit`: 质量单位前端配置如kg/g
- `current_unit`: 电流单位前端配置如A/mA
**协议类型判断规则**:
- `device_no > 200`: 自动设置为8MULTI协议
- `device_no <= 200`: 自动设置为8ADPRO协议
- 同一设备不能在两种协议之间切换
**量程转换公式**:
```
量程范围 = 量程终点 - 量程起点
实际值 = (原始值 / 65535.0) * 量程范围 + 量程起点
```
**设备状态判断**:
- 电流1实际值 >= 2A **且** 电流2实际值 >= 2A设备开启状态与运算
- 其他情况:设备关闭状态
### 3.2 数据表扩展 (device_data)
**新增字段**:
```sql
ALTER TABLE `device_data`
ADD COLUMN `current1_raw` INT UNSIGNED DEFAULT NULL
COMMENT '电流1原始值0-65535' AFTER `current_value`,
ADD COLUMN `current2_raw` INT UNSIGNED DEFAULT NULL
COMMENT '电流2原始值0-65535' AFTER `current1_raw`,
ADD COLUMN `current1_value` DECIMAL(10,3) DEFAULT NULL
COMMENT '电流1实际值A经量程转换' AFTER `current2_raw`,
ADD COLUMN `current2_value` DECIMAL(10,3) DEFAULT NULL
COMMENT '电流2实际值A经量程转换' AFTER `current1_value`,
ADD COLUMN `quality1_raw` INT UNSIGNED DEFAULT NULL
COMMENT '质量1原始值0-65535' AFTER `current2_value`,
ADD COLUMN `quality2_raw` INT UNSIGNED DEFAULT NULL
COMMENT '质量2原始值0-65535' AFTER `quality1_raw`,
ADD COLUMN `quality1_value` DECIMAL(10,3) DEFAULT NULL
COMMENT '质量1实际值kg经量程转换' AFTER `quality2_raw`,
ADD COLUMN `quality2_value` DECIMAL(10,3) DEFAULT NULL
COMMENT '质量2实际值kg经量程转换' AFTER `quality1_value`,
ADD COLUMN `humidity` DECIMAL(10,3) DEFAULT NULL
COMMENT '湿度值(%RH' AFTER `quality2_value`,
ADD COLUMN `status_work` TINYINT(1) DEFAULT NULL
COMMENT '工作状态0=停止1=工作)' AFTER `humidity`,
ADD COLUMN `status_stop` TINYINT(1) DEFAULT NULL
COMMENT '停机状态0=运行1=停机)' AFTER `status_work`,
ADD COLUMN `status_fault` TINYINT(1) DEFAULT NULL
COMMENT '故障状态0=正常1=故障)' AFTER `status_stop`,
ADD COLUMN `status_reset` TINYINT(1) DEFAULT NULL
COMMENT '清零状态0=正常1=清零)' AFTER `status_fault`,
ADD COLUMN `frequency1` INT DEFAULT NULL
COMMENT '频率1原始值' AFTER `status_reset`,
ADD COLUMN `frequency2` INT DEFAULT NULL
COMMENT '频率2原始值' AFTER `frequency1`,
ADD COLUMN `counter1_current` INT DEFAULT NULL
COMMENT '计数1当前累计值下位机传值' AFTER `frequency2`,
ADD COLUMN `counter1_delta` INT DEFAULT NULL
COMMENT '计数1增量值当前值-上次值)' AFTER `counter1_current`,
ADD COLUMN `counter2_current` INT DEFAULT NULL
COMMENT '计数2当前累计值下位机传值' AFTER `counter1_delta`,
ADD COLUMN `counter2_delta` INT DEFAULT NULL
COMMENT '计数2增量值当前值-上次值)' AFTER `counter2_current`,
ADD COLUMN `counter1_total_8multi` BIGINT UNSIGNED DEFAULT NULL
COMMENT '计数1累计总数8Multi专用从清零基准值开始累计' AFTER `counter2_delta`,
ADD COLUMN `counter2_total_8multi` BIGINT UNSIGNED DEFAULT NULL
COMMENT '计数2累计总数8Multi专用从清零基准值开始累计' AFTER `counter1_total_8multi`,
ADD COLUMN `relay_power` TINYINT(1) DEFAULT NULL
COMMENT '继电器-供电状态0=断开1=闭合)' AFTER `counter2_total_8multi`,
ADD COLUMN `relay_alarm` TINYINT(1) DEFAULT NULL
COMMENT '继电器-报警状态0=正常1=报警)' AFTER `relay_power`,
ADD COLUMN `do_soft_start` TINYINT(1) DEFAULT NULL
COMMENT 'DO-软启动0=关1=开)' AFTER `relay_alarm`,
ADD COLUMN `do_stop` TINYINT(1) DEFAULT NULL
COMMENT 'DO-停止0=关1=开)' AFTER `do_soft_start`,
ADD COLUMN `slave_data1` INT DEFAULT NULL
COMMENT '485从机数据1' AFTER `do_stop`,
ADD COLUMN `slave_data2` INT DEFAULT NULL
COMMENT '485从机数据2' AFTER `slave_data1`,
ADD COLUMN `touchscreen_data` INT DEFAULT NULL
COMMENT '触摸屏数据' AFTER `slave_data2`;
```
### 3.3 动态字段配置扩展 (device_field_header_config)
**修改字段枚举**:
```sql
ALTER TABLE `device_field_header_config`
MODIFY COLUMN `field_key` ENUM(
'analog1','analog2','analog3','analog4','analog5','analog6','analog7',
'digital1','digital2','digital3','digital4','digital5','digital6',
'current1','current2','quality1','quality2','humidity',
'frequency1','frequency2','slave_data1','slave_data2','touchscreen_data'
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL
COMMENT '字段标识支持8ADPRO和8Multi协议字段';
```
### 3.4 计数器字段设计(避免触发器冲突)
**问题**:现有触发器`update_counter_totals_on_insert`处理8ADPRO的`counter1_total`/`counter2_total`字段。
**解决方案**为8Multi添加独立的计数器总计字段不修改触发器
**字段对比**
| 协议 | 当前值字段 | 增量值字段 | 总计字段 | 触发器 |
|------|-----------|-----------|---------|--------|
| 8ADPRO | counter1/counter2 | - | counter1_total/counter2_total | ✅ 使用 |
| 8Multi | counter1_current/counter2_current | counter1_delta/counter2_delta | counter1_total_8multi/counter2_total_8multi | ❌ 不使用 |
**8Multi计数器逻辑**(修订版):
```java
// 1. 获取当前累计值(下位机传值)
int currentCounter1 = parseInt(parts[10]);
// 2. 获取清零基准值baseline
long baseline1 = device.getCounter1Baseline() != null ? device.getCounter1Baseline() : 0L;
// 3. 计算累计值(下位机当前值 - 清零基准值)
long total1 = (long)currentCounter1 - baseline1;
if (total1 < 0) total1 = 0L; // 负值保护
// 4. 计算增量值(当前值 - 上次值)
int delta1 = currentCounter1 - lastCounter1;
if (delta1 < 0) delta1 = 0; // 负值归零(下位机可能重置)
// 5. 存储
deviceData.setCounter1Current(currentCounter1); // 下位机当前累计值
deviceData.setCounter1Delta(delta1); // 本次增量
deviceData.setCounter1Total8multi(total1); // 累计总值(= 下位机值 - baseline
```
**关键设计说明**
- **累计值 = 下位机当前值 - baseline**(不是累加增量!)
- **第一条数据**:累计值 = 下位机当前值因为baseline默认为0
- **清零功能**设置baseline = 当前下位机值累计值重新从0开始
- **优点**:即使下位机重置归零,累计值仍能正确计算
**优点**
- ✅ 不修改现有触发器8ADPRO功能完全不受影响
- ✅ 8Multi使用独立字段逻辑清晰
- ✅ 两种协议完全隔离,互不干扰
## 四、数据解析规则
### 4.1 量程映射转换
**电流/质量原始值转换公式**:
```
实际值 = (原始值 / 65535.0) * 量程
```
**示例**:
- 电流1量程设置为100A
- 接收到原始值32768
- 实际电流1 = (32768 / 65535.0) * 100 = 50.0A
### 4.2 复合状态位解析
#### 4.2.1 DI状态字段解析工作/停机/故障/清零)
```
字段值: U16格式例如 0x004F
解析规则: 取最后一个F低4位然后取反
```
**解析代码示例**:
```java
int diValue = parseInt(parts[8]); // 例如 0x004F
int lastNibble = diValue & 0x0F; // 取最后4位 = 0xF = 15
int inverted = (~lastNibble) & 0x0F; // 取反并保留4位 = 0x0 = 0
// 解析各个DI状态
boolean work = (inverted & 0x01) != 0; // DI1: 工作
boolean stop = (inverted & 0x02) != 0; // DI2: 停机
boolean fault = (inverted & 0x04) != 0; // DI3: 故障
boolean reset = (inverted & 0x08) != 0; // DI4: 清零
```
#### 4.2.2 继电器+DO状态字段解析供电/报警/软启动/停止)
```
字段值: U16格式例如 0x004F
解析规则: 取最后一个F低4位然后取反与DI状态相同
下发格式: DO=0_10为通道号1为值
```
**解析代码示例**:
```java
int doValue = parseInt(parts[12]); // 例如 0x004F
int lastNibble = doValue & 0x0F; // 取最后4位 = 0xF = 15
int inverted = (~lastNibble) & 0x0F; // 取反并保留4位 = 0x0 = 0
// 解析各个继电器和DO状态
boolean power = (inverted & 0x01) != 0; // 继电器1: 供电
boolean alarm = (inverted & 0x02) != 0; // 继电器2: 报警
boolean softStart = (inverted & 0x04) != 0; // DO1: 软启动
boolean stopDo = (inverted & 0x08) != 0; // DO2: 停止
```
**下发控制示例**:
```
DO=0_1 // 通道0供电值为1开启
DO=1_0 // 通道1报警值为0关闭
```
### 4.3 计数器处理逻辑(修订版)
**核心设计原则**:
- 下位机传输的是累计值
- 累计值 = 下位机当前值 - 清零基准值baseline
- 增量值 = 当前值 - 上次值
**完整处理流程**:
```java
// 1. 获取下位机传来的当前累计值
int counter1Current = parseInt(parts[10]); // 计数器1
int counter2Current = parseInt(parts[12]); // 计数器2
// 2. 获取清零基准值(用于计算累计值)
Long baseline1 = device.getCounter1Baseline() != null ? device.getCounter1Baseline() : 0L;
Long baseline2 = device.getCounter2Baseline() != null ? device.getCounter2Baseline() : 0L;
// 3. 计算累计值(显示在前端卡片上的大数字)
Long total1 = (long)counter1Current - baseline1;
Long total2 = (long)counter2Current - baseline2;
if (total1 < 0) total1 = 0L; // 负值保护
if (total2 < 0) total2 = 0L;
// 4. 查询上次累计值(用于计算增量)
DeviceData lastData = selectLastByDeviceId(deviceId);
Integer lastCounter1 = lastData != null ? lastData.getCounter1Current() : 0;
Integer lastCounter2 = lastData != null ? lastData.getCounter2Current() : 0;
// 5. 计算增量值(显示在前端卡片上的小字)
int delta1 = counter1Current - lastCounter1;
int delta2 = counter2Current - lastCounter2;
if (delta1 < 0) delta1 = 0; // 负值归零(下位机可能重置)
if (delta2 < 0) delta2 = 0;
// 6. 存储到数据库
deviceData.setCounter1Current(counter1Current); // 下位机原始值
deviceData.setCounter1Delta(delta1); // 增量
deviceData.setCounter1Total8multi(total1); // 累计值
deviceData.setCounter2Current(counter2Current);
deviceData.setCounter2Delta(delta2);
deviceData.setCounter2Total8multi(total2);
```
**清零功能实现**:
- 前端提供清零按钮
- 点击清零:`UPDATE device SET counter1_baseline = 当前下位机值 WHERE id = ?`
- 清零后:累计值 = 下位机值 - baseline重新从0开始
- 下位机继续传累计值,不受影响
**数据库字段**:
```sql
ALTER TABLE `device`
ADD COLUMN `counter1_baseline` INT DEFAULT 0 COMMENT '计数1基准值清零后的基准',
ADD COLUMN `counter2_baseline` INT DEFAULT 0 COMMENT '计数2基准值清零后的基准',
ADD COLUMN `voltage1` DECIMAL(10,3) DEFAULT NULL COMMENT '电压1配置V用于功率1计算',
ADD COLUMN `voltage2` DECIMAL(10,3) DEFAULT NULL COMMENT '电压2配置V用于功率2计算';
```
**示例场景**:
```
时间线:
下位机值: 100 → 150 → 200 → [清零,baseline=200] → 250 → 300
累计值: 100 → 150 → 200 → 0 → 50 → 100
增量值: 0 → 50 → 50 → 0 → 50 → 50
```
## 五、接口设计方案
### 5.1 数据上报接口(复用现有)
**接口路径**: `POST /equipment/info/ingest/raw`
**请求示例**:
```
+YAV:1001,32768,16384,49152,8192,25.5,65.0,15,5000,100,3000,50,3,1000,2000,0,EEFF
```
**字段映射**:
1. 帧头: +YAV固定
2. id: 1001设备ID对应g_tDevParam.Address
3. 电流1原始值: 32768AI10-65535对应usSRegHoldBuf[0]
4. 电流2原始值: 16384AI20-65535对应usSRegHoldBuf[1]
5. 质量1原始值: 49152AI30-65535对应usSRegHoldBuf[2]
6. 质量2原始值: 8192AI40-65535对应usSRegHoldBuf[3]
7. 温度: 25.5°C保留1位小数
8. 湿度: 65.0%RH保留1位小数
9. DI状态复合: 15工作/停机/故障/清零对应usSRegHoldBuf[DI_REG]
10. 频率1: 5000Hz对应freq1
11. 计数1: 100对应num_count1
12. 频率2: 3000Hz对应freq2
13. 计数2: 50对应num_count2
14. 继电器+DO状态复合: 3供电/报警/软启动/停止对应usSRegHoldBuf[DO_REG]
15. 从机数据1: 1000对应usSRegHoldBuf[DATA_REG_1]
16. 从机数据2: 2000对应usSRegHoldBuf[DATA_REG_1+1]
17. 触摸屏: 0固定发送0
18. 帧尾: EEFF固定
### 5.2 设备配置接口(扩展现有)
**新增量程配置接口**: `PUT /equipment/info/device/range`
**请求参数**:
```json
{
"id": 1,
"current1_range": 100.0,
"current2_range": 50.0,
"quality1_range": 1000.0,
"quality2_range": 500.0,
"calibration_offset": 0.5,
"calibration_factor": 1.02
}
```
**响应**:
```json
{
"code": 200,
"msg": "量程配置成功"
}
```
### 5.3 设备创建接口(扩展现有)
**接口路径**: `POST /equipment/info/device`
**请求参数扩展**:
```json
{
"deviceNo": 1001,
"deviceName": "8Multi设备1",
"protocol_type": "8MULTI",
"current1_range": 100.0,
"current2_range": 50.0,
"quality1_range": 1000.0,
"quality2_range": 500.0,
"image_url": "/profile/upload/device.png",
"headers": {
"current1": "主电流",
"current2": "辅助电流",
"quality1": "主质量",
"quality2": "辅助质量",
"humidity": "环境湿度",
"frequency1": "主频率",
"frequency2": "辅助频率"
}
}
```
## 六、前端界面扩展
### 6.1 设备卡片展示
**8ADPRO设备卡片** (保持不变):
- 温度、电流、功率、状态
- 计数器1、计数器2
**8Multi设备卡片** (新增):
- 温度、湿度
- 电流1、电流2
- 质量1、质量2
- 工作/停机/故障状态指示灯
- 频率1、频率2
- 计数器1、计数器2
### 6.2 设备详情抽屉
**新增8Multi专属字段**:
- 电流1/电流2显示原始值和实际值
- 质量1/质量2显示原始值和实际值
- 湿度
- 工作状态、停机状态、故障状态、清零状态
- 频率1、频率2
- 继电器状态(供电、报警)
- DO状态软启动、停止
- 从机数据1、从机数据2
- 触摸屏数据
### 6.3 设备配置对话框
**新增配置项**:
1. **协议类型选择**: 8ADPRO / 8Multi
2. **量程配置** (仅8Multi显示):
- 电流1量程 (A)
- 电流2量程 (A)
- 质量1量程 (kg)
- 质量2量程 (kg)
3. **校准配置**:
- 校准偏移量
- 校准系数
## 七、实现步骤
### 7.1 数据库变更
1. ✅ 执行device表ALTER语句新增协议类型和量程字段
2. ✅ 执行device_data表ALTER语句新增8Multi专属字段
3. ✅ 修改device_field_header_config表枚举支持新字段
### 7.2 后端开发(已完成)
#### 7.2.1 实体类扩展
1.**MesDevice.java** - 添加17个8Multi字段
```java
// 协议类型、量程起点/终点、单位、计数器基准值、电压1/电压2
private String protocolType;
private BigDecimal current1RangeStart, current1RangeEnd;
private BigDecimal current2RangeStart, current2RangeEnd;
private BigDecimal quality1RangeStart, quality1RangeEnd;
private String quality1Unit, quality2Unit, currentUnit;
private Integer counter1Baseline, counter2Baseline;
private BigDecimal voltage1, voltage2;
// 辅助方法
public boolean is8Multi() { return "8MULTI".equals(this.protocolType); }
public boolean is8ADPRO() { return "8ADPRO".equals(this.protocolType) || this.protocolType == null; }
```
2. ✅ **DeviceData.java** - 添加29个8Multi字段
```java
// 电流/质量原始值和实际值、湿度、状态位、频率、计数器、继电器、DO、从机数据
private Integer current1Raw, current2Raw;
private BigDecimal current1Value, current2Value;
private Integer quality1Raw, quality2Raw;
private BigDecimal quality1Value, quality2Value;
private BigDecimal humidity;
private Integer statusWork, statusStop, statusFault, statusReset;
private Integer frequency1, frequency2;
private Integer counter1Current, counter1Delta, counter2Current, counter2Delta;
private Long counter1Total8multi, counter2Total8multi;
private Integer relayPower, relayAlarm, doSoftStart, doStop;
private Integer slaveData1, slaveData2, touchscreenData;
```
#### 7.2.2 Service层
3. ✅ **Multi8ProtocolService.java** - 8Multi协议解析服务
- `parse8MultiProtocol()` - 协议解析方法
- `convertRangeValue()` - 量程转换方法
- `resetCounter()` - 计数器清零方法
#### 7.2.3 Controller层
4. ✅ **Multi8Controller.java** - 8Multi设备控制器
- `POST /equipment/multi8/resetCounter` - 计数器清零
- `PUT /equipment/multi8/updateDeviceConfig` - 更新设备配置
- `GET /equipment/multi8/getDeviceDetail/{id}` - 获取设备详情
#### 7.2.4 Mapper层
5. ✅ **DeviceDataMapper.java** - 添加方法
```java
DeviceData selectLastByDeviceId(@Param("deviceId") Long deviceId);
```
6. ✅ **MesDeviceMapper.java** - 添加方法
```java
MesDevice selectByDeviceNo(@Param("deviceNo") Integer deviceNo);
```
### 7.3 前端开发(需手动实现)
**文件位置**`e:\Yavii_P3\MES\mes-ui\src\views\mes\equipment\info\index.vue`1178行
**修改原则**
⚠️ **在现有8ADPRO代码基础上扩展不删除任何8ADPRO功能**
⚠️ **通过`v-if`判断协议类型动态渲染不同UI**
⚠️ **所有8Multi字段都要做空值处理**
#### 7.3.1 设备卡片扩展
通过`v-if`判断协议类型动态渲染不同UI
```vue
<!-- 8ADPRO设备卡片保持不变 -->
<div v-if="!d.protocol_type || d.protocol_type === '8ADPRO'" class="device-info">
<!-- 原有字段 -->
</div>
<!-- 8Multi设备卡片新增 -->
<div v-else-if="d.protocol_type === '8MULTI'" class="device-info device-8multi">
<!-- 温度/湿度、电流1/电流2、质量1/质量2、功率1/功率2、状态 -->
</div>
```
#### 7.3.2 设备详情抽屉扩展
添加8Multi字段展示
- 温度、湿度
- 电流1/电流2、质量1/质量2
- 功率1/功率2计算显示
- 频率1/频率2
- 计数器1/计数器2含清零按钮
- 工作/停机/故障状态
- 继电器、DO状态
- 从机数据
#### 7.3.3 设备配置对话框扩展
根据`device_no > 200`动态显示8Multi配置项
- 电流1/电流2量程起点~终点)
- 质量1/质量2量程起点~终点+单位)
- 电流单位
- 电压1/电压2配置
#### 7.3.4 JavaScript方法扩展
```javascript
// 计算功率
calculatePower1(data) {
return data.voltage1 * data.current1_value
}
// 获取状态类型
getStatusType(data) {
if (data.status_fault) return 'danger' // 红色
if (data.status_stop) return 'warning' // 黄色
if (data.status_work) return 'success' // 绿色
}
// 计数器清零
resetCounter(counterNo) {
this.$http.post('/equipment/multi8/resetCounter', {...})
}
```
### 7.4 测试验证
1. 8ADPRO协议回归测试确保不受影响
2. 8Multi协议功能测试
3. 量程转换准确性测试
4. 校准功能测试
5. 混合设备场景测试
## 八、待确认事项
### 8.1 位运算解析规则
- [x] **DI状态字段**已确认U16格式取最后4位F然后取反DI1=工作DI2=停机DI3=故障DI4=清零
- [x] **继电器+DO字段**:已确认,直接读取不取反(下位机上传状态)
### 8.2 校准功能细节
- [x] **校准功能**:已确认不需要校准偏移量和校准系数
### 8.3 状态上传功能
- [x] **继电器+DO状态**:已确认为下位机上传的状态,不涉及下发控制功能
### 8.4 从机和触摸屏数据
- [x] **从机数据展示**已确认从机数据1/2直接展示原始值
- [x] **触摸屏数据**已确认固定发送0
### 8.5 协议格式细节
- [x] **设备ID映射关系**已确认device_no>200为8MULTI<=200为8ADPRO
- [x] **触摸屏固定值**已确认触摸屏字段固定发送0
- [x] **字段顺序确认**:已确认,协议字段顺序完全按照文档描述
### 8.6 量程和单位配置
- [x] **电流量程范围**已确认AI1/AI2电流范围为0-20A
- [x] **质量量程配置**已确认质量1/质量2AI3/AI4需要前端配置量程起点和终点
- [x] **量程转换公式**:已确认,实际值=(原始值/65535)*量程范围+量程起点
- [x] **量程必填**已确认8Multi设备创建时必须强制配置量程
- [x] **量程修改影响**:已确认,量程修改后不需要重新计算已存储的历史数据
- [x] **单位配置**:已确认,质量单位和电流单位由前端配置
### 8.7 计数器处理逻辑
- [x] **计数器数据类型**已确认下位机传的计数1和计数2是累计值
- [x] **计数器处理方式**:已确认,需要做减运算(当前值-上次值),存储累计值和增量值
- [x] **计数器清零**:已确认,前端提供清零按钮,清零后设置基准值,增量=当前值-基准值
- [x] **频率显示**已确认频率1和频率2显示原始值
### 8.8 数据上报和存储
- [x] **上报间隔**已确认8Multi设备上报频率为10秒
- [x] **数据去重**:已确认,短时间内收到相同数据全部存储
- [x] **离线缓存**:已确认,不需要关注离线缓存补发,由下位机处理
### 8.9 设备状态判断
- [x] **运行判断依据**已确认电流1实际值>=2A **且** 电流2实际值>=2A与运算
- [x] **功率计算规则**已确认需要计算功率前端配置电压1和电压2功率1=电压1*电流1功率2=电压2*电流2分别计算
- [x] **状态指示灯颜色**:已确认,工作=绿色,停机=黄色,故障=红色,报警=红色
### 8.10 前端展示规范
- [x] **原始值显示**:已确认,前端只显示实际值,不显示原始值
- [x] **历史数据图表**:已确认,先不做历史数据图表
### 8.11 数据有效性校验
- [x] **数据校验**:已确认,不需要数据有效性校验
### 8.12 权限和安全
- [x] **数据加密传输**:已确认,设备上报的数据不需要加密传输
### 8.13 兼容性和迁移
- [x] **协议切换**已确认同一设备不能在8ADPRO和8Multi之间切换协议
- [x] **数据迁移工具**:已确认,不需要提供数据迁移工具
- [x] **8ADPRO兼容性**已确认千万不要影响8ADPRO的功能
### 待确认事项汇总
**已确认事项**共34项
- ✅ 所有核心功能已确认
- ✅ 协议解析规则已明确
- ✅ 数据库设计已完成
- ✅ 量程配置规则已确定
- ✅ 计数器处理逻辑已明确
- ✅ 兼容性要求已明确
- ✅ 功率计算规则已明确(分别计算)
- ✅ 前端展示规范已明确(只显示实际值)
- ✅ 数据安全要求已明确(不需要加密)
- ✅ 离线缓存策略已明确(不需要关注)
**待确认事项**共0项
- 🎉 所有需求已确认完毕!
**核心要求**
- ⚠️ **千万不要影响8ADPRO的功能**
- ⚠️ **device_no>200为8MULTI<=200为8ADPRO**
- ⚠️ **同一设备不能在两种协议之间切换**
## 九、8ADPRO设备相关文件清单
**说明**以下仅列出与8ADPRO设备数据接收、解析、存储、展示相关的核心文件不包括其他IoT功能如点检、维修、车间设备等
### 9.1 后端文件Java
#### 9.1.1 Controller层
```
e:\Yavii_P3\MES\yjh-mes\src\main\java\cn\sourceplan\equipment\controller\
└── EquipmentInfoController.java # 设备信息控制器(核心:数据接收、动态表头、清零)
- ingestRawData() # 数据接收接口
- getDynamicHeaders() # 动态表头接口
- getDeviceDataPage() # 设备数据查询接口
- resetCounter() # 计数器清零接口
```
#### 9.1.2 Service层
```
e:\Yavii_P3\MES\yjh-mes\src\main\java\cn\sourceplan\equipment\service\
├── IEquipmentInfoService.java # 设备信息服务接口
└── impl\
└── EquipmentInfoServiceImpl.java # 设备信息服务实现(核心:协议解析、数据存储)
- ingestFrame() # 协议解析方法需扩展支持8Multi
- parseProtocolData() # 数据解析逻辑
- saveDeviceData() # 数据存储逻辑
- updateCounter() # 计数器累加逻辑
- mergeDynamicHeaders() # 动态表头合并逻辑
```
#### 9.1.3 Domain层实体类
```
e:\Yavii_P3\MES\yjh-mes\src\main\java\cn\sourceplan\equipment\domain\
├── MesDevice.java # 设备主表实体需添加8Multi协议字段
├── DeviceData.java # 设备数据实体需添加8Multi数据字段
├── DeviceLatest.java # 设备最新数据实体
├── DeviceLatestView.java # 设备最新数据视图实体
└── DeviceFieldHeaderConfig.java # 设备字段表头配置实体
```
#### 9.1.4 Mapper层
```
e:\Yavii_P3\MES\yjh-mes\src\main\java\cn\sourceplan\equipment\mapper\
├── MesDeviceMapper.java # 设备主表Mapper
├── DeviceDataMapper.java # 设备数据Mapper
├── DeviceLatestMapper.java # 设备最新数据Mapper
├── DeviceLatestViewMapper.java # 设备最新数据视图Mapper
├── DeviceLatestJoinMapper.java # 设备最新数据关联Mapper
└── DeviceFieldHeaderConfigMapper.java # 设备字段表头配置Mapper
```
#### 9.1.5 Mapper XML文件
```
e:\Yavii_P3\MES\yjh-mes\src\main\resources\mapper\equipment\
└── (对应Mapper的XML文件需要添加8Multi字段的SQL映射)
```
### 9.2 前端文件Vue
```
e:\Yavii_P3\MES\mes-ui\src\views\mes\equipment\info\
└── index.vue # 设备信息页面(核心:实时数据监控、动态表头、清零)
- 设备卡片展示需扩展8Multi设备卡片
- 设备详情抽屉需添加8Multi字段
- 设备配置对话框(需添加量程、单位、电压配置)
- 动态表头渲染
- 实时数据刷新
- 计数器清零按钮
- 功率计算显示
```
### 9.3 核心文件修改要点
**8Multi接入需要修改的5个核心文件**
1. **EquipmentInfoController.java**
- 数据接收接口保持不变
- 可能需要添加设备配置更新接口(量程、单位、电压)
2. **EquipmentInfoServiceImpl.java**
- `ingestFrame()`方法添加协议类型判断device_no>200为8Multi
- 新增8Multi协议解析分支
- 实现量程转换逻辑
- 实现计数器减运算逻辑
- 实现功率计算逻辑
3. **MesDevice.java**
- 添加17个新字段协议类型、量程起点/终点、单位、计数器基准值、电压1/电压2
4. **DeviceData.java**
- 添加29个新字段8Multi协议数据字段含counter1_total_8multi/counter2_total_8multi
5. **info/index.vue**
- 根据协议类型渲染不同的设备卡片
- 设备详情抽屉支持8Multi字段展示
- 设备配置对话框添加量程、单位、电压1/电压2配置
- 功率计算显示功率1=电压1*电流1功率2=电压2*电流2
### 9.4 数据库表
**核心表**
- `device`设备主表新增17个字段
- `device_data`设备数据表新增29个字段含8Multi专用计数器总计字段
- `device_latest`:设备最新数据表(视图或实体表)
- `device_field_header_config`:设备字段表头配置表
## 十、兼容性保证
### 10.1 数据库兼容
- 所有新增字段均为可空或有默认值
- 原有8ADPRO设备的protocol_type默认为'8ADPRO'
- 原有字段不做任何修改
### 10.2 代码兼容
- 通过protocol_type字段区分处理逻辑
- 8ADPRO协议解析逻辑完全保留
- 新增8Multi协议解析分支
### 10.3 前端兼容
- 根据设备协议类型动态渲染UI
- 8ADPRO设备展示保持不变
- 新增8Multi设备专属UI组件
## 十一、附录
### 11.1 8Multi协议字段详细对照表
| 序号 | 字段名 | 描述 | 数据类型 | 对应C变量 | 转换规则 |
|------|--------|------|---------|-----------|----------|
| 1 | +YAV: | 帧头 | String | 固定 | 固定发送+YAV: |
| 2 | id | 设备ID | int | g_tDevParam.Address | 设备唯一标识 |
| 3 | 电流1 | 模拟输入AI1 | int(0-65535) | usSRegHoldBuf[0] | 范围0-20A<br>实际值=(原始值/65535)*量程范围+量程起点<br>实际值>=2A为设备开启 |
| 4 | 电流2 | 模拟输入AI2 | int(0-65535) | usSRegHoldBuf[1] | 范围0-20A<br>实际值=(原始值/65535)*量程范围+量程起点 |
| 5 | 质量1 | 模拟输入AI3 | int(0-65535) | usSRegHoldBuf[2] | 实际值=(原始值/65535)*量程范围+量程起点<br>前端配置量程 |
| 6 | 质量2 | 模拟输入AI4 | int(0-65535) | usSRegHoldBuf[3] | 实际值=(原始值/65535)*量程范围+量程起点<br>前端配置量程 |
| 7 | 温度 | 温湿度传感器温度 | String | temp | 保留1位小数 |
| 8 | 湿度 | 温湿度传感器湿度 | String | dht | 保留1位小数 |
| 9 | DI状态 | 工作/停机/故障/清零 | int | usSRegHoldBuf[DI_REG] | U16格式取最后4位F然后取反<br>DI1=工作DI2=停机DI3=故障DI4=清零 |
| 10 | 频率1 | 测频器1 | String | freq1 | 实际值 |
| 11 | 计数1 | 计数器1 | int | num_count1 | 下位机传累计值 |
| 12 | 频率2 | 测频器2 | String | freq2 | 实际值 |
| 13 | 计数2 | 计数器2 | int | num_count2 | 下位机传累计值 |
| 14 | 继电器+DO | 供电/报警/软启动/停止 | int | usSRegHoldBuf[DO_REG] | 直接读取,不取反<br>下发格式: DO=0_10为通道号1为值 |
| 15 | 从机数据1 | 485从机数据1 | int | usSRegHoldBuf[DATA_REG_1] | 实际值 |
| 16 | 从机数据2 | 485从机数据2 | int | usSRegHoldBuf[DATA_REG_1+1] | 实际值 |
| 17 | 触摸屏 | 触摸屏数据 | int | 固定 | 固定发送0 |
| 18 | EEFF | 帧尾 | String | 固定 | 固定发送EEFF |
### 11.2 量程配置示例
| 设备类型 | 电流1量程 | 电流2量程 | 质量1量程 | 质量2量程 |
|---------|----------|----------|----------|----------|
| 小型设备 | 50A | 30A | 500kg | 200kg |
| 中型设备 | 100A | 80A | 1000kg | 500kg |
| 大型设备 | 200A | 150A | 2000kg | 1000kg |
---
**文档版本**: v2.1
**创建日期**: 2025-11-15
**创建人**: 周启威
**最后更新**: 2025-11-15
**更新内容**:
- v1.1: 补充待确认事项8.5-8.13,新增优先级分类
- v1.2: 根据协议详细表更新字段对照表明确C变量对应关系更新已确认事项
- v1.3: 重大更新
- 修改数据库设计:量程字段拆分为起点和终点(支持非零起点)
- 更新量程转换公式:实际值=(原始值/65535)*量程范围+量程起点
- 明确DI状态解析U16格式取最后4位F然后取反
- 明确继电器+DO解析直接读取不取反下发格式DO=通道号_值
- 明确设备开启判断电流1实际值>=2A
- 明确计数器类型:下位机传累计值
- 更新协议字段对照表,添加详细转换规则
- v2.0: 完整需求确认版本30项已确认
- 移除校准功能:不需要校准偏移量和校准系数
- 添加单位配置:质量单位和电流单位前端配置
- 协议判断规则device_no>200为8MULTI<=200为8ADPRO
- 设备状态判断电流1>=2A且电流2>=2A与运算
- 计数器处理:减运算+清零功能(存储累计值和增量值)
- 频率显示:显示原始值
- 从机数据:直接展示原始值
- 数据上报10秒间隔相同数据全部存储
- 权限管理:量程配置和下发控制需要权限
- 状态指示灯:工作=绿,停机=黄,故障=红,报警=红
- 兼容性不能切换协议不影响8ADPRO功能
- 新增计数器基准值字段和单位字段
- v2.1: 功能调整
- 移除下发控制功能继电器和DO字段改为下位机上传状态不涉及下发控制
- 移除权限管理相关需求
- 新增8ADPRO现有文件清单章节第九章
- 列出所有后端Java文件Controller、Service、Domain、Mapper、XML
- 列出所有前端Vue文件
- 标注核心文件和修改要点
- 新增功率计算功能前端配置电压1和电压2功率1=电压1*电流1功率2=电压2*电流2分别计算
- device表新增voltage1和voltage2字段共17个新字段
- 精简文件清单只列出8ADPRO设备相关文件不包括其他IoT功能
- 确认所有待确认事项:离线缓存、原始值显示、数据加密传输
- 🎉 所有34项需求已确认完毕可以开始开发
- v2.2: 代码实现完成
- ✅ 完成所有后端代码实体类、Service、Controller、Mapper
- ✅ 添加8Multi协议解析服务
- ✅ 添加设备控制器接口
- ✅ 扩展Mapper查询方法
- 📝 前端代码待实现(已提供详细说明)
## 十三、代码文件清单
### 后端文件
#### 1. 实体类2个已完成
- ✅ `MesDevice.java` - 添加17个8Multi字段
- ✅ `DeviceData.java` - 添加29个8Multi字段
#### 2. Service层1个新建 + 1个需集成
- ✅ `Multi8ProtocolService.java` - 8Multi协议解析服务新建
- ⚠️ `EquipmentInfoServiceImpl.java` - 需要集成8Multi协议判断
- 参考文档:`后端集成说明_EquipmentInfoServiceImpl.md`
- 修改点:
1. 注入Multi8ProtocolService
2. 在ingestFrame方法开头添加协议判断
3. deviceNo > 200 调用8Multi解析
#### 3. Controller层1个新建
- ✅ `Multi8Controller.java` - 8Multi设备控制器新建
#### 4. Mapper层2个已扩展
- ✅ `DeviceDataMapper.java` - 添加`selectLastByDeviceId()`方法
- ✅ `MesDeviceMapper.java` - 添加`selectByDeviceNo()`方法
#### 5. Mapper XML
- ✅ 不需要XML文件使用MyBatis-Plus注解方式
### 前端文件(待实现)
#### 1. 设备信息页面1个扩展
- `e:\Yavii_P3\MES\mes-ui\src\views\mes\equipment\info\index.vue`
- 扩展设备卡片(协议类型判断)
- 扩展详情抽屉8Multi字段展示
- 扩展配置对话框(量程、单位、电压配置)
- 添加JavaScript方法功率计算、状态判断、计数器清零
### 数据库文件
#### 1. SQL脚本1个
- `e:\Yavii_P3\MES\.tasks\2025-11-15_01_周启威_8Multi接入.sql`
- device表新增17个字段
- device_data表新增29个字段
- 不修改触发器(使用独立字段)
## 十四、后续工作
### 待完成
1. 前端代码实现参考第7.3节)
2. 集成测试
3. 8ADPRO回归测试
### 注意事项
⚠️ **千万不要影响8ADPRO的功能**
⚠️ **所有修改都要通过协议类型判断**
⚠️ **前端要在现有代码基础上扩展不要删除任何8ADPRO代码**

View File

@@ -0,0 +1,918 @@
# 连续制造业流程优化技术方案
**文档版本**: V2.0
**创建日期**: 2025-11-17
**更新日期**: 2025-11-17
**负责人**: 周启威
**审核状态**: 待审核
---
## 📋 需求概述
### 背景
当前系统中连续制造业和离散制造业在工序路线管理上存在以下问题:
1. 工序时间计算方式单一,不能满足连续生产的同步开工需求
2. 离散制造的转运时间没有明确字段,导致时间计算不够精确
3. 连续生产未绑定工序路线,缺乏标准化流程管理
4. 销售订单可混合不同制造类型的物料,导致生产混乱
5. 工单显示信息不完整,连续制造工序信息缺失
### 优化目标
1. **工序路线制造类型标识**:在工序路线上明确标识制造类型(离散/连续)
2. **时间字段差异化**:离散制造使用转运时间,连续制造使用等待开始时间
3. **连续生产标准化**:连续生产必须绑定工序路线,统一流程管理
4. **物料与工序路线匹配**:物料制造类型与工序路线制造类型必须一致
5. **销售订单校验**:禁止在同一销售订单中混合不同制造类型的物料
6. **工单信息完善**:连续制造工单显示完整的工序信息
### 核心原则
⚠️ **重要约束**
- ✅ 不修改主流程逻辑
- ✅ 不影响现有离散制造功能
- ✅ 向下兼容,支持数据迁移
- ✅ 最小化代码侵入性
---
## 🗂️ 数据库设计
### 1. 工序路线主表扩展 (pro_route)
**新增字段**
| 字段名 | 类型 | 长度 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `manufacture_type` | VARCHAR | 20 | 'DISCRETE' | 制造类型 |
**字段值说明**
#### manufacture_type制造类型
- **DISCRETE**(离散制造,默认值)
- 工序按**顺序执行**(顺序推进模式)
- 工序1结束时间 = 开始时间 + 工序1持续时间
- 工序2开始时间 = 工序1结束时间 + **转运时间**
- 工序2结束时间 = 工序2开始时间 + 工序2持续时间
- 使用 `transfer_time` 字段
- **CONTINUOUS**(连续制造)
- 所有工序**同时开始**(同步开工模式)
- 工序开始时间 = 生产起始时间 + **等待开始时间**
- 工序结束时间 = 工序开始时间 + 工序持续时间
- 使用 `wait_start_time` 字段
**设计理念**
-**制造类型直接决定时间计算模式**,无需额外字段
- ✅ 离散制造 = 顺序推进,连续制造 = 同步开工
- ✅ 简化配置,减少理解成本
**SQL DDL**
```sql
ALTER TABLE `pro_route`
ADD COLUMN `manufacture_type` VARCHAR(20) DEFAULT 'DISCRETE'
COMMENT '制造类型: DISCRETE=离散制造(顺序推进), CONTINUOUS=连续制造(同步开工)'
AFTER `status`;
```
---
### 2. 工序路线明细表扩展 (pro_route_process)
**新增字段**
| 字段名 | 类型 | 长度 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `transfer_time` | BIGINT | - | 0 | 转运时间(秒,离散制造) |
| `wait_start_time` | BIGINT | - | 0 | 等待开始时间(秒,连续制造) |
**字段说明**
#### transfer_time转运时间 - 离散制造专用)
- **适用场景**离散制造DISCRETE
- **含义**:前一道工序完成后,转运到当前工序所需的时间
- **计算公式**:下一工序开始时间 = 当前工序结束时间 + 转运时间
- **连续制造**:此字段忽略不计
#### wait_start_time等待开始时间 - 连续制造专用)
- **适用场景**连续制造CONTINUOUS
- **含义**:从生产起始时间开始,等待多久后该工序才开始
- **计算公式**:工序开始时间 = 生产起始时间 + 等待开始时间
- **离散制造**:此字段忽略不计
- **第一道工序**:通常为 0立即开始
**SQL DDL**
```sql
ALTER TABLE `pro_route_process`
ADD COLUMN `transfer_time` BIGINT DEFAULT 0
COMMENT '转运时间(秒),离散制造时从上一工序到本工序的转运耗时'
AFTER `duration`,
ADD COLUMN `wait_start_time` BIGINT DEFAULT 0
COMMENT '等待开始时间(秒),连续制造时从生产起始时间的延迟'
AFTER `transfer_time`;
```
---
### 3. 数据迁移脚本
**目的**:确保现有数据兼容新字段
```sql
-- 为现有工序路线设置默认值(离散制造)
UPDATE `pro_route`
SET `manufacture_type` = 'DISCRETE'
WHERE `manufacture_type` IS NULL;
-- 为现有工序明细设置默认转运时间和等待开始时间为0
UPDATE `pro_route_process`
SET `transfer_time` = 0,
`wait_start_time` = 0
WHERE `transfer_time` IS NULL
OR `wait_start_time` IS NULL;
```
---
## 💻 Java 代码设计
### 1. 实体类扩展
#### Route.java 扩展
```java
/**
* 制造类型
* DISCRETE: 离散制造(顺序推进,默认)
* CONTINUOUS: 连续制造(同步开工)
*/
@Excel(name = "制造类型")
private String manufactureType;
/**
* 判断是否为离散制造
*/
@TableField(exist = false)
public boolean isDiscrete() {
return "DISCRETE".equals(this.manufactureType) || this.manufactureType == null;
}
/**
* 判断是否为连续制造
*/
@TableField(exist = false)
public boolean isContinuous() {
return "CONTINUOUS".equals(this.manufactureType);
}
```
#### RouteProcess.java 扩展
```java
/**
* 转运时间(单位:秒)
* 仅在离散制造下生效
*/
@Excel(name = "转运时间(秒)")
private Long transferTime;
/**
* 等待开始时间(单位:秒)
* 仅在连续制造下生效
*/
@Excel(name = "等待开始时间(秒)")
private Long waitStartTime;
/**
* 获取转运时间如果为null则返回0
*/
@TableField(exist = false)
public Long getTransferTimeOrZero() {
return transferTime != null ? transferTime : 0L;
}
/**
* 获取等待开始时间如果为null则返回0
*/
@TableField(exist = false)
public Long getWaitStartTimeOrZero() {
return waitStartTime != null ? waitStartTime : 0L;
}
```
---
### 2. 时间计算工具类
**新增工具类**: `RouteTimeCalculator.java`
**位置**: `cn.sourceplan.production.util.RouteTimeCalculator`
**职责**: 封装工序路线的时间计算逻辑
**核心方法**
```java
package cn.sourceplan.production.util;
import cn.sourceplan.production.domain.Route;
import cn.sourceplan.production.domain.RouteProcess;
import java.util.Date;
import java.util.List;
/**
* 工序路线时间计算工具类
*
* @author 周启威
* @date 2025-11-17
*/
public class RouteTimeCalculator {
/**
* 计算工序的开始和结束时间
*
* @param route 工序路线
* @param processes 工序列表已按sort排序
* @param productionStartTime 生产起始时间
* @return 返回每个工序的时间信息列表
*/
public static List<ProcessTimeInfo> calculateProcessTimes(
Route route,
List<RouteProcess> processes,
Date productionStartTime) {
if (route.isContinuous()) {
// 连续制造:同步开工模式
return calculateContinuousTimes(processes, productionStartTime);
} else {
// 离散制造:顺序推进模式
return calculateDiscreteTimes(processes, productionStartTime);
}
}
/**
* 连续制造:同步开工模式(使用等待开始时间)
*/
private static List<ProcessTimeInfo> calculateContinuousTimes(
List<RouteProcess> processes,
Date startTime) {
List<ProcessTimeInfo> result = new ArrayList<>();
for (RouteProcess process : processes) {
ProcessTimeInfo info = new ProcessTimeInfo();
info.setProcess(process);
// 工序开始时间 = 生产起始时间 + 等待开始时间
long waitMs = process.getWaitStartTimeOrZero() * 1000;
Date processStartTime = new Date(startTime.getTime() + waitMs);
info.setStartTime(processStartTime);
// 结束时间 = 开始时间 + 持续时间
long durationMs = process.getDuration() != null ?
process.getDuration() * 1000 : 0;
info.setEndTime(new Date(processStartTime.getTime() + durationMs));
result.add(info);
}
return result;
}
/**
* 离散制造:顺序推进模式(使用转运时间)
*/
private static List<ProcessTimeInfo> calculateDiscreteTimes(
List<RouteProcess> processes,
Date startTime) {
List<ProcessTimeInfo> result = new ArrayList<>();
Date currentTime = startTime;
for (RouteProcess process : processes) {
ProcessTimeInfo info = new ProcessTimeInfo();
info.setProcess(process);
info.setStartTime(currentTime);
// 结束时间 = 开始时间 + 持续时间
long durationMs = process.getDuration() != null ?
process.getDuration() * 1000 : 0;
Date endTime = new Date(currentTime.getTime() + durationMs);
info.setEndTime(endTime);
// 下一工序开始时间 = 当前工序结束时间 + 转运时间
long transferMs = process.getTransferTimeOrZero() * 1000;
currentTime = new Date(endTime.getTime() + transferMs);
result.add(info);
}
return result;
}
/**
* 工序时间信息内部类
*/
public static class ProcessTimeInfo {
private RouteProcess process;
private Date startTime;
private Date endTime;
// getter/setter省略
}
}
```
---
### 3. AutoCompleteServiceImpl 改造方案
**改造位置**: `AutoCompleteServiceImpl.generateWorkOrders()` 方法
**改造原则**:
- ✅ 使用工具类封装时间计算逻辑
- ✅ 不修改现有离散制造逻辑
- ✅ 通过工序路线配置自动识别制造类型
**核心改造点**:
```java
// 原有代码第667-703行
// ========== 第一步:收集所有工序的持续时间,正向计算时间 ==========
List<ProcessTimeInfo> processTimeInfos = new ArrayList<>();
// ... 现有逻辑
// ====== 改造后代码 ======
// ========== 第一步:使用工具类计算工序时间 ==========
// 1. 查询工序路线信息(获取时间计算模式)
Route route = routeMapper.selectById(dto.getRouteId());
if (route == null) {
throw new RuntimeException("工序路线不存在");
}
// 2. 查询工序列表并排序
List<RouteProcess> routeProcesses = routeProcessMapper.selectList(
new QueryWrapper<RouteProcess>()
.eq("route_id", dto.getRouteId())
.orderByAsc("sort")
);
// 3. 解析生产开始时间
Date productionStartTime;
if (StringUtils.isNotBlank(dto.getProductionStartTime())) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
productionStartTime = sdf.parse(dto.getProductionStartTime());
} else {
productionStartTime = new Date();
}
// 4. 使用工具类计算时间(自动适配离散/连续模式)
List<RouteTimeCalculator.ProcessTimeInfo> calculatedTimes =
RouteTimeCalculator.calculateProcessTimes(route, routeProcesses, productionStartTime);
// 5. 构建 ProcessTimeInfo 列表(保持与现有代码兼容)
List<ProcessTimeInfo> processTimeInfos = new ArrayList<>();
for (int i = 0; i < dto.getProcessConfigs().size(); i++) {
ProcessConfigDTO config = dto.getProcessConfigs().get(i);
RouteTimeCalculator.ProcessTimeInfo calcInfo = calculatedTimes.get(i);
ProcessTimeInfo timeInfo = new ProcessTimeInfo();
timeInfo.config = config;
timeInfo.startTime = calcInfo.getStartTime();
timeInfo.duration = calcInfo.getProcess().getDuration();
processTimeInfos.add(timeInfo);
}
// 后续逻辑保持不变...
```
**改造说明**:
1. **兼容性**: 保持现有 `ProcessTimeInfo` 结构不变,只改造时间计算部分
2. **自动识别**: 通过工序路线的 `timeCalculationMode` 自动选择计算方式
3. **向下兼容**: 现有工序路线默认为 `SEQUENTIAL` 模式,行为不变
---
### 4. 连续生产改造方案
**改造位置**: `AutoCompleteServiceImpl.autoCompleteContinuous()` 方法
**改造内容**:
#### 4.1 允许连续生产绑定工序路线
```java
// 原代码第278行
workOrder.setRouteId(null); // 连续制造不需要工序路线
// 改造后
workOrder.setRouteId(dto.getRouteId()); // 连续制造也可绑定工序路线
```
#### 4.2 连续制造报工逻辑说明
**特点**:
- ✅ 连续制造只有**一个工单**(不分工序分录)
- ✅ 可以**多次报工**(针对同一个工单)
- ✅ 每次报工时间都是**最后一道工序结束时间**
**业务场景示例**:
```
销售订单: 5吨产品
工单: 1个连续制造工单绑定工序路线
报工记录:
- 早班报工: 3吨报工时间 = 生产开始时间 + 最后工序等待时间 + 最后工序持续时间
- 中班报工: 2吨报工时间 = 生产开始时间 + 最后工序等待时间 + 最后工序持续时间
```
**代码改造**:
```java
// 连续生产必须绑定工序路线
if (dto.getRouteId() == null) {
throw new RuntimeException("连续制造必须选择工序路线");
}
// 连续制造不创建工序分录,只创建工单
// 工单的 currentProcess 字段显示所有工序名称
Route route = routeMapper.selectById(dto.getRouteId());
if (route != null && route.isContinuous()) {
List<RouteProcess> processes = routeProcessMapper.selectList(
new QueryWrapper<RouteProcess>()
.eq("route_id", route.getId())
.orderByAsc("sort")
);
String processNames = processes.stream()
.map(RouteProcess::getProcessName)
.collect(Collectors.joining("-"));
workOrder.setCurrentProcess(processNames);
}
```
#### 4.3 报工时间计算(连续制造)
**关键修改**: 连续制造的报工时间需要改为最后一道工序的完成时间
```java
// 获取最后一道工序的时间信息
RouteProcess lastProcess = routeProcesses.get(routeProcesses.size() - 1);
// 报工时间 = 生产起始时间 + 最后工序的等待开始时间 + 最后工序的持续时间
Date productionStartTime = DateUtils.parseDate(dto.getProductionStartTime());
long waitMs = lastProcess.getWaitStartTimeOrZero() * 1000;
long durationMs = (lastProcess.getDuration() != null ? lastProcess.getDuration() : 0) * 1000;
Date reportTime = new Date(productionStartTime.getTime() + waitMs + durationMs);
report.setReportTime(reportTime);
```
---
### 5. 业务校验逻辑
#### 5.1 物料与工序路线制造类型匹配校验
**位置**: 一键完成功能、手动创建工单等涉及工序路线选择的地方
**校验逻辑**:
```java
/**
* 校验物料制造类型与工序路线是否匹配
*/
private void validateMaterialAndRoute(Material material, Route route) {
if (material == null || route == null) {
return;
}
String materialType = material.getManufactureType();
String routeType = route.getManufactureType();
// 如果物料是离散制造,只能选择离散制造的工序路线
if ("DISCRETE".equals(materialType) && !"DISCRETE".equals(routeType)) {
throw new RuntimeException("离散制造的物料只能选择离散制造的工序路线");
}
// 如果物料是连续制造,只能选择连续制造的工序路线
if ("CONTINUOUS".equals(materialType) && !"CONTINUOUS".equals(routeType)) {
throw new RuntimeException("连续制造的物料只能选择连续制造的工序路线");
}
}
```
**应用场景**:
- 一键完成销售订单时
- 手动创建生产工单时
- 修改工单的工序路线时
#### 5.2 销售订单混合制造类型校验
**位置**: 销售订单保存时
**校验逻辑**:
```java
/**
* 校验销售订单明细是否混合了不同制造类型的物料
*/
@Override
public void validateSaleOrderEntries(List<SalOrderEntry> entries) {
if (entries == null || entries.isEmpty()) {
return;
}
// 收集所有物料ID
List<Long> materialIds = entries.stream()
.map(SalOrderEntry::getMaterialId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (materialIds.isEmpty()) {
return;
}
// 批量查询物料
List<Material> materials = materialMapper.selectBatchIds(materialIds);
Map<Long, String> materialTypeMap = materials.stream()
.collect(Collectors.toMap(Material::getId,
m -> m.getManufactureType() != null ? m.getManufactureType() : "DISCRETE"));
// 检查是否存在多种制造类型
Set<String> types = entries.stream()
.map(entry -> materialTypeMap.get(entry.getMaterialId()))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (types.size() > 1) {
throw new RuntimeException("一个销售订单不能同时包含离散制造和连续制造的物料,请分开下单");
}
}
```
**触发时机**:
- 新建销售订单保存时
- 修改销售订单明细时
- 批量导入销售订单时
#### 5.3 连续制造必须绑定工序路线
**位置**: `AutoCompleteServiceImpl.autoCompleteContinuous()``WorkOrderServiceImpl`
**校验逻辑**:
```java
// 连续制造必须有工序路线
if ("CONTINUOUS".equals(material.getManufactureType()) && dto.getRouteId() == null) {
throw new RuntimeException("连续制造的物料必须选择工序路线");
}
```
---
### 6. 工单显示信息完善
#### 6.1 当前工序字段补充
**问题**: 连续制造工单的 `currentProcess` 字段为空
**解决方案**:
```java
// 在创建连续制造工单时设置当前工序
workOrder.setCurrentProcess("连续制造");
// 或者显示所有工序名称
if (route != null && route.isContinuous()) {
List<RouteProcess> processes = routeProcessMapper.selectList(
new QueryWrapper<RouteProcess>()
.eq("route_id", route.getId())
.orderByAsc("sort")
);
String processNames = processes.stream()
.map(RouteProcess::getProcessName)
.collect(Collectors.joining("-"));
workOrder.setCurrentProcess(processNames);
}
```
#### 6.2 工单详情页工序展示
**文件**: `mes-ui/src/views/mes/production/workOrder/detail.vue`
**改造内容**:
1. **字段名称修改**: "查看工序" → "工序"
2. **连续制造工序展示**:
```vue
<template>
<div v-if="workOrderDetail.manufactureType === 'CONTINUOUS'">
<!-- 连续制造显示所有工序-隔开 -->
<el-descriptions-item label="工序">
<span v-for="(process, index) in workOrderProcesses" :key="process.id">
{{ process.processName }}
<span v-if="index < workOrderProcesses.length - 1"> - </span>
</span>
</el-descriptions-item>
</div>
<div v-else>
<!-- 离散制造显示当前工序 -->
<el-descriptions-item label="工序">
{{ workOrderDetail.currentProcess }}
</el-descriptions-item>
</div>
</template>
<script>
export default {
data() {
return {
workOrderProcesses: [] // 工序列表
};
},
methods: {
async loadWorkOrderDetail(id) {
// 加载工单详情
const res = await getWorkOrder(id);
this.workOrderDetail = res.data;
// 如果是连续制造,加载工序列表
if (res.data.manufactureType === 'CONTINUOUS' && res.data.routeId) {
this.loadRouteProcesses(res.data.routeId);
}
},
async loadRouteProcesses(routeId) {
// 查询工序路线的所有工序
const res = await getRouteProcessList(routeId);
this.workOrderProcesses = res.data;
}
}
};
</script>
```
---
## 🔄 前端界面改造
### 1. 工序路线管理界面
**文件**: `mes-ui/src/views/mes/production/route/index.vue`
**新增表单字段**:
```vue
<!-- 制造类型选择 -->
<el-form-item label="制造类型" prop="manufactureType">
<el-select v-model="form.manufactureType" placeholder="请选择制造类型">
<el-option label="离散制造(顺序推进)" value="DISCRETE" />
<el-option label="连续制造(同步开工)" value="CONTINUOUS" />
</el-select>
<div class="el-form-item__tip">
<span v-if="form.manufactureType === 'DISCRETE'">
工序按顺序执行使用转运时间
</span>
<span v-else-if="form.manufactureType === 'CONTINUOUS'">
所有工序同时开始使用等待开始时间
</span>
</div>
</el-form-item>
```
---
### 2. 工序明细管理界面
**新增字段**: 转运时间 和 等待开始时间
```vue
<!-- 工序明细表格 -->
<el-table-column label="工序持续时间(秒)" prop="duration" width="150" />
<!-- 转运时间离散制造 -->
<el-table-column label="转运时间(秒)" prop="transferTime" width="130"
v-if="routeForm.manufactureType === 'DISCRETE'">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.transferTime"
:min="0"
placeholder="0" />
</template>
</el-table-column>
<!-- 等待开始时间连续制造 -->
<el-table-column label="等待开始时间(秒)" prop="waitStartTime" width="150"
v-if="routeForm.manufactureType === 'CONTINUOUS'">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.waitStartTime"
:min="0"
placeholder="0" />
<div class="el-form-item__tip">从生产开始延迟时间</div>
</template>
</el-table-column>
```
**动态显示逻辑**:
```javascript
computed: {
// 根据制造类型动态显示不同的时间字段
showTransferTime() {
return this.routeForm.manufactureType === 'DISCRETE';
},
showWaitStartTime() {
return this.routeForm.manufactureType === 'CONTINUOUS';
}
}
```
---
## 📊 业务流程图
### 离散制造(顺序推进)时间计算
```
生产开始时间: 2025-11-17 08:00:00
工序1: 下料
├─ 开始: 08:00:00
├─ 持续: 3600秒(1小时)
├─ 结束: 09:00:00
└─ 转运: 600秒(10分钟)
工序2: 焊接
├─ 开始: 09:10:00 (工序1结束 + 转运时间)
├─ 持续: 7200秒(2小时)
├─ 结束: 11:10:00
└─ 转运: 300秒(5分钟)
工序3: 喷漆
├─ 开始: 11:15:00 (工序2结束 + 转运时间)
├─ 持续: 10800秒(3小时)
└─ 结束: 14:15:00
```
### 连续制造(同步开工)时间计算
```
生产开始时间: 2025-11-17 08:00:00
工序1: 灌装
├─ 等待开始时间: 0秒
├─ 开始: 08:00:00 + 0秒 = 08:00:00
├─ 持续: 7200秒(2小时)
└─ 结束: 10:00:00
工序2: 封盖
├─ 等待开始时间: 0秒
├─ 开始: 08:00:00 + 0秒 = 08:00:00
├─ 持续: 3600秒(1小时)
└─ 结束: 09:00:00
工序3: 贴标
├─ 等待开始时间: 3600秒(1小时等待封盖完成)
├─ 开始: 08:00:00 + 1小时 = 09:00:00
├─ 持续: 5400秒(1.5小时)
└─ 结束: 10:30:00
一键完成报工时间: 10:30:00 (最后一道工序的结束时间)
说明:
1. 工序1和工序2同时开始等待时间为0
2. 工序3等待1小时后开始等待封盖完成
3. 连续制造不使用转运时间字段
```
---
## ✅ 测试验证方案
### 1. 单元测试
**测试类**: `RouteTimeCalculatorTest.java`
```java
@Test
public void testSequentialMode() {
// 测试顺序推进模式
// 验证: 工序2开始时间 = 工序1结束时间 + 转运时间
}
@Test
public void testSynchronizedMode() {
// 测试同步开工模式
// 验证: 所有工序开始时间相同
}
```
### 2. 集成测试
| 测试场景 | 测试点 | 预期结果 |
|---------|--------|---------|
| 离散制造创建工单 | 顺序推进 + 转运时间 | 工序时间正确计算 |
| 连续制造创建工单 | 同步开工 | 所有工序同时开始 |
| 数据迁移 | 现有工序路线 | 自动设置为SEQUENTIAL |
| 向下兼容 | 不设置时间模式 | 默认使用SEQUENTIAL |
### 3. 回归测试
- ✅ 现有离散制造流程不受影响
- ✅ 现有工单创建功能正常
- ✅ 现有报工流程正常
---
## 📝 实施计划
### 阶段一数据库改造1天
- [ ] 执行DDL脚本添加新字段
- [ ] 执行数据迁移脚本
- [ ] 验证现有数据完整性
### 阶段二后端改造2天
- [ ] 扩展实体类 Route 和 RouteProcess
- [ ] 创建时间计算工具类 RouteTimeCalculator
- [ ] 改造 AutoCompleteServiceImpl
- [ ] 单元测试
### 阶段三前端改造1天
- [ ] 工序路线管理界面添加新字段
- [ ] 工序明细界面添加转运时间
- [ ] 联动逻辑实现
### 阶段四测试验证1天
- [ ] 功能测试
- [ ] 集成测试
- [ ] 回归测试
### 阶段五上线部署0.5天)
- [ ] 生产环境数据库升级
- [ ] 后端服务部署
- [ ] 前端部署
- [ ] 验证
**总计**: 5.5 工作日
---
## ⚠️ 风险评估
| 风险项 | 影响程度 | 应对措施 |
|--------|---------|---------|
| 数据迁移失败 | 高 | 提前备份,准备回滚脚本 |
| 现有功能受影响 | 中 | 充分回归测试,灰度发布 |
| 性能影响 | 低 | 新增字段已建索引,性能影响可控 |
| 用户培训 | 低 | 提供操作文档和视频 |
---
## 📚 附录
### A. SQL 完整脚本
见配套文件: `2025-11-17_01_周启威_连续制造业流程优化_V2.sql`
### B. 代码文件清单
| 文件路径 | 改动类型 | 说明 |
|---------|---------|------|
| Route.java | 修改 | 新增manufacture_type字段和判断方法 |
| RouteProcess.java | 修改 | 新增transfer_time和wait_start_time字段 |
| RouteTimeCalculator.java | 新增 | 时间计算工具类(支持离散/连续模式) |
| AutoCompleteServiceImpl.java | 修改 | 使用工具类计算时间、连续制造绑定工序路线 |
| SalOrderServiceImpl.java | 修改 | 新增销售订单混合制造类型校验 |
| WorkOrderServiceImpl.java | 修改 | 物料与工序路线匹配校验、工单currentProcess补充 |
### C. 数据字典
#### pro_route 表
| 字段名 | 类型 | 说明 | 新增 |
|--------|------|------|------|
| manufacture_type | VARCHAR(20) | 制造类型: DISCRETE=离散制造(顺序推进), CONTINUOUS=连续制造(同步开工) | ✅ |
#### pro_route_process 表
| 字段名 | 类型 | 说明 | 新增 |
|--------|------|------|------|
| transfer_time | BIGINT | 转运时间(秒),离散制造专用 | ✅ |
| wait_start_time | BIGINT | 等待开始时间(秒),连续制造专用 | ✅ |
---
## 📞 联系方式
- **技术负责人**: 周启威
- **文档维护**: AI Assistant
- **最后更新**: 2025-11-17
---
**文档状态**: ✅ 待审核
**下一步**: 等待技术评审通过后进入开发阶段

View File

@@ -0,0 +1,990 @@
# 工序执行情况表 - 一键完成和批量生成优化
## 文档信息
- **创建日期:** 2025-11-21
- **创建人:** 周启威
- **功能模块:** 生产管理 - 工序执行情况表
---
## 需求概述
针对工序执行情况表的一键完成和批量生成功能进行三项优化:
### 问题1一键完成/批量完成对话框 - 时间支持修改并自动重算
**现状:**
- 在一键完成/批量完成对话框中
- 工序开始时间是只读的disabled不能修改
- 报工时间(完成时间)可以修改,但修改后:
- 后续工序的开始时间不会自动更新
- 需要手动逐个修改每个工序的时间
- 缺少时间提示信息(工序持续时间、转运时间等)
**需求:**
1. **所有时间支持修改**:工序开始时间也应该可以修改
2. **新增提示信息**:显示工序持续时间、转运时间等
3. **自动重算**:修改某个工序的时间后,自动重新计算后续所有工序的时间
4. **保持连续性**:前工序完成时间 = 后工序开始时间
**影响范围:**
- ✅ 一键完成对话框(单个订单)
- ✅ 批量完成对话框(多个订单)
- ✅ 离散制造 + 连续制造
---
### 问题2批量完成 - 跨订单按工序批量设置执行人
**现状:**
- 批量完成多个订单时
- 只能对单个订单的所有工序批量设置执行人
- 如果多个订单都有"工序A",需要在每个订单中分别设置
- 效率低,重复操作多
**需求:**
- 支持对所有订单的相同工序批量设置执行人
- 例如:一次操作将所有订单的"工序A"都设为"张三"
- 提高批量操作效率
**影响范围:**
- ✅ 批量完成对话框(多个订单)
- ✅ 仅针对离散制造
---
### 问题3报工表单保存后 - 后续工序时间不同步
**现状:**
- 在报工表单中修改了某个工序的报工时间
- 保存表单后,系统自动生成下一个工序的执行记录
- **问题:** 下一工序的开始时间仍使用旧时间(修改前的时间),而不是修改后的时间
**场景示例:**
```
1. 工序1原本的报工时间是 10:00
2. 打开报工表单,修改为 11:00
3. 保存表单
4. 系统自动生成工序2的执行记录
5. 问题工序2的开始时间是 10:00而不是 11:00
```
**原因:**
- 后端生成下一工序时,使用的是内存中的旧时间
- 而不是从数据库读取最新保存的时间
**需求:**
- 后端生成下一工序时,从数据库读取最新的执行记录时间
- 确保时间链条的连续性
**影响范围:**
- ✅ 报工表单保存后的后续工序生成逻辑(后端)
- ✅ 离散制造 + 连续制造
---
## 功能设计
### 1. 报工时间修改后自动重算问题1
#### UI设计
**离散制造:**
```
┌─────────────────────────────────────────────────────────┐
│ 工序1下料 │
├─────────────────────────────────────────────────────────┤
│ 工序开始时间:[2025-11-21 08:00] 📅 ← 可修改 │
此时间为该工序的实际开始时间,根据生产开始时间和 │
│ 前工序的持续时间和此工序的转运时间自动计算 │
转运时间600秒10分钟
│ ⚠️ 修改后,后续工序时间将自动重算 │
│ │
│ 报工时间(完成时间):[2025-11-21 10:00] 📅 ← 可修改 │
此工序持续时间7200秒2小时
│ ⚠️ 修改后,后续工序时间将自动重算 │
│ │
│ 报工人:[张三▼] 报工车间:[车间A▼] │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 工序2焊接 │
├─────────────────────────────────────────────────────────┤
│ 工序开始时间:[2025-11-21 10:10] 📅 ← 可修改 │
根据前工序完成时间 + 转运时间自动计算 │
转运时间300秒5分钟
│ ⚠️ 修改后,后续工序时间将自动重算 │
│ │
│ 报工时间(完成时间):[2025-11-21 13:10] 📅 ← 可修改 │
此工序持续时间10800秒3小时
│ ⚠️ 修改后,后续工序时间将自动重算 │
│ │
│ 报工人:[李四▼] 报工车间:[车间B▼] │
└─────────────────────────────────────────────────────────┘
```
**连续制造:**
```
┌─────────────────────────────────────────────────────────┐
│ 报工单1 │
├─────────────────────────────────────────────────────────┤
│ 工序开始时间:[2025-11-21 08:00] 📅 │
工序等待开始时间0秒连续制造工序同时开始
│ │
│ 工序完成时间:[2025-11-21 16:00] 📅 ← 可修改 │
此工序持续时间28800秒8小时
│ ⚠️ 修改后,后续报工单时间将自动重算 │
│ │
│ 报工人:[张三▼] 报工车间:[车间A▼] │
│ 报工数量:[100] │
└─────────────────────────────────────────────────────────┘
```
#### 时间计算规则
**离散制造:**
```
工序N的开始时间 = 工序(N-1)的完成时间 + 工序N的转运时间
工序N的完成时间 = 工序N的开始时间 + 工序N的持续时间
示例:
- 工序1开始08:00
- 工序1持续2小时7200秒
- 工序1完成10:00
- 工序2转运10分钟600秒
- 工序2开始10:10 ← 自动计算
- 工序2持续3小时10800秒
- 工序2完成13:10 ← 自动计算
```
**连续制造:**
```
报工单N的开始时间 = 报工单(N-1)的完成时间
报工单N的完成时间 = 报工单N的开始时间 + 报工单N的持续时间
注意连续制造的所有工序同时进行工序等待开始时间通常为0
```
#### 前端实现要点
**1. 在时间选择器下方添加提示信息**
离散制造:
```vue
<!-- 工序开始时间 -->
<el-form-item label="工序开始时间">
<el-date-picker
v-model="autoCompleteForm.processConfigs[index].processStartTime"
type="datetime"
placeholder="工序开始时间"
@change="handleProcessStartTimeChange(index)"
/>
<!-- 提示信息 -->
<div style="color: #909399; font-size: 12px; margin-top: 5px; line-height: 1.5">
<div>
<i class="el-icon-info"></i>
此时间为该工序的实际开始时间根据生产开始时间和前工序的持续时间和此工序的转运时间自动计算
</div>
<div v-if="selectedRouteProcessList[index].transportTime">
<i class="el-icon-info"></i>
转运时间{{ formatDuration(selectedRouteProcessList[index].transportTime) }}
</div>
<div v-if="processStartTimeChanged[index]" style="color: #E6A23C">
<i class="el-icon-warning"></i>
修改后后续工序时间将自动重算
</div>
</div>
</el-form-item>
<!-- 报工时间完成时间 -->
<el-form-item label="报工时间(完成时间)">
<el-date-picker
v-model="autoCompleteForm.processConfigs[index].reportTime"
type="datetime"
placeholder="选择报工时间(完成时间)"
@change="handleReportTimeChange(index)"
/>
<!-- 提示信息 -->
<div style="color: #909399; font-size: 12px; margin-top: 5px; line-height: 1.5">
<div>
<i class="el-icon-info"></i>
此工序持续时间{{ formatDuration(selectedRouteProcessList[index].duration) }}
</div>
<div v-if="reportTimeChanged[index]" style="color: #E6A23C">
<i class="el-icon-warning"></i>
修改后后续工序时间将自动重算
</div>
</div>
</el-form-item>
```
连续制造:
```vue
<el-form-item label="工序完成时间">
<el-date-picker
v-model="autoCompleteForm.continuousReports[index].reportPeriodEnd"
type="datetime"
placeholder="选择完成时间"
@change="handleContinuousReportTimeChange(index)"
/>
<!-- 提示信息 -->
<div style="color: #909399; font-size: 12px; margin-top: 5px; line-height: 1.5">
<div>
<i class="el-icon-info"></i>
工序等待开始时间0连续制造工序同时开始
</div>
<div v-if="continuousReportTimeChanged[index]" style="color: #E6A23C">
<i class="el-icon-warning"></i>
修改后后续报工单时间将自动重算
</div>
</div>
</el-form-item>
```
**2. 添加时间格式化方法**
```javascript
methods: {
// 格式化持续时间(秒 -> 小时分钟)
formatDuration(seconds) {
if (!seconds) return '未设置'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
let result = `${seconds}秒`
if (hours > 0) {
result += ` (${hours}小时`
if (minutes > 0) result += `${minutes}分钟`
result += ')'
} else if (minutes > 0) {
result += ` (${minutes}分钟`
if (secs > 0) result += `${secs}秒`
result += ')'
}
return result
}
}
```
**3. 监听报工时间修改,自动重算后续工序**
```javascript
data() {
return {
processStartTimeChanged: {}, // 记录哪些工序的开始时间被修改过
reportTimeChanged: {}, // 记录哪些工序的报工时间被修改过
continuousReportTimeChanged: {} // 连续制造报工单时间修改标记
}
},
methods: {
// 离散制造:工序开始时间修改处理
handleProcessStartTimeChange(index) {
// 标记该工序开始时间已修改
this.$set(this.processStartTimeChanged, index, true)
const configs = this.autoCompleteForm.processConfigs
const currentProcess = this.selectedRouteProcessList[index]
// 根据新的开始时间和工序持续时间,重新计算完成时间
const startTime = new Date(configs[index].processStartTime)
const duration = currentProcess.duration || 3600
const finishTime = new Date(startTime.getTime() + duration * 1000)
configs[index].reportTime = this.parseTime(finishTime, '{y}-{m}-{d} {h}:{i}:{s}')
// 重新计算后续所有工序的时间
this.recalculateSubsequentProcessTimes(index)
},
// 离散制造:报工时间修改处理
handleReportTimeChange(index) {
// 标记该工序时间已修改
this.$set(this.reportTimeChanged, index, true)
// 重新计算后续所有工序的时间
this.recalculateSubsequentProcessTimes(index)
},
// 重新计算后续工序时间
recalculateSubsequentProcessTimes(startIndex) {
const configs = this.autoCompleteForm.processConfigs
for (let i = startIndex + 1; i < configs.length; i++) {
const prevConfig = configs[i - 1]
const currentProcess = this.selectedRouteProcessList[i]
// 前工序完成时间
const prevFinishTime = new Date(prevConfig.reportTime)
// 当前工序转运时间(秒)
const transportTime = currentProcess.transportTime || 0
// 当前工序开始时间 = 前工序完成时间 + 转运时间
const currentStartTime = new Date(prevFinishTime.getTime() + transportTime * 1000)
configs[i].processStartTime = this.parseTime(currentStartTime, '{y}-{m}-{d} {h}:{i}:{s}')
// 当前工序持续时间(秒)
const duration = currentProcess.duration || 3600
// 当前工序完成时间 = 开始时间 + 持续时间
const currentFinishTime = new Date(currentStartTime.getTime() + duration * 1000)
configs[i].reportTime = this.parseTime(currentFinishTime, '{y}-{m}-{d} {h}:{i}:{s}')
}
this.$message.success(`已自动重算后续 ${configs.length - startIndex - 1} 个工序的时间`)
},
// 连续制造:报工时间修改处理
handleContinuousReportTimeChange(index) {
this.$set(this.continuousReportTimeChanged, index, true)
this.recalculateSubsequentContinuousReports(index)
},
// 重新计算后续报工单时间
recalculateSubsequentContinuousReports(startIndex) {
const reports = this.autoCompleteForm.continuousReports
for (let i = startIndex + 1; i < reports.length; i++) {
const prevReport = reports[i - 1]
// 下一报工单开始时间 = 前一报工单结束时间
reports[i].reportPeriodStart = prevReport.reportPeriodEnd
}
this.$message.success(`已自动重算后续 ${reports.length - startIndex - 1} 个报工单的时间`)
}
}
```
**4. 时间验证**
- 完成时间不能早于开始时间
- 修改时给出警告提示
---
### 2. 定时批量完成 - 时间可选
#### UI设计
```
┌─────────────────────────────────────┐
│ 批量完成工序 │
├─────────────────────────────────────┤
│ 已选择 3 个工序 │
│ │
│ 统一设置: │
│ 开始时间:[2025-11-21 08:00] 📅 │
│ 完成时间:[2025-11-21 17:00] 📅 │
│ 执行人: [张三] ▼ │
│ │
│ ⚠️ 将按工序顺序依次完成,时间自动衔接 │
│ │
│ 工序列表: │
│ ☑ 工序A 08:00-10:00 张三 │
│ ☑ 工序B 10:00-12:00 张三 │
│ ☑ 工序C 12:00-14:00 张三 │
│ │
│ [取消] [确定] │
└─────────────────────────────────────┘
```
#### 时间分配策略
```
第1个工序开始时间 = 用户选择,完成时间 = 开始时间 + 标准工时
第N个工序开始时间 = 第(N-1)个完成时间,完成时间 = 开始时间 + 标准工时
```
---
### 3. 表单时间修改同步
#### 问题场景
```
1. 打开工序A执行记录表单
2. 修改报工时间10:00 → 11:00
3. 保存表单
4. 系统生成工序B执行记录
5. 问题工序B开始时间仍是10:00而不是11:00
```
#### 解决方案
**后端实现:** 生成下一工序时,从数据库读取最新的执行记录,使用最新完成时间
```java
private void generateNextProcessReport(Long workOrderId, Long currentProcessId) {
// 从数据库读取最新执行记录
ProcessReport currentReport = processReportMapper.selectLatestByProcessId(currentProcessId);
if (currentReport == null || currentReport.getFinishTime() == null) {
return;
}
// 使用最新完成时间作为下一工序开始时间
Date nextStartTime = currentReport.getFinishTime();
Date nextFinishTime = calculateFinishTime(nextStartTime, nextProcess.getDuration());
// 创建下一工序执行记录
ProcessReport nextReport = new ProcessReport();
nextReport.setStartTime(nextStartTime);
nextReport.setFinishTime(nextFinishTime);
processReportMapper.insert(nextReport);
}
```
---
### 4. 批量设置执行人
#### UI设计
```
┌─────────────────────────────────────────────┐
│ 批量生成工序执行记录 │
├─────────────────────────────────────────────┤
│ 工单编号WO202511210001 │
│ │
│ 工序列表: │
│ ┌──────────────────────────────────────┐ │
│ │ 工序名称 标准工时 执行人 数量 │ │
│ ├──────────────────────────────────────┤ │
│ │ 下料 2h [张三▼] 5 │ │
│ │ 焊接 3h [李四▼] 5 │ │
│ │ 打磨 1h [王五▼] 5 │ │
│ └──────────────────────────────────────┘ │
│ │
│ 快捷操作: │
│ 工序:[下料▼] 执行人:[张三▼] [应用] │ ← 新增
│ │
│ [全部设为同一人] [取消] [生成] │ ← 新增
└─────────────────────────────────────────────┘
```
#### 功能说明
1. **按工序设置**:选择工序和执行人,点击应用,该工序的所有记录都设置为该执行人
2. **全部设为同一人**:所有工序的所有记录都设置为同一个执行人
---
## 实施计划
### 阶段0需求确认 🔍
- [ ] 确认"工序执行情况表"的具体页面位置
- [ ] 确认"一键完成"功能的当前实现
- [ ] 确认"批量生成"功能的当前实现
- [ ] 确认相关前后端文件路径
**说明:**
- 需要用户提供具体的页面路径或功能入口
- 或者提供相关的菜单名称、页面截图
### 阶段1一键完成优化 ⏳
- [ ] 前端:添加完成时间选择器
- [ ] 前端:时间验证逻辑
- [ ] 后端:接口支持完成时间参数
- [ ] 后端:实现后续工序时间重算
### 阶段2表单时间同步 ⏳
- [ ] 后端:修改生成下一工序逻辑
- [ ] 后端:从数据库读取最新执行记录
- [ ] 测试:验证时间链条连续性
### 阶段3批量设置执行人 ⏳
- [ ] 前端快捷操作区UI
- [ ] 前端:按工序设置功能
- [ ] 前端:全部设为同一人功能
- [ ] 测试:批量设置功能
### 阶段4测试验证 ⏳
- [ ] 功能测试
- [ ] 边界测试
- [ ] 用户验收测试
---
## 实施进度
### 2025-11-21 10:35 - 需求确认阶段 ✅
**页面路径:** `mes/statement/saleOrderExecution`
**找到的文件:**
-`mes-ui/src/views/mes/statement/saleOrderExecution/index.vue` - 销售订单执行情况表
**当前功能分析:**
#### 制造类型说明
- **离散制造 (DISCRETE)** 工序按顺序执行,有明确的工序路线
- **连续制造 (CONTINUOUS)** 所有工序同时进行,按时间段或班次报工
---
#### 1. 一键完成功能(单个订单)
**离散制造:**
- ✅ 有生产开始时间选择器 (`productionStartTime`)
- ✅ 每个工序有独立的报工时间选择器 (`reportTime`)
- ✅ 工序开始时间自动计算(前工序完成时间 + 转运时间)
- ✅ 已有批量设置功能:
- `统一设置报工人` - 所有工序设为同一人 ✅
- `统一设置报工时间` - 按工序持续时间递增 ✅
- `统一设置报工车间` - 所有工序设为同一车间 ✅
**连续制造:**
- ✅ 有工序开始时间和完成时间选择器
- ✅ 可添加多条报工单
- ✅ 已有批量设置功能:
- `统一设置报工人`
- `统一设置车间`
**问题1报工时间修改后后续工序时间不会自动重算**
- 当前:修改某个工序的报工时间后,后续工序的开始时间不会更新
- 需要:修改时间后,自动重新计算后续所有工序的时间
---
#### 2. 批量完成功能(多个订单)
**按钮:** "定时完成(批量完成)"
**对话框:** `batchAutoCompleteDialog`
**当前实现:**
- ✅ 支持批量选择多个订单
- ✅ 每个订单独立配置工序路线和时间
- ✅ 已有订单级批量设置功能:
- `统一设置报工人` - 该订单所有工序设为同一人 ✅
- `统一设置报工时间` - 按工序持续时间递增 ✅
- `统一设置报工车间` - 该订单所有工序设为同一车间 ✅
**问题2缺少跨订单的批量设置**
- 当前:只能对单个订单的所有工序批量设置
- 需要:支持对所有订单的相同工序批量设置执行人
- 例如:所有订单的"工序A"都设为"张三"
---
#### 3. 表单时间修改同步问题
**问题场景:**
1. 在报工表单中修改了某个工序的报工时间
2. 保存后,系统生成下一个工序的执行记录
3. **问题:** 下一工序的开始时间仍使用旧时间,而不是修改后的时间
**原因:** 后端生成下一工序时,使用的是内存中的时间,而不是数据库中最新保存的时间
**需要:** 后端从数据库读取最新的执行记录时间
---
### 2025-11-21 11:00 - 明确实施策略
**重新理解需求后的实施策略:**
#### 问题1报工时间修改后自动重算 ⏳
- **位置:** 一键完成对话框、批量完成对话框
- **实现:** 监听报工时间修改事件,自动重算后续工序时间
- **代码位置:** `index.vue` 中的 `@change` 事件处理
#### 问题2跨订单按工序批量设置 ⏳
- **位置:** 批量完成对话框(仅离散制造)
- **实现:** 添加全局批量设置区域,支持按工序名称批量设置
- **代码位置:** `batchAutoCompleteDialog` 对话框顶部
#### 问题3表单时间修改同步 ⏳
- **位置:** 后端报工表单保存逻辑
- **实现:** 从数据库读取最新执行记录时间
- **代码位置:** 后端 Service 层
**实施顺序:**
1. ✅ 问题1最简单先实现
2. ✅ 问题2前端功能其次实现
3. ✅ 问题3需要后端修改最后实现
---
### 2025-11-21 11:15 - 完善功能设计文档
**已完成:**
- ✅ 更新UI设计添加详细的时间提示信息
- 离散制造:显示工序持续时间、转运时间
- 连续制造:显示工序等待开始时间
- ✅ 添加时间计算规则说明和示例
- ✅ 编写前端实现代码(含提示信息、时间格式化、自动重算逻辑)
---
### 2025-11-21 11:20 - 重新明确需求
**用户确认的三个问题:**
**问题1** 表单中的所有时间支持修改 + 新增提示 + 修改后自动计算后续时间
- ✅ 工序开始时间改为可修改原来是disabled
- ✅ 报工时间(完成时间)可修改
- ✅ 添加时间提示信息(持续时间、转运时间)
- ✅ 修改任一时间后,自动重算后续所有工序
**问题2** 离散的批量生成,能够批量设置同一个工序的指定人
- ✅ 跨订单按工序批量设置
- ✅ 例如:所有订单的"工序A"都设为"张三"
**问题3** 报工表单保存后后续工序时间不同步场景B
- ✅ 后端从数据库读取最新时间
- ✅ 而不是使用内存中的旧时间
**下一步:**
- ⏳ 开始实施问题1在实际代码中添加时间修改监听和自动重算功能
---
### 2025-11-21 11:35 - 实施问题1时间支持修改并自动重算
**已完成:**
#### 1. 前端UI修改
- ✅ 去掉工序开始时间的 `disabled` 属性,改为可修改
- ✅ 为工序开始时间添加 `@change="handleProcessStartTimeChange(index)"` 监听
- ✅ 为报工时间添加 `@change="handleReportTimeChange(index)"` 监听
- ✅ 添加详细的提示信息:
- 工序开始时间:显示转运时间
- 报工时间:显示工序持续时间
- 修改后显示警告:后续工序时间将自动重算
#### 2. 数据属性
- ✅ 添加 `processStartTimeChanged: {}` - 记录工序开始时间修改标记
- ✅ 添加 `reportTimeChanged: {}` - 记录报工时间修改标记
- ✅ 添加 `continuousReportTimeChanged: {}` - 连续制造时间修改标记
#### 3. 方法实现
-`formatDuration(seconds)` - 格式化持续时间(秒 → 小时分钟)
-`handleProcessStartTimeChange(index)` - 工序开始时间修改处理
-`handleReportTimeChange(index)` - 报工时间修改处理
-`recalculateSubsequentProcessTimes(startIndex)` - 重新计算后续工序时间
**实现效果:**
- ✅ 修改任一工序的开始时间或报工时间
- ✅ 自动重新计算后续所有工序的时间
- ✅ 保持时间链条的连续性
- ✅ 显示友好的提示信息
**待测试:**
- ⏳ 一键完成对话框中修改时间
- ⏳ 验证后续工序时间自动重算
- ⏳ 验证时间链条连续性
**下一步:**
- ⏳ 实施问题2批量完成 - 跨订单按工序批量设置执行人
---
### 2025-11-21 11:45 - 实施问题2跨订单按工序批量设置
**已完成:**
#### 1. UI修改
- ✅ 在批量完成对话框的全局操作区域添加新按钮
- ✅ 区分"全部设置"和"按工序设置"两种操作
- ✅ 添加"按工序批量设置执行人"按钮
#### 2. 新增对话框
- ✅ 创建"按工序批量设置执行人"对话框
- ✅ 显示所有工序列表(仅离散制造)
- ✅ 显示每个工序涉及的订单数
- ✅ 为每个工序提供执行人选择器
- ✅ 提供"应用"按钮立即生效
#### 3. 数据属性
- ✅ 添加 `processBatchSetDialog` - 按工序批量设置对话框状态
-`processList` - 工序列表(包含工序名、订单数、执行人)
#### 4. 方法实现
-`openProcessBatchSetDialog()` - 打开对话框,收集所有工序信息
-`handleProcessUserChange()` - 处理执行人选择变化
-`applyProcessBatchSet()` - 应用按工序批量设置
**实现效果:**
- ✅ 批量完成时,可以按工序名称批量设置执行人
- ✅ 例如:将所有订单的"工序A"都设为"张三"
- ✅ 显示每个工序涉及的订单数量
- ✅ 点击"应用"后立即生效,并显示成功提示
**功能示例:**
```
工序列表:
- 下料 涉及 3 个订单 [选择执行人: 张三▼] [应用]
- 焊接 涉及 3 个订单 [选择执行人: 李四▼] [应用]
- 打磨 涉及 2 个订单 [选择执行人: 王五▼] [应用]
```
**待测试:**
- ⏳ 批量完成对话框中打开按工序设置
- ⏳ 验证工序列表正确显示
- ⏳ 验证应用后所有订单的该工序都被设置
**下一步:**
- ⏳ 实施问题3报工表单保存后 - 后续工序时间不同步(后端)
---
### 2025-11-21 11:50 - 实施问题3后端使用前端修改的时间
**问题确认:**
用户反馈:在一键完成/批量完成对话框中修改了报工时间后,提交时后端生成的报工单没有使用修改后的时间,而是用系统自动计算的时间。
**问题原因:**
`AutoCompleteServiceImpl.batchGenerateReports()` 方法中第1163-1173行后端在生成报工单时**重新计算了报工时间**
```java
// 原代码:直接用工序开始时间 + 持续时间计算
Date reportTime = workOrder.getProcessStartTime();
if (reportTime != null && workOrder.getDuration() != null && workOrder.getDuration() > 0) {
reportTime = new Date(reportTime.getTime() + workOrder.getDuration() * 1000);
}
```
这导致前端传来的用户修改的时间被覆盖了。
**解决方案:**
修改后端逻辑,**优先使用前端传来的报工时间**
```java
// 🔧 优先使用前端传来的报工时间(用户可能已修改)
Date reportTime = null;
try {
if (config.getReportTime() != null && !config.getReportTime().isEmpty()) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
reportTime = sdf.parse(config.getReportTime());
System.out.println("✓ 使用前端传来的报工时间: " + config.getReportTime());
}
} catch (ParseException e) {
System.err.println("✗ 解析前端报工时间失败: " + e.getMessage());
}
// 如果前端没有传报工时间,则使用后端计算的时间
if (reportTime == null) {
reportTime = workOrder.getProcessStartTime();
if (reportTime != null && workOrder.getDuration() != null && workOrder.getDuration() > 0) {
reportTime = new Date(reportTime.getTime() + workOrder.getDuration() * 1000);
}
}
```
**修改的文件:**
-`yjh-mes/src/main/java/cn/sourceplan/production/service/impl/AutoCompleteServiceImpl.java`
- 修改 `batchGenerateReports()` 方法
- 优先使用前端传来的 `config.getReportTime()`
- 如果前端没传,则使用后端计算的时间(兼容旧逻辑)
**实现效果:**
- ✅ 用户在前端修改报工时间后,后端会使用修改后的时间
- ✅ 如果用户没有修改,后端仍会自动计算时间(向后兼容)
- ✅ 添加日志输出,方便调试
---
## 总结
### ✅ 已完成的优化
#### 问题1时间支持修改并自动重算 ✅
- ✅ 工序开始时间和报工时间都可以修改
- ✅ 修改后自动重算后续所有工序时间
- ✅ 添加详细的提示信息
- ✅ 保持时间链条连续性
#### 问题2跨订单按工序批量设置 ✅
- ✅ 批量完成时支持按工序名称批量设置执行人
- ✅ 显示每个工序涉及的订单数
- ✅ 一次操作设置所有订单的相同工序
#### 问题3后端使用前端修改的时间 ✅
- ✅ 修改后端逻辑,优先使用前端传来的报工时间
- ✅ 用户修改时间后,后端不会再重新计算覆盖
- ✅ 向后兼容:如果前端没传时间,后端仍会自动计算
---
## 实施完成总结
**修改的文件:**
-`mes-ui/src/views/mes/statement/saleOrderExecution/index.vue`(前端)
- 添加时间修改监听和自动重算逻辑
- 添加按工序批量设置对话框
- 添加详细的提示信息
-`yjh-mes/src/main/java/cn/sourceplan/production/service/impl/AutoCompleteServiceImpl.java`(后端)
- 修改 `batchGenerateReports()` 方法
- 优先使用前端传来的报工时间
- 添加日志输出
**新增功能:**
1. 工序时间可修改并自动重算
2. 按工序批量设置执行人
3. 时间格式化显示(秒 → 小时分钟)
**待测试:**
-**问题1测试** 一键完成对话框中修改时间,验证前端自动重算
-**问题2测试** 批量完成对话框中使用按工序批量设置执行人
-**问题3测试** 修改时间后提交,验证后端生成的报工单使用了修改后的时间
- ⏳ 验证时间链条的连续性
- ⏳ 验证批量设置后所有订单的该工序都被正确设置
---
### 2025-11-21 12:00 - 补充修改:报工单时间显示时分秒
**需求:**
报工单表单中的报工时间改成带时分秒的,并且可以编辑修改。
**修改内容:**
- ✅ 将报工时间选择器从 `type="date"` 改为 `type="datetime"`
- ✅ 增加列宽从 240px 到 280px以容纳时分秒显示
- ✅ 保持 `value-format="yyyy-MM-dd HH:mm:ss"` 格式不变
**修改的文件:**
-`mes-ui/src/views/mes/production/report/form.vue`
-`mes-ui/src/views/mes/production/report/formA.vue`
**修改前:**
```vue
<el-date-picker type="date" ... />
<!-- 只能选择日期2025-11-21 -->
```
**修改后:**
```vue
<el-date-picker type="datetime" ... />
<!-- 可以选择日期和时间2025-11-21 14:30:00 -->
```
**实现效果:**
- ✅ 报工时间显示完整的日期时间(年月日 时分秒)
- ✅ 可以在表单中直接编辑修改时分秒
- ✅ 更精确的报工时间记录
---
### 2025-11-21 12:05 - 补充修改UI样式优化
**需求1统一按钮样式**
全局操作按钮样式要统一,"统一设置所有报工人"等按钮和"按工序批量设置执行人"按钮格式要一样。
**修改内容:**
- ✅ 所有全局操作按钮统一添加 `plain` 属性
- ✅ 移除"按工序批量设置执行人"按钮的 `type="warning"`
- ✅ 保持统一的扁平化按钮风格
**需求2订单边框颜色区分**
订单外层边框要根据制造类型显示不同颜色,内部工序/报工单不需要边框。
**修改内容:**
- ✅ 批量完成对话框:订单外层边框根据制造类型显示颜色
- **离散制造**:蓝色边框 `#409EFF`2px
- **连续制造**:绿色边框 `#67C23A`2px
- ✅ 去掉内部工序和报工单的边框(简化视觉)
**修改位置:**
1. 批量完成对话框
- 订单外层边框第1196行、第1407行根据制造类型
- 去掉工序表单边框第1263行
- 去掉报工单表单边框第1347行
2. 一键完成对话框
- 去掉工序表单边框第409行
- 去掉报工单表单边框第594行
**实现效果:**
- ✅ 按钮样式统一,视觉更协调
- ✅ 通过订单外层边框颜色快速区分制造类型
-**蓝色 = 离散制造****绿色 = 连续制造**
- ✅ 内部表单无边框,界面更简洁
---
### 2025-11-21 12:15 - 修复:批量完成对话框时间自动重算
**问题:**
用户在批量完成对话框中修改了报工时间,但后续工序的时间没有自动重算。
**原因:**
批量完成对话框中的时间字段没有添加 `@change` 监听事件,导致修改时间后不会触发自动重算。
**修改内容:**
1. ✅ 去掉工序开始时间的 `disabled` 属性,允许编辑
2. ✅ 为工序开始时间添加 `@change="handleBatchProcessStartTimeChange(orderIndex, pIndex)"`
3. ✅ 为报工时间添加 `@change="handleBatchReportTimeChange(orderIndex, pIndex)"`
4. ✅ 添加三个新方法:
- `handleBatchProcessStartTimeChange` - 处理工序开始时间修改
- `handleBatchReportTimeChange` - 处理报工时间修改
- `recalculateBatchSubsequentProcessTimes` - 重新计算后续工序时间
**修改位置:**
- 模板第1279行左侧订单、第1491行右侧订单
- 方法第2585-2641行
**实现效果:**
- ✅ 修改工序开始时间后,自动计算该工序的报工时间
- ✅ 修改报工时间后,自动重算后续所有工序的时间
- ✅ 保持时间链条连续性
- ✅ 显示友好的提示信息
**2025-11-21 14:25 - 修复和完善:**
1. ✅ 修复了 `recalculateBatchSubsequentProcessTimes` 方法的逻辑错误
- 原来从 `order.routeProcessList` 获取工序信息(不存在)
- 现在直接从 `configs` 中获取工序信息duration、transferTime等
2. ✅ 添加了工序持续时间和转运时间的可视化提示
- 蓝色标签显示持续时间(分钟)
- 黄色标签显示转运时间(分钟)
- 方便用户了解时间计算依据
**2025-11-21 14:32 - 最终修复:**
1.**修复转运时间未保存的问题**
-`handleBatchRouteChange` 方法中,工序配置对象添加了 `transferTime` 字段
- 确保时间重算时能正确获取并使用转运时间
2.**优化报工时间标签显示**
- 一键完成对话框:标签改为两行显示 "报工时间" + "(此工序完成时间)"
- 批量完成对话框:同样改为两行显示
- 说明文字使用灰色小字体,更加清晰
---
## 相关文件
### 前端文件
-`mes-ui/src/views/mes/statement/saleOrderExecution/index.vue` - 销售订单执行情况表(主文件)
- 包含一键完成对话框
- 包含批量完成对话框
- 包含定时完成功能
-`mes-ui/src/views/mes/production/report/form.vue` - 报工单表单
- 修改报工时间为datetime类型
-`mes-ui/src/views/mes/production/report/formA.vue` - 报工单表单A
- 修改报工时间为datetime类型
### 后端文件
-`yjh-mes/src/main/java/cn/sourceplan/production/service/impl/AutoCompleteServiceImpl.java` - 一键完成服务实现
- 修改了 `batchGenerateReports()` 方法
- 优先使用前端传来的报工时间
- `yjh-mes/src/main/java/cn/sourceplan/statement/service/impl/SaleOrderExecutionServiceImpl.java`
---
## 测试用例
### 测试1完成时间可选
1. 打开一键完成对话框
2. 修改完成时间
3. 验证:完成时间不能早于开始时间
4. 提交后验证:后续工序时间已重算
### 测试2时间同步
1. 完成工序A时间为10:00
2. 修改工序A完成时间为11:00
3. 生成工序B
4. 验证工序B开始时间为11:00
### 测试3批量设置执行人
1. 打开批量生成对话框
2. 选择工序"下料",执行人"张三",点击应用
3. 验证:所有下料工序的执行人都变为张三
4. 点击"全部设为同一人",选择"李四"
5. 验证:所有工序的执行人都变为李四
---
**文档版本:** v1.0
**创建日期:** 2025-11-21
**最后更新:** 2025-11-21

View File

@@ -0,0 +1,655 @@
# 生产流程新增成品入库流程
## 需求概述
在销售订单页面新增"生产入库"功能,实现从生产完成到成品入库的流程闭环。
## 业务流程
### 当前流程
```
待生产(A) → 生产中(B) → 生产完成(F) → 已发货(C)
```
### 新增流程
```
待生产(A) → 生产中(B) → 生产完成(F) → 已入库(G) → 部分发货(E)/已发货(C)
```
## 功能详细设计
### 1. 数据字典修改
#### 1.1 新增销售订单状态
`sys_dict_data` 表中新增状态:
| 字段 | 值 |
|------|-----|
| dict_label | 已入库 |
| dict_value | G |
| dict_type | salorder_status |
| dict_sort | 5 |
| css_class | success |
| remark | 生产完成后已入库,待发货 |
**SQL语句**
```sql
-- 注意ID 需要根据实际数据库中的最大ID来设置这里假设使用 206 之后的ID
INSERT INTO `sys_dict_data`
VALUES (NULL, 7, '已入库', 'G', 'salorder_status', NULL, 'success', 'N', '0',
'admin', NOW(), '', NULL, '生产完成后已入库,待发货');
```
**说明:**
- 当前数据库中已有状态A(待生产)、B(生产中)、C(已发货)、D(已关闭)、E(部分发货)、F(生产完成)
- 新增 G(已入库) 状态dict_sort 设为 7在F之后
### 2. 前端页面修改
#### 2.1 销售订单列表页面 (`index.vue`)
**新增按钮位置:**
在"销售出库"按钮后面添加"生产入库"按钮
```vue
<el-col :span="1.5">
<el-button
plain
style="color: #0E7C7B; background-color: #A7E8BD"
icon="el-icon-download"
size="mini"
@click="openManufactureIntoForm()"
:disabled="multiple"
>生产入库</el-button>
</el-col>
```
**按钮启用条件:**
- 必须选中至少一条订单明细
- 选中的订单明细状态必须为 `F`(生产完成)
#### 2.2 生产入库弹窗设计
**弹窗标题:** 生产入库
**表单字段:**
| 字段名 | 字段说明 | 数据来源 | 是否必填 | 备注 |
|--------|----------|----------|----------|------|
| 入库单编号 | number | 自动生成 | 是 | 禁用输入 |
| 入库日期 | intoDate | 默认当前日期 | 是 | 可修改 |
| 交货人 | delivererId | 手动选择 | 是 | 下拉选择 |
| 仓库 | warehouseId | 手动选择 | 是 | 下拉选择 |
| 备注 | remark | 手动输入 | 否 | 文本域 |
**明细表格字段:**
| 字段名 | 字段说明 | 数据来源 | 是否必填 | 备注 |
|--------|----------|----------|----------|------|
| 序号 | sort | 自动生成 | 是 | 禁用 |
| 产品名 | materialName | 销售订单 | 是 | 禁用 |
| 产品编号 | materialNumber | 销售订单 | 是 | 禁用 |
| 规格型号 | specification | 销售订单 | 否 | 可修改 |
| 单位 | materialUnitName | 销售订单 | 是 | 禁用 |
| 数量 | quantity | 销售订单 | 是 | 可修改 |
| 批次编号 | batchNumber | 手动输入 | 否 | 可输入 |
| 生产日期 | manufactureDate | 默认当前日期 | 否 | 可修改 |
### 3. 后端接口设计
#### 3.1 新增接口
**接口路径:** `/sale/saleOrder/createManufactureInto`
**请求方法:** POST
**请求参数:**
```json
{
"saleOrderEntryIds": [1, 2, 3], // 销售订单明细ID数组
"manufactureInto": {
"intoDate": "2025-11-21",
"delivererId": 1,
"delivererName": "张三",
"warehouseId": 1,
"warehouseNumber": "WH001",
"warehouseName": "成品仓",
"remark": "备注信息"
}
}
```
**返回结果:**
```json
{
"code": 200,
"msg": "生产入库成功",
"data": {
"manufactureIntoId": 123,
"number": "MI202511210001"
}
}
```
#### 3.2 业务逻辑
1. **数据验证**
- 验证所有订单明细状态必须为 `F`(生产完成)
- 验证仓库、交货人等必填字段
2. **创建完工入库单**
- 生成入库单编号
- 创建入库单主表记录
- 根据销售订单明细创建入库单明细
3. **更新销售订单状态**
- 将选中的销售订单明细状态从 `F` 更新为 `G`(已入库)
- 检查主表所有明细状态,如果全部为 `G`,则更新主表状态为 `G`
4. **库存处理**
- 调用库存模块接口,增加成品库存
### 4. 状态流转规则
#### 4.1 允许的状态流转
```
A(待生产) → B(生产中) [工序排产]
B(生产中) → F(生产完成) [工单完成]
F(生产完成) → G(已入库) [生产入库] ✨新增
G(已入库) → E(部分发货) [销售出库-部分]
G(已入库) → C(已发货) [销售出库-全部]
E(部分发货) → C(已发货) [销售出库-剩余]
任意状态 → D(已关闭) [手动关闭]
```
#### 4.2 状态验证规则
- **生产入库按钮**:只能对状态为 `F` 的订单明细操作
- **销售出库按钮**:只能对状态为 `G` 的订单明细操作(需修改原有逻辑)
- **发货完成按钮**:只能对状态为 `G``E` 的订单明细操作
### 5. 前端方法实现
#### 5.1 打开生产入库弹窗
```javascript
openManufactureIntoForm() {
// 验证选中的订单明细状态
const selectedEntries = this.saleOrderList.filter(item =>
this.entryIds.includes(item.id)
);
// 检查是否都是生产完成状态
const hasInvalidStatus = selectedEntries.some(item => item.status !== 'F');
if (hasInvalidStatus) {
this.$modal.msgWarning("只能对生产完成(F)状态的订单进行入库操作!");
return;
}
// 跳转到生产入库页面携带订单明细ID
this.$router.push({
path: "/mes/manufactureInto-add/index",
query: { saleOrderEntryIds: this.entryIds.join(',') }
});
}
```
#### 5.2 完工入库单页面自动填充
`manufactureInto/form.vue``created()` 方法中:
```javascript
created() {
// 检查是否从销售订单跳转过来
const saleOrderEntryIds = this.$route.query.saleOrderEntryIds;
if (saleOrderEntryIds) {
this.loadFromSaleOrder(saleOrderEntryIds);
}
// 原有逻辑...
}
async loadFromSaleOrder(entryIds) {
try {
// 调用接口获取销售订单明细
const { listEntryByIds } = await import("@/api/mes/sale/saleOrder");
const response = await listEntryByIds({ ids: entryIds });
const saleOrderEntries = response.data;
// 自动填充明细数据
this.form.manufactureIntoEntryList = saleOrderEntries.map((entry, index) => ({
sort: index + 1,
materialId: entry.materialId,
materialName: entry.materialName,
materialNumber: entry.materialNumber,
specification: entry.materialSpecification,
materialUnitId: entry.unitId,
materialUnitName: entry.unitName,
quantity: entry.quantity,
batchNumber: '', // 需要手动填写
manufactureDate: new Date(), // 默认当前日期
saleOrderEntryId: entry.id // 保存关联关系
}));
this.maxIndex = saleOrderEntries.length;
} catch (error) {
console.error('加载销售订单数据失败:', error);
this.$modal.msgError("加载订单数据失败:" + error.message);
}
}
```
### 6. 后端实现要点
#### 6.1 Service层新增方法
**接口定义:** `ISalOrderService.java`
```java
/**
* 从销售订单创建生产入库单
*
* @param entryIds 销售订单明细ID数组
* @param manufactureInto 入库单信息
* @return 结果
*/
AjaxResult createManufactureIntoFromSaleOrder(String entryIds, ManufactureInto manufactureInto);
```
**实现逻辑:** `SalOrderServiceImpl.java`
```java
@Override
@Transactional
public AjaxResult createManufactureIntoFromSaleOrder(String entryIds, ManufactureInto manufactureInto) {
// 1. 查询销售订单明细
String[] ids = entryIds.split(",");
List<SalOrderEntry> entries = salOrderEntryMapper.selectByIds(ids);
// 2. 验证状态
for (SalOrderEntry entry : entries) {
if (!"F".equals(entry.getStatus())) {
throw new ServiceException("订单明细【" + entry.getMaterialName() + "】状态不是生产完成,无法入库");
}
}
// 3. 创建入库单
manufactureIntoService.insertManufactureInto(manufactureInto);
// 4. 更新销售订单状态为已入库(G)
for (String id : ids) {
SalOrderEntry entry = new SalOrderEntry();
entry.setId(Long.parseLong(id));
entry.setStatus("G");
salOrderEntryMapper.updateSalOrderEntry(entry);
}
// 5. 更新主表状态
updateMainOrderStatus(entries);
return AjaxResult.success("生产入库成功", manufactureInto);
}
```
#### 6.2 Controller层新增接口
**文件:** `SalOrderController.java`
```java
/**
* 从销售订单创建生产入库单
*/
@PreAuthorize("@ss.hasPermi('sale:saleOrder:manufactureInto')")
@Log(title = "销售订单-生产入库", businessType = BusinessType.INSERT)
@PostMapping("/createManufactureInto")
public AjaxResult createManufactureInto(@RequestBody Map<String, Object> params) {
String entryIds = (String) params.get("saleOrderEntryIds");
ManufactureInto manufactureInto = JSON.parseObject(
JSON.toJSONString(params.get("manufactureInto")),
ManufactureInto.class
);
return salOrderService.createManufactureIntoFromSaleOrder(entryIds, manufactureInto);
}
```
### 7. 数据库修改
#### 7.1 完工入库单明细表添加关联字段
```sql
ALTER TABLE `wm_manufacture_into_entry`
ADD COLUMN `sale_order_entry_id` BIGINT(20) NULL COMMENT '销售订单明细ID' AFTER `manufacture_date`;
ALTER TABLE `wm_manufacture_into_entry`
ADD INDEX `idx_sale_order_entry_id` (`sale_order_entry_id`);
```
### 8. 权限配置
#### 8.1 新增权限标识
在系统管理 → 菜单管理中,为销售订单菜单添加按钮权限:
| 权限名称 | 权限标识 | 备注 |
|---------|---------|------|
| 生产入库 | sale:saleOrder:manufactureInto | 销售订单-生产入库按钮 |
### 9. 测试用例
#### 9.1 正常流程测试
1. 创建销售订单
2. 进行工序排产,生成工单
3. 完成工单报工,订单状态变为 `F`
4. 选中状态为 `F` 的订单,点击"生产入库"
5. 验证弹窗数据自动填充正确
6. 填写必填项(交货人、仓库)
7. 提交入库单
8. 验证订单状态变为 `G`
9. 验证库存增加
10. 进行销售出库
11. 验证订单状态变为 `C``E`
#### 9.2 异常流程测试
1. **状态验证**:选中非 `F` 状态的订单,点击生产入库,应提示错误
2. **必填项验证**:不填交货人或仓库,应提示错误
3. **并发测试**:同一订单同时进行入库操作,应避免重复入库
4. **回滚测试**:入库失败时,订单状态不应变更
### 10. 注意事项
1. **状态流转限制**
- 必须严格按照 F → G → E/C 的顺序流转
- 不允许跳过 G 状态直接从 F 到 C
2. **库存处理**
- 生产入库时增加成品库存
- 销售出库时减少成品库存
- 需要考虑库存不足的情况
3. **数据一致性**
- 入库单与销售订单的关联关系必须准确
- 状态更新需要事务保证
4. **用户体验**
- 自动填充数据减少手工录入
- 明确的状态提示和错误信息
- 按钮的启用/禁用状态要准确
### 11. 实施步骤
1.**数据库修改**
- ✅ 添加字典数据 (已创建SQL文件)
- ✅ 修改入库单明细表结构 (已创建SQL文件)
2.**后端开发**
- ✅ Service层方法实现
- ✅ 状态流转逻辑
- ✅ 实体类字段添加
3.**前端开发**
- ✅ 销售订单页面添加按钮
- ✅ 完工入库单页面自动填充逻辑
- ✅ 销售出库状态验证更新
4.**测试验证**
- ⏳ 数据库脚本执行
- ⏳ 功能测试
- ⏳ 用户验收测试
5.**上线部署**
- ⏳ 数据库脚本执行
- ⏳ 代码部署
- ⏳ 权限配置
---
## 已完成工作 (2025-11-21)
### 1. 数据库脚本 ✅
#### 1.1 字典数据
**文件:** `2025-11-21_02_周启威_字典新增销售订单状态.sql`
```sql
INSERT INTO `sys_dict_data`
VALUES (NULL, 7, '已入库', 'G', 'salorder_status', NULL, 'success', 'N', '0',
'admin', NOW(), '', NULL, '生产完成后已入库,待发货');
```
#### 1.2 表结构修改
**文件:** `2025-11-21_03_周启威_入库单明细表添加销售订单关联字段.sql`
```sql
ALTER TABLE `wm_manufacture_into_entry`
ADD COLUMN `sale_order_entry_id` BIGINT(20) NULL COMMENT '销售订单明细ID' AFTER `manufacture_date`;
ALTER TABLE `wm_manufacture_into_entry`
ADD INDEX `idx_sale_order_entry_id` (`sale_order_entry_id`);
```
### 2. 后端实现 ✅
#### 2.1 实体类修改
**文件:** `ManufactureIntoEntry.java`
新增字段:
```java
/** 销售订单明细ID */
@Excel(name = "销售订单明细ID")
private Long saleOrderEntryId;
```
#### 2.2 Service层实现
**文件:** `ManufactureIntoServiceImpl.java`
**修改内容:**
1. 注入 `SalOrderEntryMapper`
2.`insertManufactureInto()` 方法中添加状态更新调用
3. 新增 `updateSaleOrderStatus()` 方法
**核心代码:**
```java
/**
* 更新销售订单状态为已入库(G)
*/
private void updateSaleOrderStatus(ManufactureInto manufactureInto) {
List<ManufactureIntoEntry> entryList = manufactureInto.getManufactureIntoEntryList();
if (StringUtils.isNotNull(entryList)) {
for (ManufactureIntoEntry entry : entryList) {
if (entry.getSaleOrderEntryId() != null) {
SalOrderEntry salOrderEntry = new SalOrderEntry();
salOrderEntry.setId(entry.getSaleOrderEntryId());
salOrderEntry.setStatus("G"); // G=已入库
salOrderEntryMapper.updateById(salOrderEntry);
}
}
}
}
```
### 3. 前端实现 ✅
#### 3.1 销售订单列表页面
**文件:** `mes-ui/src/views/mes/sale/saleOrder/index.vue`
**修改内容:**
1. **新增"生产入库"按钮**
```vue
<el-col :span="1.5">
<el-button
plain
style="color: #0E7C7B; background-color: #A7E8BD"
icon="el-icon-download"
size="mini"
@click="openManufactureIntoForm()"
:disabled="multiple"
>生产入库</el-button>
</el-col>
```
2. **新增 `openManufactureIntoForm()` 方法**
```javascript
openManufactureIntoForm(){
const selectedEntries = this.saleOrderList.filter(item =>
this.entryIds.includes(item.id)
);
const hasInvalidStatus = selectedEntries.some(item => item.status !== 'F');
if (hasInvalidStatus) {
this.$modal.msgWarning("只能对生产完成(F)状态的订单进行入库操作!");
return;
}
this.$router.push({
path: "/wm/manufactureInto-add/index",
query: { saleOrderEntryIds: this.entryIds.join(',') }
});
}
```
3. **修改 `openSalOutForm()` 方法**
```javascript
// 必须是G(已入库)或E(部分发货)状态才能发货
if(saleOrder[0].status !="G" && saleOrder[0].status !="E"){
this.$modal.msgWarning("只能对已入库(G)或部分发货(E)状态的订单进行发货操作!请重新勾选!")
return;
}
```
#### 3.2 完工入库单表单页面
**文件:** `mes-ui/src/views/mes/warehouse/manufactureInto/form.vue`
**修改内容:**
1. **修改 `created()` 方法**
```javascript
created() {
this.formType=false;
this.getUserList();
this.getWarehouseList();
// 检查是否从销售订单跳转过来
const saleOrderEntryIds = this.$route.query.saleOrderEntryIds;
if (saleOrderEntryIds) {
this.loadFromSaleOrder(saleOrderEntryIds);
}
const id = this.$route.params && this.$route.params.id;
if ( typeof(id) !='undefined' ){
this.getForm(id);
}
}
```
2. **新增 `loadFromSaleOrder()` 方法**
```javascript
async loadFromSaleOrder(entryIds) {
try {
this.$modal.loading("正在加载订单数据...");
const { listEntryByIds } = await import("@/api/mes/sale/saleOrder");
const response = await listEntryByIds({ ids: entryIds });
const saleOrderEntries = response.data;
if (!saleOrderEntries || saleOrderEntries.length === 0) {
this.$modal.msgError("未找到销售订单数据");
return;
}
this.form.manufactureIntoEntryList = saleOrderEntries.map((entry, index) => ({
sort: index + 1,
materialId: entry.materialId,
materialName: entry.materialName,
materialNumber: entry.materialNumber,
specification: entry.materialSpecification,
materialUnitId: entry.unitId,
materialUnitName: entry.unitName,
quantity: entry.quantity,
batchNumber: '',
manufactureDate: new Date().format("yyyy-MM-dd"),
saleOrderEntryId: entry.id
}));
this.maxIndex = saleOrderEntries.length;
this.$modal.closeLoading();
this.$modal.msgSuccess(`已自动填充 ${saleOrderEntries.length} 条订单明细数据`);
} catch (error) {
this.$modal.closeLoading();
console.error('加载销售订单数据失败:', error);
this.$modal.msgError("加载订单数据失败:" + (error.message || "未知错误"));
}
}
```
---
## 测试要点
### 功能测试清单
1.**生产入库按钮**
- 按钮位置正确(工序排产和销售出库之间)
- 按钮样式正确(绿色系)
- 未选中订单时按钮禁用
2.**状态验证**
- 选中非F状态订单提示错误
- 选中F状态订单正常跳转
3.**数据自动填充**
- 明细数据自动填充
- 字段映射正确
- 关联ID保存正确
4.**状态流转**
- 入库后订单状态变为G
- 销售出库只能选G或E状态
- 状态显示正确
### 待测试项
- [ ] 执行数据库脚本
- [ ] 完整流程测试F → G → E/C
- [ ] 并发测试
- [ ] 异常回滚测试
---
## 附录
### A. 相关文件清单
**前端文件:**
- `mes-ui/src/views/mes/sale/saleOrder/index.vue` - 销售订单列表页
- `mes-ui/src/views/mes/warehouse/manufactureInto/form.vue` - 完工入库单表单
- `mes-ui/src/api/mes/sale/saleOrder.js` - 销售订单API
**后端文件:**
- `yjh-mes/src/main/java/cn/sourceplan/sale/service/ISalOrderService.java` - Service接口
- `yjh-mes/src/main/java/cn/sourceplan/sale/service/impl/SalOrderServiceImpl.java` - Service实现
- `yjh-mes/src/main/java/cn/sourceplan/sale/controller/SalOrderController.java` - Controller
**数据库文件:**
- `.sql/upgrade_add_manufacture_into_status.sql` - 升级脚本
### B. 状态码对照表
| 状态码 | 状态名称 | 颜色 | 说明 |
|-------|---------|------|------|
| A | 待生产 | info (灰色) | 订单已创建,待排产 |
| B | 生产中 | warning (橙色) | 已排产,生产中 |
| F | 生产完成 | success (绿色) | 工单已完成,待入库 |
| G | 已入库 | success (绿色) | 已完成入库,待发货 ✨新增 |
| E | 部分发货 | primary (蓝色) | 已部分发货 |
| C | 已发货 | success (绿色) | 已全部发货 |
| D | 已关闭 | danger (红色) | 订单已关闭 |
---
**文档版本:** v1.0
**创建日期:** 2025-11-21
**创建人:** Cascade AI
**最后更新:** 2025-11-21

View File

@@ -0,0 +1,619 @@
# YJH-MES 8Multi协议接入优化文档
> **版本**: v1.0.30
> **更新日期**: 2025-12-01
> **项目**: YJH-MES (yjh-mes + mes-ui)
> **说明**: 本文档基于项目实际代码编写,准确反映当前实现
---
## 一、项目概述
### 1.1 技术栈
| 层级 | 技术 | 说明 |
|------|------|------|
| **后端** | Spring Boot + MyBatis-Plus | yjh-mes模块 |
| **前端** | Vue 2 + Element UI | mes-ui模块 |
| **数据库** | MySQL 8.0 | device / device_data 表 |
### 1.2 协议类型
| 协议 | 数据格式 | 判断条件 | 设备号来源 |
|------|---------|---------|-----------|
| **8ADPRO** | 帧格式 | 以`+`开头且以`EEFF`结尾 | parts[0] |
| **8MULTI** | JSON格式 | 以`{`开头且以`}`结尾 | id字段后4位 |
---
## 二、数据上报接口
### 2.1 接口信息
```
POST /equipment/info/ingest/raw
Content-Type: text/plain
```
### 2.2 8ADPRO帧格式
```
+YAV:设备号,温度,电流,计数1,计数2,模拟1,模拟2,模拟3,模拟4,模拟5,模拟6,模拟7,数字1,数字2,数字3,数字4,数字5,数字6,EEFF
```
**示例**:
```
+YAV:1,25.5,3.2,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,EEFF
```
### 2.3 8MULTI JSON格式
```json
{
"header": "+YAV",
"Card": "8multi",
"id": 101126010002,
"dt": 300,
"a1": 0.106,
"a2": 0.106,
"q1": 0.098,
"q2": 0.098,
"t1": 25.5,
"t2": 26.0,
"dht1": 65.0,
"dht2": 60.0,
"cn_reg": 1,
"f1": 50.00,
"f2": 50.00,
"c1": 5,
"c2": 3,
"do_reg": 0,
"reg1": 0.000,
"reg2": 0.000,
"tv": 0,
"save": 0,
"oee": 100,
"end": "EEFF"
}
```
---
## 三、8MULTI卡号ID格式
```
卡号ID共12位: 品牌1位 + 年份1位 + 月日4位 + 用户号2位 + 序号4位
示例: 101126010002
1 - 品牌前缀
0 - 年份(0=2025, 1=2026, 2=2027...)
1126 - 月日(11月26日)
01 - 用户号
0002 - 设备序号后端使用后4位作为设备号
```
---
## 四、8MULTI JSON字段映射
### 4.1 字段对照表
| JSON字段 | 数据库字段 | 含义 | 说明 |
|---------|-----------|------|------|
| id | device_no | 卡号 | 取后4位作为设备号 |
| a1 | current1_value | 电流1 | 已换算实际值(A) |
| a2 | current2_value | 电流2 | 已换算实际值(A) |
| q1 | quality1_value | 质量1 | 已换算实际值 |
| q2 | quality2_value | 质量2 | 已换算实际值 |
| t1 | temperature_c | 温度1 | °C |
| t2 | analog4 | 温度2 | °C(预留) |
| dht1 | humidity | 湿度1 | % |
| dht2 | analog5 | 湿度2 | %(预留) |
| c1 | counter1 | 计数1 | 发送完清零 |
| c2 | counter2 | 计数2 | 发送完清零 |
| f1 | f1 | 测频1 | Hz |
| f2 | f2 | 测频2 | Hz |
| cn_reg | cn_reg | 屏幕状态 | 0-7 |
| do_reg | do_reg | 状态输出 | 位编码 |
| reg1 | reg1 | 从机数据1 | |
| reg2 | reg2 | 从机数据2 | |
| tv | tv | 显示 | 1有0无 |
| save | save_flag | 边缘存储 | 1有0无 |
| oee | oee | OEE计算 | 0或100 |
| dt | dt | 采样间隔 | 秒 |
### 4.2 cn_reg屏幕状态定义
| 值 | 含义 | 前端颜色 |
|----|------|---------|
| 0 | 计划停机 | 灰色(info) |
| 1 | 正常工作 | 绿色(success) |
| 2 | 待机 | 黄色(warning) |
| 3 | 故障 | 红色(danger) |
| 4 | 在修 | 黄色(warning) |
| 5 | 缺人 | 黄色(warning) |
| 6 | 缺料 | 黄色(warning) |
| 7 | 清零 | 灰色(info) |
### 4.3 cn_reg自动判断逻辑下位机执行
```
cn_reg=0计划停机: 3次都是 [(a1'<20%) & (a2'<20%)] || [(f1=0) & (f2=0)]
cn_reg=1正常工作: 3次都是 (a1'>40%) || (a2'>40%) || (f1>0.1) || (f2>0.1)
cn_reg=2待机 : 3次都是 (a1介于20-40%) || (a2介于20-40%)
cn_reg=3-7 : 人工设置(触摸屏点击)
```
### 4.4 OEE计算规则
```
下位机逻辑: if(cn_reg=1) oee=100; else oee=0;
上位机: 计算时间段内oee均值
```
### 4.5 上报频率
```
1. 开机上发一次dt=0
2. 开机60s内10s发一次
3. 开机超过60s按设置间隔发送默认300s
4. 计数值改变时上发间隔至少60sdt=60
5. 不接计数器时300s发一次dt=300
```
---
## 五、数据库设计
### 5.1 device表扩展字段
```sql
-- 协议类型
ALTER TABLE `device` ADD COLUMN `protocol_type` VARCHAR(16) DEFAULT '8ADPRO'
COMMENT '协议类型(8ADPRO/8MULTI)';
-- 量程配置(用于超量程警告)
ALTER TABLE `device` ADD COLUMN `current1_range_start` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `current1_range_end` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `current2_range_start` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `current2_range_end` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality1_range_start` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality1_range_end` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality2_range_start` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality2_range_end` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality1_unit` VARCHAR(10) DEFAULT 'kg';
ALTER TABLE `device` ADD COLUMN `quality2_unit` VARCHAR(10) DEFAULT 'kg';
ALTER TABLE `device` ADD COLUMN `current_unit` VARCHAR(10) DEFAULT 'A';
ALTER TABLE `device` ADD COLUMN `voltage1` DECIMAL(10,3) DEFAULT NULL COMMENT '电压1(用于功率计算)';
ALTER TABLE `device` ADD COLUMN `voltage2` DECIMAL(10,3) DEFAULT NULL COMMENT '电压2(用于功率计算)';
ALTER TABLE `device` ADD COLUMN `counter1_baseline` INT DEFAULT 0 COMMENT '计数1清零基准';
ALTER TABLE `device` ADD COLUMN `counter2_baseline` INT DEFAULT 0 COMMENT '计数2清零基准';
-- 设备扩展信息
ALTER TABLE `device` ADD COLUMN `location` VARCHAR(255) DEFAULT NULL COMMENT '设备位置';
ALTER TABLE `device` ADD COLUMN `brand` VARCHAR(128) DEFAULT NULL COMMENT '设备品牌';
ALTER TABLE `device` ADD COLUMN `model` VARCHAR(128) DEFAULT NULL COMMENT '设备型号';
ALTER TABLE `device` ADD COLUMN `workshop_id` BIGINT DEFAULT NULL COMMENT '车间ID(关联md_workshop)';
ALTER TABLE `device` ADD COLUMN `section` VARCHAR(64) DEFAULT NULL COMMENT '工序';
```
### 5.2 device_data表扩展字段
```sql
-- 8Multi V2.8 专用字段
ALTER TABLE `device_data` ADD COLUMN `current1_value` DECIMAL(10,3) DEFAULT NULL COMMENT '电流1实际值';
ALTER TABLE `device_data` ADD COLUMN `current2_value` DECIMAL(10,3) DEFAULT NULL COMMENT '电流2实际值';
ALTER TABLE `device_data` ADD COLUMN `quality1_value` DECIMAL(10,3) DEFAULT NULL COMMENT '质量1实际值';
ALTER TABLE `device_data` ADD COLUMN `quality2_value` DECIMAL(10,3) DEFAULT NULL COMMENT '质量2实际值';
ALTER TABLE `device_data` ADD COLUMN `humidity` DECIMAL(10,3) DEFAULT NULL COMMENT '湿度';
ALTER TABLE `device_data` ADD COLUMN `f1` DECIMAL(10,2) DEFAULT NULL COMMENT '测频1(Hz)';
ALTER TABLE `device_data` ADD COLUMN `f2` DECIMAL(10,2) DEFAULT NULL COMMENT '测频2(Hz)';
ALTER TABLE `device_data` ADD COLUMN `cn_reg` INT DEFAULT NULL COMMENT '屏幕状态(0-7)';
ALTER TABLE `device_data` ADD COLUMN `do_reg` VARCHAR(16) DEFAULT NULL COMMENT '状态输出';
ALTER TABLE `device_data` ADD COLUMN `reg1` DECIMAL(12,3) DEFAULT NULL COMMENT '从机数据1';
ALTER TABLE `device_data` ADD COLUMN `reg2` DECIMAL(12,3) DEFAULT NULL COMMENT '从机数据2';
ALTER TABLE `device_data` ADD COLUMN `tv` INT DEFAULT NULL COMMENT '显示';
ALTER TABLE `device_data` ADD COLUMN `save_flag` INT DEFAULT NULL COMMENT '边缘存储';
ALTER TABLE `device_data` ADD COLUMN `oee` INT DEFAULT NULL COMMENT 'OEE计算值';
ALTER TABLE `device_data` ADD COLUMN `dt` INT DEFAULT NULL COMMENT '采样间隔(秒)';
ALTER TABLE `device_data` ADD COLUMN `power1` DECIMAL(12,3) DEFAULT NULL COMMENT '功率1';
ALTER TABLE `device_data` ADD COLUMN `power2` DECIMAL(12,3) DEFAULT NULL COMMENT '功率2';
```
### 5.3 计数器触发器仅8ADPRO使用
```sql
DROP TRIGGER IF EXISTS `update_counter_totals_on_insert`;
DELIMITER $$
CREATE TRIGGER `update_counter_totals_on_insert`
BEFORE INSERT ON `device_data`
FOR EACH ROW
BEGIN
DECLARE last_c1 BIGINT UNSIGNED DEFAULT 0;
DECLARE last_c2 BIGINT UNSIGNED DEFAULT 0;
DECLARE proto VARCHAR(16) DEFAULT '8ADPRO';
SELECT IFNULL(protocol_type, '8ADPRO') INTO proto FROM device WHERE id = NEW.device_id LIMIT 1;
-- 仅8ADPRO使用触发器累计
IF proto = '8ADPRO' OR proto IS NULL THEN
SELECT IFNULL(counter1_total, 0), IFNULL(counter2_total, 0) INTO last_c1, last_c2
FROM device_data WHERE device_id = NEW.device_id ORDER BY collected_at DESC LIMIT 1;
SET NEW.counter1_total = IF(NEW.counter1 > 0, last_c1 + NEW.counter1, last_c1);
SET NEW.counter2_total = IF(NEW.counter2 > 0, last_c2 + NEW.counter2, last_c2);
END IF;
END$$
DELIMITER ;
```
---
## 六、后端核心代码
### 6.1 文件清单
```
yjh-mes/src/main/java/cn/sourceplan/equipment/
├── controller/
│ └── EquipmentInfoController.java # 设备信息控制器
├── service/
│ ├── IEquipmentInfoService.java # 服务接口
│ ├── Multi8ProtocolService.java # 8Multi协议解析服务 ⭐
│ └── impl/
│ └── EquipmentInfoServiceImpl.java # 服务实现
├── domain/
│ ├── MesDevice.java # 设备实体
│ └── DeviceData.java # 设备数据实体
└── mapper/
├── MesDeviceMapper.java # 设备Mapper
└── DeviceDataMapper.java # 数据Mapper
```
### 6.2 协议判断入口 (EquipmentInfoServiceImpl.java)
```java
@Override
@Transactional(rollbackFor = Exception.class)
public void ingestFrame(String frameRaw) {
if (StringUtils.isBlank(frameRaw)) throw new IllegalArgumentException("frame is empty");
String raw = frameRaw.trim();
// 8Multi: 以{开头且以}结尾(JSON格式)
if (raw.startsWith("{") && raw.endsWith("}")) {
log.info("检测到8Multi协议(JSON格式)");
multi8ProtocolService.parse8MultiJson(raw);
return;
}
// 8AdPro: 以+开头且以EEFF结尾(帧格式)
if (!raw.startsWith("+") || !raw.endsWith("EEFF")) {
throw new IllegalArgumentException("协议格式无效");
}
// ... 8ADPRO处理逻辑
}
```
### 6.3 8Multi JSON解析 (Multi8ProtocolService.java)
```java
public void parse8MultiJson(String rawJson) {
JSONObject json = JSONUtil.parseObj(rawJson);
// 1. 提取设备号ID后4位
String idStr = json.getStr("id");
String lastFour = idStr.substring(idStr.length() - 4);
Integer deviceNo = Integer.valueOf(lastFour);
// 2. 查找或创建设备
MesDevice device = findOrCreateDevice(deviceNo);
// 3. 构建DeviceData
DeviceData data = new DeviceData();
data.setDeviceId(device.getId());
data.setCollectedAt(new Timestamp(System.currentTimeMillis()));
data.setFrameRaw(rawJson);
// 4. 字段映射(下位机已换算,直接存储)
data.setCurrent1Value(parseDecimal(json.getStr("a1")));
data.setCurrent2Value(parseDecimal(json.getStr("a2")));
data.setQuality1Value(parseDecimal(json.getStr("q1")));
data.setQuality2Value(parseDecimal(json.getStr("q2")));
data.setTemperatureC(parseDecimal(json.getStr("t1")));
data.setHumidity(parseDecimal(json.getStr("dht1")));
data.setCounter1(parseInt(json.getStr("c1")));
data.setCounter2(parseInt(json.getStr("c2")));
data.setF1(parseDecimal(json.getStr("f1")));
data.setF2(parseDecimal(json.getStr("f2")));
data.setCnReg(parseInt(json.getStr("cn_reg")));
data.setDoReg(json.getStr("do_reg"));
data.setReg1(parseDecimal(json.getStr("reg1")));
data.setReg2(parseDecimal(json.getStr("reg2")));
data.setTv(parseInt(json.getStr("tv")));
data.setSaveFlag(parseInt(json.getStr("save")));
data.setOee(parseInt(json.getStr("oee")));
data.setDt(parseInt(json.getStr("dt")));
// 5. 计数器累计
DeviceData lastData = deviceDataMapper.selectLastByDeviceId(device.getId());
Long counter1Total = (lastData != null ? lastData.getCounter1Total() : 0L);
Long counter2Total = (lastData != null ? lastData.getCounter2Total() : 0L);
if (data.getCounter1() != null && data.getCounter1() > 0) {
counter1Total += data.getCounter1();
}
if (data.getCounter2() != null && data.getCounter2() > 0) {
counter2Total += data.getCounter2();
}
data.setCounter1Total(counter1Total);
data.setCounter2Total(counter2Total);
// 6. 保存
deviceDataMapper.insert(data);
}
```
### 6.4 OEE均值查询 (DeviceDataMapper.java)
```java
@Select({
"<script>",
"SELECT AVG(oee) FROM device_data ",
"WHERE device_id = #{deviceId} AND oee IS NOT NULL",
"<if test='from != null and from != \"\"'>",
" AND collected_at &gt;= #{from}",
"</if>",
"<if test='to != null and to != \"\"'>",
" AND collected_at &lt;= #{to}",
"</if>",
"</script>"
})
Double selectOeeAvg(@Param("deviceId") Long deviceId, @Param("from") String from, @Param("to") String to);
```
---
## 七、前端实现
### 7.1 文件位置
```
mes-ui/src/views/mes/equipment/info/index.vue # 设备监控页面
mes-ui/src/api/mes/equipment/info.js # API接口
```
### 7.2 API接口
```javascript
// 获取设备OEE均值
export function getOeeAvg(deviceId, from, to) {
return request({
url: '/equipment/info/oee/avg',
method: 'get',
params: { deviceId, from, to }
})
}
```
### 7.3 协议类型判断显示
```vue
<!-- 8ADPRO设备显示 -->
<div v-if="!d.protocol_type || d.protocol_type === '8ADPRO'" class="metrics">
<!-- 温度电流计数器等 -->
</div>
<!-- 8MULTI设备显示 -->
<div v-else-if="d.protocol_type === '8MULTI'" class="metrics">
<!-- 电流1/2质量1/2温湿度OEE状态等 -->
</div>
```
### 7.4 cn_reg状态显示
```javascript
// 状态文本
cnRegText(val) {
const map = {
0: '计划停机', 1: '正常工作', 2: '待机', 3: '故障',
4: '在修', 5: '缺人', 6: '缺料', 7: '清零'
}
return map[val] || '未知'
},
// 状态颜色
cnRegType(val) {
const map = {
0: 'info', 1: 'success', 2: 'warning', 3: 'danger',
4: 'warning', 5: 'warning', 6: 'warning', 7: 'info'
}
return map[val] || 'info'
}
```
### 7.5 超量程红色警告
```javascript
// 判断是否超量程
isOutOfRange(value, rangeStart, rangeEnd) {
if (value === null || value === undefined) return false
if (rangeStart === null && rangeEnd === null) return false
if (rangeStart !== null && value < rangeStart) return true
if (rangeEnd !== null && value > rangeEnd) return true
return false
}
```
```vue
<!-- 超量程显示红色 -->
<span :style="{color: isOutOfRange(d.current1_value, d.current1RangeStart, d.current1RangeEnd) ? '#F56C6C' : ''}">
{{ fmt(d.current1_value, 'A', 2) }}
</span>
```
### 7.6 设备编辑表单扩展
新增字段:
- **设备位置** (location)
- **设备品牌** (brand)
- **设备型号** (model)
- **所属车间** (workshopId) - 从md_workshop表读取
- **所属工序** (section) - 从pro_process表读取
- **量程配置** (current1/2RangeStart/End, quality1/2RangeStart/End) - 用于超量程警告
---
## 八、废弃字段说明
8Multi V2.8协议中不再使用但保留以兼容8ADPRO
| 废弃字段 | 替代方案 | 说明 |
|---------|---------|------|
| current1_raw, current2_raw | current1_value | 下位机已换算 |
| quality1_raw, quality2_raw | quality1_value | 下位机已换算 |
| status_work/stop/fault/reset | cn_reg (0-7) | 统一用屏幕状态 |
| frequency1, frequency2 | f1, f2 | 使用新测频字段 |
| counter1_total_8multi | counter1_total | 统一使用 |
| relay_power/alarm, do_soft_start/stop | do_reg | 统一用状态输出 |
| slave_data1/2 | reg1, reg2 | 使用新从机数据字段 |
| touchscreen_data | tv | 使用新显示字段 |
---
## 九、设备OEE功能
### 9.1 OEE页面改造
将设备OEE页面与8ADPRO/8MULTI设备关联不再使用车间设备。
**文件清单**
```
mes-ui/src/views/mes/equipment/oeeGantt/index.vue # OEE时序图页面
mes-ui/src/api/mes/equipment/equipment.js # API接口
yjh-mes/.../controller/EquipmentController.java # 控制器
yjh-mes/.../service/impl/EquipmentServiceImpl.java # 服务实现
yjh-mes/.../mapper/EquipmentMapper.java # Mapper接口
yjh-mes/.../resources/mapper/equipment/EquipmentMapper.xml # SQL
```
### 9.2 OEE页面筛选功能
```vue
<!-- 协议类型筛选 -->
<el-select v-model="queryParams.deviceType" @change="onDeviceTypeChange">
<el-option label="全部" value="" />
<el-option label="8ADPRO" value="8ADPRO" />
<el-option label="8MULTI" value="8MULTI" />
</el-select>
<!-- 设备筛选 -->
<el-select v-model="queryParams.deviceId" filterable>
<el-option v-for="d in deviceList" :key="d.id" :label="d.name" :value="d.id" />
</el-select>
```
### 9.3 设备列表API
```java
// EquipmentController.java
@GetMapping("/allDevices")
public AjaxResult listAllDevices(String deviceType) {
return success(equipmentService.listAllDevices(deviceType));
}
```
```xml
<!-- EquipmentMapper.xml -->
<select id="selectMesDevices" resultType="Map">
SELECT id, device_no, device_name, protocol_type
FROM device WHERE is_active = 1
<if test="deviceType != null and deviceType != ''">
AND protocol_type = #{deviceType}
</if>
ORDER BY device_no
</select>
```
### 9.4 OEE数据查询
```xml
<!-- EquipmentMapper.xml -->
<select id="selectMesDeviceOee" parameterType="Map" resultType="Map">
SELECT
d.id as equipment_id,
d.device_name as equipment_name,
d.device_no as equipment_number,
dd.collected_at as record_time,
dd.cn_reg as equipment_status,
dd.oee as oee_value,
d.protocol_type
FROM device d
LEFT JOIN device_data dd ON dd.device_id = d.id
WHERE d.is_active = 1
AND dd.collected_at IS NOT NULL
AND dd.collected_at >= #{map.beginDate}
AND dd.collected_at <= #{map.endDate}
<if test="map.deviceType != null and map.deviceType != ''">
AND d.protocol_type = #{map.deviceType}
</if>
<if test="map.deviceId != null and map.deviceId != ''">
AND d.id = #{map.deviceId}
</if>
ORDER BY d.id DESC, dd.collected_at DESC
</select>
```
### 9.5 OEE状态映射
cn_reg值映射到OEE图表状态
| cn_reg值 | 含义 | OEE状态 | 颜色 |
|----------|------|---------|------|
| 1 | 正常工作 | 运行中 | 绿色(#67C23A) |
| 3 | 故障 | 故障 | 红色(#F56C6C) |
| 0,2,4,5,6,7 | 其他 | 停机 | 灰色(#909399) |
```java
// EquipmentServiceImpl.java - 状态转换
Integer cnReg = ((Number)cnRegObj).intValue();
Integer status;
switch (cnReg) {
case 1: status = 0; break; // 正常工作 -> 运行中
case 3: status = 2; break; // 故障
default: status = 1; break; // 其他 -> 停机
}
```
### 9.6 前端调整
**移除的功能**
- 设备卡片上的OEE显示
- 设备详情中的OEE时段均值查询
**保留的功能**
- OEE时序图页面独立页面
- 设备类型/设备筛选
- 时序图和饼状图显示
- 设备状态统计卡片
---
## 十、计数器处理差异
| 协议 | counter1/2值 | 累计方式 | 触发器 |
|------|-------------|---------|--------|
| **8ADPRO** | limitToBinary(>0→1) | 数据库触发器累加 | ✅使用 |
| **8MULTI** | 原始增量值 | Java代码累加 | ❌不使用 |
---
## 十一、注意事项
1. **8MULTI协议识别**: 根据数据格式判断JSON格式即为8MULTI
2. **设备号提取**: 8MULTI使用ID后4位作为设备号
3. **量程配置**: 8MULTI下位机已换算量程配置仅用于超量程警告显示
4. **计数器清零**: 通过设置baseline实现软清零
5. **OEE页面**: 仅显示8ADPRO/8MULTI设备不显示车间设备
6. **OEE状态**: 基于cn_reg字段判断1=运行中3=故障,其他=停机)
7. **兼容性**: 所有修改不影响8ADPRO原有功能

View File

@@ -0,0 +1,111 @@
# 设备维修单页面增加附件、富文本功能
## 功能说明
为设备维修单添加富文本编辑和附件上传功能,支持以下内容:
1. 在维修单主表增加富文本编辑器,用于详细描述维修内容(维修描述)
2. 在维修单明细中增加富文本编辑器,用于详细描述每项维修说明
3. 支持上传附件到维修单(通过附件管理按钮)
4. 支持查看、下载和删除已上传的附件
5. 富文本编辑器支持插入图片(自动上传到服务器)
## 数据库变更
### 新增表
1. `dm_repair_order_attachment` - 维修单附件表
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | bigint | 主键ID |
| repair_order_id | bigint | 维修单ID |
| entry_id | bigint | 明细ID可为空表示主单附件 |
| file_name | varchar(255) | 文件名称 |
| file_path | varchar(1000) | 文件路径(格式:/profile/upload/yyyy/MM/dd/xxx.jpg |
| file_size | bigint | 文件大小(字节) |
| file_type | varchar(100) | 文件类型 |
| create_by | varchar(32) | 创建人 |
| create_time | datetime | 创建时间 |
| update_by | varchar(32) | 更新人 |
| update_time | datetime | 更新时间 |
| status | char(1) | 状态0正常 1删除 |
| remark | varchar(500) | 备注 |
### 表结构变更
1. `dm_repair_order` 主表新增字段:
- `rich_text_content` longtext - 富文本内容/维修描述位于remark字段之后
2. `dm_repair_order_entry` 明细表新增字段:
- `rich_text_content` longtext - 富文本内容/维修说明位于fault_name字段之后
## API 变更
### 新增接口
1. `POST /equipment/repairOrder/attachment/upload` - 上传附件
- 参数file文件、repairOrderId维修单ID、entryId明细ID可选
- 返回:附件信息
2. `GET /equipment/repairOrder/attachment/download/{id}` - 下载附件(匿名访问)
3. `GET /equipment/repairOrder/attachment/list/{repairOrderId}` - 获取附件列表
4. `DELETE /equipment/repairOrder/attachment/{ids}` - 删除附件
### 修改接口
1. `POST /equipment/repairOrder` - 支持保存富文本内容和附件信息
2. `PUT /equipment/repairOrder` - 支持更新富文本内容
## 前端修改
### 主表单
1. 新增"维修描述"字段使用Editor富文本编辑器组件
### 明细表格
1. 新增"维修说明"列,点击弹出富文本编辑对话框
2. 新增"附件管理"按钮,点击弹出附件管理对话框
### 附件管理对话框
1. 支持上传附件(新增模式:临时存储,保存时一起提交;修改模式:直接上传到服务器)
2. 显示附件列表(文件名、大小、上传时间)
3. 支持下载和删除附件
### 查看详情对话框
1. 显示主表维修描述(富文本渲染)
2. 明细列表中显示"维修说明"查看按钮
3. 显示附件列表,支持下载
### Editor组件修复
1. 修复图片上传按钮点击无效的问题v-if条件修复
2. 兼容不同版本Element UI的文件选择触发方式
## 约束条件
- 单个文件大小限制50MB
- 支持的文件类型图片jpg/png/gif、文档doc/docx/pdf/xls/xlsx、压缩包zip/rar
- 富文本图片大小限制5MB
## 涉及文件
### 后端
- `RepairOrder.java` - 主表实体新增richTextContent和attachmentList字段
- `RepairOrderEntry.java` - 明细实体新增richTextContent字段
- `RepairOrderAttachment.java` - 附件实体(新增)
- `RepairOrderAttachmentMapper.java` - 附件Mapper接口新增
- `RepairOrderAttachmentMapper.xml` - 附件Mapper XML新增
- `IRepairOrderAttachmentService.java` - 附件Service接口新增
- `RepairOrderAttachmentServiceImpl.java` - 附件Service实现新增
- `RepairOrderController.java` - 新增附件上传/下载/删除接口
- `RepairOrderServiceImpl.java` - 新增时处理附件保存
- `RepairOrderMapper.xml` - 查询时映射richTextContent字段
### 前端
- `repairOrder.js` - 新增getAttachmentList、deleteAttachment接口
- `index.vue` - 主页面,新增富文本编辑、附件管理、查看详情功能
- `Editor/index.vue` - 修复图片上传功能
### SQL脚本
- `2025-12-05_v1.0.37_周启威_设备维修单增加附件、富文本.sql`
## 测试要点
1. 主表富文本编辑和保存功能
2. 明细富文本编辑和保存功能
3. 富文本中插入图片功能
4. 新增模式下附件上传和保存
5. 修改模式下附件上传和删除
6. 附件下载功能
7. 查看详情页面富文本和附件展示
8. 文件大小和类型校验

View File

@@ -0,0 +1,372 @@
# 普罗生物MES报警页面 - LabVIEW对接
> 版本v1.0.46
> 日期2025-12-13
> 状态:需求分析
---
## 一、需求背景
建设基于参数判断的设备故障分析与预警系统实现LabVIEW数据采集端与MES系统的对接完成故障数据的记录、展示和趋势分析功能。
---
## 二、系统架构
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ LabVIEW │ │ 数据库 │ │ MES │
│ 数据采集端 │ ──▶ │ 故障记录表 │ ──▶ │ 报警页面 │
│ (报警运算) │ │ │ │ (读取展示) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
**数据流向:**
1. LabVIEW采集设备传感器数据
2. LabVIEW进行报警运算三次一致性确认
3. 超限数据写入数据库故障记录表
4. MES读取数据库并展示
---
## 三、数据库设计
### 3.1 故障记录表 (device_fault_record)
| 字段名 | 类型 | 说明 | 示例 | 来源 |
|--------|------|------|------|------|
| id | bigint | 主键ID | 1 | 自增 |
| device_name | varchar(100) | 设备名称 | 发酵罐1号 | LabVIEW |
| sensor_name | varchar(100) | 传感器名/通道 | 温度传感器-CH01 | LabVIEW |
| device_status | tinyint | 设备状态0-停机1-待机2-生产 | 2 | LabVIEW |
| nominal_value | decimal(10,2) | 标称值(标准/理想值) | 25.00 | LabVIEW |
| normal_min | decimal(10,2) | 正常范围下限(下限) | 20.00 | LabVIEW |
| normal_max | decimal(10,2) | 正常范围上限(上限) | 30.00 | LabVIEW |
| lower_lower_limit | decimal(10,2) | 下下限(严重超限下限) | 15.00 | LabVIEW |
| upper_upper_limit | decimal(10,2) | 上上限(严重超限上限) | 35.00 | LabVIEW |
| actual_value | decimal(10,2) | 实测值 | 35.50 | LabVIEW |
| deviation_rate | decimal(10,2) | 偏离度(%)MES计算 | 18.33 | MES计算 |
| fault_level | tinyint | 故障等级1-轻微2-一般3-严重 | 2 | LabVIEW |
| fault_tag | varchar(200) | 故障标注/描述 | 温度超上限 | LabVIEW |
| analysis_type | tinyint | 分析类型1-瞬态2-趋势 | 1 | LabVIEW |
| fault_time | datetime | 故障发生时间 | 2025-12-13 10:00:00 | LabVIEW |
| creator | varchar(64) | 创建者LabVIEW写入时可为系统 | labview | LabVIEW |
| create_time | datetime | 记录创建时间 | 2025-12-13 10:00:05 | LabVIEW |
| updater | varchar(64) | 更新者 | | MES |
| update_time | datetime | 更新时间 | | MES |
| deleted | bit | 是否删除 | 0 | 默认0 |
### 3.2 门限说明
```
门限层级(从内到外):
├── 标称值 (nominal_value):标准/理想值
├── 正常范围normal_min ~ normal_max一般报警
└── 严重范围lower_lower_limit ~ upper_upper_limit严重报警
故障等级判断:
- 实测值在 normal_min ~ normal_max 范围内:正常
- 实测值超出 normal 但在 upper_upper/lower_lower 范围内:一般故障
- 实测值超出 upper_upper_limit 或 lower_lower_limit严重故障
```
### 3.3 偏离度计算公式MES端计算
```
偏离度(%) = (实测值 - 门限值) / 门限值 × 100
其中门限值:
- 实测值 > 正常上限时,门限值 = 正常上限 (normal_max)
- 实测值 < 正常下限时,门限值 = 正常下限 (normal_min)
- 实测值在正常范围内时,门限值 = 标称值 (nominal_value)
```
---
## 四、MES功能设计
### 4.1 故障记录页面(含趋势分析)
**页面路径:** `/mes/device/fault-record`
**页面布局:**
```
┌─────────────────────────────────────────────────────────┐
│ 查询条件:设备名、传感器名、设备状态、故障等级、时间范围 │
├─────────────────────────────────────────────────────────┤
│ 趋势分析图表区域 │
│ (圆滑折线图,显示实测值走势和门限参考线) │
├─────────────────────────────────────────────────────────┤
│ 故障记录列表区域 │
│ (分页表格,展示故障详情) │
└─────────────────────────────────────────────────────────┘
```
**功能点:**
- [x] 列表展示故障记录
- [x] 支持分页查询
- [x] 支持条件筛选:设备名、传感器名、设备状态、故障等级、时间范围
- [x] 支持导出Excel
- [x] 趋势分析图表(圆滑折线图)
**列表字段:**
| 序号 | 字段 | 说明 |
|------|------|------|
| 1 | 设备名称 | device_name |
| 2 | 传感器/通道 | sensor_name |
| 3 | 设备状态 | 停机/待机/生产 |
| 4 | 标称值 | nominal_value |
| 5 | 正常范围 | normal_min ~ normal_max |
| 6 | 严重范围 | lower_lower_limit ~ upper_upper_limit |
| 7 | 实测值 | actual_value |
| 8 | 偏离度 | deviation_rate%MES计算 |
| 9 | 故障等级 | 轻微/一般/严重(带颜色标识) |
| 10 | 故障标注 | fault_tag |
| 11 | 故障时间 | fault_time |
### 4.2 趋势分析图表(集成在故障记录页面)
**图表类型:** 圆滑折线图smooth line chart
**功能点:**
- [x] 设备下拉选择
- [x] 传感器/通道下拉选择(联动设备)
- [x] 时间范围选择(按天)
- [x] 圆滑折线图展示趋势
- [x] 显示门限参考线
**图表设计:**
```
Y轴实测值
X轴时间按天聚合
曲线圆滑折线smooth: true
参考线:
- 上上限(红色虚线)
- 上限(橙色虚线)
- 标称值(绿色实线)
- 下限(橙色虚线)
- 下下限(红色虚线)
数据点:每日平均值/最大值/最小值
```
### 4.3 故障预测(二期)
基于趋势数据的二次分析:
- 线性回归预测
- 异常趋势识别
- 预警通知
---
## 五、接口设计
### 5.1 LabVIEW写入接口数据库直连
LabVIEW直接写入数据库表 `device_fault_record`,需提供:
- 数据库连接信息
- 表结构DDL脚本
### 5.2 MES读取接口
#### 5.2.1 故障记录分页查询
```
GET /admin-api/mes/device-fault-record/page
请求参数:
- deviceName: 设备名称(模糊查询)
- sensorName: 传感器名(模糊查询)
- deviceStatus: 设备状态
- faultLevel: 故障等级
- faultTimeStart: 故障时间开始
- faultTimeEnd: 故障时间结束
- pageNo: 页码
- pageSize: 每页条数
响应:
{
"code": 0,
"data": {
"list": [...],
"total": 100
}
}
```
#### 5.2.2 设备列表(下拉用)
```
GET /admin-api/mes/device-fault-record/device-list
响应:
{
"code": 0,
"data": ["发酵罐1号", "发酵罐2号", ...]
}
```
#### 5.2.3 传感器列表(下拉用,按设备筛选)
```
GET /admin-api/mes/device-fault-record/sensor-list
请求参数:
- deviceName: 设备名称
响应:
{
"code": 0,
"data": ["温度传感器-CH01", "压力传感器-CH02", ...]
}
```
#### 5.2.4 趋势数据查询
```
GET /admin-api/mes/device-fault-record/trend
请求参数:
- deviceName: 设备名称(必填)
- sensorName: 传感器名(必填)
- startDate: 开始日期yyyy-MM-dd
- endDate: 结束日期yyyy-MM-dd
响应:
{
"code": 0,
"data": {
"normalMin": 20.00,
"normalMax": 30.00,
"trendList": [
{
"date": "2025-12-01",
"avgValue": 25.5,
"maxValue": 28.0,
"minValue": 23.0,
"faultCount": 2
},
...
]
}
}
```
---
## 六、LabVIEW端说明
### 6.1 报警运算逻辑
```
三次一致性确认机制:
├── 采集周期10秒/次
├── 使用全局变量 + 移位寄存器
├── 连续3次超限才触发报警
└── 目的:抗干扰,避免误报
```
### 6.2 标准范围配置
- 存储位置:`.xls` 文件
- 便于现场升级和修订
- 包含字段:设备名、传感器名、正常下限、正常上限、故障等级规则
### 6.3 数据写入规范
LabVIEW写入数据库时需填写以下字段
- device_name必填
- sensor_name必填
- device_status必填
- normal_min必填
- normal_max必填
- actual_value必填
- deviation_rate必填需计算
- fault_level必填
- fault_tag选填
- analysis_type必填默认1-瞬态)
- fault_time必填
- creator固定为 'labview'
- create_time当前时间
- deleted固定为 0
- tenant_id固定为 1
---
## 七、开发任务清单
### 7.1 后端开发
| 序号 | 任务 | 优先级 | 状态 |
|------|------|--------|------|
| 1 | 创建数据库表 device_fault_record | P0 | 待开发 |
| 2 | 创建实体类 DeviceFaultRecordDO | P0 | 待开发 |
| 3 | 创建 Mapper 接口 | P0 | 待开发 |
| 4 | 创建 Service 层 | P0 | 待开发 |
| 5 | 创建 Controller 接口 | P0 | 待开发 |
| 6 | 趋势数据聚合查询 | P1 | 待开发 |
### 7.2 前端开发
| 序号 | 任务 | 优先级 | 状态 |
|------|------|--------|------|
| 1 | 故障记录列表页面 | P0 | 待开发 |
| 2 | 查询表单组件 | P0 | 待开发 |
| 3 | 趋势分析图表 | P1 | 待开发 |
| 4 | 导出功能 | P2 | 待开发 |
### 7.3 对接工作
| 序号 | 任务 | 负责方 | 状态 |
|------|------|--------|------|
| 1 | 提供数据库连接信息 | MES | 待提供 |
| 2 | 提供DDL脚本 | MES | 待提供 |
| 3 | LabVIEW数据写入测试 | LabVIEW | 待测试 |
| 4 | 联调验证 | 双方 | 待联调 |
---
## 八、后续规划YIOT升级FIIH
```
当前方案YIOT数据采集 → LabVIEW处理 → 数据库 → MES
升级方案FIIH内置门限 → UMS软件处理 → 数据库 → MES
```
升级后优势:
- 门限参数内置于采集设备
- 减少中间处理环节
- 普罗生物定制UMS软件直接处理
---
## 九、附录
### 9.1 设备状态枚举
```java
public enum DeviceStatusEnum {
SHUTDOWN(0, "停机"),
STANDBY(1, "待机"),
PRODUCTION(2, "生产");
}
```
### 9.2 故障等级枚举
```java
public enum FaultLevelEnum {
MINOR(1, "轻微"),
NORMAL(2, "一般"),
SEVERE(3, "严重");
}
```
### 9.3 分析类型枚举
```java
public enum AnalysisTypeEnum {
TRANSIENT(1, "瞬态"),
TREND(2, "趋势");
}
```

View File

@@ -0,0 +1,673 @@
# FIIH质量管理模块菜单改造方案无需修改数据库
## 1. 需求背景
目前FIIH质量管理模块的所有质检数据都在同一个质检配置表中展示需求是将不同对象体(`fiih_object_name`)的质检数据拆分为独立的二级菜单,并实现自动创建这些二级菜单的功能。且要求不修改数据库结构,不添加新表。
## 2. 现有结构分析
### 2.1 菜单结构
当前FIIH质量管理模块的菜单结构
- FIIH质量管理一级菜单ID: 2421
- 质检配置表二级菜单ID: 2422
### 2.2 相关数据表
- `sys_menu`: 系统菜单表
- `ymes_fiih_config`: FIIH质量管理配置表包含fiih_object_name字段
- `sys_config`: 系统参数表(用于存储菜单相关配置)
### 2.3 代码结构
- 前端:
- 质检配置表页面:`mes-ui/src/views/mes/fiih/index.vue`
- 质检详情页面:`mes-ui/src/views/mes/fiih/fiihDetailByTaskId.vue`
- API接口`mes-ui/src/api/mes/fiih/fiihConfig.js`
- 后端:
- 控制器:`FiihConfigController.java`
- 服务接口:`IFiihConfigService.java`
- 服务实现:`FiihConfigServiceImpl.java`
### 2.4 FIIH配置表结构
```sql
CREATE TABLE `ymes_fiih_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`fiih_object_name` varchar(50) NOT NULL COMMENT '对象体名称', -- 作为菜单分类依据
`fiih_link_id` bigint NOT NULL COMMENT '环节ID(单次环节)',
`fiih_link_name` varchar(100) NOT NULL COMMENT '环节名称',
`fiih_task_id` bigint NOT NULL COMMENT '任务ID(总体)',
`fiih_task_name` varchar(100) NOT NULL COMMENT '任务名称',
-- 其他字段省略
`fiih_info_json` text COMMENT '以上信息属性JSON',
`fiih_query_json` text COMMENT '以上信息查询属性JSON',
-- 其他字段省略
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='FIIH质量管理配置表';
```
## 3. 改造方案
### 3.1 利用现有数据结构
不创建新表,直接利用现有的`fiih_object_name`字段作为菜单分类依据:
1. **使用`fiih_object_name`字段区分不同类型的质检数据**
每个独特的`fiih_object_name`值对应一个二级菜单
2. **使用系统参数表sys_config存储已创建的菜单映射**
```json
config_key: fiih_object_menus
config_value: [
{"objectName":"产品A","menuId":2450,"orderNum":1},
{"objectName":"产品B","menuId":2451,"orderNum":2}
]
```
### 3.2 后端实现
#### 3.2.1 基于对象名称实现菜单创建
需要实现的核心功能是:对`fiih_object_name`进行分组,为每个唯一值创建一个二级菜单。
1. **添加获取所有对象名称的方法**
```java
/**
* 获取所有唯一的对象名称
*/
public List<String> getAllObjectNames() {
return fiihConfigMapper.selectDistinctObjectNames();
}
```
2. **添加对应的Mapper方法**
```xml
<!-- 查询所有唯一的对象名称 -->
<select id="selectDistinctObjectNames" resultType="String">
SELECT DISTINCT fiih_object_name
FROM ymes_fiih_config
WHERE tenant_id = #{tenantId}
ORDER BY fiih_object_name
</select>
```
#### 3.2.2 实现菜单创建和更新
在`FiihConfigController.java`中添加自动创建菜单的方法:
```java
/**
* 自动创建所有对象的菜单
*/
@PreAuthorize("@ss.hasPermi('fiih:config:menu')")
@PostMapping("/createObjectMenus")
public AjaxResult createObjectMenus() {
try {
// 获取所有对象名称
List<String> objectNames = fiihConfigService.getAllObjectNames();
// 获取已存在的对象菜单映射
Map<String, Long> existingMenus = getExistingObjectMenus();
// 创建或更新对象菜单
List<Map<String, Object>> results = new ArrayList<>();
for (String objectName : objectNames) {
if (!existingMenus.containsKey(objectName)) {
// 创建新菜单
Long menuId = createObjectMenu(objectName);
if (menuId != null) {
Map<String, Object> result = new HashMap<>();
result.put("objectName", objectName);
result.put("menuId", menuId);
result.put("status", "created");
results.add(result);
// 更新已存在菜单映射
existingMenus.put(objectName, menuId);
}
} else {
Map<String, Object> result = new HashMap<>();
result.put("objectName", objectName);
result.put("menuId", existingMenus.get(objectName));
result.put("status", "existing");
results.add(result);
}
}
// 更新系统参数中的对象菜单映射
updateObjectMenusConfig(existingMenus);
return AjaxResult.success("FIIH对象菜单创建成功", results);
} catch (Exception e) {
log.error("创建对象菜单失败", e);
return AjaxResult.error("创建对象菜单失败: " + e.getMessage());
}
}
/**
* 为对象创建二级菜单
*/
private Long createObjectMenu(String objectName) {
try {
// 生成路径:将对象名称转换为小写并移除空格
String path = objectName.toLowerCase().replaceAll("\\s+", "");
// 检查菜单是否已存在
SysMenu existingMenu = sysMenuMapper.selectMenuByPath("fiih/" + path);
if (existingMenu != null) {
return existingMenu.getMenuId();
}
// 获取FIIH质量管理菜单ID
Long parentId = 2421L; // FIIH质量管理的菜单ID
// 创建新菜单
SysMenu menu = new SysMenu();
menu.setMenuName(objectName);
menu.setParentId(parentId);
menu.setOrderNum(10);
menu.setPath(path);
menu.setComponent("mes/fiih/objectView/index");
menu.setIsFrame("1");
menu.setIsCache("0");
menu.setMenuType("C");
menu.setVisible("0");
menu.setStatus("0");
menu.setPerms("fiih:config:list");
menu.setIcon("clipboard");
int result = sysMenuMapper.insertMenu(menu);
if (result > 0) {
return menu.getMenuId();
}
} catch (Exception e) {
log.error("创建对象菜单失败: " + objectName, e);
}
return null;
}
/**
* 获取已存在的对象菜单映射
*/
private Map<String, Long> getExistingObjectMenus() {
String configValue = configService.selectConfigByKey("fiih_object_menus");
Map<String, Long> result = new HashMap<>();
if (StringUtils.isNotEmpty(configValue)) {
try {
JSONArray jsonArray = JSON.parseArray(configValue);
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject json = jsonArray.getJSONObject(i);
String objectName = json.getString("objectName");
Long menuId = json.getLong("menuId");
if (StringUtils.isNotEmpty(objectName) && menuId != null) {
result.put(objectName, menuId);
}
}
} catch (Exception e) {
log.error("解析对象菜单映射失败", e);
}
}
return result;
}
/**
* 更新系统参数中的对象菜单映射
*/
private void updateObjectMenusConfig(Map<String, Long> objectMenus) {
JSONArray jsonArray = new JSONArray();
int orderNum = 1;
for (Map.Entry<String, Long> entry : objectMenus.entrySet()) {
JSONObject json = new JSONObject();
json.put("objectName", entry.getKey());
json.put("menuId", entry.getValue());
json.put("orderNum", orderNum++);
jsonArray.add(json);
}
String configValue = jsonArray.toJSONString();
// 检查配置是否已存在
SysConfig config = configService.checkConfigKeyUnique("fiih_object_menus");
if (config != null) {
// 更新现有配置
config.setConfigValue(configValue);
configService.updateConfig(config);
} else {
// 创建新配置
config = new SysConfig();
config.setConfigName("FIIH对象菜单映射");
config.setConfigKey("fiih_object_menus");
config.setConfigValue(configValue);
config.setConfigType("N"); // 非内置
configService.insertConfig(config);
}
}
```
#### 3.2.3 实现按对象名称查询配置的接口
在`FiihConfigController.java`中添加方法:
```java
/**
* 根据对象名称查询FIIH质量管理配置列表
*/
@PreAuthorize("@ss.hasPermi('fiih:config:list')")
@GetMapping("/listByObject/{objectName}")
public TableDataInfo listByObjectName(@PathVariable("objectName") String objectName) {
startPage();
FiihConfig fiihConfig = new FiihConfig();
fiihConfig.setFiihObjectName(objectName);
List<FiihConfig> list = fiihConfigService.selectFiihConfigList(fiihConfig);
return getDataTable(list);
}
/**
* 获取所有对象名称及其菜单ID
*/
@PreAuthorize("@ss.hasPermi('fiih:config:list')")
@GetMapping("/objects")
public AjaxResult getAllObjects() {
List<String> objectNames = fiihConfigService.getAllObjectNames();
Map<String, Long> objectMenus = getExistingObjectMenus();
List<Map<String, Object>> result = new ArrayList<>();
for (String objectName : objectNames) {
Map<String, Object> obj = new HashMap<>();
obj.put("objectName", objectName);
obj.put("menuId", objectMenus.getOrDefault(objectName, null));
result.add(obj);
}
return AjaxResult.success(result);
}
### 3.3 前端实现
#### 3.3.1 创建对象菜单API接口
在 `mes-ui/src/api/mes/fiih/fiihConfig.js` 中添加新的接口方法:
```javascript
import request from '@/utils/request'
// 获取所有对象名称
export function getAllObjects() {
return request({
url: '/fiih/config/objects',
method: 'get'
})
}
// 根据对象名称查询配置数据
export function listFiihConfigByObject(objectName) {
return request({
url: '/fiih/config/listByObject/' + encodeURIComponent(objectName),
method: 'get'
})
}
// 自动创建所有对象菜单
export function createObjectMenus() {
return request({
url: '/fiih/config/createObjectMenus',
method: 'post'
})
}
```
#### 3.3.2 创建对象视图组件
创建 `mes-ui/src/views/mes/fiih/objectView/index.vue` 页面,用于展示特定对象的数据:
```vue
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="任务名称" prop="fiihTaskName">
<el-input
v-model="queryParams.fiihTaskName"
placeholder="请输入任务名称"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="环节名称" prop="fiihLinkName">
<el-input
v-model="queryParams.fiihLinkName"
placeholder="请输入环节名称"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['fiih:config:add']"
>新增</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table
v-loading="loading"
:data="fiihList"
@selection-change="handleSelectionChange"
>
<el-table-column label="序号" type="index" width="50" align="center">
<template slot-scope="scope">
<span>{{(queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1}}</span>
</template>
</el-table-column>
<el-table-column label="任务ID" align="center" prop="fiihTaskId" />
<el-table-column label="任务名称" align="center" prop="fiihTaskName" />
<el-table-column label="环节名称" align="center" prop="fiihLinkName" />
<el-table-column label="状态" align="center" prop="fiihStatus">
<template slot-scope="scope">
<dict-tag :options="statusOptions" :value="scope.row.fiihStatus"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
v-hasPermi="['fiih:config:query']"
>查看数据</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['fiih:config:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['fiih:config:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script>
import { listFiihConfigByObject, delFiihConfig } from "@/api/mes/fiih/fiihConfig";
export default {
name: "FiihObjectView",
data() {
return {
// 默认展开搜索
showSearch: true,
// 总条目数
total: 0,
// FIIH数据列表
fiihList: [],
// 状态选项
statusOptions: [
{ dictValue: 1, dictLabel: '进行中' },
{ dictValue: 2, dictLabel: '完成' },
{ dictValue: 3, dictLabel: '作废' }
],
// 当前对象名称
objectName: "",
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
fiihTaskName: null,
fiihLinkName: null
},
// 加载状态
loading: false
};
},
created() {
// 从路由获取对象名称
const path = this.$route.path;
// 提取路径中的对象名称
const pathSegments = path.split("/");
const objectNameEncoded = pathSegments[pathSegments.length - 1];
this.objectName = decodeURIComponent(objectNameEncoded);
document.title = this.objectName + " - FIIH质量管理";
this.getList();
},
methods: {
/** 查询指定对象的数据列表*/
getList() {
this.loading = true;
listFiihConfigByObject(this.objectName, this.queryParams).then(response => {
this.fiihList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
/** 构造查询的参数列表 */
getQueryParams() {
const params = { ...this.queryParams };
params.objectName = this.objectName;
return params;
},
// 其他方法如handleAdd/handleUpdate/handleView/handleDelete等保持不变
}
};
</script>
```
#### 3.3.3 修改主页面以添加菜单创建功能
在 `mes-ui/src/views/mes/fiih/index.vue` 中添加自动创建菜单的功能:
```vue
<!-- 在页面操作区域添加一个新的按钮 -->
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-menu"
size="mini"
@click="handleCreateMenus"
v-hasPermi="['fiih:config:menu']"
>创建对象菜单</el-button>
</el-col>
```
添加对应的方法:
```javascript
// 自动创建对象菜单
handleCreateMenus() {
this.$modal.confirm('确认要自动创建所有对象的菜单吗?').then(() => {
this.loading = true;
createObjectMenus().then(response => {
this.$modal.msgSuccess("菜单创建成功");
this.loading = false;
// 刷新页面以显示新菜单
setTimeout(() => {
window.location.reload();
}, 1000);
}).catch(() => {
this.loading = false;
});
});
}
```
#### 3.3.4 添加对象名称筛选功能
在 `mes-ui/src/views/mes/fiih/index.vue` 中添加对象名称筛选功能:
```vue
<el-form-item label="对象名称" prop="fiihObjectName">
<el-select v-model="queryParams.fiihObjectName" placeholder="请选择对象名称" clearable>
<el-option
v-for="item in objectOptions"
:key="item.objectName"
:label="item.objectName"
:value="item.objectName">
</el-option>
</el-select>
</el-form-item>
```
添加相关数据和方法:
```javascript
data() {
return {
// 其他数据...
objectOptions: [], // 对象名称选项
queryParams: {
// 原有参数...
fiihObjectName: null
}
}
},
created() {
this.getList();
this.getObjectOptions();
},
methods: {
// 获取对象名称选项
getObjectOptions() {
getAllObjects().then(response => {
this.objectOptions = response.data;
});
}
}
```
#### 3.3.5 修改动态路由处理
在系统的路由配置中添加对象视图的路由处理。由于对象视图的路由是动态创建的,需要确保路由配置能正确处理。
在Vue路由配置中确保有如下配置
```javascript
// 在路由文件中添加动态路由匹配(默认系统已有)
{
path: 'fiih/:path*',
component: () => import('@/views/mes/fiih/objectView/index'),
name: 'FiihObjectView',
meta: { title: 'FIIH对象视图', activeMenu: '/mes/fiih' }
}
```
## 4. 使用流程
### 4.1 管理员配置流程
1. 管理员进入FIIH质量管理配置页面
2. 点击创建对象菜单按钮,自动扫描已有的`fiih_object_name`值
3. 系统自动创建对应的二级菜单,并将映射保存在`sys_config`表中
### 4.2 数据管理流程
1. 添加新配置时,指定`fiih_object_name`值
2. 系统自动将该数据与对应的对象菜单关联
3. 可通过对象名称筛选相关配置数据
### 4.3 用户访问流程
1. 用户点击FIIH质量管理下的二级菜单如某个对象名称
2. 系统自动调用`listByObject`接口,查询并显示对应对象的所有数据
## 5. 测试计划
1. 测试自动获取对象名称
- 验证`selectDistinctObjectNames`方法是否正常返回所有唯一对象名称
- 验证数据完整性
2. 测试自动创建菜单功能
- 用不同的对象名称测试菜单创建
- 验证菜单权限是否正确
- 验证菜单映射存储是否正常
3. 测试对象数据筛选
- 在主页面上验证对象名称筛选功能
- 验证筛选结果的准确性
4. 测试二级菜单数据访问
- 通过生成的二级菜单访问特定对象数据
- 验证数据筛选是否正确
- 验证数据展示是否正常
## 6. 无需修改数据库的部署计划
1. 系统参数初始化
```sql
-- 初始化对象菜单映射配置
INSERT INTO `sys_config` (`config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `remark`)
VALUES ('FIIH对象菜单映射', 'fiih_object_menus', '[]', 'N', 'admin', NOW(), 'FIIH对象菜单映射用于存储对象和菜单的关联关系');
```
2. 后端代码部署
- 增加获取所有对象名称的方法
- 实现自动创建菜单的功能
- 添加按对象名称查询的接口
- 实现对象菜单映射的存储和读取
3. 前端代码部署
- 添加创建对象菜单按钮
- 添加对象名称筛选功能
- 创建对象视图组件,用于显示特定对象的数据
- 更新路由配置以支持动态对象菜单
## 7. 方案优势
1. **利用现有数据结构**:直接使用现有表中的`fiih_object_name`字段作为分类依据,无需添加额外字段或新表
2. **实现简单**:只需实现自动扫描和菜单创建功能,无需修改现有数据存储逻辑
3. **自动发现**:系统自动发现并创建所有对象菜单,无需手动配置
4. **动态扩展**:新添加的对象名称可自动创建相应菜单,无需修改代码
5. **向后兼容**:不破坏现有功能,原有的质检配置表页面保持不变
6. **部署简便**:无需执行数据库变更脚本,降低了部署风险

View File

@@ -0,0 +1,72 @@
# FIIH数据总览 - 趋势分析图表功能
## 一、需求概述
在现有的FIIH数据总览页面`dataOverview/index.vue`顶部新增趋势分析图表区域参考IOT数据趋势分析页面`iot/trend/index.vue`)的实现方式。
**变更说明**:原计划新建独立的"质检分析"页面,现改为直接在数据总览页面添加趋势图表,减少页面跳转,提升用户体验。
## 二、数据结构说明
### 2.1 通道配对规则
**从ch4开始偶数通道存储质检项名称奇数通道存储对应的质检数据值**
| 质检项通道 | 质检数据通道 | 说明 |
|-----------|-------------|------|
| fiih_data_ch0~ch3 | - | 前4个通道为其他用途 |
| fiih_data_ch4 | fiih_data_ch5 | ch4存质检项名称ch5存ch4的检测值 |
| fiih_data_ch6 | fiih_data_ch7 | ch6存质检项名称ch7存ch6的检测值 |
| ... | ... | 以此类推 |
| fiih_data_ch22 | fiih_data_ch23 | ch22存质检项名称ch23存ch22的检测值 |
### 2.2 配置表通道说明
配置表 `ymes_fiih_config` 中的 `fiih_config_ch0~ch23` 字段存储JSON格式的通道配置
```json
{
"data_name": "温度",
"unit": "℃",
"lower_limit": 20,
"upper_limit": 40,
"enabled": true
}
```
## 三、功能设计
### 3.1 趋势分析功能
1. **对象体多选**:支持同时选择多个对象体进行对比分析
2. **质检项多选**:支持同时选择多个质检项,质检项会合并所有选中对象体的质检项并显示支持数量
3. **日期范围选择**支持自定义日期范围和快速日期按钮近7天、近30天、近90天
4. **上下限线**:仅当选择单个对象体+单个质检项时显示上下限线
5. **多系列图表**:每条线显示"对象体-质检项"(单对象体时只显示质检项名称)
### 3.2 图表展示
- X轴采集时间
- Y轴质检数据值
- 多条数据线:不同颜色区分不同对象体/质检项组合
- 上下限线:橙色虚线(仅单对象体+单质检项时显示)
## 四、已完成的修改
### 4.1 后端文件
| 文件路径 | 修改内容 |
|----------|----------|
| `FiihDataController.java` | 添加 `/inspectionItems``/trend` 接口 |
| `IFiihDataService.java` | 添加 `getInspectionItems``getTrendData` 方法 |
| `FiihDataServiceImpl.java` | 实现趋势数据查询,支持多对象体多质检项 |
### 4.2 前端文件
| 文件路径 | 修改内容 |
|----------|----------|
| `fiihData.js` | 添加 `getInspectionItems``getTrendData` API |
| `dataOverview/index.vue` | 添加趋势分析卡片支持多选和ECharts图表 |
## 五、无需新增SQL
不需要新建菜单,直接在现有数据总览页面添加功能。

View File

@@ -0,0 +1,125 @@
# 生产计划、FIIH质量管理优化
## 原始需求(用户提供)
### 数据库字段新增
1. 在设备表中增加产能字段,单位为吨/天
2. 在供应商信息表中增加供货时间字段,单位为天
### 生产计划优化
#### 需求1来源类型关联销售订单
- 若来源类型选择"销售订单"且来源编号关联了具体的某个订单
- 则新增表单中的计划结束日期为发货日期,不可修改
- 同理,在计划产量那里也去读关联的订单产量,不可修改
#### 需求2新增页面增加两个控件
**a. 休息日配置**
- 两个复选按钮:周六、周日
- 若都选择,则计划日期中包含周六与周日的需要排除周六与周日的产能
- 同时也支持单选
**b. 计划分析(只读文本框)**
**物料分析部分:**
- 把现在的物料提醒放入
- 如果缺物料的话根据产量、计划开始日期、供应商对应BOM表中的物料供货时间和物料剩余来提示
- 提示格式:`最晚xx日前完成xx物料的采购采购为xx吨物料供应商为xx供货时间为xx天`
- 若有多个物料,则多条显示
**产能分析部分:**
- 产能从当前订单的工序路线中对应设备的**最低产能**获取(瓶颈设备)
- 计算公式:`(计划结束日期 - 计划开始日期 - 休息日天数) * 产能`
- 与计划产量对比:
-`可用产能 >= 计划产量`:提示"根据目前产能可以正常完成此计划产能为xx吨/天"
-`可用产能 < 计划产量`:提示"在当前计划日期内无法完成此计划需加班加点进行生产产能为xx吨/天"
#### 需求3主列表操作列
- 增加"计划分析"按钮
- 点击后弹窗展示计划分析内容
### FIIH质量管理优化
1. **新增时支持上传附件**
2. **主列表增加查看附件列**(创建时间前一列,固定列)
3. **适用页面**objectDataView 和 dataOverview
---
## 已确认信息
| 问题 | 答案 |
|------|------|
| 产能来源 | 工序路线→工序→设备的最低产能 |
| 工序与设备 | 工序表增加设备字段,支持多个设备 |
| 工序校验 | 改为弹窗二次确认 |
| 生产计划与工序路线 | pro_plan增加route_id按产品带入可修改 |
| 物料供应商 | BOM明细表已有supplier_id |
| 销售订单发货日期 | sal_order_entry.delivery_date |
| FIIH附件 | 方案B表中增加attachment字段 |
---
## 需求核对清单
### 一、数据库字段变更9项
| 表名 | 字段名 | 类型 | 说明 |
|------|--------|------|------|
| dm_equipment | capacity | decimal(10,2) | 产能(吨/天) |
| md_supplier | delivery_days | int | 供货时间(天) |
| pro_process | equipment_ids | varchar(500) | 设备ID多个逗号分隔 |
| pro_process | equipment_names | varchar(500) | 设备名称(多个逗号分隔) |
| pro_plan | route_id | bigint | 工序路线ID |
| pro_plan | route_name | varchar(64) | 工序路线名称 |
| pro_plan | rest_days | varchar(20) | 休息日配置 |
| pro_plan | plan_analysis | text | 计划分析结果 |
| ymes_fiih_data | attachment | varchar(500) | 附件路径 |
### 二、生产计划功能9项
1. 关联销售订单时,结束日期=发货日期,只读
2. 关联销售订单时,计划产量=订单数量,只读
3. 工序路线下拉框,按产品带入可修改
4. 休息日-周六复选框
5. 休息日-周日复选框
6. 物料分析(含供应商供货时间)
7. 产能分析(取最低产能)
8. 计划分析只读文本框
9. 主列表"计划分析"按钮
### 三、工序管理功能2项
1. 工序绑定设备(多选)
2. 工序校验改为弹窗二次确认
### 四、FIIH功能5项
1. 新增时附件上传
2. objectDataView附件列
3. dataOverview附件列
4. 无附件显示"-"
5. 支持下载附件
---
## 产能计算逻辑
```
工序路线 → 工序列表 → 每个工序的设备 → 取最低产能
工作日 = 总天数 - 排除的周六数 - 排除的周日数
可用产能 = 工作日 × 瓶颈产能
```
## 物料分析逻辑
```
BOM明细 → 需求量 = 计划产量 × 用量
缺口 = 需求量 - 库存
最晚采购日期 = 开始日期 - 供货时间
```
---
请确认以上需求是否完整准确。

View File

@@ -0,0 +1,64 @@
# 设备维修计划优化 v1.0.64
## 需求概述
优化设备项目inspectionItem的添加/修改表单,增强用户体验和功能完整性。
## 需求详情
### 1. 项目类型快速添加
**需求描述:** 在"项目类型"下拉框后面增加一个"+"按钮,点击后弹出对话框,可以快速添加项目类型字典值。
**涉及字典:** `inspection_item_type`
**实现要点:**
- 在 el-select 控件右侧添加 el-button圆形+图标)
- 点击按钮弹出添加字典对话框
- 对话框包含:字典标签(必填)、字典键值(必填)、显示排序
- 保存成功后自动刷新下拉框选项
### 2. 增加设备下拉框
**需求描述:** 在表单中增加"设备"下拉框字段。
**数据源:** 与设备维修单repairOrder一致调用 `listEquipment` 接口
**实现要点:**
- 添加设备下拉框el-select
- 支持搜索过滤filterable
- 显示设备名称存储设备ID
### 3. 增加设备点位下拉框
**需求描述:** 在表单中增加"设备点位"下拉框,并支持快速添加点位字典。
**涉及字典:** `equipment_point`(新增字典类型)
**实现要点:**
- 添加设备点位下拉框
- 下拉框右侧增加"+"按钮,支持快速添加点位字典
- 保存成功后自动刷新下拉框选项
## 数据库变更
需要新增字典类型 `equipment_point`(设备点位)
## 涉及文件
- `mes-ui/src/views/mes/equipment/inspectionItem/index.vue` - 前端页面
- `mes-ui/src/api/system/dict/data.js` - 字典API已有
- `.sql/2026-01-04_v1.0.65_周启威_设备维修计划优化.sql` - SQL脚本
## 任务清单
- [x] 1. 创建SQL脚本新增设备点位字典类型
- [x] 2. 修改 inspectionItem/index.vue
- [x] 2.1 引入字典API和设备API
- [x] 2.2 添加设备下拉框
- [x] 2.3 项目类型下拉框增加快速添加按钮
- [x] 2.4 添加设备点位下拉框及快速添加按钮
- [x] 2.5 添加字典添加对话框组件
- [x] 2.6 实现快速添加字典的方法
- [x] 2.7 添加相关data属性和表单验证
- [ ] 3. 测试验证功能

View File

@@ -0,0 +1,309 @@
# 生产报表模块设计文档
## 版本信息
- **版本号**: v1.6.014
- **日期**: 2026-01-17
- **开发者**: 周启威
---
## 一、模块概述
### 1.1 功能目标
生产报表模块用于统计和展示生产相关的各类数据报表,帮助管理人员了解生产进度、产量、合格率、工时等关键指标。
### 1.2 主要功能
1. **日报表** - 按日统计生产数据(产量、合格率、报工人员等)
2. **月报表** - 按月汇总生产数据
3. **产量统计** - 按产品/工序/车间/工位统计产量
4. **合格率分析** - 统计各维度的合格率数据
5. **工时统计** - 统计员工/设备工时
6. **导出功能** - 支持Excel导出
---
## 二、数据库设计
### 2.1 涉及的现有表
| 表名 | 说明 |
|------|------|
| `pro_report` | 报工单表 |
| `pro_workorder` | 生产工单表 |
| `pro_workorder_entry` | 工单分录表 |
| `md_material` | 物料表 |
| `md_workshop` | 车间表 |
| `md_station` | 工位表 |
| `sys_user` | 用户表 |
### 2.2 报表视图(可选,用于优化查询性能)
暂不创建视图直接使用SQL查询聚合数据。
### 2.3 数据来源说明
#### 2.3.1 生产数据来源pro_report表
| 数据项 | 来源表 | 字段 | 说明 |
|--------|--------|------|------|
| 报工数量 | `pro_report` | `report_quantity` | 每次报工的数量 |
| 合格数量 | `pro_report` | `qualified_quantity` | 报工合格数量 |
| 不合格数量 | `pro_report` | `unqualified_quantity` | 报工不合格数量 |
| 报工时间 | `pro_report` | `report_time` | 用于按日期统计 |
| 报工人 | `pro_report` | `report_user_id`, `report_user_name` | 用于按人员统计 |
| 车间 | `pro_report` | `workshop_id`, `workshop_name` | 用于按车间统计 |
| 工位 | `pro_report` | `station_id`, `station_name` | 用于按工位统计 |
| 设备 | `pro_report` | `equipment_id`, `equipment_name` | 用于按设备统计 |
| 班次 | `pro_report` | `shift_name` | 用于按班次统计 |
| 报工时段 | `pro_report` | `report_period_start`, `report_period_end` | 用于工时计算 |
| 停机时间 | `pro_report` | `downtime_minutes` | 用于设备利用率计算 |
#### 2.3.2 质检数据来源FIIH新版质检
| 数据项 | 来源表 | 字段 | 说明 |
|--------|--------|------|------|
| 质检总数 | `fiih_qc_data` | `COUNT(id)` | 质检记录总条数 |
| 不合格数 | `fiih_qc_data` | `fiih_qc_result` | 统计结论为"不合格"的条数 |
| 合格数 | `fiih_qc_data` | 计算值 | 总数 - 不合格数 |
| 质检结论 | `fiih_qc_data` | `fiih_qc_result` | 合格/不合格 |
| 不良原因 | `fiih_qc_data` | `fiih_bad_reason` | 不合格原因 |
| 对象名称 | `fiih_qc_data` | `fiih_object_name` | 质检对象 |
| 任务名称 | `fiih_qc_data` | `fiih_task_name` | 质检任务 |
| 环节名称 | `fiih_qc_data` | `fiih_link_name` | 质检环节 |
| 采集时间 | `fiih_qc_data` | `fiih_collect_time` | 用于按日期统计 |
| 任务状态 | `fiih_qc_data` | `fiih_status` | 1进行中 2完成 3作废 |
#### 2.3.3 质检配置来源
| 数据项 | 来源表 | 字段 | 说明 |
|--------|--------|------|------|
| 配置信息 | `fiih_qc_config` | 多字段 | 质检项配置 |
| 分项配置 | `fiih_qc_config` | `fiih_config_ch0~23` | 24个通道的JSON配置 |
| 负责人 | `fiih_qc_config` | `fiih_leader_id`, `fiih_leader_name` | 质检负责人 |
#### 2.3.4 关联数据来源
| 数据项 | 来源表 | 关联方式 | 说明 |
|--------|--------|----------|------|
| 产品信息 | `pro_workorder` | 通过 `work_order_id` 关联 | 获取物料名称、规格型号等 |
| 工序信息 | `pro_workorder_entry` | 通过 `work_order_entry_id` 关联 | 获取工序名称、工序顺序 |
| 工单信息 | `pro_workorder` | 通过 `work_order_id` 关联 | 获取工单编号、批次号等 |
| 客户信息 | `sal_order` | 通过工单关联销售订单 | 获取客户名称 |
#### 2.3.5 统计计算逻辑
| 指标 | 计算公式 | 数据来源 |
|------|----------|----------|
| 总产量 | `SUM(report_quantity)` | pro_report |
| 生产合格率 | `SUM(qualified_quantity) / SUM(report_quantity) * 100%` | pro_report |
| 报工次数 | `COUNT(id)` | pro_report |
| 工时(分钟) | `SUM(TIMESTAMPDIFF(MINUTE, report_period_start, report_period_end))` | pro_report |
| 有效工时 | 工时 - 停机时间 | pro_report |
| 设备利用率 | 有效工时 / 总工时 * 100% | pro_report |
| **质检合格率** | `(总条数 - 不合格条数) / 总条数 * 100%` | **fiih_qc_data** |
| **质检总数** | `COUNT(id)` | **fiih_qc_data** |
| **不合格数** | `COUNT(id) WHERE fiih_qc_result = '不合格'` | **fiih_qc_data** |
---
## 2.4 首页Dashboard质检数据修改
### 2.4.1 当前实现(需修改)
- 文件: `DashboardMapper.xml`
- 方法: `selectProductQualityRate`
- 数据源: `qc_report_quality` 表(旧版质检)
### 2.4.2 修改方案
将首页质检合格率改为从 `fiih_qc_data` 表读取:
```sql
SELECT
COUNT(*) AS totalCount,
SUM(CASE WHEN fiih_qc_result = '不合格' THEN 1 ELSE 0 END) AS unqualifiedCount,
COUNT(*) - SUM(CASE WHEN fiih_qc_result = '不合格' THEN 1 ELSE 0 END) AS qualifiedCount,
ROUND((COUNT(*) - SUM(CASE WHEN fiih_qc_result = '不合格' THEN 1 ELSE 0 END)) / COUNT(*) * 100, 2) AS rate
FROM fiih_qc_data
WHERE fiih_status != 3 -- 排除作废的记录
```
---
## 三、接口设计
### 3.1 后端接口
#### 3.1.1 日报表查询
```
GET /statement/productionStatement/dailyReport
```
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| reportDate | String | 否 | 报工日期 (yyyy-MM-dd),默认当天 |
| workshopId | Long | 否 | 车间ID |
| stationId | Long | 否 | 工位ID |
| materialId | Long | 否 | 产品ID |
**返回数据**:
```json
{
"code": 200,
"data": {
"summary": {
"totalReportQuantity": 1000,
"totalQualifiedQuantity": 980,
"totalUnqualifiedQuantity": 20,
"qualifiedRate": 98.0,
"reportCount": 50
},
"byWorkshop": [...],
"byMaterial": [...],
"byUser": [...]
}
}
```
#### 3.1.2 月报表查询
```
GET /statement/productionStatement/monthlyReport
```
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| yearMonth | String | 否 | 年月 (yyyy-MM),默认当月 |
| workshopId | Long | 否 | 车间ID |
**返回数据**:
```json
{
"code": 200,
"data": {
"summary": {...},
"dailyTrend": [...],
"byMaterial": [...]
}
}
```
#### 3.1.3 产量统计
```
GET /statement/productionStatement/outputStatistics
```
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| startDate | String | 是 | 开始日期 |
| endDate | String | 是 | 结束日期 |
| groupBy | String | 否 | 分组维度: material/workshop/station/user |
| workshopId | Long | 否 | 车间ID |
#### 3.1.4 合格率分析
```
GET /statement/productionStatement/qualifiedRateAnalysis
```
#### 3.1.5 工时统计
```
GET /statement/productionStatement/workHourStatistics
```
#### 3.1.6 导出报表
```
GET /statement/productionStatement/export
```
---
## 四、前端设计
### 4.1 目录结构
```
mes-ui/src/
├── api/mes/statement/
│ └── productionStatement.js # API接口
└── views/mes/statement/
└── productionStatement/
└── index.vue # 生产报表主页面
```
### 4.2 页面布局
#### 4.2.1 主页面结构
```
┌─────────────────────────────────────────────────────────────┐
│ 筛选条件区域 │
│ [日期范围] [车间] [工位] [产品] [查询] [重置] [导出] │
├─────────────────────────────────────────────────────────────┤
│ Tab切换: [日报表] [月报表] [产量统计] [合格率分析] [工时统计] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 汇总卡片区域 │ │
│ │ [总产量] [合格数] [不合格数] [合格率] [报工次数] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 图表区域 (ECharts) │ │
│ │ - 日报表: 按小时产量柱状图 │ │
│ │ - 月报表: 日趋势折线图 │ │
│ │ - 产量统计: 分组柱状图/饼图 │ │
│ │ - 合格率: 趋势图 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 数据表格区域 │ │
│ │ - 明细数据列表 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 五、后端实现
### 5.1 文件清单
| 文件路径 | 说明 |
|----------|------|
| `cn.sourceplan.statement.controller.ProductionStatementController` | 控制器 |
| `cn.sourceplan.statement.service.IProductionStatementService` | 服务接口 |
| `cn.sourceplan.statement.service.impl.ProductionStatementServiceImpl` | 服务实现 |
| `cn.sourceplan.statement.mapper.ProductionStatementMapper` | Mapper接口 |
| `mapper/statement/ProductionStatementMapper.xml` | Mapper XML |
| `cn.sourceplan.statement.domain.vo.DailyReportVO` | 日报表VO |
| `cn.sourceplan.statement.domain.vo.MonthlyReportVO` | 月报表VO |
| `cn.sourceplan.statement.domain.vo.OutputStatisticsVO` | 产量统计VO |
| `cn.sourceplan.statement.domain.query.ProductionStatementQuery` | 查询参数 |
---
## 六、菜单与权限
### 6.1 菜单配置
- **父菜单**: 报表管理 (已存在)
- **菜单名称**: 生产报表
- **路由地址**: statement/productionStatement
- **组件路径**: mes/statement/productionStatement/index
- **权限标识**: statement:productionStatement:list
### 6.2 权限配置
| 权限标识 | 说明 |
|----------|------|
| statement:productionStatement:list | 查询生产报表 |
| statement:productionStatement:export | 导出生产报表 |
---
## 七、开发计划
### 7.1 任务分解
| 序号 | 任务 | 预估工时 |
|------|------|----------|
| 1 | 后端Controller/Service/Mapper | 4h |
| 2 | 前端页面开发 | 4h |
| 3 | 图表集成 | 2h |
| 4 | 导出功能 | 1h |
| 5 | 测试与调优 | 1h |
### 7.2 里程碑
- [ ] 后端接口完成
- [ ] 前端页面完成
- [ ] 联调测试通过
- [ ] 发布上线
---
## 八、SQL脚本
详见: `.sql/2026-01-17_v1.6.014_周启威_生产报表.sql`

View File

@@ -0,0 +1,347 @@
# 生产计划优化 - 全流程改进总结
## 项目概述
本项目对生产计划管理系统进行了全面优化,通过改进生产计划的创建流程、支持多订单批量处理、自动时间设置、工序路线动态加载、关联订单管理和自动订单创建等功能,显著提升了用户体验和系统效率。
**项目时间**: 2026-02-25
**功能名称**: 生产计划优化 (production-plan-optimization)
**版本号**: v1.6.037
## 核心改进点
### 1. 时间设置优化
- **改进前**: 用户需要手动输入时间范围
- **改进后**: 根据选择的年/月/周计划类型自动设置时间范围,用户可快速创建计划
- **影响**: 减少用户手动输入,提高计划创建效率
### 2. 工序路线动态加载
- **改进前**: 工序路线需要手动选择
- **改进后**: 产品选择后自动读取并显示工序路线,支持下拉框修改,不显示休息日
- **影响**: 简化工序路线选择流程,减少操作步骤
### 3. 时间维度整合显示
- **改进前**: 年、月、周分散显示
- **改进后**: 年、月、周在同一组件中显示和管理,月计划下的周计划自动分类
- **影响**: 提升用户界面的直观性和易用性
### 4. 质检等级隐藏
- **改进前**: 质检等级显示在界面上
- **改进后**: 完全隐藏质检等级相关信息或与FIIH关联
- **影响**: 简化界面,减少用户困惑
### 5. 订单选择流程优化
- **改进前**: 订单选择流程不清晰
- **改进后**: 订单放在最后选,先筛选后选择,支持按时间、产品、订单范围筛选,支持多订单选择
- **影响**: 用户能更高效地找到需要的订单
### 6. 关联订单管理
- **改进前**: 订单关联不清晰
- **改进后**: 清晰显示关联订单,支持自动创建
- **影响**: 用户能清楚了解计划关联的订单
### 7. 多订单明细管理
- **改进前**: 多订单明细混乱,难以区分
- **改进后**: 生成多个分析计划和明细,清楚标注每条明细所属的订单、工单、报工单
- **影响**: 用户能准确追踪每条明细的来源
### 8. 自动订单创建
- **改进前**: 未选择订单时无法继续
- **改进后**: 未选择订单时弹窗询问是否自动创建,根据计划数据自动创建订单
- **影响**: 简化流程,支持快速创建计划
## 数据库改动
### 使用现有表
本次优化**不新增表**,使用现有的表结构:
#### 1. pro_plan (生产计划表) - 已有字段
- `id`: 计划ID
- `plan_number`: 计划编号
- `plan_name`: 计划名称
- `plan_type`: 计划类型
- `route_id`: 工序路线ID已有
- `route_name`: 工序路线名称(已有)
- `rest_days`: 休息日配置(已有)
- `plan_analysis`: 计划分析结果(已有,用于存储多订单分析数据)
- `plan_source_type`: 来源类型(已有)
- `plan_source_id`: 来源ID已有
#### 2. pro_workorder (工单主表) - 使用现有字段
- `id`: 工单ID
- `number`: 工单编号
- `material_id`: 产品ID
- `material_name`: 产品名称
- `quantity`: 生产数量
- `route_id`: 工序路线ID已有
- `source_type`: 来源类型(用于标识来自计划)
- `source_info`: 来源信息用于存储计划ID和订单信息
#### 3. pro_workorder_entry (工单子表) - 使用现有字段
- `id`: 工单子表ID
- `workorder_id`: 工单主表ID
- `process_id`: 工序ID
- `process_name`: 工序名称
- 其他工序相关字段
#### 4. pro_report (报工单表) - 使用现有字段
- `id`: 报工单ID
- `number`: 报工单编号
- `work_order_entry_id`: 工单子表ID
- 其他报工相关字段
### 字段扩展(如需要)
如果现有字段不足以支持新功能,可能需要添加以下字段:
```sql
-- pro_plan表可能需要的扩展字段
ALTER TABLE `pro_plan`
ADD COLUMN `schedule_cycle` varchar(20) DEFAULT NULL COMMENT '计划周期year=年计划,month=月计划,week=周计划' AFTER `plan_type`,
ADD COLUMN `plan_year` int DEFAULT NULL COMMENT '计划年份' AFTER `schedule_cycle`,
ADD COLUMN `month_number` int DEFAULT NULL COMMENT '月份1-12' AFTER `plan_year`,
ADD COLUMN `week_number` int DEFAULT NULL COMMENT '周数1-53' AFTER `month_number`,
ADD COLUMN `start_time` datetime DEFAULT NULL COMMENT '开始时间' AFTER `week_number`,
ADD COLUMN `end_time` datetime DEFAULT NULL COMMENT '结束时间' AFTER `start_time`,
ADD COLUMN `related_order_ids` text DEFAULT NULL COMMENT '关联订单ID列表JSON格式' AFTER `plan_analysis`;
-- 添加索引
ALTER TABLE `pro_plan`
ADD INDEX `idx_schedule_cycle` (`schedule_cycle`),
ADD INDEX `idx_year_week` (`plan_year`, `week_number`),
ADD INDEX `idx_year_month` (`plan_year`, `month_number`);
```
## 后端改动
### 新增/修改实体类
#### 1. ProPlan (生产计划) - 扩展现有实体
```java
public class ProPlan extends BaseEntity {
private Long id;
private String planNumber; // 计划编号
private String planName; // 计划名称
private String planType; // 计划类型
private String scheduleCycle; // 计划周期year/month/week
private Integer planYear; // 计划年份
private Integer monthNumber; // 月份
private Integer weekNumber; // 周数
private Date startTime; // 开始时间
private Date endTime; // 结束时间
private Long routeId; // 工序路线ID
private String routeName; // 工序路线名称
private String restDays; // 休息日配置
private String planAnalysis; // 计划分析结果JSON格式存储多订单分析
private String relatedOrderIds; // 关联订单ID列表JSON格式
// 关联对象(不存数据库)
private List<ProWorkorder> workorderList; // 关联的工单列表
}
```
### 新增服务类
#### 1. TimeRangeService (时间范围服务)
**功能**:
- 根据计划类型获取默认时间范围
- 验证时间范围有效性
- 获取指定月份的周列表
**关键方法**:
```java
TimeRange getDefaultTimeRange(String scheduleCycle);
boolean validateTimeRange(Date startTime, Date endTime);
List<Week> getWeeksByMonth(int year, int month);
boolean weekPlanExistsInMonth(Long monthPlanId, int weekNumber);
```
#### 2. ProcessRouteService (工序路线服务)
**功能**:
- 根据产品获取工序路线
- 获取可用工序路线列表
- 过滤休息日工序
**关键方法**:
```java
ProcessRoute getRouteByProduct(Long productId);
List<ProcessRoute> getAvailableRoutes(Long productId);
ProcessRouteDetail getRouteDetails(Long routeId, boolean excludeRestDays);
```
#### 3. ProductionPlanService (生产计划服务) - 扩展现有服务
**新增功能**:
- 创建计划并关联多个订单
- 生成多个工单(每个订单一个工单)
- 自动创建订单
- 获取计划明细(按订单分组)
**关键方法**:
```java
Long createPlanWithOrders(ProductionPlanRequest planRequest);
List<ProWorkorder> generateWorkordersForOrders(Long planId, List<Long> orderIds);
Long autoCreateOrder(ProductionPlanData planData);
Map<Long, List<WorkorderDetail>> getPlanDetailsGroupedByOrder(Long planId);
```
### 新增API接口
#### 1. ProductionPlanController - 扩展现有Controller
```java
@PostMapping("/createWithOrders")
public ResponseEntity<Long> createPlanWithOrders(@RequestBody ProductionPlanRequest request);
@GetMapping("/{planId}/detailsByOrder")
public ResponseEntity<Map<Long, List<WorkorderDetail>>> getDetailsByOrder(@PathVariable Long planId);
@GetMapping("/defaultTimeRange/{scheduleCycle}")
public ResponseEntity<TimeRange> getDefaultTimeRange(@PathVariable String scheduleCycle);
```
#### 2. ProcessRouteController - 新增或扩展
```java
@GetMapping("/product/{productId}")
public ResponseEntity<ProcessRoute> getRouteByProduct(@PathVariable Long productId);
@GetMapping("/product/{productId}/available")
public ResponseEntity<List<ProcessRoute>> getAvailableRoutes(@PathVariable Long productId);
```
## 前端改动
### 新增/修改组件
#### 1. TimeRangeSelector.vue (时间范围选择器) - 新增
**功能**:
- 整合年、月、周的时间选择
- 自动设置默认时间范围
- 验证时间范围有效性
#### 2. ProcessRouteSelector.vue (工序路线选择器) - 新增
**功能**:
- 显示产品的工序路线(不显示休息日)
- 支持下拉框修改工序路线
#### 3. OrderFilterAndSelection.vue (订单筛选和多选) - 新增
**功能**:
- 筛选生产订单
- 支持多选订单
- 显示选择统计
#### 4. PlanDetailList.vue (计划明细展示) - 新增
**功能**:
- 按订单分组显示工单明细
- 标注订单/工单/报工单信息
#### 5. ProductionPlanCreate.vue (生产计划创建页面) - 修改现有页面
**主要修改**:
1. 集成时间范围选择器
2. 集成工序路线选择器
3. 订单选择放在最后
4. 隐藏质检等级字段
5. 支持多订单选择
6. 支持自动创建订单
## 测试改动
### 后端测试
#### 1. 单元测试
- TimeRangeService的时间范围设置和验证
- ProcessRouteService的工序路线加载排除休息日
- ProductionPlanService的多订单处理
- 自动创建订单功能
#### 2. 集成测试
- 完整的计划创建流程
- 多订单工单生成
- 数据一致性验证
### 前端测试
#### 1. 单元测试
- TimeRangeSelector组件测试
- ProcessRouteSelector组件测试
- OrderFilterAndSelection组件测试
## 性能优化
### 数据库索引
```sql
-- pro_plan表索引
CREATE INDEX idx_schedule_cycle ON pro_plan(schedule_cycle);
CREATE INDEX idx_year_week ON pro_plan(plan_year, week_number);
CREATE INDEX idx_year_month ON pro_plan(plan_year, month_number);
CREATE INDEX idx_route_id ON pro_plan(route_id);
-- pro_workorder表索引如需要
CREATE INDEX idx_source_type_info ON pro_workorder(source_type, source_info(100));
```
## 实现计划
### 第一阶段:数据库和后端基础
1. 扩展pro_plan表字段如需要
2. 修改ProPlan实体类
3. 实现TimeRangeService
4. 实现ProcessRouteService
5. 编写单元测试
### 第二阶段:后端业务逻辑
1. 扩展ProductionPlanService
2. 实现多订单工单生成
3. 实现自动创建订单
4. 创建/扩展API接口
5. 编写集成测试
### 第三阶段:前端实现
1. 创建时间范围选择器组件
2. 创建工序路线选择器组件
3. 创建订单筛选多选组件
4. 修改计划创建页面
5. 编写前端单元测试
### 第四阶段:优化和验证
1. 创建数据库索引
2. 性能测试
3. 功能验证
4. 文档编写
## 关键需求映射
| 需求 | 实现方式 | 状态 |
|------|--------|------|
| 默认时间设置 | TimeRangeService + TimeRangeSelector组件 | 待实现 |
| 工序路线不显示休息日 | ProcessRouteService过滤 + ProcessRouteSelector组件 | 待实现 |
| 年月周放在一起 | TimeRangeSelector组件 | 待实现 |
| 质检等级隐藏 | 前端移除相关字段 | 待实现 |
| 订单放在最后选 | 调整页面布局和流程 | 待实现 |
| 订单多选(通过筛选) | OrderFilterAndSelection组件 | 待实现 |
| 多个分析计划和明细 | ProductionPlanService生成多个工单 | 待实现 |
| 明细标注订单/工单/报工单 | PlanDetailList组件 + 数据结构 | 待实现 |
| 未绑定订单自动创建 | ProductionPlanService自动创建 | 待实现 |
## 总结
本项目通过优化现有表结构和业务流程避免新增表充分利用现有的pro_plan、pro_workorder等表来实现多订单管理功能。通过plan_analysis字段存储JSON格式的分析数据通过source_info字段关联计划和订单信息实现了需求的所有功能点。
**预期收益**:
- 用户创建计划的时间减少50%以上
- 支持批量处理多个订单,提高工作效率
- 通过自动创建订单功能,简化业务流程
- 通过清晰的明细标注,提高数据追溯能力

View File

@@ -0,0 +1,740 @@
# ATS 批量排产报工接口文档
## 版本信息
- 版本号v2.0.0
- 创建日期2026-02-24
- 当前局域网测试地址:
- **前端地址**http://192.168.1.244:80
- **后端地址**http://192.168.1.244:8080
## 接口说明
### 1. 批量自动完成接口(异步模式)
#### 1.1 基本信息
- **接口名称**:批量自动完成销售订单(异步提交)
- **接口路径**`后端地址/production/autoComplete/batchAutoComplete`
- **请求方法**POST
- **Content-Type**application/json;charset=UTF-8
- **认证方式**:无需认证(已配置匿名访问)
- **执行模式**异步执行立即返回任务ID
#### 1.2 请求参数
##### 请求体JSON
```json
{
"orderNumbers": ["SO202602240001", "SO202602240002"],//订单编号列表,必填
"routeId": 1001,//工序路线id可选
"productionStartTime": "2026-02-24 08:00:00"//生产计划开始时间,可选
}
```
##### 参数说明
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| orderNumbers | Array<String> | 是 | 销售订单编号列表,不能为空 |
| routeId | Long | 否 | 工序路线ID如不指定则使用物料默认路线 |
| productionStartTime | String | 否 | 生产开始时间格式yyyy-MM-dd HH:mm:ss |
##### 参数约束
- `orderNumbers`:必填,至少包含一个有效订单编号
- 订单编号会自动过滤空字符串和null值
- `routeId`:可选,不指定时系统会根据物料配置自动选择路线
- `productionStartTime`:可选,指第一条工单的计划开始时间,不指定时系统会生成模拟时间(生产订单日期作为日期早8:00-9:00随机时间后续工单按照系统逻辑读取工序持续时间生成)
#### 1.3 响应结果(立即返回)
##### 成功响应示例
```json
{
"code": 200,
"msg": "任务已提交,正在后台处理",
"data": {
"taskId": "TASK_20260224_001",
"totalCount": 50,
"estimatedSeconds": 50,
"status": "PENDING",
"message": "任务已创建预计需要50秒完成"
}
}
```
##### 响应字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| code | Integer | 响应状态码200表示成功 |
| msg | String | 响应消息 |
| data.taskId | String | 任务ID用于后续查询任务状态 |
| data.totalCount | Integer | 总订单数 |
| data.estimatedSeconds | Integer | 预计耗时(秒) |
| data.status | String | 任务状态PENDING-待处理 |
| data.message | String | 提示信息 |
---
### 2. 任务状态查询接口
#### 2.1 基本信息
- **接口名称**:查询批量任务状态
- **接口路径**`后端地址/production/autoComplete/taskStatus/{taskId}`
- **请求方法**GET
- **认证方式**:无需认证(已配置匿名访问)
#### 2.2 请求参数
##### 路径参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| taskId | String | 是 | 任务ID由提交接口返回 |
##### 请求示例
```
GET /production/autoComplete/taskStatus/TASK_20260224_001
```
#### 2.3 响应结果
##### 处理中响应示例
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"taskId": "TASK_20260224_001",
"status": "PROCESSING",
"totalCount": 50,
"processedCount": 25,
"successCount": 23,
"failedCount": 2,
"completedCount": 0,
"percentage": 50,
"startTime": "2026-02-24 10:00:00",
"estimatedSeconds": 50,
"elapsedSeconds": 25,
"message": "正在处理中已完成50%"
}
}
```
##### 已完成响应示例
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"taskId": "TASK_20260224_001",
"status": "COMPLETED",
"totalCount": 50,
"processedCount": 50,
"successCount": 48,
"failedCount": 2,
"completedCount": 0,
"percentage": 100,
"startTime": "2026-02-24 10:00:00",
"endTime": "2026-02-24 10:00:52",
"estimatedSeconds": 50,
"elapsedSeconds": 52,
"message": "任务已完成",
"result": {
"successOrders": ["SO001", "SO002", "..."],
"failedOrders": ["SO025", "SO038"],
"completedOrders": [],
"failureReasons": {
"SO025": "订单不存在",
"SO038": "物料未配置工序路线"
}
}
}
}
```
##### 响应字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| code | Integer | 响应状态码200表示成功 |
| msg | String | 响应消息 |
| data.taskId | String | 任务ID |
| data.status | String | 任务状态PENDING-待处理/PROCESSING-处理中/COMPLETED-已完成/FAILED-失败 |
| data.totalCount | Integer | 总订单数 |
| data.processedCount | Integer | 已处理数量 |
| data.successCount | Integer | 成功数量 |
| data.failedCount | Integer | 失败数量 |
| data.completedCount | Integer | 已完成(跳过)数量 |
| data.percentage | Integer | 完成百分比0-100 |
| data.startTime | String | 开始时间 |
| data.endTime | String | 结束时间(仅完成时有值) |
| data.estimatedSeconds | Integer | 预计耗时(秒) |
| data.elapsedSeconds | Integer | 已耗时(秒) |
| data.message | String | 状态描述信息 |
| data.result | Object | 详细结果(仅完成时有值) |
| data.result.successOrders | Array<String> | 成功处理的订单编号列表 |
| data.result.failedOrders | Array<String> | 处理失败的订单编号列表 |
| data.result.completedOrders | Array<String> | 已完成(跳过)的订单编号列表 |
| data.result.failureReasons | Map<String, String> | 失败原因映射 |
#### 2.4 错误响应
##### 任务不存在
```json
{
"code": 500,
"msg": "任务不存在"
}
```
---
## 业务流程
### 3.1 异步处理流程
```
客户端 服务端 数据库
| | |
|--1.提交批量请求------->| |
| |--2.创建任务记录--------->|
| | |
|<--3.返回任务ID---------| |
| | |
| |--4.异步执行任务--------->|
| | (后台线程池) |
| | |
|--5.查询任务状态------->| |
| |--6.查询任务记录--------->|
|<--7.返回当前进度-------|<--返回任务数据-----------|
| | |
|--8.再次查询状态------->| |
| |--9.查询任务记录--------->|
|<--10.返回完成结果------|<--返回完成数据-----------|
```
### 3.2 详细处理步骤
#### 步骤1提交任务同步<1秒
1. **参数验证**
- 验证订单编号列表不为空
- 过滤无效的订单编号null或空字符串
2. **创建任务记录**
- 生成唯一任务ID格式TASK_yyyyMMdd_HHmmss_随机数
- 计算预计耗时(订单数 × 1秒
- 初始化任务状态为 PENDING
- 写入数据库
3. **立即返回**
- 返回任务ID、总订单数、预计耗时
- 客户端收到响应,可以开始轮询查询状态
#### 步骤2异步执行后台线程池
1. **更新任务状态为 PROCESSING**
- 记录开始时间
- 写入数据库
2. **循环处理订单**
- 验证订单是否存在
- 检查订单状态(已完成的订单会跳过)
- 根据订单类型(连续制造/离散制造)执行不同流程
- 生成工单、报工单
- 更新工单状态
3. **批量更新进度每5个订单**
- 更新 processedCount、successCount、failedCount
- 写入数据库
- 减少数据库写入频率,提升性能
4. **任务完成**
- 更新任务状态为 COMPLETED 或 FAILED
- 记录结束时间
- 保存完整结果数据JSON格式
- 写入数据库
#### 步骤3查询任务状态同步<100ms
1. **根据任务ID查询数据库**
2. **计算实时数据**
- 完成百分比 = (processedCount / totalCount) × 100
- 已耗时 = 当前时间 - 开始时间
3. **返回任务状态和进度**
### 3.3 数据库更新策略
为了平衡性能和实时性,采用**批量更新策略**
| 更新时机 | 说明 | 频率 |
|---------|------|------|
| 任务创建 | 初始化任务记录 | 1次 |
| 任务开始 | 更新状态为PROCESSING | 1次 |
| 处理过程 | 每处理5个订单更新一次进度 | N/5次 |
| 任务完成 | 更新状态为COMPLETED保存完整结果 | 1次 |
| 任务失败 | 更新状态为FAILED保存错误信息 | 1次 |
**示例**处理50个订单
- 总更新次数1创建+ 1开始+ 10进度+ 1完成= 13次
- 相比实时更新53次减少了75%的数据库写入
### 3.4 订单处理流程
#### 连续制造流程
1. 为每个订单明细生成工单
2. 生成默认工序配置
3. 批量生成报工单
4. 更新工单状态为"已完成"
#### 离散制造流程
1. 为每个订单明细生成工单
2. 生成默认工序配置
3. 批量生成报工单
4. 更新工单状态为"已完成"
### 3.5 订单状态说明
| 状态 | 说明 | 处理方式 |
|------|------|----------|
| 待生产 | 订单未开始生产 | 正常处理 |
| 生产中 | 订单正在生产 | 正常处理 |
| 已完成 | 订单已完成 | 跳过处理计入completedCount |
| 已删除 | 订单已删除 | 返回错误 |
### 3.6 工序路线选择逻辑
1. 如果请求中指定了`routeId`,优先使用指定的路线
2. 如果订单明细中配置了路线,使用明细配置的路线
3. 如果物料配置了默认路线,使用物料默认路线
4. 如果以上都没有,返回错误
## 性能说明
### 4.1 执行时长估算
| 订单数 | 预计耗时 | 数据库更新次数 |
|--------|---------|--------------|
| 5个 | ~5秒 | 4次 |
| 10个 | ~10秒 | 5次 |
| 50个 | ~50秒 | 13次 |
| 100个 | ~100秒 | 23次 |
**说明**
- 单个订单平均处理时间1秒包含生成工单、报工单、更新状态
- 实际耗时可能因服务器性能、数据库负载而有所差异
### 4.2 并发限制
- **线程池大小**10个线程
- **最大并发任务数**10个
- **任务队列长度**100个
- **超过限制时**:新任务会进入队列等待
### 4.3 超时设置
- **单个任务最大执行时间**5分钟300秒
- **超时后**任务状态标记为FAILED
- **建议**单次批量不超过100个订单
---
## 注意事项
### 5.1 性能考虑
- 建议单次批量处理的订单数量不超过100个
- 大批量处理建议分批调用
- 查询状态的轮询间隔建议3-5秒
### 5.2 事务处理
- 每个订单的处理是独立的事务
- 单个订单失败不会影响其他订单的处理
- 任务记录的更新是独立事务
### 5.3 幂等性
- 已完成的订单会被跳过,不会重复处理
- 可以安全地重复调用接口
- 相同订单号不会重复生成工单
### 5.4 匿名访问
- 该接口已配置为允许匿名访问
- 系统会自动使用"system"用户进行数据创建和更新
- 无需传递token或其他认证信息
### 5.5 日志记录
- 所有操作都会记录详细日志
- 建议在生产环境中监控日志以便排查问题
- 任务失败时会记录详细的错误信息
### 5.6 日志记录
- 所有操作都会记录详细日志
- 建议在生产环境中监控日志以便排查问题
- 任务失败时会记录详细的错误信息
---
---
## 为什么必须使用异步模式?
### 同步方式的严重问题
如果不使用异步批量处理方法,而是用同步方式处理大批量订单,会带来以下严重问题:
### 1. HTTP超时问题 ⚠️ 严重
**问题描述**
- 假设处理50个订单需要50秒
- 大多数HTTP客户端默认超时时间30-60秒
- Nginx/负载均衡器默认超时60秒
- 浏览器默认超时通常2分钟
**后果**
```
客户端发送请求 → 服务器开始处理50个订单
30秒后...客户端超时断开连接
服务器还在继续处理已经处理了30个
客户端收到504 Gateway Timeout 或 Connection Reset
客户端不知道:订单到底处理了多少?成功了多少?
```
**实际影响**
- ❌ 客户端认为请求失败,但服务器可能已经处理了部分订单
- ❌ 无法知道哪些订单成功,哪些失败
- ❌ 可能重复提交,导致数据重复
- ❌ 用户体验极差,不知道发生了什么
### 2. 数据库连接占用 ⚠️ 严重
**问题描述**
- 同步处理会长时间占用数据库连接
- 处理50个订单可能需要50秒
- 这期间数据库连接一直被占用
**后果**
```
数据库连接池大小20个连接
10个客户端同时提交批量请求每个50个订单
10个连接被占用50秒
其他正常业务请求无法获取连接
整个系统卡死!
```
**实际影响**
- ❌ 数据库连接池耗尽
- ❌ 其他业务功能无法使用
- ❌ 系统整体性能下降
- ❌ 可能导致系统崩溃
### 3. 线程阻塞问题 ⚠️ 严重
**问题描述**
- Tomcat默认最大线程数200
- 每个同步请求占用一个线程直到完成
**后果**
```
Tomcat线程池200个线程
20个客户端同时提交批量请求每个处理50秒
20个线程被阻塞50秒
如果有100个这样的请求...
所有线程被占满,新请求无法处理
系统完全无响应!
```
**实际影响**
- ❌ 线程池耗尽
- ❌ 新请求被拒绝503 Service Unavailable
- ❌ 系统无法处理任何请求
- ❌ 需要重启服务器才能恢复
### 4. 用户体验问题 ⚠️ 中等
**问题描述**
- 用户点击提交后,页面一直转圈
- 50秒内无法做任何操作
- 不知道进度,不知道还要等多久
**后果**
```
用户点击提交
页面loading...
10秒...20秒...30秒...
用户:是不是卡死了?要不要刷新?
用户刷新页面或关闭浏览器
但服务器还在处理...
数据可能处理了一半,状态不一致
```
**实际影响**
- ❌ 用户体验极差
- ❌ 用户不知道进度
- ❌ 容易误操作(刷新、重复提交)
- ❌ 客户投诉增加
### 5. 事务管理问题 ⚠️ 中等
**问题描述**
- 如果50个订单在一个事务中
- 第49个订单失败前48个都要回滚
**后果**
```
开始事务
处理订单1...成功
处理订单2...成功
...
处理订单48...成功
处理订单49...失败!
整个事务回滚
前48个订单的工作全部白做
浪费了48秒的处理时间
```
**实际影响**
- ❌ 一个订单失败,全部失败
- ❌ 无法部分成功
- ❌ 处理效率低下
- ❌ 资源浪费
### 6. 无法监控和追踪 ⚠️ 中等
**问题描述**
- 同步处理无法查询进度
- 不知道处理到第几个订单
- 出错后无法定位问题
**后果**
```
提交50个订单
处理中...(黑盒)
30秒后超时
问题:
- 处理了多少个?不知道
- 哪些成功了?不知道
- 哪些失败了?不知道
- 为什么失败?不知道
```
**实际影响**
- ❌ 无法追踪进度
- ❌ 问题难以排查
- ❌ 无法提供客户支持
- ❌ 数据状态不明确
### 7. 系统稳定性问题 ⚠️ 严重
**问题描述**
- 大批量同步请求会导致系统资源耗尽
- 可能引发雪崩效应
**后果**
```
高峰期100个客户端同时提交批量请求
线程池满 + 数据库连接池满
系统响应变慢
客户端超时重试
更多请求涌入
系统完全崩溃
需要紧急重启
```
**实际影响**
- ❌ 系统不稳定
- ❌ 容易崩溃
- ❌ 影响所有用户
- ❌ 需要人工干预
### 8. 扩展性问题 ⚠️ 中等
**问题描述**
- 同步方式无法应对业务增长
- 订单量增加时系统无法承受
**后果**
```
现在每次处理50个订单
业务增长每次需要处理200个订单
处理时间200秒3分20秒
必然超时,系统无法使用
需要重新设计架构
```
**实际影响**
- ❌ 无法应对业务增长
- ❌ 需要重构代码
- ❌ 技术债务增加
- ❌ 开发成本增加
---
## 同步 vs 异步对比
### 性能对比表
| 问题 | 同步方式 | 异步方式 |
|------|---------|---------|
| HTTP超时 | ❌ 必然超时(>30秒 | ✅ 立即返回(<1秒 |
| 数据库连接 | 长时间占用50秒 | 短时间占用<1秒 |
| 线程阻塞 | 长时间阻塞50秒 | 立即释放<1秒 |
| 用户体验 | 长时间等待无反馈 | 立即响应可查询进度 |
| 事务管理 | 全部回滚一个失败全失败 | 独立事务部分成功 |
| 进度追踪 | 无法追踪黑盒处理 | 实时查询进度和结果 |
| 系统稳定性 | 容易崩溃资源耗尽 | 稳定可靠资源可控 |
| 扩展性 | 无法扩展订单量增加即崩溃 | 易于扩展支持大批量 |
| 错误处理 | 难以定位状态不明 | 详细错误信息和原因 |
| 并发能力 | 极差10个请求即可拖垮系统 | 优秀支持高并发 |
### 实际场景对比
**场景**: 10个客户端同时提交50个订单
#### 同步方式:
```
每个请求耗时50秒
占用线程10个持续50秒
占用数据库连接10个持续50秒
用户等待时间50秒
超时风险100%
系统可用性:严重下降
其他业务影响:严重(连接池和线程池被占用)
```
#### 异步方式:
```
每个请求耗时:<1秒立即返回
占用线程10个持续<1秒
占用数据库连接10个持续<1秒
用户等待时间:<1秒可查询进度
超时风险0%
系统可用性:正常
其他业务影响:无(资源立即释放)
```
### 资源占用对比图
```
同步方式资源占用:
时间轴0s -------- 10s -------- 20s -------- 30s -------- 40s -------- 50s
线程: ████████████████████████████████████████████████████████ (持续占用)
连接: ████████████████████████████████████████████████████████ (持续占用)
用户: ⏳等待中...................................................
异步方式资源占用:
时间轴0s - 1s
线程: █ (立即释放)
连接: █ (立即释放)
用户: ✅已收到任务ID可查询进度
后台处理:
时间轴0s -------- 10s -------- 20s -------- 30s -------- 40s -------- 50s
线程: ████████████████████████████████████████████████████████ (后台线程池)
连接: █ █ █ █ █ █ █ █ █ █ (批量更新,间歇占用)
用户: ✅可随时查询进度20%...40%...60%...80%...100%
```
---
## 异步模式的核心优势
### 1. 立即响应
- 接口在1秒内返回任务ID
- 用户无需等待
- 不会超时
### 2. 进度可追踪
- 实时查询处理进度百分比
- 查看成功/失败数量
- 获取详细错误信息
### 3. 资源高效利用
- HTTP线程立即释放
- 数据库连接短时间占用
- 后台线程池统一管理
### 4. 部分成功机制
- 单个订单失败不影响其他订单
- 独立事务可部分成功
- 详细记录每个订单的处理结果
### 5. 系统稳定性
- 不会因大批量请求导致系统崩溃
- 线程池和连接池可控
- 支持高并发场景
### 6. 易于扩展
- 订单量增加不影响系统稳定性
- 可通过调整线程池大小扩展
- 支持未来业务增长
---
## 结论
**强烈建议使用异步模式!**
异步批量处理是处理大批量长时间任务的行业标准做法不使用异步方式会导致
1. **系统不可用** - HTTP超时线程阻塞连接池耗尽
2. **用户体验差** - 长时间等待无进度反馈
3. **数据不一致** - 超时后状态不明确
4. **无法扩展** - 业务增长后系统崩溃
5. **运维困难** - 问题难以排查和监控
使用异步模式后
1. **系统稳定** - 资源可控不会崩溃
2. **用户体验好** - 立即响应实时进度
3. **数据一致** - 详细记录状态明确
4. **易于扩展** - 支持大批量和高并发
5. **运维友好** - 完整日志易于监控

View File

@@ -0,0 +1,287 @@
# Token滑动机制技术文档
## 一、概述
本系统采用基于Redis的Token滑动窗口机制实现用户会话的自动续期功能。当用户在系统中持续活动时Token会自动刷新避免频繁登录提升用户体验。
## 二、核心配置
### 2.1 配置文件application.yml
```yaml
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期默认30分钟单位分钟
expireTime: 10080
```
**配置说明:**
- `header`: HTTP请求头中Token的字段名
- `secret`: JWT签名密钥
- `expireTime`: Token有效期当前配置为10080分钟7天
## 三、核心实现
### 3.1 数据模型LoginUser
```java
public class LoginUser implements UserDetails {
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
// ... 其他字段
}
```
### 3.2 Token服务TokenService
#### 3.2.1 关键常量
```java
// 令牌有效期(从配置文件读取)
@Value("${token.expireTime}")
private int expireTime;
// 毫秒常量
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
// 滑动窗口触发阈值20分钟
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
```
#### 3.2.2 创建Token
```java
public String createToken(LoginUser loginUser) {
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser); // 初始化Token过期时间
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
```
#### 3.2.3 验证Token滑动机制核心
```java
/**
* 验证令牌有效期相差不足20分钟自动刷新缓存
*
* @param loginUser
*/
public void verifyToken(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
// 如果距离过期时间不足20分钟触发刷新
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(loginUser);
}
}
```
#### 3.2.4 刷新Token
```java
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser) {
// 更新登录时间为当前时间
loginUser.setLoginTime(System.currentTimeMillis());
// 重新计算过期时间 = 当前时间 + 配置的有效期
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 将更新后的用户信息存入Redis并设置过期时间
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
```
#### 3.2.5 获取登录用户
```java
public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
try {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
} catch (Exception e) {
// Token解析失败
}
}
return null;
}
```
## 四、滑动机制工作流程
### 4.1 流程图
```
用户请求 → 获取Token → 从Redis获取LoginUser → 验证Token有效期
距离过期 ≤ 20分钟
↓ ↓
是 否
↓ ↓
刷新Token 继续使用
更新过期时间
更新Redis缓存
```
### 4.2 详细说明
1. **用户登录**
- 创建TokenUUID
- 设置初始过期时间 = 当前时间 + expireTime
- 存入Redis设置过期时间
2. **用户请求**
- 从请求头获取Token
- 从Redis获取LoginUser对象
- 调用`verifyToken()`验证
3. **滑动窗口判断**
- 计算剩余有效时间 = expireTime - currentTime
- 如果剩余时间 ≤ 20分钟触发刷新
- 否则继续使用当前Token
4. **Token刷新**
- 更新loginTime为当前时间
- 重新计算expireTime
- 更新Redis中的LoginUser对象
## 五、关键特性
### 5.1 滑动窗口策略
- **触发条件**距离过期时间不足20分钟
- **刷新动作**:重置过期时间为当前时间 + expireTime
- **优势**用户持续活动时Token自动续期无需重新登录
### 5.2 Redis存储
- **Key格式**`login_tokens:{uuid}`
- **Value**LoginUser对象序列化
- **过期时间**与Token过期时间一致
- **优势**:分布式环境下共享会话,自动清理过期数据
### 5.3 双重过期机制
1. **LoginUser.expireTime**:业务层面的过期时间判断
2. **Redis TTL**:存储层面的自动清理机制
## 六、配置建议
### 6.1 生产环境配置
```yaml
token:
expireTime: 120 # 2小时
```
**说明**
- 滑动窗口触发阈值固定为20分钟
- 如果用户在2小时内有任何操作且距离过期不足20分钟会自动续期2小时
- 如果用户超过2小时无操作Token过期需要重新登录
### 6.2 开发环境配置
```yaml
token:
expireTime: 10080 # 7天
```
**说明**
- 方便开发调试,减少频繁登录
- 生产环境不建议设置过长
## 七、安全考虑
### 7.1 Token安全
- 使用JWT签名防止Token篡改
- Token存储在Redis中支持主动失效
- 支持单点登录控制
### 7.2 滑动窗口安全
- 20分钟的滑动窗口阈值平衡用户体验和安全性
- 即使Token被盗用最长有效期仍受expireTime限制
- 可通过删除Redis中的Token实现强制下线
## 八、扩展功能
### 8.1 积木报表Token验证
```java
// JimuReportTokenService.java
public boolean isTokenValid(String token) {
LoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser != null) {
// 检查token是否过期
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
return currentTime < expireTime;
}
return false;
}
```
### 8.2 用户代理信息记录
```java
public void setUserAgent(LoginUser loginUser) {
UserAgent userAgent = UserAgent.parseUserAgentString(
ServletUtils.getRequest().getHeader("User-Agent")
);
String ip = IpUtils.getIpAddr();
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
}
```
## 九、总结
本系统的Token滑动机制通过以下方式实现了高效的会话管理
1. **自动续期**:用户活跃时自动延长会话,提升体验
2. **灵活配置**:通过配置文件调整有效期和滑动窗口
3. **分布式支持**基于Redis实现支持集群部署
4. **安全可控**:双重过期机制,支持主动失效
**核心优势**:在保证安全性的前提下,最大化提升用户体验,避免频繁登录带来的困扰。

View File

@@ -0,0 +1,520 @@
# ATS 批量排产报工接口文档
## 版本信息
- **版本号**v2.0.002
- **创建日期**2026-02-27
- **更新说明**:通过开关参数灵活控制计划、工单、报工单的生成
- **测试地址**
- 前端http://192.168.1.244:80
- 后端http://192.168.1.244:8080
---
## 快速开始
### 核心概念
接口支持通过开关参数灵活控制三个业务模块的生成:
| 模块 | 开关参数 | 默认值 | 说明 |
|------|---------|--------|------|
| 生产计划 | `plan.isGeneratePlan` | `1` | `0`=不生成,`1`=生成 |
| 生产工单 | `workOrder.isGenerateWorkOrder` | `1` | `0`=不生成,`1`=生成 |
| 报工单 | `reportWorkOrder.isGenerateReportWorkOrder` | `1` | `0`=不生成,`1`=生成 |
**核心规则**
- **不传对象** = 使用默认值(生成)
- **传了对象但不传开关** = 使用默认值(生成)
- **传了开关=0** = 明确不生成
- **传了开关=1** = 明确生成
- **传了空字符串 ""** = 使用默认值
- **传了空数组 []** = 使用默认值
- **传了 null** = 使用默认值
**示例**
```json
// 不传plan对象 → 默认生成计划
{"orderNumbers": ["SO001"], "workOrder": {...}}
// 传了plan但不传开关 → 默认生成计划
{"orderNumbers": ["SO001"], "plan": {"assignedUserIds": "101"}}
// 传了开关=0 → 不生成计划
{"orderNumbers": ["SO001"], "plan": {"isGeneratePlan": 0}}
// 传了空字符串 → 使用默认值
{"orderNumbers": ["SO001"], "plan": {"assignedUserIds": ""}} // 从工序路线获取
// 传了空数组 → 使用默认值
{"orderNumbers": ["SO001"], "reportWorkOrder": {"reporters": []}} // 从工序路线获取
```
---
## 常用场景
### 场景1全部生成默认
```json
{
"orderNumbers": ["SO202602270001"]
}
```
**结果**:生成计划 → 生成工单 → 生成报工单
---
### 场景2只生成工单
```json
{
"orderNumbers": ["SO202602270001"],
"plan": {
"isGeneratePlan": 0
},
"workOrder": {
"routeId": 1005,
"processStartTime": "2026-02-27 08:00:00"
},
"reportWorkOrder": {
"isGenerateReportWorkOrder": 0
}
}
```
**结果**:只生成工单,工单状态保持"未完成"
**说明**
- `plan.isGeneratePlan=0` 明确不生成计划
- `workOrder` 传了对象但没传开关,默认生成工单
- `reportWorkOrder.isGenerateReportWorkOrder=0` 明确不生成报工单
---
### 场景3只生成报工单订单已有工单
```json
{
"orderNumbers": ["SO202602270001"],
"plan": {
"isGeneratePlan": 0
},
"workOrder": {
"isGenerateWorkOrder": 0
},
"reportWorkOrder": {
"reporters": [
{"operatorId": 101, "reportTime": "2026-02-27 10:00:00"}
]
}
}
```
**结果**:为已有工单生成报工单,工单状态变为"已完成"
**说明**
- `plan.isGeneratePlan=0` 明确不生成计划
- `workOrder.isGenerateWorkOrder=0` 明确不生成工单
- `reportWorkOrder` 传了对象但没传开关,默认生成报工单
---
### 场景4生成工单+报工单(不生成计划)
```json
{
"orderNumbers": ["SO202602270001"],
"plan": {
"isGeneratePlan": 0
}
}
```
**结果**:生成工单 → 生成报工单(不生成计划)
**说明**:通过设置 `isGeneratePlan=0` 明确不生成计划workOrder和reportWorkOrder使用默认值1会生成工单和报工单
---
### 场景5空值使用默认值
```json
{
"orderNumbers": ["SO202602270001"],
"plan": {
"isGeneratePlan": 1,
"assignedUserIds": ""
},
"workOrder": {
"isGenerateWorkOrder": 1,
"processStartTime": ""
},
"reportWorkOrder": {
"isGenerateReportWorkOrder": 1,
"reporters": []
}
}
```
**结果**:全部生成,所有空值参数都使用系统默认值
**说明**
- `assignedUserIds: ""` → 从工序路线获取所有报工人(去重)
- `processStartTime: ""` → 使用当前时间
- `reporters: []` → 从工序路线获取每个工序的报工人
---
## 接口详情
### 1. 批量自动完成接口
**POST** `http://192.168.1.244:8080/production/autoComplete/batchAutoComplete`
#### 请求参数
##### 必填参数
| 参数 | 类型 | 说明 |
|------|------|------|
| `orderNumbers` | Array\<String\> | 销售订单编号列表 |
##### 可选参数 - plan生产计划
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `isGeneratePlan` | Integer | `1` | `0`=不生成,`1`=生成 |
| `assignedUserIds` | String | 工序路线所有报工人(去重) | 负责人ID逗号分隔`"101,102"`不传、传null或传空字符串 `""` 时,自动从工序路线中获取所有工序的报工人(去重)作为计划负责人 |
##### 可选参数 - workOrder生产工单
| 参数 | 类型 | 默认值 | 说明 |
|------|------|------|------|
| `isGenerateWorkOrder` | Integer | `1` | `0`=不生成,`1`=生成 |
| `routeId` | Long | 自动选择 | 工序路线ID不传、传null时自动选择 |
| `processStartTime` | String | 自动生成 | 首道工序开始时间,格式 `yyyy-MM-dd HH:mm:ss`不传、传null或传空字符串 `""` 时使用当前时间 |
**工序路线选择优先级**
1. `workOrder.routeId`(最高优先级)
2. 订单明细配置的路线
3. 物料默认路线
4. 无路线则返回错误
##### 可选参数 - reportWorkOrder报工单
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `isGenerateReportWorkOrder` | Integer | `1` | `0`=不生成,`1`=生成 |
| `reporters` | Array | 工序路线的报工人 | 报工人列表不传、传null或传空数组 `[]` 时,自动从工序路线中获取每个工序的报工人 |
**reporters 子字段**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `operatorId` | Long | 是 | 报工人用户ID |
| `reportTime` | String | 是 | 报工时间,格式 `yyyy-MM-dd HH:mm:ss` |
**说明**
- 系统通过订单编号反查关联工单
- 只对未完成的工单创建报工记录
- **不传reporters、传null或传空数组 `[]` 时**:系统自动从工序路线中获取每个工序配置的报工人,按工序顺序为每个工单生成报工记录(每个工序一个报工人)
- **传1条reporter时**:该报工人对所有未完成工单均生成报工记录
- **传多条reporters时**:按工序顺序与未完成工单一一映射
---
#### 完整请求示例
```json
{
"orderNumbers": ["SO202602270001", "SO202602270002"],
"plan": {
"isGeneratePlan": 1,
"assignedUserIds": "101,102"
},
"workOrder": {
"isGenerateWorkOrder": 1,
"routeId": 1005,
"processStartTime": "2026-02-27 08:00:00"
},
"reportWorkOrder": {
"isGenerateReportWorkOrder": 1,
"reporters": [
{"operatorId": 101, "reportTime": "2026-02-27 10:00:00"},
{"operatorId": 102, "reportTime": "2026-02-27 14:00:00"}
]
}
}
```
---
#### 响应结果(立即返回)
```json
{
"code": 200,
"msg": "任务已提交,正在后台处理",
"data": {
"taskId": "TASK_20260227_001",
"totalCount": 2,
"estimatedSeconds": 10,
"status": "PENDING",
"message": "任务已创建预计需要10秒完成"
}
}
```
---
### 2. 任务状态查询接口
**GET** `http://192.168.1.244:8080/production/autoComplete/taskStatus/{taskId}`
#### 请求示例
```
GET /production/autoComplete/taskStatus/TASK_20260227_001
```
#### 响应示例(处理中)
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"taskId": "TASK_20260227_001",
"status": "PROCESSING",
"totalCount": 50,
"processedCount": 25,
"successCount": 23,
"failedCount": 2,
"percentage": 50,
"message": "正在处理中已完成50%"
}
}
```
#### 响应示例(已完成)
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"taskId": "TASK_20260227_001",
"status": "COMPLETED",
"totalCount": 50,
"processedCount": 50,
"successCount": 48,
"failedCount": 2,
"percentage": 100,
"message": "任务已完成",
"result": {
"successOrders": ["SO001", "SO002"],
"failedOrders": ["SO025", "SO038"],
"failureReasons": {
"SO025": "订单不存在",
"SO038": "物料未配置工序路线"
}
}
}
}
```
---
## 辅助接口
### 3. 库存式生产自动创建订单
**POST** `http://192.168.1.244:8080/production/plan/autoCreateOrder`
用于库存式生产MTS自动生成以"生产备货"为客户的销售订单。
#### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `materialId` | Long | 是 | 产品ID |
| `materialName` | String | 建议 | 产品名称 |
| `totalQuantity` | BigDecimal | 是 | 生产数量(必须 > 0 |
| `endTime` | String | 是 | 交货日期,格式 `yyyy-MM-dd HH:mm:ss` |
#### 请求示例
```json
{
"materialId": 101,
"materialName": "产品A",
"totalQuantity": 100,
"endTime": "2026-03-31 18:00:00"
}
```
#### 响应示例
```json
{
"code": 200,
"msg": "订单创建成功",
"data": {
"orderId": 999,
"orderNumber": "PL20260227001"
}
}
```
---
### 4. 按用户查询报工单
**GET** `http://192.168.1.244:8080/production/report/list`
#### 查询参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `reportUserName` | String | 是 | 报工人姓名(模糊匹配) |
| `reportTimeQuery[0]` | String | 否 | 报工时间起,格式 `yyyy-MM-dd` |
| `reportTimeQuery[1]` | String | 否 | 报工时间止,格式 `yyyy-MM-dd` |
| `workOrderId` | Long | 否 | 工单ID |
| `pageNum` | Integer | 否 | 页码默认1 |
| `pageSize` | Integer | 否 | 每页条数默认10 |
#### 请求示例
```
GET /production/report/list?reportUserName=张三&reportTimeQuery[0]=2026-02-01&reportTimeQuery[1]=2026-02-27
```
#### 响应示例
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"id": 501,
"reportUserId": 1,
"reportUserName": "张三",
"reportTime": "2026-02-27 10:00:00",
"reportQuantity": 50,
"qualifiedQuantity": 50,
"workOrderId": 301,
"status": "A"
}
],
"total": 1
}
```
---
## 业务规则
### 开关控制规则
**核心原则**:所有开关默认值都是 `1`(生成),只有明确设置为 `0` 才不生成
1. **不传任何对象**:使用默认值,全部生成(计划+工单+报工单)
```json
{"orderNumbers": ["SO001"]}
```
2. **传了对象但不传开关**:使用默认值,该业务生成
```json
{"orderNumbers": ["SO001"], "plan": {"assignedUserIds": "101"}}
// plan没有传isGeneratePlan默认为1生成计划
```
3. **传了开关=0**:明确不生成
```json
{"orderNumbers": ["SO001"], "plan": {"isGeneratePlan": 0}}
// 明确设置为0不生成计划
```
4. **传了开关=1**:明确生成(与默认行为相同)
```json
{"orderNumbers": ["SO001"], "plan": {"isGeneratePlan": 1}}
// 明确设置为1生成计划
```
### 依赖关系
| 场景 | 说明 |
|------|------|
| 报工单依赖工单 | 如果 `isGenerateWorkOrder=0` 且 `isGenerateReportWorkOrder=1`,则只为已有工单生成报工单;如果订单无工单,跳过该订单 |
| 计划独立 | 计划的生成不依赖工单和报工单 |
| 工单独立 | 工单可以单独生成,不依赖计划和报工单 |
### 异步处理流程
1. **提交任务**<1秒验证参数 → 创建任务记录 → 立即返回任务ID
2. **异步执行**(后台):循环处理订单 → 每5个订单更新一次进度
3. **查询状态**<100ms根据任务ID查询实时进度
### 性能说明
| 订单数 | 预计耗时 | 数据库更新次数 |
|--------|---------|--------------|
| 5个 | ~5秒 | 4次 |
| 10个 | ~10秒 | 5次 |
| 50个 | ~50秒 | 13次 |
| 100个 | ~100秒 | 23次 |
**并发限制**
- 线程池大小10个线程
- 最大并发任务数10个
- 单个任务最大执行时间5分钟
- 建议单次批量不超过100个订单
---
## 场景速查表
| 场景 | 参数配置示例 | 结果 |
|------|------------|------|
| 全部生成 | `{}` 不传任何对象 | 计划+工单+报工单 |
| 只生成计划 | `{plan: {}, workOrder: {isGenerateWorkOrder: 0}, report: {isGenerateReportWorkOrder: 0}}` | 只生成计划 |
| 只生成工单 | `{plan: {isGeneratePlan: 0}, workOrder: {}, report: {isGenerateReportWorkOrder: 0}}` | 只生成工单 |
| 只生成报工单 | `{plan: {isGeneratePlan: 0}, workOrder: {isGenerateWorkOrder: 0}, report: {}}` | 只为已有工单生成报工单 |
| 工单+报工单 | `{plan: {isGeneratePlan: 0}}` | 生成工单和报工单 |
| 计划+工单 | `{report: {isGenerateReportWorkOrder: 0}}` | 生成计划和工单 |
**说明**
- 开关默认值都是1生成
- 不传对象 = 使用默认值1 = 生成
- 传了对象但不传开关 = 使用默认值1 = 生成
- 只有明确设置开关=0才不生成
---
## 版本历史
| 版本号 | 日期 | 变更说明 |
|--------|------|---------|
| v2.0.000 | 2026-02-24 | 初始版本,支持基础批量排产 |
| v2.0.002 | 2026-02-27 | 新增开关参数,支持灵活控制计划、工单、报工单的生成 |
---
## 注意事项
1. **认证方式**:接口已配置匿名访问,无需认证
2. **异步执行**接口立即返回任务ID实际处理在后台进行
3. **进度查询**:通过任务状态查询接口轮询获取进度
4. **错误处理**:单个订单失败不影响其他订单,最终返回详细的成功/失败列表
5. **工序路线**:优先使用 `workOrder.routeId`,其次使用订单/物料配置的路线
6. **默认值逻辑**
- **计划负责人**:不传 `plan.assignedUserIds`、传null或传空字符串 `""` 时,系统自动从工序路线中获取所有工序的报工人(去重)作为计划负责人
- **工序开始时间**:不传 `workOrder.processStartTime`、传null或传空字符串 `""` 时,系统使用当前时间
- **报工单报工人**:不传 `reportWorkOrder.reporters`、传null或传空数组 `[]` 时,系统自动从工序路线中获取每个工序配置的报工人,按工序顺序生成报工记录
7. **空值处理规则**
- 字符串类型:`null`、`""`(空字符串)、不传 → 都使用默认值
- 数组类型:`null`、`[]`(空数组)、不传 → 都使用默认值
- 数值类型:`null`、不传 → 使用默认值

View File

@@ -0,0 +1,256 @@
# Knife4j 成功配置说明
`http://localhost:8080/doc.html`
## 📋 查看所有模块分组
### 方法1使用分组下拉框
在Knife4j界面的**左上角**,有一个下拉框,点击它可以切换不同的模块分组:
1. 全部接口
2. 生产模块
3. 销售模块
4. 仓库模块
5. 主数据模块
6. 设备模块
7. 质量模块
8. 系统模块
**位置**:页面左上角,"YJH-MES 制造执行系统 API文档" 标题下方
### 方法2直接访问分组API文档
你也可以直接访问特定分组的API文档
```
http://localhost:8080/v3/api-docs?group=全部接口
http://localhost:8080/v3/api-docs?group=生产模块
http://localhost:8080/v3/api-docs?group=销售模块
http://localhost:8080/v3/api-docs?group=仓库模块
http://localhost:8080/v3/api-docs?group=主数据模块
http://localhost:8080/v3/api-docs?group=设备模块
http://localhost:8080/v3/api-docs?group=质量模块
http://localhost:8080/v3/api-docs?group=系统模块
```
## 🎯 如何使用Knife4j
### 1. 切换模块分组
- 点击左上角的下拉框
- 选择你想查看的模块(如"生产模块"
- 左侧会显示该模块的所有接口
### 2. 查看接口详情
- 在左侧接口列表中点击接口
- 右侧会显示接口的详细信息:
- 接口路径
- 请求方法GET/POST/PUT/DELETE
- 请求参数
- 响应示例
### 3. 测试接口
#### 步骤1获取Token
1. 切换到"系统模块"或"全部接口"
2. 找到 `POST /login` 接口
3. 点击接口,切换到"调试"标签
4. 在请求参数中输入:
```json
{
"username": "admin",
"password": "admin123"
}
```
5. 点击"发送"按钮
6. 在响应中找到 `token` 字段复制token值
#### 步骤2设置全局Token
1. 点击页面右上角的"文档管理"按钮(齿轮图标)
2. 选择"全局参数设置"
3. 点击"添加参数"按钮
4. 填写参数信息:
- **参数名称**`Authorization`
- **参数值**粘贴刚才复制的token
- **参数类型**:选择 `header`
- **参数位置**:选择 `header`
5. 点击"确定"保存
现在所有接口都会自动带上这个token
#### 步骤3测试其他接口
1. 切换到你想测试的模块(如"生产模块"
2. 选择一个接口(如批量自动完成接口)
3. 点击接口,切换到"调试"标签
4. 填写必要的参数
5. 点击"发送"按钮
6. 查看响应结果
### 4. 批量自动完成接口测试示例
1. 切换到"生产模块"
2. 找到 `POST /production/autoComplete/batchAutoComplete` 接口
3. 切换到"调试"标签
4. 在请求体中输入:
```json
{
"orderNumbers": ["XS20260306002"],
"plan": {
"isGeneratePlan": 1,
"assignedUserIds": "1,2,3"
},
"workOrder": {
"isGenerateWorkOrder": 1,
"routeId": 1,
"processStartTime": "2026-03-06 12:00:00"
},
"reportWorkOrder": {
"isGenerateReportWorkOrder": 0,
"reporters": []
}
}
```
5. 点击"发送"
6. 查看响应结果
## 🎨 Knife4j特色功能
### 1. 搜索接口
- 点击右上角的搜索图标(🔍)
- 输入接口路径或描述
- 快速定位到目标接口
### 2. 离线文档
- 点击右上角"离线文档"按钮
- 选择格式:
- Markdown适合文档编辑
- HTML可以直接在浏览器打开
- 点击"下载"
### 3. 主题切换
- 点击右上角的主题切换按钮
- 选择你喜欢的主题:
- 默认主题(蓝色)
- 暗黑主题(黑色)
- 简约主题(白色)
### 4. 接口排序
- 在左侧接口列表上方有排序按钮
- 可以按:
- 默认排序
- 字母排序
- 标签排序
## 📊 界面说明
```
┌─────────────────────────────────────────────────────────┐
│ [分组下拉框▼] YJH-MES API文档 [搜索🔍] [主题] [文档管理⚙️] │
├──────────────┬──────────────────────────────────────────┤
│ │ │
│ 接口列表 │ 接口详情 / 调试区域 │
│ │ │
│ 📁 Controller1 │ 接口路径: POST /api/xxx │
│ └─ 接口1 │ 请求参数: [...] │
│ └─ 接口2 │ 响应示例: [...] │
│ │ │
│ 📁 Controller2 │ [调试] [文档] 标签 │
│ └─ 接口3 │ │
│ │ │
└──────────────┴──────────────────────────────────────────┘
```
## 🔧 如果看不到其他模块
### 检查1确认分组下拉框位置
分组下拉框在页面**左上角**,标题"YJH-MES 制造执行系统 API文档"的**下方**。
### 检查2刷新页面
按 `Ctrl+F5` 强制刷新页面,清除缓存。
### 检查3查看浏览器控制台
1. 按 `F12` 打开开发者工具
2. 切换到 `Console` 标签
3. 查看是否有错误信息
### 检查4验证分组是否存在
访问以下地址查看是否返回JSON数据
```
http://localhost:8080/v3/api-docs?group=生产模块
```
如果返回JSON数据说明分组配置正确。
## 📝 导出API文档
### 导出为Markdown
1. 点击右上角"离线文档"
2. 选择"Markdown"
3. 点击"下载"
4. 得到 `.md` 文件
### 导出为HTML
1. 点击右上角"离线文档"
2. 选择"HTML"
3. 点击"下载"
4. 得到 `.html` 文件,可以直接在浏览器打开
### 导出为OpenAPI JSON
访问:
```
http://localhost:8080/v3/api-docs
```
复制JSON内容可以导入到
- Postman
- Apifox
- Insomnia
- 其他支持OpenAPI 3.0的工具
## 🎉 成功标志
你已经成功配置了Knife4j现在可以
- ✅ 访问 `http://localhost:8080/doc.html`
- ✅ 看到美观的Knife4j界面
- ✅ 切换不同的模块分组
- ✅ 查看接口详情
- ✅ 测试接口
- ✅ 设置全局Token
- ✅ 导出离线文档
## 📚 相关文档
- **Knife4j使用指南**`.tasks/2026-03-06_Knife4j使用指南.md`
- **批量自动完成接口文档**`.tasks/2026-02-27_v2.0.002_ATS接口文档.md`
## 💡 小贴士
1. **全局Token设置**:设置一次,所有接口都会自动带上
2. **搜索功能**:快速定位接口,支持模糊搜索
3. **离线文档**:可以导出给团队成员查看
4. **主题切换**:根据个人喜好选择主题
5. **响应示例**:每个接口都有详细的响应示例
---
**恭喜你成功配置了API文档系统** 🎉
**更新时间**2026-03-02

View File

@@ -0,0 +1,551 @@
# 报工单升级优化分析文档
**版本:** v2.0.003 **日期:** 2026-03-03 **负责人:** 周启威
---
## 一、现有 API 数据摸底
```json
{
"workOrderId": 3555, "workOrderNumber": "20260308054",
"workOrderEntryId": 3923, "workOrderName": "普罗酪蛋白酸钠",
"materialId": 28, "materialName": "普罗酪蛋白酸钠",
"batchNumber": "XS20260308009", "quantity": 189.000,
"planFinishDate": "2026-03-08",
"processName": "碘酸", "processId": 6,
"routeName": "酪蛋白酸钠工序路线",
"workshopId": 1, "workshopName": "酪蛋白酸钠车间",
"stationId": 1, "stationName": "配料工位",
"customerName": null
}
```
**重要发现:**
- `processName`/`processId` 虽在 `Report.java` 标注 `@TableField(exist=false)`,但 Mapper XML 已 JOIN 返回真实值
- `customerName` 字段已有 JOIN 逻辑;当前示例返回为 null说明在某些数据下仍可能为空。按业务约束“无销售订单就无工单”时该字段应有值若线上仍出现 null需后端在查询时补齐或明确业务允许为空
- `materialName``batchNumber``quantity` 均已返回,可在新增时直接回显
**与你本次确认的补充:**
- 工单表 `pro_workorder` 已存在字段 `process_start_time`(实体字段 `processStartTime`),本次 `plan_start_time` 以此字段作为来源,避免自创字段名
- 工序时长来源为工单工序明细 `pro_workorder_entry.duration`(实体字段 `WorkOrderEntry.duration`,单位:秒)
---
## 二、字段说明(含冗余字段处理)
### 2.1 已移除字段(见 SQL 注释)
| 字段 | 原说明 | 现阶段处理 |
|------|--------|-----------|
| `report_count` | 报工数 | 与 `report_quantity` 语义完全相同,**已从 SQL 中移除**,直接用 `report_quantity` |
| `quality_check_status` | 质检合格(合格/不合格) | 与现有 `quality_status`B=待检验/C=已通过/D=有异常)重叠,**已从 SQL 中移除** |
### 2.2 字段修改(现有字段调整)
| 字段 | 原说明 | 现阶段调整 |
|------|--------|-----------|
| `is_settle` | 是否结算工资(只读) | **改为可编辑**,前端去掉 `:disabled="true"` |
| `wages` | 结算工资(只读) | **改为可编辑**,前端去掉 `:disabled="true"` |
---
## 三、新增字段完整说明
### 3.1 新增时自动回显字段(进入页面即自动填充,用户可见)
> 区别于"保存时赋值"——这些字段在打开新增报工单表单时就已经显示出来,用户无需输入。
| 新增字段 | 数据来源 | 来源字段已在API响应中 |
|----------|---------|------------------------|
| `customer_name` / `customer_id` | 工单关联销售订单客户 | `customerName`(当前示例可能为 null`customer_id` 需要后端同步返回(或在保存时从工单关联关系写入) |
| `workpiece` | 工件/成品名称 | = `materialName` |
| `product_batch` | 产品批次 | = `batchNumber` |
| `process_name` / `process_id` | 工序 | = `processName` / `processId`(已 JOIN |
| `plan_start_time` | 计划开始时间 | = 工单 `process_start_time`(实体字段 `processStartTime` |
| `plan_end_time` | 计划完工时间 | = `process_start_time` + 工单工序明细 `duration``WorkOrderEntry.duration`,单位秒)(**注意:不是 planFinishDate** |
| `plan_count` | 计划数 | = 工单排产数量 `quantity` |
### 3.2 默认值字段(手动填写,但有明确默认值)
| 新增字段 | 默认值 | 说明 |
|----------|--------|------|
| `report_type` | `NORMAL`(正常报工) | 字典 `production_report_type`,用户可修改 |
| `actual_start_time` | = `plan_start_time` | 用户可修改为实际时间 |
| `actual_end_time` | = `plan_end_time` | 用户可修改为实际时间 |
| `performance_wages` | `0` | 手动填写 |
| `energy_consumption` | `0` | 手动填写 |
| `material_consumption` | `0` | 手动填写 |
| `other_costs` | `0` | 手动填写 |
| `value_added` | `0` | 手动填写 |
| `downtime_minutes` | `0` | 已有字段默认0 |
| `yield_rate` | 自动计算 = `qualifiedQuantity / reportQuantity × 100` | 前端实时更新 |
| `station_oee` | 自动计算(公式待完善,暂可手动编辑) | 前端计算 |
| `current_benefit` | 自动计算 = `value_added - energy_consumption - material_consumption - other_costs` | 前端计算 |
### 3.3 纯手动填写字段无默认值SQL 新增列)
| 新增字段 | 说明 |
|----------|------|
| `execution_standard` | 执行标准,手动输入,默认空 |
| `team_name` / `team_id` | 班组,字典 `production_team` 下拉选择,默认空 |
| `theoretical_cycle_time` | 理论节拍(分钟/件),用于 OEE 参考,默认空 |
| `station_exception` | 工位异常情况,文本域,默认空 |
| `rectification_suggestion` | 整改建议,文本域,默认空 |
### 3.4 已有字段——本次仅扩展 UI 支持(**不在 SQL 中重复 ADD**
> 以下字段在 `2025-11-11_02_周启威_连续制造业改进.sql` 中已添加到 `pro_report`
> 且 `Report.java` 中无 `@TableField(exist=false)` 标注(已持久化),
> **本次 SQL 不得重复添加,否则报错。仅需前端 UI 向离散制造也开放该字段。**
| 字段 | DB 现状 | 本次动作 |
|------|---------|---------|
| `shift_name`(班次) | 已存在,已持久化 | 离散制造前端新增选择控件 |
| `downtime_reason`(停机原因) | 已存在,已持久化 | 离散制造前端新增输入控件 |
| `downtime_minutes`(停机时间) | 已存在,已持久化,默认 0 | 离散制造前端新增输入控件 |
### 3.5 后端自动写入字段(不暴露到前端)
| 字段 | 写入时机 | 值 |
|------|---------|-----|
| `report_submit_time` | 保存时 | `new Date()` |
---
## 四、字段适用范围(离散 vs 连续制造)
> 原来部分字段仅限"连续制造",现统一:**离散和连续制造均需支持以下字段**。
| 字段 | 离散制造formA.vue | 连续制造form.vue |
|------|---------------------|-------------------|
| 班次 `shift_name` | ✅ 需要 | ✅ 需要 |
| 班组 `team_name` | ✅ 需要 | ✅ 需要 |
| 实际开始时间 `actual_start_time` | ✅ 需要(默认=计划开始时间) | ✅ 需要(默认=reportPeriodStart |
| 实际完工时间 `actual_end_time` | ✅ 需要(默认=计划完工时间) | ✅ 需要(默认=reportPeriodEnd |
| 停机时间 `downtime_minutes` | ✅ 需要默认0 | ✅ 需要已有默认0 |
| 停机原因 `downtime_reason` | ✅ 需要 | ✅ 需要(已有) |
---
## 五、前端计算字段说明
### 5.1 良品率(`yield_rate`
```
良品率 = qualifiedQuantity / reportQuantity × 100%
触发时机reportQuantity 或 qualifiedQuantity 任意变化时实时重算
颜色规则:>= 95% → 绿色90%-95% → 橙色;< 90% → 红色
```
### 5.2 本次效益(`current_benefit`
```
本次效益 = value_added - (energy_consumption + material_consumption + other_costs)
触发时机:相关数字字段变化时实时重算
颜色规则:正值 → 绿色;负值 → 红色0 → 灰色
```
### 5.3 工位OEE`station_oee`
```
现阶段字段可编辑用户可手动填写百分比值0-100
后续待定:当有完整数据后补充自动计算逻辑(可能需要 theoretical_cycle_time
```
---
## 六、快捷字典编辑按钮需求
### 6.1 需求说明
报工单表单中,凡是字典下拉字段,旁边增加 ⚙️ 小按钮。
点击后在新 Tab 打开该字典的数据维护页,无需导航系统菜单。
### 6.2 涉及字典字段
| 字段 | 字典类型 | 分组 | 需要按钮 |
|------|----------|------|---------|
| 报工类型 | `production_report_type` | A-基础信息 | ✅ |
| 班次 | `sys_shift_type` | A-基础信息 | ✅ |
| 班组 | `production_team` | A-基础信息 | ✅ |
| 是否结算工资 | `sys_yes_no` | D-成本效益 | ❌ 系统字典不建议编辑 |
### 6.3 技术方案
```js
// 引入
import { listType } from '@/api/system/dict/type'
// 方法
openDictEdit(dictType, dictName) {
listType({ dictType, pageSize: 10 }).then(response => {
if (response.rows && response.rows.length > 0) {
const dictId = response.rows[0].dictId
this.$tab.openPage('字典数据-' + dictName, `/system/dict/data/${dictId}`)
} else {
this.$modal.msgWarning('未找到字典:' + dictName)
}
})
}
```
```html
<!-- 按钮布局 -->
<div style="display:flex;align-items:center;gap:4px">
<el-select v-model="..." style="flex:1">...</el-select>
<el-tooltip content="快速编辑字典" placement="top">
<el-button type="text" icon="el-icon-setting"
@click.stop="openDictEdit('production_report_type','报工类型')"
v-hasPermi="['system:dict:edit']" />
</el-tooltip>
</div>
```
---
## 七、页面布局优化方案
### 7.1 整体布局结构
```
[工单信息只读区 - el-descriptions]
[+ 添加记录] [- 删除] [保存]
[报工记录 #1 - el-collapse 卡片]
▶ A. 基础信息(默认展开)
▶ B. 数量信息(默认展开)
▶ C. 时间节拍(默认折叠)
▶ D. 成本效益(默认折叠)
▶ E. 异常记录(默认折叠,有内容时标题标红)
[报工记录 #2 ...]
```
### 7.2 工单信息只读区(扩展现有 el-descriptions
现有字段保持,新增第三行:
| 客户名称 | 工件/成品名称 | 产品批次 | 执行标准(只读占位) | 工序路线 |
### 7.3 A. 基础信息(默认展开)
| 字段 | 控件 | 默认值 | 必填 | 字典按钮 |
|------|------|--------|------|---------|
| 报工人 | el-select | 当前登录用户 | 是 | — |
| 报工时间 | el-date-picker datetime | 当前时间 | 是 | — |
| 报工类型 | el-select`production_report_type` | NORMAL | 是 | ✅ |
| 班次 | el-select`sys_shift_type` | 空 | 否 | ✅ |
| 班组 | el-select`production_team` | 空 | 否 | ✅ |
| 执行标准 | el-input | 空 | 否 | — |
| 车间 | el-select | 用户工位绑定 | 是 | — |
| 工位 | el-select | 用户工位绑定 | 是 | — |
### 7.4 B. 数量信息(默认展开)
| 字段 | 控件 | 默认值 | 说明 |
|------|------|--------|------|
| 计划数(只读) | 文本展示 | = workOrder.quantity | 自动回显 |
| 报工数量 | el-input-number | — | 必填 |
| 合格数量 | el-input-number | = 报工数量 | 质检后修改 |
| 不合格数量 | el-input-number | 0 | 自动 = 报工数 - 合格数 |
| 良品率(只读) | 百分比 + 颜色 | 自动计算 | >=95%绿/90-95%橙/<90% |
### 7.5 C. 时间节拍(默认折叠)
| 字段 | 控件 | 默认值 | 说明 |
|------|------|--------|------|
| 计划开始时间只读 | 文本展示 | = processStartTime工单 `process_start_time` | 自动回显 |
| 计划完工时间只读 | 文本展示 | = processStartTime + duration工单工序明细 `WorkOrderEntry.duration` | 自动回显 |
| 实际开始时间 | el-date-picker datetime | = plan_start_time | 离散/连续均有 |
| 实际完工时间 | el-date-picker datetime | = plan_end_time | 离散/连续均有 |
| 理论节拍分钟/ | el-input-number | | 手动OEE参考 |
| 停机时间分钟 | el-input-number | 0 | 离散/连续均有 |
| 停机原因 | el-input | | 停机时间>0时高亮提示填写 |
### 7.6 D. 成本效益(默认折叠)
| 字段 | 控件 | 默认值 | 说明 |
|------|------|--------|------|
| 绩效工资 | el-input-number | 0 | 手动 |
| 能耗 | el-input-number | 0 | 手动 |
| 工耗 | el-input-number | 0 | 手动 |
| 其他成本 | el-input-number | 0 | 手动 |
| 增值 | el-input-number | 0 | 手动 |
| 工位OEE | el-input-number暂可编辑 | 空 | 前端计算,公式待完善 |
| 本次效益(自动) | 数字展示 + 颜色 | 自动计算 | = 增值-能耗-工耗-其他 |
| 是否结算工资 | el-select`sys_yes_no` | N | **可编辑**(原只读改为可编辑) |
| 结算工资 | el-input-number | 0 | **可编辑**(原只读改为可编辑) |
### 7.7 E. 异常记录(默认折叠,有内容标题变红)
| 字段 | 控件 | 默认值 |
|------|------|--------|
| 工位异常情况 | el-input textarea | 空 |
| 整改建议 | el-input textarea | 空 |
| 备注 | el-input textarea | 空(已有字段移至此处) |
---
## 八、移动端适配要求(重要)
> 移动端适配为本次升级的重点之一,与 PC 端同等优先级。
### 8.1 折叠面板
- 移动端(`isMobile === true`)所有分组**默认全部展开**
- 折叠/展开控制隐藏(移动端无折叠手势)
### 8.2 布局
- 所有字段单列排布(`el-col :xs="24"`
- PC 端双列(`el-col :sm="12"`
### 8.3 控件适配
- `el-date-picker` 使用 `type="datetime"`,移动端保持原生表现
- `el-input-number` 设置 `controls-position="right"` 避免移动端误触
- 字典编辑按钮在移动端隐藏(`v-if="!isMobile"`),按你确认项 #4 执行
### 8.4 工单信息只读区
- 移动端改为竖向列表(`el-descriptions direction="vertical"`
- 每项占满整行
### 8.5 按钮区
- 底部操作按钮固定(`position: sticky; bottom: 0`
- 保存按钮宽度 100%
---
## 九、SQL 变更清单
见文件 `.sql/2026-03-03_v2.0.003_周启威_报工单升级优化.sql`
| 操作 | 说明 |
|------|------|
| ALTER TABLE pro_report ADD COLUMN... | 新增 **27 个字段**shift_name/downtime_minutes/downtime_reason 已有,**不在本 SQL 中** |
| 移除 report_count | 与 report_quantity 重复,已从 SQL 删除,附注释说明 |
| 移除 quality_check_status | 与 quality_status 重复,已从 SQL 删除,附注释说明 |
| is_settle / wages 改为可编辑 | 数据库字段不变,仅前端去掉 disabledSQL 注释说明 |
| 新增字典 production_report_type | 4个枚举值 |
| 新增字典 production_team | 甲/乙/丙班(字典管理中可维护) |
| 新增索引 idx_customer_id 等 | 4个索引 |
**必填项核对(以你项目现状为准,不做“强行 NOT NULL 创新”):**
- `pro_report` 现有核心字段为强制必填(数据库 `NOT NULL`
- `work_order_entry_id``report_user_id``report_user_name``report_channel``status``create_by``create_time``report_time``report_quantity``is_settle``wages`
- 本次新增字段在 SQL 中均为 `DEFAULT NULL` 或有默认值(如成本类默认 0、`report_type` 默认 NORMAL。原因
- 兼容历史数据/已存在报工单记录
- 避免上线时因存量数据缺字段导致迁移失败
- 业务必填更多应由前端校验/后端保存校验保证
**⚠️ SQL 执行后后端必须同步Report.java 三处 `@TableField(exist=false)` 需删除**
```java
// 删除以下三行注解,让 MyBatis-Plus 把新列作为正常 DB 字段写入
// processName
@TableField(exist = false) // ← 删除此行
private String processName;
// processId
@TableField(exist = false) // ← 删除此行
private Long processId;
// customerName
@TableField(exist = false) // ← 删除此行
private String customerName;
```
**⚠️ 前端 form.vue dicts 数组需补充两个新字典类型**
```js
// 现有line 233
dicts: ['currency','production_status','report_channel','sys_yes_no','qc_type','sys_shift_type']
// 修改后(新增 production_report_type 和 production_team
dicts: ['currency','production_status','report_channel','sys_yes_no','qc_type','sys_shift_type','production_report_type','production_team']
```
**⚠️ processStartTime 为空时的 fallback 策略**
`pro_workorder.process_start_time` 在未排产/未开工时可能为 null。
前端回显逻辑(优先级依次降低):
1. 优先使用 `form.processStartTime`
2. 若为空fallback 用 `form.beginProDate`(工单开始生产日期)
3. 若仍为空,`plan_start_time` 显示为空,用户手动填写
---
## 十、工作量评估
> 图例:**XS** <2h**S** 半天|**M** 1天**L** 2天
| 模块 | 内容 | 工作量 |
|------|------|--------|
| SQL 执行 | 已写好,执行即可 | XS |
| 后端实体 | Report.java 新增 27 字段;删除 processName/processId/customerName 的 `@TableField(exist=false)` | S |
| 后端 Mapper | XML 增删字段映射INSERT/SELECT | S |
| 后端 Service | insertReport 时将已有字段赋值到新列(直接复制,无额外查询) | S |
| 前端工单信息区 | el-descriptions 扩展新只读字段 | XS |
| 前端字典按钮 | openDictEdit 方法 + 3处按钮 | XS |
| 前端表单重构PC | form.vue + formA.vue 折叠分组布局 | L |
| 前端表单重构(移动端) | 响应式布局、全展开、按钮固定 | M |
| 前端计算逻辑 | 良品率/本次效益实时计算OEE 暂编辑框 | S |
| 联调测试 | 自动回显验证、计算验证、移动端真机测试 | S |
| **合计** | | **约 4 个工作日** |
---
## 十一、实施顺序建议
| 顺序 | 任务 | 依赖 |
|------|------|------|
| 1 | 执行 SQL 脚本 | 无 |
| 2 | 后端实体 + Mapper + Service | SQL 完成后 |
| 3 | 前端工单信息区扩展(快速见效) | 后端完成后 |
| 4 | 前端折叠表单重构PC | — |
| 5 | 前端移动端适配 | 步骤4基础上 |
| 6 | 字典编辑按钮 | — |
| 7 | 联调测试 + 修复 | 全部完成后 |
---
## 十二、待确认事项(你确认后再进入代码实现)
| 序号 | 待确认点 | 当前文档建议 | 影响范围 |
|------|----------|-------------|----------|
| 1 | `customer_name/customer_id` 是否必须存在 | 按业务应必有;若线上仍出现 null需明确是否允许为空并决定 `customer_id` 获取方式(查询返回 or 保存时回填) | 后端查询/保存逻辑、前端只读展示 |
| 2 | `plan_start_time` 的准确来源字段 | **已按你确认:使用工单 `process_start_time``processStartTime`** | 后端赋值、前端只读展示 |
| 3 | `plan_end_time` 的工序时长 `duration` 来源 | **已按你确认:使用工单工序明细 `WorkOrderEntry.duration`** | 后端赋值、前端只读展示 |
| 4 | 移动端是否需要“字典快捷按钮” | **已按你确认:移动端隐藏** | 前端交互设计 |
---
## 十三、项目内可复用点(能复用就不创新)
### 13.1 字段与表结构复用(避免新造字段名)
| 目标 | 复用项 | 位置 |
|------|--------|------|
| 计划开始时间来源 | `pro_workorder.process_start_time` / `WorkOrder.processStartTime` | `WorkOrder.java` + `WorkOrderMapper.xml` 已映射 |
| 工序时长来源 | `pro_workorder_entry.duration` / `WorkOrderEntry.duration`(秒) | `WorkOrderEntry.java` 已存在 |
| 工单-工序明细联动 | 工单查询已 `left join pro_workorder_entry` 并返回 `workOrderEntryList` | `WorkOrderMapper.xml``selectWorkOrderVo` |
### 13.2 时间计算逻辑复用(不自写新公式)
项目内已有工具类:`cn.sourceplan.production.util.RouteTimeCalculator`
- 连续制造:使用等待开始时间 `waitStartTime` + `duration`
- 离散制造:顺序推进 + 转运时间 `transferTime` + `duration`
本次仅需“单工序的 plan_start_time/plan_end_time”时优先直接用
- `plan_start_time = workOrder.processStartTime`
- `plan_end_time = plan_start_time + workOrderEntry.duration`
若后续需要“整条路线的各工序时间”再复用 `RouteTimeCalculator.calculateProcessTimes(...)`,不在本次强行扩展。
### 13.3 前端打开字典维护页复用
项目内已有页签能力:`this.$tab.openPage(title, path)`(见 `mes-ui/src/plugins/tab.js`
本次字典快捷按钮仅在 PC 显示:
- 复用 `@/api/system/dict/type``listType` 查询 `dictId`
- 再用 `$tab.openPage` 打开 `/system/dict/data/{dictId}`
### 13.4 报工现有字段复用(不新增重复字段)
报工表 `pro_report` 现有已具备(尤其连续制造已在使用):
- `shift_name``report_period_start``report_period_end`
- `downtime_minutes``downtime_reason`
本次升级仅做“离散/连续统一支持 + UI 分组优化 + 新增成本/效益字段持久化”,不重复造相同语义字段。
---
## 十四、字段来源全景说明(项目模块 + 行业参考)
> 本节结合本系统实际模块关系,以及 SAP PP / Oracle MES / 用友U9C / 金蝶云星空等行业主流 MES 系统的报工单设计,说明每个字段在实际生产场景中的来源和意义。
### 14.1 A 区 — 基础信息字段
| 字段 | 本系统来源模块 | 本系统来源表/字段 | 行业做法参考 |
|------|-------------|----------------|-------------|
| `report_user_id` / `report_user_name` | **系统用户模块** | `sys_user.user_id / nick_name`,前端当前登录用户 | SAP PP`Operator ID`,由 HR 工号绑定Oracle`Resource Person` 来自员工主数据 |
| `report_time` | **前端自动** | 当前时间 `new Date()`,用户可修改(补报场景) | 通用做法默认系统时间允许补报修改SAP PP 称 `Confirmation Date` |
| `report_type` | **字典模块** | `sys_dict_data`(字典类型 `production_report_type`,本次新增) | SAP PP`Confirmation Type`(正式/取消/返工用友U9C报工类型正常/返工/补报);金蝶:报工性质 |
| `shift_name` | **字典模块** | `sys_dict_data`(字典类型 `sys_shift_type`,原有字典) | 全行业标配SAP PP 称 `Shift`从工厂日历继承Oracle MES 称 `Shift Code` |
| `team_name` / `team_id` | **字典模块** | `sys_dict_data`(字典类型 `production_team`,本次新增);`team_id` 存字典 `remark` 字段 | SAP PP`Work Center Group`金蝶云星空生产班组独立主数据用友U9C班组来自 HR 班组台账 |
| `execution_standard` | **手动录入** | 无系统来源,用户自填(如 GB/T 标准号) | ISO 9001 要求SAP PP`Control Key` 间接关联作业标准;食品/化工行业强制要求 |
| `workshop_id` / `workshop_name` | **车间工位模块** | `pro_workshop.id / name`[getWorkshops()](cci:1://file:///e:/Yavii_P3/MES/mes-ui/src/views/mes/production/report/formA.vue:874:4-880:5) API 加载 | 全行业标配SAP PPPlant + Work Center 两级;本系统:车间→工位两级结构 |
| `station_id` / `station_name` | **车间工位模块** | `pro_station.id / name`,级联车间下拉 | SAP PP`Work Center`Oracle MES`Resource / Machine`;金蝶:工作中心 |
### 14.2 B 区 — 数量信息字段
| 字段 | 本系统来源模块 | 本系统来源表/字段 | 行业做法参考 |
|------|-------------|----------------|-------------|
| `plan_count` | **生产工单模块** | `pro_workorder.quantity`(工单排产总数),[insertReport](cci:1://file:///e:/Yavii_P3/MES/yjh-mes/src/main/java/cn/sourceplan/production/service/impl/ReportServiceImpl.java:495:4-718:5) 时自动填入 | SAP PP`Order Quantity`Oracle MES`Job Quantity`;全行业报工必带,用于进度对比 |
| `report_quantity` | **手动录入(必填)** | 用户输入本次完成数量 | SAP PP`Yield Quantity`Oracle`Transaction Quantity`;金蝶:完工数量 |
| `qualified_quantity` | **手动录入 / 质检回填** | 用户输入或质检单确认后回填;触发良品率实时计算 | SAP PP`Confirmed Yield`用友U9C合格数量来自质检单金蝶合格入库数量 |
| `unqualified_quantity` | **前端计算** | = `report_quantity - qualified_quantity`,前端实时计算后保存持久化 | SAP PP`Scrap Quantity`Oracle`Rejected Quantity`;全行业通常自动推算 |
| `yield_rate` | **前端计算** | = `qualified_quantity / report_quantity × 100`,前端实时计算 | SAP PP由 QM 模块统计Oracle MES`Yield %`;化工/食品行业生产 KPI 核心指标 |
### 14.3 C 区 — 时间节拍字段
| 字段 | 本系统来源模块 | 本系统来源表/字段 | 行业做法参考 |
|------|-------------|----------------|-------------|
| `plan_start_time` | **生产工单模块** | `pro_workorder.process_start_time`[insertReport](cci:1://file:///e:/Yavii_P3/MES/yjh-mes/src/main/java/cn/sourceplan/production/service/impl/ReportServiceImpl.java:495:4-718:5) 时自动填入 | SAP PP`Scheduled Start Date`,来自 MRP/APS 排程结果;本系统暂由工单手工维护 |
| `plan_end_time` | **工单模块 + 工序明细** | = `process_start_time + pro_workorder_entry.duration`(秒);前端 [fillDefaultPlanEndTime](cci:1://file:///e:/Yavii_P3/MES/mes-ui/src/views/mes/production/report/formA.vue:837:4-850:5) 计算默认值,可手动修改 | SAP PP`Scheduled Finish Date`,由 Work Center 标准工时自动推算Oracle`Scheduled End Date` |
| `actual_start_time` | **手动录入(默认=计划开始)** | 默认 = `plan_start_time`,用户调整为实际打卡/扫码时间 | SAP PP`Actual Start Date`Oracle MES`Actual Start`,常由 WIP 事务自动记录;金蝶:实际开工时间 |
| `actual_end_time` | **手动录入(默认=计划完工)** | 默认 = `plan_end_time`,用户调整为实际完成时间 | SAP PP`Actual Finish Date`;是工时统计和效率分析的核心数据 |
| `theoretical_cycle_time` | **手动录入** | 来自 IE 工业工程部门的标准工时,单位:分钟/件,系统无自动来源 | SAP PP`Standard Work`(每件标准工时)来自 Routing 工艺路线主数据;本系统暂无 IE 模块,手工录入 |
| `downtime_minutes` | **手动录入(已有字段)** | `pro_report.downtime_minutes`,原连续制造已用,本次离散开放 | SAP PM`Breakdown Duration`Oracle MES`Downtime` 来自 EAM 设备维护模块;金蝶:停机时长 |
| `downtime_reason` | **手动录入(已有字段)** | `pro_report.downtime_reason`,原连续制造已用 | SAP PM`Notification Type`;行业做法建议结合停机原因字典;本系统暂为自由文本 |
| `report_submit_time` | **后端自动写入** | `new Date()` 于 [insertReport](cci:1://file:///e:/Yavii_P3/MES/yjh-mes/src/main/java/cn/sourceplan/production/service/impl/ReportServiceImpl.java:495:4-718:5) 保存时写入,不暴露前端 | 全行业通用审计时间戳SAP 称 `Posting Date`;用于识别补报行为 |
### 14.4 D 区 — 成本效益字段
| 字段 | 本系统来源模块 | 本系统来源表/字段 | 行业做法参考 |
|------|-------------|----------------|-------------|
| `performance_wages` | **手动录入 / 薪资模块(待接)** | 用户填写;未来可由薪资结算模块推送计件工资 | SAP HR`Labor Cost`,工时×工资率由 CO 成本中心自动计算;本系统暂无薪资模块 |
| `energy_consumption` | **手动录入 / 设备模块(待接)** | 用户填写能耗数值(如 kWh未来可对接设备能耗传感器自动采集 | 行业称 `Utility Cost`SAP PP 通过活动类型 `Activity Type=Power` 自动计算;化工/食品行业必填 |
| `material_consumption` | **手动录入 / 领料模块(待接)** | 用户填写工耗;系统已有领料单 `pro_pick`,暂未自动关联金额 | SAP PP`Goods Movement 261` 领料冲减BOM 消耗量自动计算;本系统有领料流程但报工暂不自动带入 |
| `other_costs` | **手动录入** | 用户填写其他杂项成本 | SAP CO`Overhead Cost`(制造费用分摊);本系统暂无分摊机制 |
| `value_added` | **手动录入** | 用户填写本道工序产生的增值(如加工费、工序增值) | SAP PP+CO`Value Added = Confirmed Activity × Price`;金蝶云星空:工序增值来自 BOM 标准成本;本系统暂手工 |
| `station_oee` | **手动录入(暂)/ 设备模块(待接)** | 用户手填百分比0-100OEE = 可用率 × 性能率 × 质量率 | 全行业 KPI 标配SAP MII / Oracle MES 由设备数据自动计算;本系统已有 `pro_equipment`,后续可自动化 |
| `current_benefit` | **前端计算** | = `value_added - energy_consumption - material_consumption - other_costs`,前端实时计算 | SAP CO`Contribution Margin`(贡献毛利);金蝶:毛利;精益生产看板核心指标 |
| `is_settle` | **薪资结算模块** | `pro_report.is_settle`Y/N本次改为可编辑薪资结算完成后更新 | SAP HR工资确认标志金蝶 HR计件结算状态报工→薪资结算两步确认标准流程 |
| `wages` | **薪资结算模块** | `pro_report.wages`,实际结算工资金额,本次改为可编辑 | SAP HR`Piece Rate Pay`计件工资用友U9C报工工资由薪资引擎批量计算后写回 |
### 14.5 E 区 — 异常记录字段
| 字段 | 本系统来源模块 | 本系统来源表/字段 | 行业做法参考 |
|------|-------------|----------------|-------------|
| `station_exception` | **手动录入** | `pro_report.station_exception`,用户描述工位发生的异常 | SAP QM`Quality Notification`质量通知单Oracle MESNCRNon-Conformance Report金蝶生产异常记录行业通常为独立异常模块本系统暂为报工附属字段 |
| `rectification_suggestion` | **手动录入** | `pro_report.rectification_suggestion`,班长/用户填写整改建议 | 来自精益生产 PDCA 循环的 Act 环节SAP QM`Corrective Action`ISO 9001 纠正措施记录 |
| `remark` | **手动录入(已有字段)** | `BaseEntity.remark`,通用备注 | 全行业通用自由文本字段 |
### 14.6 自动回显字段来源追溯
| 字段 | 来源路径(从销售到报工单) | 行业做法参考 |
|------|----------------------|-------------|
| `customer_name` / `customer_id` | `sal_order`(销售模块)→ `pro_workorder.customer_name`(工单 JOIN 查询)→ `pro_report.customer_name`(报工时自动填入) | SAP PP客户信息通过 Sales Order → Production Order 链路从 SD 模块传递;食品行业定制生产必须追溯到客户 |
| `workpiece` | `pro_workorder.material_name`(工单模块)→ `pro_report.workpiece`(报工时自动填入) | SAP PP`Material Description`Oracle MES`Item Description`;直观展示生产对象名称 |
| `product_batch` | `pro_workorder.batch_number`(工单/批次管理模块)→ `pro_report.product_batch` | SAP`Batch Number`,食品/制药全程追溯Oracle MES`Lot Number`;本系统工单创建时手工维护批次 |
| `process_name` / `process_id` | `pro_route_process`(工艺路线模块)→ `pro_workorder_entry`(工单工序明细)→ `pro_report.process_name / process_id` | SAP PP`Operation`(工序)来自 `Routing`工艺路线主数据Oracle MES`Operation Code` |
| `plan_start_time` | `pro_workorder.process_start_time`(工单排程时间)→ `pro_report.plan_start_time` | SAP PP来自 MRP/APS 排程;本系统暂工单手工维护 |
| `plan_end_time` | `pro_workorder.process_start_time` + `pro_workorder_entry.duration`(秒)→ 前端计算 → `pro_report.plan_end_time` | SAP PP由 Work Center 标准工时自动推算;本系统用 `RouteTimeCalculator` 可扩展 |
| `plan_count` | `pro_workorder.quantity`(工单排产数量)→ `pro_report.plan_count` | SAP PP`Order Quantity` 直接传递 |
### 14.7 系统能力评估(与行业对标)
| 维度 | 本系统升级后 | SAP PP 标准 | 差距与后续规划 |
|------|------------|------------|--------------|
| **基础信息** | 报工人、时间、类型、班次、班组、工位 ✅ | 同等,另有 HR 工号强绑定 | 班组可升级为独立主数据模块 |
| **数量追踪** | 计划数、报工数、合格数、不合格数、良品率 ✅ | 同等,另有废品原因代码字段 | 可增加废品原因字典字段 |
| **时间管理** | 计划/实际开始完工、节拍、停机时间 ✅ | 同等,另有 `Setup Time`(换线准备时间) | 可增加 `setup_time` 字段 |
| **成本核算** | 绩效工资、能耗、工耗、其他成本、增值、效益 ✅(手工录入) | SAP CO 基于 BOM + 活动类型自动计算 | 后续对接领料单/能耗传感器实现自动化 |
| **质量管理** | 良品率、工位异常、整改建议 ✅(基础) | 独立 QM 质量通知单,流程完整 | 本系统已有质检单模块,异常字段为补充 |
| **设备效率** | OEE 暂手工、理论节拍 ⚠️ | SAP MII 自动采集设备数据计算 OEE | 本系统已有 `pro_equipment`,后续可自动计算 |
| **追溯链路** | 客户→工单→工序→报工单 ✅ | 销售订单→生产订单→工序→确认单,完整 | 本系统已完整,通过 `sourceInfo` JSON 关联销售订单 |

View File

@@ -0,0 +1,887 @@
# 报工单页面类别与字段控制设计文档
**版本:** v2.0.003
**日期:** 2026-03-05
**负责人:** 周启威
---
## 需求整理
### 原始需求
1. **报工单中分了 ABCDE 五类**,能否在新增/编辑的时候把五类中任一一类停用,然后写一个配置表、页面上一个按钮可以控制五个类别的停用与启用
2. **报工单中分了 ABCDE 五类**,在这五类中的字段命名/增删改查能不能也放入配置表,五类中的字段也需要控制与自定义,现在目前程序中现有的字段也需要控制与自定义了
3. **规则**:停用/启用 对应的是 隐藏/显示,只要是停用就隐藏该字段
4. **数据类型也需要自定义**,然后控件类型也需要自定义
5. **分类也可以自己增加并修改**,我新增的自定义字段应该可以指定进入任一分类中
6. **ABCDE 的话还是自动排吧**,比如我 C 停用了,应该 D→C, E→D默认是这样的但是也可以自定义
### 需求总结
#### 一、类别管理需求
1. **系统类别A/B/C/D/E**
- 可以停用/启用(停用=隐藏)
- 可以修改类别名称
- 不可删除
- 支持名称重置为默认值
2. **自定义类别F/G/H...**
- 可以新增自定义类别
- 可以修改类别名称
- 可以删除(删除前需确认该类别下无字段)
- 可以停用/启用
3. **类别排序**
- 支持手动调整排序(拖拽或上下移动)
- 前端显示时,启用的类别自动重新编号为 A、B、C、D...
- 例如C 停用后D 自动显示为 CE 自动显示为 D
#### 二、字段管理需求
1. **系统字段(现有字段)**
- 可以停用/启用(停用=隐藏)
- 可以修改显示名称(如"报工人"改为"操作员"
- 可以修改控件类型
- 不可删除
- 支持名称重置为默认值
2. **自定义字段(新增字段)**
- 可以新增自定义字段
- 可以配置字段名称、数据类型、控件类型
- 可以选择放入任一类别中(包括自定义类别)
- 可以移动到其他类别
- 可以删除
- 可以停用/启用
3. **字段配置项**
- 字段名称(可自定义)
- 数据类型STRING/NUMBER/DATE/DATETIME/BOOLEAN/SELECT
- 控件类型INPUT/NUMBER/DATE/DATETIME/SELECT/TEXTAREA/SWITCH
- 下拉选项SELECT 类型时配置)
- 是否必填
- 排序
#### 三、数据存储需求
1. **系统字段**:存储在 `pro_report` 表对应列中
2. **自定义字段**:存储在 `pro_report.custom_fields` JSON 字段中
#### 四、显示规则
1. **停用=隐藏**:停用的类别和字段在表单中完全隐藏
2. **启用=显示**:启用的类别和字段在表单中显示
3. **自动编号**:前端显示时,启用的类别自动重新编号为 A、B、C、D...
---
## 一、需求背景
报工单表单中分为五类A-基础信息、B-数量信息、C-时间节拍、D-成本效益、E-异常记录),需要实现:
1. **类别控制**
- 能够在配置中停用/启用任一类别,停用后该类别在新增/编辑时隐藏
- 支持新增自定义类别(如 F、G、H 等)
- 支持修改类别名称(如将"基础信息"改为"基本信息"
- 系统类别A/B/C/D/E不可删除只能停用自定义类别可删除
2. **字段控制**:五类中的字段能够单独控制停用/启用(隐藏/显示)
3. **字段自定义**
- 现有字段可以自定义显示名称(改名),例如将"报工人"改为"操作员"
- 支持新增自定义字段,可配置字段名称、数据类型、控件类型
- **新增自定义字段时可以选择放入任一类别中(包括自定义类别)**
- 自定义字段的值存储在 JSON 扩展字段中
4. **规则**:停用=隐藏,启用=显示
---
## 二、设计方案
### 2.1 数据库设计
#### 2.1.1 类别配置表pro_report_category_config
存储系统类别和自定义类别的配置
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | bigint | 主键ID |
| category_code | varchar(50) | 类别编码A/B/C/D/E 为系统类别F/G/H... 为自定义类别) |
| category_name | varchar(100) | 类别名称(可自定义修改) |
| default_category_name | varchar(100) | 默认类别名称(系统初始名称,用于重置) |
| category_type | varchar(20) | 类别类型SYSTEM=系统类别/CUSTOM=自定义类别) |
| is_enabled | char(1) | 是否启用Y=启用/N=停用默认Y |
| sort_order | int | 排序号 |
| remark | varchar(500) | 备注 |
| create_by | varchar(64) | 创建者 |
| create_time | datetime | 创建时间 |
| update_by | varchar(64) | 更新者 |
| update_time | datetime | 更新时间 |
#### 2.1.2 字段配置表pro_report_field_config
存储每个类别下字段的启用/停用状态、自定义显示名称、数据类型和控件类型
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | bigint | 主键ID |
| category_code | varchar(50) | 所属类别编码A/B/C/D/E |
| field_code | varchar(100) | 字段编码(对应实体类字段名或自定义字段唯一标识,不可修改) |
| field_name | varchar(100) | 字段显示名称(可自定义修改,如"报工人"可改为"操作员" |
| default_field_name | varchar(100) | 默认字段名称(系统初始名称,用于重置) |
| field_type | varchar(20) | 字段类型SYSTEM=系统字段/CUSTOM=自定义字段) |
| data_type | varchar(50) | 数据类型STRING=文本/NUMBER=数字/DATE=日期/DATETIME=日期时间/BOOLEAN=布尔/SELECT=下拉选择) |
| control_type | varchar(50) | 控件类型INPUT=文本框/NUMBER=数字框/DATE=日期选择/DATETIME=日期时间选择/SELECT=下拉框/TEXTAREA=文本域/SWITCH=开关) |
| options | text | 下拉选项JSON格式仅当control_type=SELECT时有效[{"label":"选项1","value":"1"}] |
| is_enabled | char(1) | 是否启用Y=启用/N=停用默认Y |
| is_required | char(1) | 是否必填Y=必填/N=非必填默认N |
| sort_order | int | 排序号 |
| remark | varchar(500) | 备注 |
| create_by | varchar(64) | 创建者 |
| create_time | datetime | 创建时间 |
| update_by | varchar(64) | 更新者 |
| update_time | datetime | 更新时间 |
### 2.2 字段清单
#### A. 基础信息category_code='A'
| field_code | field_name | field_type | data_type | control_type | is_required | 说明 |
|------------|------------|------------|-----------|--------------|-------------|------|
| reportUserId | 报工人 | SYSTEM | NUMBER | SELECT | Y | 系统字段,必填 |
| reportTime | 报工时间 | SYSTEM | DATETIME | DATETIME | Y | 系统字段,必填 |
| reportType | 报工类型 | SYSTEM | STRING | SELECT | N | 系统字段,字典 |
| shiftName | 班次 | SYSTEM | STRING | SELECT | N | 系统字段,字典 |
| teamName | 班组 | SYSTEM | STRING | SELECT | N | 系统字段,字典 |
| executionStandard | 执行标准 | SYSTEM | STRING | INPUT | N | 系统字段 |
| workshopId | 车间 | SYSTEM | NUMBER | SELECT | Y | 系统字段,必填 |
| stationId | 工位 | SYSTEM | NUMBER | SELECT | Y | 系统字段,必填 |
#### B. 数量信息category_code='B'
| field_code | field_name | field_type | data_type | control_type | is_required | 说明 |
|------------|------------|------------|-----------|--------------|-------------|------|
| planCount | 计划数 | SYSTEM | NUMBER | INPUT | N | 系统字段,只读 |
| reportQuantity | 报工数量 | SYSTEM | NUMBER | NUMBER | Y | 系统字段,必填 |
| qualifiedQuantity | 合格数量 | SYSTEM | NUMBER | NUMBER | Y | 系统字段,必填 |
| unqualifiedQuantity | 不合格数量 | SYSTEM | NUMBER | INPUT | N | 系统字段,自动计算 |
| yieldRate | 良品率 | SYSTEM | NUMBER | INPUT | N | 系统字段,自动计算 |
#### C. 时间节拍category_code='C'
| field_code | field_name | field_type | data_type | control_type | is_required | 说明 |
|------------|------------|------------|-----------|--------------|-------------|------|
| planStartTime | 计划开始时间 | SYSTEM | DATETIME | INPUT | N | 系统字段,只读 |
| planEndTime | 计划完工时间 | SYSTEM | DATETIME | DATETIME | N | 系统字段 |
| actualStartTime | 实际开始时间 | SYSTEM | DATETIME | DATETIME | N | 系统字段 |
| actualEndTime | 实际完工时间 | SYSTEM | DATETIME | DATETIME | N | 系统字段 |
| theoreticalCycleTime | 理论节拍 | SYSTEM | NUMBER | NUMBER | N | 系统字段 |
| downtimeMinutes | 停机时间 | SYSTEM | NUMBER | NUMBER | N | 系统字段 |
| downtimeReason | 停机原因 | SYSTEM | STRING | INPUT | N | 系统字段 |
#### D. 成本效益category_code='D'
| field_code | field_name | field_type | data_type | control_type | is_required | 说明 |
|------------|------------|------------|-----------|--------------|-------------|------|
| performanceWages | 绩效工资 | SYSTEM | NUMBER | NUMBER | N | 系统字段 |
| energyConsumption | 能耗 | SYSTEM | NUMBER | NUMBER | N | 系统字段 |
| materialConsumption | 工耗 | SYSTEM | NUMBER | NUMBER | N | 系统字段 |
| otherCosts | 其他成本 | SYSTEM | NUMBER | NUMBER | N | 系统字段 |
| valueAdded | 增值 | SYSTEM | NUMBER | NUMBER | N | 系统字段 |
| stationOee | 工位OEE | SYSTEM | NUMBER | NUMBER | N | 系统字段 |
| currentBenefit | 本次效益 | SYSTEM | NUMBER | INPUT | N | 系统字段,自动计算 |
| isSettle | 是否结算工资 | SYSTEM | STRING | SELECT | N | 系统字段,字典 |
| wages | 结算工资 | SYSTEM | NUMBER | NUMBER | N | 系统字段 |
#### E. 异常记录category_code='E'
| field_code | field_name | field_type | data_type | control_type | is_required | 说明 |
|------------|------------|------------|-----------|--------------|-------------|------|
| stationException | 工位异常情况 | SYSTEM | STRING | TEXTAREA | N | 系统字段 |
| rectificationSuggestion | 整改建议 | SYSTEM | STRING | TEXTAREA | N | 系统字段 |
| remark | 备注 | SYSTEM | STRING | TEXTAREA | N | 系统字段 |
---
### 2.3 自定义字段存储方案
#### 2.3.1 存储位置
自定义字段的值存储在 `pro_report` 表的 `custom_fields` 字段中JSON 类型)。
```sql
ALTER TABLE `pro_report` ADD COLUMN `custom_fields` json DEFAULT NULL COMMENT '自定义字段值JSON格式';
```
#### 2.3.2 存储格式
```json
{
"custom_field_001": "自定义文本值",
"custom_field_002": 123.45,
"custom_field_003": "2026-03-05 10:30:00",
"custom_field_004": true
}
```
#### 2.3.3 字段编码规则
- 系统字段:使用实体类字段名(如 `reportUserId`
- 自定义字段:使用 `custom_field_` + 序号(如 `custom_field_001`
#### 2.3.4 数据类型映射
| data_type | 存储格式 | 示例 |
|-----------|---------|------|
| STRING | 字符串 | "文本内容" |
| NUMBER | 数字 | 123.45 |
| DATE | 字符串yyyy-MM-dd | "2026-03-05" |
| DATETIME | 字符串yyyy-MM-dd HH:mm:ss | "2026-03-05 10:30:00" |
| BOOLEAN | 布尔值 | true / false |
| SELECT | 字符串(选项值) | "option1" |
#### 2.3.5 字段保存与读取规则
**保存报工单时**
1. **系统字段field_type=SYSTEM**
- 直接保存到 `pro_report` 表对应的列
- 例如:`reportUserId` 保存到 `report_user_id`
- 使用 MyBatis-Plus 自动映射
2. **自定义字段field_type=CUSTOM**
- 保存到 `pro_report.custom_fields` JSON 字段
- 例如:`custom_field_001` 保存为 `{"custom_field_001": "值"}`
- 后端需要将自定义字段值序列化为 JSON
**查询报工单时**
1. **系统字段**
- 直接从 `pro_report` 表对应列读取
- MyBatis-Plus 自动映射到实体类
2. **自定义字段**
-`pro_report.custom_fields` JSON 字段解析
- 后端需要将 JSON 反序列化为 Map 或对象
- 前端合并系统字段和自定义字段数据
**示例代码(后端保存)**
```java
// 保存报工单
public void saveReport(Report report, Map<String, Object> customFields) {
// 1. 保存系统字段MyBatis-Plus 自动处理)
reportMapper.insert(report);
// 2. 保存自定义字段到 JSON
if (customFields != null && !customFields.isEmpty()) {
String customFieldsJson = JSON.toJSONString(customFields);
report.setCustomFields(customFieldsJson);
reportMapper.updateById(report);
}
}
```
**示例代码(后端查询)**
```java
// 查询报工单
public ReportVO getReport(Long id) {
Report report = reportMapper.selectById(id);
ReportVO vo = new ReportVO();
// 1. 复制系统字段
BeanUtils.copyProperties(report, vo);
// 2. 解析自定义字段
if (StringUtils.isNotEmpty(report.getCustomFields())) {
Map<String, Object> customFields = JSON.parseObject(
report.getCustomFields(),
new TypeReference<Map<String, Object>>() {}
);
vo.setCustomFields(customFields);
}
return vo;
}
```
**示例代码(前端保存)**
```javascript
// 提交报工单
submitForm() {
const formData = {
// 系统字段
reportUserId: this.form.reportUserId,
reportTime: this.form.reportTime,
reportQuantity: this.form.reportQuantity,
// ... 其他系统字段
// 自定义字段(单独传递)
customFields: {
custom_field_001: this.form.custom_field_001,
custom_field_002: this.form.custom_field_002,
// ... 其他自定义字段
}
};
saveReport(formData).then(response => {
this.$modal.msgSuccess("保存成功");
});
}
```
---
---
## 三、功能设计
### 3.1 配置管理页面
#### 3.1.1 类别配置
- 页面路径:`/production/report/categoryConfig`
- 功能:
- 列表展示所有类别(系统类别 + 自定义类别)的启用状态
- 支持单个类别的启用/停用切换
- **支持新增自定义类别**(点击"新增类别"按钮)
- **支持修改类别名称**(双击编辑或点击编辑按钮)
- **支持删除自定义类别**(系统类别不可删除,只能停用)
- 支持类别名称重置为默认值(仅系统类别)
- 支持批量启用/停用
- 支持排序调整(拖拽排序)
#### 3.1.2 字段配置
- 页面路径:`/production/report/fieldConfig`
- 功能:
- 按类别分组展示字段列表(包括系统类别和自定义类别)
- **支持新增自定义字段**(点击"新增字段"按钮,选择所属类别)
- 支持单个字段的启用/停用切换
- **支持字段显示名称自定义修改**(双击编辑或点击编辑按钮)
- 支持字段名称重置为默认值(仅系统字段)
- **支持删除自定义字段**(系统字段不可删除,只能停用)
- 支持字段必填属性设置
- 支持字段排序调整
- 支持按类别筛选
- **支持修改字段所属类别**(可将字段移动到其他类别)
### 3.2 报工单表单适配
#### 3.2.1 类别显示控制
- 前端加载配置接口,获取启用的类别列表
- 根据配置动态渲染 `el-collapse-item`
- 停用的类别完全不渲染
- **启用的类别按 sort_order 排序后,自动重新编号为 A、B、C、D...**
- **显示格式:`{自动编号}. {类别名称}`(如 `A. 基础信息`**
**前端实现示例**
```javascript
// 加载类别配置
async loadCategoryConfig() {
const res = await listCategoryConfig({ isEnabled: 'Y' });
// 按 sort_order 排序
const enabledCategories = res.rows.sort((a, b) => a.sortOrder - b.sortOrder);
// 自动重新编号
this.displayCategories = enabledCategories.map((cat, index) => ({
...cat,
displayCode: String.fromCharCode(65 + index), // A, B, C, D...
displayName: `${String.fromCharCode(65 + index)}. ${cat.categoryName}`
}));
}
```
#### 3.2.2 字段显示控制
- 前端加载字段配置接口,获取每个类别下启用的字段列表
- 根据配置动态渲染字段控件
- 停用的字段完全不渲染
- 必填字段根据配置动态添加校验规则
---
## 四、接口设计
### 4.1 类别配置接口
#### 4.1.1 查询类别配置列表
```
GET /production/report/categoryConfig/list
响应:
[
{
"id": 1,
"categoryCode": "A",
"categoryName": "基础信息",
"isEnabled": "Y",
"sortOrder": 1
},
...
]
```
#### 4.1.2 更新类别配置
```
PUT /production/report/categoryConfig/{id}
请求体:
{
"isEnabled": "N"
}
```
### 4.2 字段配置接口
#### 4.2.1 查询字段配置列表
```
GET /production/report/fieldConfig/list?categoryCode=A
响应:
[
{
"id": 1,
"categoryCode": "A",
"fieldCode": "reportUserId",
"fieldName": "报工人",
"isEnabled": "Y",
"isRequired": "Y",
"sortOrder": 1
},
...
]
```
#### 4.2.2 更新字段配置
```
PUT /production/report/fieldConfig/{id}
请求体:
{
"fieldName": "操作员", // 自定义显示名称
"isEnabled": "N",
"isRequired": "N"
}
```
#### 4.2.3 重置字段名称
```
PUT /production/report/fieldConfig/{id}/resetName
响应:
{
"code": 200,
"msg": "重置成功",
"data": {
"fieldName": "报工人" // 恢复为默认名称
}
}
```
#### 4.2.4 批量更新字段配置
```
PUT /production/report/fieldConfig/batch
请求体:
[
{
"id": 1,
"isEnabled": "Y"
},
{
"id": 2,
"isEnabled": "N"
}
]
```
---
## 五、前端实现要点
### 5.1 配置加载
```javascript
// 在 formA.vue 和 form.vue 的 created 钩子中加载配置
async loadReportConfig() {
// 加载类别配置
const categoryRes = await listCategoryConfig()
this.enabledCategories = categoryRes.rows.filter(c => c.isEnabled === 'Y')
// 加载字段配置
const fieldRes = await listFieldConfig()
this.fieldConfigMap = {}
fieldRes.rows.forEach(f => {
if (!this.fieldConfigMap[f.categoryCode]) {
this.fieldConfigMap[f.categoryCode] = []
}
if (f.isEnabled === 'Y') {
this.fieldConfigMap[f.categoryCode].push(f)
}
})
}
```
### 5.2 动态渲染类别
```vue
<el-collapse v-model="record._sections">
<template v-for="category in enabledCategories">
<el-collapse-item :name="category.categoryCode" :key="category.categoryCode">
<template slot="title">
<b>{{ category.categoryCode }}. {{ category.categoryName }}</b>
</template>
<!-- 动态渲染字段 -->
<component :is="getCategoryComponent(category.categoryCode)"
:record="record"
:fields="fieldConfigMap[category.categoryCode]" />
</el-collapse-item>
</template>
</el-collapse>
```
### 5.3 动态渲染字段
```vue
<el-row :gutter="16">
<template v-for="field in fields">
<el-col :xs="24" :sm="12" :md="8" :key="field.fieldCode" v-if="field.isEnabled === 'Y'">
<el-form-item
:label="field.fieldName"
:prop="'reportList.'+rIdx+'.'+field.fieldCode"
:rules="field.isRequired === 'Y' ? rules[field.fieldCode] : []"
label-width="100px">
<!-- 根据字段类型渲染不同控件 -->
<component :is="getFieldComponent(field.fieldCode)"
v-model="record[field.fieldCode]"
:disabled="record.disabledFlag" />
</el-form-item>
</el-col>
</template>
</el-row>
```
---
## 六、后端实现要点
### 6.1 实体类
```java
// ReportCategoryConfig.java
@Data
@TableName("pro_report_category_config")
public class ReportCategoryConfig extends BaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String categoryCode;
private String categoryName;
private String isEnabled;
private Integer sortOrder;
}
// ReportFieldConfig.java
@Data
@TableName("pro_report_field_config")
public class ReportFieldConfig extends BaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String categoryCode;
private String fieldCode;
private String fieldName;
private String isEnabled;
private String isRequired;
private Integer sortOrder;
}
```
### 6.2 Controller
```java
@RestController
@RequestMapping("/production/report/categoryConfig")
public class ReportCategoryConfigController extends BaseController {
@Autowired
private IReportCategoryConfigService categoryConfigService;
@GetMapping("/list")
public TableDataInfo list(ReportCategoryConfig config) {
startPage();
List<ReportCategoryConfig> list = categoryConfigService.selectList(config);
return getDataTable(list);
}
@PutMapping("/{id}")
public AjaxResult update(@PathVariable Long id, @RequestBody ReportCategoryConfig config) {
config.setId(id);
return toAjax(categoryConfigService.updateById(config));
}
}
@RestController
@RequestMapping("/production/report/fieldConfig")
public class ReportFieldConfigController extends BaseController {
@Autowired
private IReportFieldConfigService fieldConfigService;
@GetMapping("/list")
public TableDataInfo list(ReportFieldConfig config) {
startPage();
List<ReportFieldConfig> list = fieldConfigService.selectList(config);
return getDataTable(list);
}
@PutMapping("/{id}")
public AjaxResult update(@PathVariable Long id, @RequestBody ReportFieldConfig config) {
config.setId(id);
return toAjax(fieldConfigService.updateById(config));
}
@PutMapping("/{id}/resetName")
public AjaxResult resetName(@PathVariable Long id) {
return fieldConfigService.resetFieldName(id);
}
@PutMapping("/batch")
public AjaxResult batchUpdate(@RequestBody List<ReportFieldConfig> configs) {
return toAjax(fieldConfigService.updateBatchById(configs));
}
}
```
---
## 七、菜单权限配置
### 7.1 新增菜单
| 菜单名称 | 菜单路径 | 权限标识 | 父菜单 |
|---------|---------|---------|--------|
| 报工单配置 | /production/report/config | production:report:config | 生产管理 |
| 类别配置 | /production/report/categoryConfig | production:report:categoryConfig | 报工单配置 |
| 字段配置 | /production/report/fieldConfig | production:report:fieldConfig | 报工单配置 |
### 7.2 权限标识
- `production:report:categoryConfig:list` - 查询类别配置
- `production:report:categoryConfig:edit` - 修改类别配置
- `production:report:fieldConfig:list` - 查询字段配置
- `production:report:fieldConfig:edit` - 修改字段配置
- `production:report:fieldConfig:batchEdit` - 批量修改字段配置
---
## 八、实施步骤
| 步骤 | 任务 | 工作量 |
|------|------|--------|
| 1 | 执行 SQL 脚本创建配置表和初始化数据 | 0.5h |
| 2 | 后端创建实体类、Mapper、Service、Controller含重置名称接口 | 2.5h |
| 3 | 前端:创建配置管理页面(类别配置、字段配置,含字段名称编辑) | 5h |
| 4 | 前端:报工单表单适配(动态加载配置、动态渲染、使用自定义名称) | 4h |
| 5 | 联调测试、修复问题 | 2h |
| **合计** | | **约 2 个工作日** |
---
## 九、核心规则说明
### 9.1 字段类型规则
| 规则项 | 系统字段SYSTEM | 自定义字段CUSTOM |
|--------|-------------------|---------------------|
| **字段编码** | 使用实体类字段名(如 `reportUserId` | 使用 `custom_field_` + 序号(如 `custom_field_001` |
| **存储位置** | `pro_report` 表对应列(如 `report_user_id` | `pro_report.custom_fields` JSON 字段 |
| **字段名称** | 可自定义修改显示名称 | 可自定义修改显示名称 |
| **数据类型** | 固定(由数据库列类型决定) | 可配置STRING/NUMBER/DATE/DATETIME/BOOLEAN/SELECT |
| **控件类型** | 可配置 | 可配置 |
| **所属类别** | 固定(不可修改) | 可修改(可移动到其他类别) |
| **是否可删除** | 不可删除,只能停用 | 可删除 |
### 9.1.1 类别类型规则
| 规则项 | 系统类别SYSTEM | 自定义类别CUSTOM |
|--------|-------------------|---------------------|
| **类别编码** | A/B/C/D/E固定 | F/G/H/I...(自动生成) |
| **类别名称** | 可自定义修改 | 可自定义修改 |
| **是否可删除** | 不可删除,只能停用 | 可删除(删除前需确认该类别下无字段) |
| **排序** | 可调整 | 可调整 |
### 9.2 数据保存规则
**规则 1系统字段直接保存到对应列**
- 系统字段field_type=SYSTEM的值直接保存到 `pro_report` 表对应的列
- 例如:`reportUserId``report_user_id`
- 使用 MyBatis-Plus 自动映射,无需特殊处理
**规则 2自定义字段保存到 JSON**
- 自定义字段field_type=CUSTOM的值保存到 `custom_fields` JSON 字段
- 所有自定义字段值合并为一个 JSON 对象
- 例如:`{"custom_field_001": "值1", "custom_field_002": 123}`
**规则 3JSON 字段格式要求**
- 必须是有效的 JSON 格式
- 键名为字段编码field_code
- 值类型根据 data_type 确定(字符串、数字、布尔值等)
### 9.3 数据查询规则
**规则 1系统字段直接读取**
-`pro_report` 表对应列直接读取
- MyBatis-Plus 自动映射到实体类
**规则 2自定义字段从 JSON 解析**
-`custom_fields` JSON 字段解析
- 后端反序列化为 Map 或对象
- 前端合并系统字段和自定义字段数据
**规则 3字段配置动态加载**
- 前端根据字段配置表动态渲染表单
- 系统字段和自定义字段统一处理
- 根据 field_type 判断数据来源
### 9.4 字段配置规则
**规则 1字段编码不可修改**
- field_code 一旦创建不可修改
- 系统字段编码对应实体类字段名
- 自定义字段编码使用 `custom_field_` 前缀
**规则 2字段名称可自定义**
- field_name 可随时修改
- 支持重置为 default_field_name
**规则 3停用=隐藏**
- is_enabled='N' 时,字段在表单中完全隐藏
- 停用的字段不参与表单验证
- 停用的字段不影响已保存的数据
**规则 4必填字段保护**
- 核心必填字段(报工人、报工时间、报工数量等)不允许停用
- 前端和后端都需要校验
### 9.5 自定义字段管理规则
**规则 1新增自定义字段**
- 在字段配置页面点击"新增字段"
- **选择所属类别(可选择系统类别或自定义类别)**
- 配置字段名称、数据类型、控件类型
- 系统自动生成 field_codecustom_field_xxx
**规则 2删除自定义字段**
- 只能删除自定义字段field_type=CUSTOM
- 系统字段不可删除,只能停用
- 删除前需确认是否有数据使用该字段
**规则 3修改自定义字段**
- 可修改字段名称、数据类型、控件类型
- **可修改字段所属类别(移动到其他类别)**
- 修改数据类型可能导致已有数据格式不兼容
- 建议修改前备份数据
### 9.6 自定义类别管理规则
**规则 1新增自定义类别**
- 在类别配置页面点击"新增类别"
- 输入类别名称
- 系统自动生成 category_codeF/G/H...
- 新增的类别默认启用
**规则 2删除自定义类别**
- 只能删除自定义类别category_type=CUSTOM
- 系统类别A/B/C/D/E不可删除只能停用
- 删除前需确认该类别下没有字段
- 如果类别下有字段,需要先删除或移动字段
**规则 3修改自定义类别**
- 可修改类别名称
- 可调整排序
- 系统类别名称可修改,但不可删除
**规则 4类别编码生成规则**
- 系统类别A/B/C/D/E固定
- 自定义类别按字母顺序自动生成F/G/H/I/J...
- 如果字母用完,使用 AA/AB/AC...26 个字母后)
### 9.7 类别显示与排序规则
**规则 1类别编号自动排序默认行为**
- 前端显示时,只显示启用的类别
- 启用的类别按 `sort_order` 排序后,自动重新编号为 A、B、C、D...
- 例如:
- 配置A(启用)、B(启用)、C(停用)、D(启用)、E(启用)
- 显示A. 基础信息、B. 数量信息、C. 成本效益、D. 异常记录
- 实际对应A→A、B→B、D→C、E→D
**规则 2自定义排序用户手动调整**
- 用户可以通过拖拽或上下移动按钮调整类别顺序
- 调整后更新 `sort_order` 字段
- 前端按照 `sort_order` 排序显示
- 自定义排序后,仍然自动重新编号
**规则 3类别编号显示规则**
- 前端显示格式:`{自动编号}. {类别名称}`
- 例如:`A. 基础信息``B. 数量信息`
- 自动编号不存储在数据库,仅用于前端显示
- 数据库中的 `category_code` 保持不变
**规则 4停用类别的处理**
- 停用的类别在报工单表单中完全隐藏
- 停用的类别不参与自动编号
- 停用的类别在配置页面仍然显示(标记为停用状态)
**示例说明**
| 数据库配置 | sort_order | is_enabled | 前端显示 |
|-----------|-----------|-----------|---------|
| A-基础信息 | 1 | Y | A. 基础信息 |
| B-数量信息 | 2 | Y | B. 数量信息 |
| C-时间节拍 | 3 | N | (隐藏) |
| D-成本效益 | 4 | Y | C. 成本效益 |
| E-异常记录 | 5 | Y | D. 异常记录 |
| F-自定义类别 | 6 | Y | E. 自定义类别 |
## 十、注意事项
1. **必填字段保护**:报工人、报工时间、报工数量、合格数量等核心必填字段不允许停用
2. **字段编码不可修改**field_code字段编码对应数据库字段名或自定义字段标识不允许修改
3. **类别编号自动排序**:前端显示时,启用的类别自动重新编号为 A、B、C、D...,停用的类别不参与编号
4. **类别编码固定**:数据库中的 category_code 保持不变A/B/C/D/E/F/G...),仅前端显示编号会自动调整
5. **JSON 字段性能**:自定义字段存储在 JSON 中,查询性能较差,不建议在 WHERE 条件中使用
6. **数据类型兼容**:修改自定义字段的数据类型时,需要考虑已有数据的兼容性
7. **配置缓存**:前端可以缓存配置数据,避免每次打开表单都请求接口
8. **配置变更通知**:配置修改后,需要刷新页面才能生效(或使用 WebSocket 推送配置变更)
9. **移动端适配**:配置管理页面仅在 PC 端显示,移动端隐藏配置入口
10. **历史数据兼容**:已保存的报工单不受配置影响,配置仅影响新增/编辑时的表单显示
11. **名称重置功能**:支持将自定义的字段名称和类别名称重置为系统默认名称
12. **自定义字段数量限制**:建议每个类别的自定义字段不超过 20 个,避免表单过于复杂
13. **删除类别前检查**:删除自定义类别前,需确认该类别下没有字段
---
## 十、扩展规划
### 10.1 自定义字段(后续版本)
- 支持用户自定义新增字段
- 字段类型:文本、数字、日期、下拉、多选等
- 自定义字段存储在 JSON 字段中
### 10.2 多配置方案(后续版本)
- 支持不同车间/工位使用不同的配置方案
- 配置方案可以复制、导入、导出
### 10.3 字段联动(后续版本)
- 支持字段之间的显示/隐藏联动
- 支持字段值的计算联动

View File

@@ -0,0 +1,371 @@
# YJH-MES v2.0.004 — 8Multi di_reg 报工 & 设备状态接口
> **版本**: v2.0.004
> **更新日期**: 2026-03-06
> **项目**: YJH-MES (yjh-mes + mes-ui)
> **说明**: 在 8Multi JSON 协议中新增 `di_reg` 字段处理,实现硬件触发报工和设备状态修改
---
## 一、需求概述
### 1.1 新增字段
原 8Multi JSON 格式中新增一个字段:
```json
"di_reg": %d // 0000-0111 触发报工1000-1111 修改设备状态
```
### 1.2 di_reg 编码规则
| 十进制值 | 二进制 | 动作类型 | 含义 |
|---------|--------|---------|------|
| 0 | 0000 | 报工 | 对工序路线中**第1道工序**的全部在产工单执行报工 |
| 1 | 0001 | 报工 | 对工序路线中**第2道工序**的全部在产工单执行报工 |
| 2 | 0010 | 报工 | 对工序路线中**第3道工序**的全部在产工单执行报工 |
| 3 | 0011 | 报工 | 对工序路线中**第4道工序**的全部在产工单执行报工 |
| 4 | 0100 | 报工 | 对工序路线中**第5道工序**的全部在产工单执行报工 |
| 5 | 0101 | 报工 | 对工序路线中**第6道工序**的全部在产工单执行报工 |
| 6 | 0110 | 报工 | 对工序路线中**第7道工序**的全部在产工单执行报工 |
| 7 | 0111 | 报工 | 对工序路线中**第8道工序**的全部在产工单执行报工 |
| 8 | 1000 | 设备状态 | 计划停机cn_reg = 0 |
| 9 | 1001 | 设备状态 | 正常工作cn_reg = 1 |
| 10 | 1010 | 设备状态 | 待机cn_reg = 2 |
| 11 | 1011 | 设备状态 | 故障cn_reg = 3 |
| 12 | 1100 | 设备状态 | 在修cn_reg = 4 |
| 13 | 1101 | 设备状态 | 缺人cn_reg = 5 |
| 14 | 1110 | 设备状态 | 缺料cn_reg = 6 |
| 15 | 1111 | 设备状态 | 清零cn_reg = 7 |
| null/其他 | — | 忽略 | 不触发任何动作 |
**判断逻辑**
```
di_reg < 8 → 报工模式processSort = di_reg + 1
di_reg >= 8 → 设备状态模式cn_reg_new = di_reg - 8
```
---
## 二、数据库设计
### 2.1 表设计总览(报工与设备状态两个功能独立)
| 功能 | 表 | 说明 |
|------|----|------|
| 报工结果 | `pro_report`(已有)| 生成报工单,工单状态变更为 D |
| 设备状态 | `device_data.cn_reg`(已有)| 本次上报的 cn_reg 被 di_reg 覆盖 |
| di_reg 原始值 | `device_data.di_reg`**新增列**| 随每次上报落库 |
| **触发动作日志** | `device_di_reg_log`**新建表**| 记录两类触发事件,用于追溯和审计 |
### 2.2 DDL — device_data 新增列
```sql
ALTER TABLE `device_data`
ADD COLUMN `di_reg` INT DEFAULT NULL COMMENT '输入寄存器(0-7报工/8-15设备状态)';
```
### 2.3 DDL — device_di_reg_log 新建表
```sql
CREATE TABLE `device_di_reg_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`device_id` BIGINT NOT NULL COMMENT '设备ID(关联device.id)',
`device_no` INT NOT NULL COMMENT '设备号(device.device_no)',
`di_reg` INT NOT NULL COMMENT 'di_reg原始值(0-15)',
`action_type` VARCHAR(16) NOT NULL COMMENT '动作类型: REPORT=报工 / STATUS=设备状态',
`process_sort` INT DEFAULT NULL COMMENT '工序顺序(REPORT时有效, = di_reg + 1)',
`cn_reg_new` INT DEFAULT NULL COMMENT '新设备状态值(STATUS时有效, = di_reg - 8)',
`affected_count` INT DEFAULT 0 COMMENT '本次处理的工单数量(REPORT时)',
`work_order_ids` VARCHAR(512) DEFAULT NULL COMMENT '处理的工单ID列表,逗号分隔(REPORT时)',
`trigger_time` DATETIME NOT NULL COMMENT '触发时间',
`remark` VARCHAR(512) DEFAULT NULL COMMENT '备注',
`del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0正常/1删除)',
PRIMARY KEY (`id`),
KEY `idx_device_id_time` (`device_id`, `trigger_time`),
KEY `idx_action_type_time` (`action_type`, `trigger_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='8Multi di_reg触发动作日志表';
```
> **字段说明**`action_type=REPORT` 时 `process_sort`、`affected_count`、`work_order_ids` 有效;`action_type=STATUS` 时 `cn_reg_new` 有效。
---
## 三、后端实现方案
### 3.1 涉及文件
```
yjh-mes/src/main/java/cn/sourceplan/equipment/service/
└── Multi8ProtocolService.java ← 主改动di_reg 解析分发、日志写入
yjh-mes/src/main/java/cn/sourceplan/equipment/domain/
├── DeviceData.java ← 新增 diReg 字段
└── DeviceDiRegLog.java ← 新建:日志实体
yjh-mes/src/main/java/cn/sourceplan/equipment/mapper/
└── DeviceDiRegLogMapper.java ← 新建:日志 Mapperextends BaseMapper
yjh-mes/src/main/java/cn/sourceplan/production/mapper/
└── WorkOrderEntryMapper.java ← 新增 selectActiveEntriesByProcessSort 方法
yjh-mes/src/main/resources/mapper/production/
└── WorkOrderEntryMapper.xml ← 新建:对应 SQL
.sql/
└── 2026-03-06_v2.0.004_周启威_8Multi报工、设备状态修改.sql ← DDL 脚本
```
### 3.2 Multi8ProtocolService — parse8MultiJson 改动
`deviceDataMapper.insert(data)` 之前,解析 `di_reg` 并路由:
```java
// 解析 di_reg
Integer diReg = parseInt(json.getStr("di_reg"));
data.setDiReg(diReg);
// di_reg 分发处理
if (diReg != null) {
if (diReg >= 0 && diReg <= 7) {
// 报工模式processSort = diReg + 1全局、全量、幂等
handleAutoReport(device, diReg + 1);
} else if (diReg >= 8 && diReg <= 15) {
// 设备状态模式:强制覆盖 cn_reg在 insert 之前覆盖,确保落库值正确)
int newCnReg = diReg - 8;
data.setCnReg(newCnReg);
log.info("di_reg={} → 设备状态覆盖 cn_reg={}", diReg, newCnReg);
// saveStatusLog 在 deviceDataMapper.insert(data) 之后调用见注意事项4
}
}
```
### 3.3 handleAutoReport — 报工逻辑
**三条核心原则**
1. **报工数量** = 工单计划数量(全量报工,一次完成)
2. **幂等性** = 报工后工单状态置为 D已完成下次查询自动过滤无需额外判断
3. **工单范围** = 全局所有工单,不按工位/车间过滤
```java
/**
* di_reg 触发报工
* 全局查找 sort=processSort 且 pro_status IN('A','B') 的工单分录,全量报工后置为已完成
* 幂等保证:已完成(D)的工单在查询时自动过滤,无需重复判断
*
* @param device 上报设备(用于写日志)
* @param processSort 工序顺序1-based= di_reg + 1
*/
@Transactional(rollbackFor = Exception.class) // 保证报工单创建与工单状态更新原子性
private void handleAutoReport(MesDevice device, int processSort) {
List<WorkOrderEntry> entries = workOrderEntryMapper.selectActiveEntriesByProcessSort(processSort);
if (entries == null || entries.isEmpty()) {
log.info("di_reg 报工:未找到第{}道工序的在产工单分录", processSort);
saveReportLog(device, processSort - 1, processSort, 0, "");
return;
}
List<Long> processedIds = new ArrayList<>();
for (WorkOrderEntry entry : entries) {
try {
WorkOrder workOrder = workOrderMapper.selectById(entry.getWorkorderId());
if (workOrder == null || "D".equals(workOrder.getProStatus())) {
continue;
}
// 报工数量 = 工单计划数量(全量)
BigDecimal reportQty = workOrder.getQuantity() != null
? workOrder.getQuantity() : BigDecimal.ONE;
// 构建报工单
Report report = new Report();
report.setWorkOrderEntryId(entry.getId());
report.setWorkOrderId(workOrder.getId());
report.setProcessName(entry.getProcessName());
report.setProcessId(entry.getProcessId());
report.setWorkshopId(entry.getWorkshopId());
report.setWorkshopName(entry.getWorkshopName());
report.setStationId(entry.getStationId());
report.setStationName(entry.getStationName());
report.setReportTime(new Date());
report.setReportChannel("8Multi");
report.setReportQuantity(reportQty);
report.setQualifiedQuantity(reportQty);
report.setUnqualifiedQuantity(BigDecimal.ZERO);
report.setQualityStatus("A"); // 免检
// 创建报工单insertReportSimple 不处理工单状态,需手动更新)
reportService.insertReportSimple(report);
// 手动将工单标记为已完成 → 下次同 di_reg 触发时自动幂等跳过
// 注意:两步操作在同一 @Transactional 内,任一失败均回滚
workOrder.setTotalReportedQuantity(reportQty);
workOrder.setProStatus("D");
workOrder.setActualEndTime(new Date());
workOrder.setRealFinishDate(new Date());
workOrderMapper.updateById(workOrder);
processedIds.add(workOrder.getId());
log.info("di_reg 报工成功workOrderId={}, processSort={}, qty={}",
workOrder.getId(), processSort, reportQty);
} catch (Exception e) {
log.error("di_reg 报工失败entryId={}", entry.getId(), e);
}
}
// 写触发日志
String woIds = processedIds.stream().map(String::valueOf)
.collect(java.util.stream.Collectors.joining(","));
saveReportLog(device, processSort - 1, processSort, processedIds.size(), woIds);
}
```
### 3.4 日志写入方法
```java
private void saveReportLog(MesDevice device, int diReg, int processSort, int count, String woIds) {
DeviceDiRegLog entry = new DeviceDiRegLog();
entry.setDeviceId(device.getId());
entry.setDeviceNo(device.getDeviceNo());
entry.setDiReg(diReg);
entry.setActionType("REPORT");
entry.setProcessSort(processSort);
entry.setAffectedCount(count);
entry.setWorkOrderIds(woIds);
entry.setTriggerTime(new Date());
deviceDiRegLogMapper.insert(entry);
}
private void saveStatusLog(MesDevice device, int diReg, int cnRegNew) {
DeviceDiRegLog entry = new DeviceDiRegLog();
entry.setDeviceId(device.getId());
entry.setDeviceNo(device.getDeviceNo());
entry.setDiReg(diReg);
entry.setActionType("STATUS");
entry.setCnRegNew(cnRegNew);
entry.setTriggerTime(new Date());
deviceDiRegLogMapper.insert(entry);
}
```
### 3.5 WorkOrderEntryMapper — 新增查询方法
**接口**WorkOrderEntryMapper.java
```java
/**
* 查询所有在产工单非D状态中 processSort = processSort 的工单分录
*/
List<WorkOrderEntry> selectActiveEntriesByProcessSort(@Param("processSort") int processSort);
```
**XML**WorkOrderEntryMapper.xml新建文件
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.sourceplan.production.mapper.WorkOrderEntryMapper">
<select id="selectActiveEntriesByProcessSort"
resultType="cn.sourceplan.production.domain.WorkOrderEntry">
SELECT e.*
FROM pro_workorder_entry e
INNER JOIN pro_workorder w ON w.id = e.workorder_id
WHERE e.sort = #{processSort}
AND w.pro_status IN ('A', 'B')
</select>
</mapper>
```
> **说明**`sort` 对应 `WorkOrderEntry.sort`工序顺序1-based只取状态为 A未开始或 B生产中的工单已完成D的工单自动排除实现天然幂等。
---
## 四、设备状态覆盖说明
`di_reg >= 8` 时,本次上报的 `cn_reg` 值将被 `di_reg - 8` **覆盖**(无论 JSON 中 `cn_reg` 是什么)。这样 OEE 时序图、设备状态页面均可立即反映该状态,无需额外接口。
| di_reg | 覆盖后 cn_reg | 含义 |
|--------|------------|------|
| 8 | 0 | 计划停机 |
| 9 | 1 | 正常工作 |
| 10 | 2 | 待机 |
| 11 | 3 | 故障 |
| 12 | 4 | 在修 |
| 13 | 5 | 缺人 |
| 14 | 6 | 缺料 |
| 15 | 7 | 清零 |
---
## 五、完整 JSON 示例(含 di_reg
### 5.1 报工示例第2道工序
```json
{
"header": "+YAV",
"Card": "8multi",
"id": 101126010002,
"dt": 300,
"a1": 5.2,
"a2": 0.0,
"q1": 0.0,
"q2": 0.0,
"t1": 26.5,
"t2": 25.0,
"dht1": 60.0,
"dht2": 58.0,
"cn_reg": 1,
"f1": 50.00,
"f2": 0.00,
"c1": 50,
"c2": 0,
"di_reg": 1,
"do_reg": 0,
"reg1": 0.000,
"reg2": 0.000,
"tv": 0,
"save": 0,
"oee": 100,
"end": "EEFF"
}
```
→ 全局查找 `sort=2``pro_status IN('A','B')` 的工单分录 → 报工数量 = 工单计划数量 → 工单状态置为 D
再次收到 `di_reg=1` → 同工单已为 D查询结果为空 → 跳过,**天然幂等**
### 5.2 设备状态示例设置为故障di_reg=11
```json
{
"cn_reg": 1,
"di_reg": 11,
...
}
```
`cn_reg` 强制覆盖为 3故障`device_di_reg_log` 写入 STATUS 记录OEE 状态显示红色
---
## 六、实施步骤(清单)
- [ ] 执行 DDL 脚本 `2026-03-06_v2.0.004_周启威_8Multi报工、设备状态修改.sql`ALTER + CREATE TABLE
- [ ] `DeviceData.java` 新增 `diReg` 字段(`@TableField("di_reg")`
- [ ] 新建 `DeviceDiRegLog.java` 实体(对应 `device_di_reg_log` 表)
- [ ] 新建 `DeviceDiRegLogMapper.java``extends BaseMapper<DeviceDiRegLog>`
- [ ] `WorkOrderEntryMapper.java` 新增 `selectActiveEntriesByProcessSort` 方法
- [ ] 新建 `WorkOrderEntryMapper.xml`,添加对应 SQL
- [ ] `Multi8ProtocolService.java` 注入 `WorkOrderMapper``WorkOrderEntryMapper``IReportService``DeviceDiRegLogMapper`
- [ ] `Multi8ProtocolService.java``parse8MultiJson` 中新增 `di_reg` 解析分发
- [ ] `Multi8ProtocolService.java` 新增 `handleAutoReport``saveReportLog``saveStatusLog` 方法
- [ ] 联调测试di_reg=0 触发报工di_reg=11 触发故障状态
---
## 七、注意事项
1. **幂等性**:第一次触发时全量报工并将工单置为 D已完成后续同 `di_reg` 触发时,`pro_status IN('A','B')` 查询自动排除 D 状态工单,**无需额外判断**。
2. **报工数量**:固定取 `workOrder.getQuantity()`(计划数量),全量一次性完成,与 c1 计数器无关。
3. **di_reg = null**:旧版固件不携带此字段时,`parseInt` 返回 null分发代码不执行**不影响原有解析逻辑**。
4. **`saveStatusLog` 调用时机**:应在 `deviceDataMapper.insert(data)` **之后**调用,避免 insert 失败时日志已落库;`parse8MultiJson` 中需将 `saveStatusLog` 调用挪至 insert 语句后。
5. **`DeviceDiRegLog` 不继承 BaseEntity**:该实体仅需 `@TableName``@TableId` 注解,手动设置 `triggerTime`,不依赖框架自动填充的 `create_time`/`create_by` 等字段DDL 也不必添加这些列。
6. **事务隔离**`handleAutoReport``@Transactional`,每个工单报工+状态更新原子提交;但各工单之间彼此独立(外层 try-catch单个工单失败不影响其他工单。

View File

@@ -0,0 +1,161 @@
# batchAutoComplete 接口销售订单状态说明
## 接口地址
`POST /production/autoComplete/batchAutoComplete`
---
## 销售订单状态码说明
| 状态码 | 状态名称 | 说明 |
|--------|---------|------|
| A | 待生产 | 订单刚创建,未进入生产 |
| B | 生产中 | 已生成工单,正在生产 |
| F | 生产完成 | 已报工完成,待入库 |
| G | 已入库 | 已完工入库 |
| E | 部分发货 | 部分产品已发货 |
| C | 已发货 | 全部产品已发货 |
| D | 已关闭 | 订单已关闭 |
---
## 情况1只生成工单不生成报工单
### 请求参数
```json
{
"orderNumbers": ["SO202603120001"],
"plan": {
"isGeneratePlan": 0
},
"workOrder": {
"routeId": 1005,
"processStartTime": "2026-03-12 08:00:00"
},
"reportWorkOrder": {
"isGenerateReportWorkOrder": 0
}
}
```
### 离散制造业
**分录状态变化**`A`(待生产)→ `B`(生产中)
**主表状态变化**`A`(待生产)→ `B`(生产中)
### 连续制造业
**分录状态变化**`A`(待生产)→ `A`(待生产)- **保持不变**
**主表状态变化**`A`(待生产)→ `A`(待生产)- **保持不变**
**说明**:连续制造业只生成工单不生成报工单时,订单状态不会改变。
## 情况2生成报工单订单已有工单
### 请求参数
```json
{
"orderNumbers": ["SO202603120001"],
"plan": {
"isGeneratePlan": 0
},
"workOrder": {
"isGenerateWorkOrder": 0
},
"reportWorkOrder": {
"reporters": [
{"operatorId": 101, "reportTime": "2026-03-12 10:00:00"}
]
}
}
```
### 前提条件
订单已经有工单之前已生成分录状态为B
### 离散制造业
**分录状态变化**`B`(生产中)→ `F`(生产完成)
**主表状态变化**`B`(生产中)→ `F`(生产完成)
### 连续制造业
**分录状态变化**`B`(生产中)→ `F`(生产完成)
**主表状态变化**`B`(生产中)→ `F`(生产完成)
---
## 情况3完工入库分录改为已入库
### 操作方式
通过完工入库单ManufactureInto关联销售订单分录
### 前提条件
订单分录状态为F生产完成
### 所有制造类型
**分录状态变化**`F`(生产完成)→ `G`(已入库)
**主表状态变化**`F`(生产完成)→ `G`(已入库)
**说明**
- 完工入库不是通过 batchAutoComplete 接口实现
- 完工入库通过仓库管理模块的完工入库单实现
- 完工入库单明细关联销售订单分录ID后会自动更新分录和主表状态
---
---
## 主表状态更新规则
主表状态由所有分录的状态决定,优先级从高到低:
### 状态优先级
**C已发货> E部分发货> G已入库> F生产完成> D已关闭> B生产中> A待生产**
### 决策规则
1. **所有分录都是C已发货** → 主表状态为 **C已发货**
2. **有分录是C已发货但不是全部** → 主表状态为 **E部分发货**
3. **所有分录都是G已入库或更高状态E/C** → 主表状态为 **G已入库**
4. **所有分录都是F生产完成或更高状态G/E/C** → 主表状态为 **F生产完成**
5. **所有分录都是D已关闭或更高状态F/G/E/C** → 主表状态为 **D已关闭**
6. **有任何分录在B生产中** → 主表状态为 **B生产中**
7. **其他情况** → 主表状态为 **A待生产**
### 多分录订单示例
#### 示例1全部完成
- 分录1`F`生产完成分录2`F`生产完成分录3`F`(生产完成) → 主表:`F`(生产完成)
#### 示例2部分完成
- 分录1`F`生产完成分录2`B`生产中分录3`A`(待生产) → 主表:`B`(生产中)- 有分录在生产中
#### 示例3全部已入库
- 分录1`G`已入库分录2`G`已入库分录3`G`(已入库) → 主表:`G`(已入库)
#### 示例4混合状态
- 分录1`G`已入库分录2`F`生产完成分录3`F`(生产完成) → 主表:`F`(生产完成)- 不是所有分录都≥G
#### 示例5部分发货
- 分录1`C`已发货分录2`G`已入库分录3`F`(生产完成) → 主表:`E`(部分发货)- 有分录已发货但不是全部
---
## 快速对照表
| 操作 | 分录状态变化 | 主表状态变化 | 适用制造类型 |
|------|------------|------------|------------|
| 只生成工单 | A → B | A → B | 离散制造 |
| 只生成工单 | A → A | A → A | 连续制造 |
| 生成报工单 | B → F | B → F | 所有类型 |
| 完工入库 | F → G | F → G | 所有类型 |
---
## 版本信息
- 文档版本v1.0
- 创建日期2026-03-12
- 最后更新2026-03-12

View File

@@ -0,0 +1,397 @@
# MES系统授权续费功能 - 任务文档
**版本**: v2.0.008
**作者**: 周启威
**日期**: 2026-03-12
**状态**: 开发完成
---
## 功能概述
实现MES系统授权到期提醒和续费管理功能包括
1. 首页弹窗提醒(剩余天数/已过期天数)
2. 首页顶部到期时间显示(版本说明左侧)
3. 到期后限制普通用户访问业务模块
4. 管理员续费操作和历史记录管理
5. 联系方式管理(邮箱+电话)
---
## 已完成工作
### 1. 数据库设计 ✅
#### 1.1 系统授权配置表sys_license_config
- 存储系统全局到期时间和联系方式
- 字段id, expire_date, contact_email, contact_phone, create_time, update_time
- SQL文件`.sql/2026-03-12_v2.0.008_周启威_到期续费功能.sql`
#### 1.2 续费记录表sys_license_renewal
- 存储每次续费操作的历史记录
- 字段id, company_name, previous_expire_date, expire_date, operator, operate_time, remark
- 索引idx_operate_time
- **previous_expire_date**: 记录续费前的到期时间,便于查看每次续费延长的时间
### 2. 后端代码 ✅
#### 2.1 实体类
- `SysLicenseConfig.java` - 系统授权配置实体
- `SysLicenseRenewal.java` - 续费记录实体
#### 2.2 Mapper层
- `SysLicenseConfigMapper.java` + XML
- `SysLicenseRenewalMapper.java` + XML
#### 2.3 Service层
- `ISysLicenseService.java` - 接口定义
- `SysLicenseServiceImpl.java` - 业务实现
- 获取授权信息(含剩余天数计算)
- 更新到期时间
- 更新联系方式
- 新增续费记录(事务保证,自动记录续费前到期时间)
- 查询续费记录列表(按时间倒序)
- 获取最新续费记录
- 检查是否过期
#### 2.4 Controller层
- `SysLicenseController.java`
- GET /system/license/info - 获取授权信息(所有用户)
- PUT /system/license/expire - 更新到期时间(仅管理员,@RequestBody
- PUT /system/license/contact - 更新联系方式(仅管理员,@RequestBody
- POST /system/license/renewal - 新增续费记录(仅管理员)
- GET /system/license/renewal/list - 查询续费记录(仅管理员)
- GET /system/license/renewal/latest - 获取最新续费记录(所有用户)
### 3. 前端代码 ✅
#### 3.1 API封装
- `src/api/system/license.js` - 授权相关接口
#### 3.2 组件开发
- `src/components/LicenseExpireDialog/index.vue` - 首页弹窗提醒组件
- 距离到期≤30天或已过期时显示
- 显示剩余/过期天数
- 显示联系邮箱和电话
- **智能提醒机制**
- 剩余天数 > 5天可选择"今日不再提示",当天不再弹窗
- 剩余天数 ≤ 5天每次进入都提示无法忽略
- 已过期:每次进入都提示,无法忽略
- 使用 sessionStorage 存储今日忽略状态
- `src/components/LicenseExpireInfo/index.vue` - 首页到期时间显示组件
- 显示位置:首页顶部,版本说明左侧
- 动态颜色:正常(灰色)/警告(橙色)/过期(红色+脉冲动画)
- **智能Tooltip**:鼠标悬停显示详细信息
- 下次到期时间
- 剩余天数/已过期天数
- 上次续费时间
- 联系邮箱和电话
- 管理员点击跳转续费管理页面
- 每小时自动刷新数据
#### 3.3 页面开发
- `src/views/system/license/renewal.vue` - 续费管理页面
- 上方:续费操作表单(企业名称、到期时间、备注)
- 下方:历史续费记录表格(支持分页)
- 显示续费前到期时间和续费后到期时间对比
- 按操作时间倒序排列
- 仅管理员可访问
---
## 核心功能说明
### 1. 到期提醒机制
- **弹窗提醒**距离到期≤30天或已过期时显示弹窗
- **剩余 > 5天**:显示"今日不再提示"和"我知道了"两个按钮,用户可选择今日忽略
- **剩余 ≤ 5天**:只显示"我知道了"按钮,每次进入都提示
- **已过期**:只显示"我知道了"按钮,每次进入都提示
- **首页显示**:实时显示到期时间和剩余天数,根据状态变色,下拉菜单显示详细续费信息
- **关闭控制**
- 点击"今日不再提示":使用 sessionStorage 记录,当天不再显示(仅剩余>5天时可用
- 点击"我知道了":关闭弹窗,下次进入系统会再次显示
### 2. 权限控制
- **管理员**:不受任何限制,可正常访问所有模块和续费管理
- **普通用户**
- 系统未到期:正常访问所有模块
- 系统已过期:只能访问首页和个人中心,访问业务模块时拦截
- **路由守卫实现**
-`permission.js` 中添加授权过期检查
- 使用缓存机制1分钟刷新避免频繁请求接口
- 过期后访问业务模块会弹窗提示并强制跳转首页
- **黑名单拦截**(仅拦截以下业务模块):
- `/mes/` - 生产管理
- `/warehouse/` - 仓库管理
- `/quality/` - 质量管理
- `/sale/` - 销售管理
- `/purchase/` - 采购管理
- `/energy/` - 能源管理
- `/safe/` - 安全管理
- `/finance/` - 财务管理
- **允许访问**:首页、个人中心、系统管理、授权管理等非业务模块
### 3. 续费操作
- 管理员填写企业名称、续费到期时间、备注
- 点击确认续费后:
1. 自动记录续费前的到期时间
2. 更新系统全局到期时间
3. 插入续费历史记录
4. 自动记录操作人和操作时间
5. 刷新历史记录表格
### 4. 数据展示
- **历史续费记录**按操作时间倒序显示SQL层面保证
- **字段**:企业名称、续费前到期时间、续费后到期时间、操作人、操作时间、备注
- **分页**:支持分页查询
- **续费信息Tooltip**:首页到期时间显示处,鼠标悬停可查看最新续费记录
---
## 集成工作(已完成)
### 1. 首页集成
#### 1.1 弹窗组件集成
已在 `src/views/index.vue` 中集成 `LicenseExpireDialog` 组件
- 登录后自动检查授权状态
- 剩余天数 ≤30 或已过期时自动弹窗
- 使用 `sessionStorage` 存储关闭状态(关闭浏览器后重新显示)
#### 1.2 导航栏授权信息显示
已在 `src/layout/components/Navbar.vue` 中集成 `LicenseExpireInfo` 组件
- 显示位置:版本说明左侧
- 下拉菜单格式,与版本说明样式一致
- 显示内容:到期时间、剩余天数、上次续费、联系方式
- 管理员可点击"进入续费管理"
### 2. 路由配置
`src/router/index.js` 中添加:
```javascript
{
path: '/system/license',
component: Layout,
hidden: true,
permissions: ['system:license:view'],
children: [
{
path: 'renewal',
component: () => import('@/views/system/license/renewal'),
name: 'LicenseRenewal',
meta: { title: '续费管理', icon: 'license', activeMenu: '/system/license' }
}
]
}
```
### 3. 权限拦截(需手动完成)
#### 3.1 前端路由守卫
`src/permission.js` 中添加:
```javascript
import { getLicenseInfo } from '@/api/system/license'
const RESTRICTED_MODULES = [
'/mes/production', // 生产管理
'/warehouse', // 仓库管理
'/quality', // 质量管理
'/equipment' // 设备管理
]
router.beforeEach(async (to, from, next) => {
// 检查系统授权状态
try {
const response = await getLicenseInfo()
const { isExpired } = response.data
const roles = store.getters.roles
if (isExpired && !roles.includes('admin')) {
const isRestrictedPath = RESTRICTED_MODULES.some(module =>
to.path.startsWith(module)
)
if (isRestrictedPath) {
Message.error('系统已过期,请联系管理员续费')
next({ path: '/' })
return
}
}
} catch (error) {
console.error('检查授权状态失败:', error)
}
next()
})
```
#### 3.2 后端拦截器(可选)
创建 `LicenseInterceptor.java` 并注册到 `WebMvcConfig`
---
## 验收清单
### 数据库
- [ ] sys_license_config 表创建成功,包含初始数据
- [ ] sys_license_renewal 表创建成功
### 后端接口
- [ ] GET /system/license/info 返回正确的授权信息和剩余天数
- [ ] PUT /system/license/expire 仅管理员可调用,成功更新到期时间
- [ ] PUT /system/license/contact 仅管理员可调用,成功更新联系方式
- [ ] POST /system/license/renewal 仅管理员可调用,成功新增续费记录并更新系统到期时间
- [ ] GET /system/license/renewal/list 仅管理员可调用,返回续费记录列表(按时间倒序)
### 前端页面
- [ ] 首页弹窗正确显示剩余天数或已过期天数
- [ ] 首页弹窗显示联系邮箱和电话
- [ ] 首页弹窗关闭后当天不再显示
- [ ] 首页顶部版本说明左侧正确显示到期时间
- [ ] 根据剩余天数显示不同颜色(正常/警告/错误)
- [ ] 管理员点击可跳转到续费管理页面
- [ ] 续费管理页面仅管理员可访问
- [ ] 续费操作成功后,系统到期时间更新
- [ ] 续费操作成功后,历史续费记录表格显示最新记录
- [ ] 历史续费记录按操作时间倒序显示
### 权限控制
- [ ] 系统已过期时,普通用户无法访问生产管理、仓库管理、质量管理、设备管理模块
- [ ] 系统已过期时,管理员可正常访问所有模块
- [ ] 访问受限模块时提示"系统已过期,请联系管理员续费"
---
## 文件清单
### SQL文件
- `.sql/2026-03-12_v2.0.008_周启威_到期续费功能.sql`
### 后端文件
- `mes-system/src/main/java/cn/sourceplan/system/domain/SysLicenseConfig.java`
- `mes-system/src/main/java/cn/sourceplan/system/domain/SysLicenseRenewal.java`
- `mes-system/src/main/java/cn/sourceplan/system/mapper/SysLicenseConfigMapper.java`
- `mes-system/src/main/java/cn/sourceplan/system/mapper/SysLicenseRenewalMapper.java`
- `mes-system/src/main/resources/mapper/system/SysLicenseConfigMapper.xml`
- `mes-system/src/main/resources/mapper/system/SysLicenseRenewalMapper.xml`
- `mes-system/src/main/java/cn/sourceplan/system/service/ISysLicenseService.java`
- `mes-system/src/main/java/cn/sourceplan/system/service/impl/SysLicenseServiceImpl.java`
- `mes-admin/src/main/java/cn/sourceplan/web/controller/system/SysLicenseController.java`
### 前端文件
- `mes-ui/src/api/system/license.js`
- `mes-ui/src/components/LicenseExpireDialog/index.vue`
- `mes-ui/src/components/LicenseExpireInfo/index.vue`
- `mes-ui/src/views/system/license/renewal.vue`
- `mes-ui/src/permission.js` - 路由守卫(授权过期拦截)
- `mes-ui/src/layout/components/Navbar.vue` - 顶部导航栏集成
- `mes-ui/src/views/index.vue` - 首页弹窗集成
---
## 优化改进 (2026-03-13)
### 1. 用户体验优化
-**弹窗显示机制**:每次进入系统都显示
- 确保用户不会错过重要的续费提醒
- 避免因关闭弹窗而长期不看到提醒的情况
### 2. 数据完整性增强
-**续费前到期时间记录**:新增 `previous_expire_date` 字段
- 自动记录每次续费前的系统到期时间
- 可清晰看到每次续费延长了多少时间
- 便于续费历史追溯和审计
### 3. 接口规范化
-**RESTful规范**:统一使用 `@RequestBody` 接收JSON
- `PUT /system/license/expire` 改用 JSON body
- `PUT /system/license/contact` 改用 JSON body
- 前后端交互更规范,便于后续扩展
### 4. 数据查询优化
-**SQL排序保证**Mapper XML 添加 `ORDER BY operate_time DESC`
- 确保续费记录始终按时间倒序显示
- 不依赖数据库默认顺序,提高可靠性
### 5. 信息展示增强
-**智能Tooltip**:首页到期时间显示增强
- 鼠标悬停显示详细续费信息
- 包含:下次到期时间、剩余天数、上次续费时间、联系方式
- 类似版本说明的交互方式,用户体验一致
### 6. 弹窗提醒优化
-**今日不再提示功能**:智能提醒策略
- **剩余 > 5天**:用户可选择"今日不再提示",避免频繁打扰
- **剩余 ≤ 5天**:强制提醒,确保临近到期不会被忽略
- **已过期**:强制提醒,确保过期状态得到重视
- 使用 sessionStorage 存储忽略状态,关闭浏览器后重置
- 平衡用户体验和提醒效果,既不过度打扰,又不遗漏重要提醒
### 7. 路由拦截策略优化
-**黑名单拦截模式**:从白名单改为黑名单
- 仅拦截核心业务模块(生产、仓库、质量、销售、采购、能源、安全、财务)
- 允许访问系统管理、授权管理等非业务模块
- 避免过度限制,提升用户体验
### 8. 弹窗UI现代化
-**视觉设计优化**现代化UI风格
- 大图标 + 渐变背景 + 动画效果
- 根据状态变色(橙色警告/红色过期)
- 卡片式布局,信息层次清晰
- 圆角设计,阴影效果,视觉更友好
---
## 注意事项
1. **时区问题**:确保前后端时间格式一致,使用 `yyyy-MM-dd HH:mm:ss` 格式
2. **权限校验**:所有管理员操作接口必须添加 `@PreAuthorize("@ss.hasRole('admin')")` 注解
3. **事务管理**:续费操作涉及两张表,已使用 `@Transactional` 保证事务一致性
4. **日志记录**:所有授权相关操作已添加 `@Log` 注解记录操作日志
5. **弹窗提醒**:每次进入系统都会显示到期提醒弹窗,确保用户不会错过续费
6. **自动记录**:续费操作会自动记录续费前到期时间,无需手动填写
---
## 设计原则
**轻量化** - 仅2张表6个接口3个前端组件
**简洁高效** - 无复杂依赖,无授权密钥验证
**权限分离** - 管理员全权限,普通用户受限
**用户友好** - 弹窗可关闭,首页显示直观
**可维护性** - 代码简洁,逻辑清晰
---
## 开发状态
**✅ 开发完成** - 2026-03-13
所有功能已实现并集成完毕:
- ✅ 后端接口和数据库
- ✅ 前端组件和页面
- ✅ 首页弹窗和导航栏显示
- ✅ 路由守卫和权限控制
- ✅ 样式优化(下拉菜单格式)
**测试要点**
1. 登录后首页是否显示授权到期弹窗剩余≤30天或已过期
2. 导航栏"授权信息"下拉菜单是否显示正确
3. 普通用户在授权过期后访问业务模块是否被拦截
4. 管理员是否可以正常续费
5. 续费后授权信息是否实时更新
---
## 排除范围
以下功能**不在本次需求范围内**
- ❌ 授权密钥验证
- ❌ 趋势分析
- ❌ 邮件通知
- ❌ 系统消息推送
- ❌ 模块粒度授权控制
- ❌ 多租户支持
- ❌ 自动续费
- ❌ 在线支付

View File

@@ -0,0 +1,155 @@
# 移动端组件全面检查报告
## 检查日期
2026-03-14
## 检查范围
所有 mes-ui/src/views/mobile 目录下的 Vue 组件
## 检查结果
### ✅ 组件正确导入和注册的页面
#### 1. 工单模块 (workOrder)
- **list.vue** - 工单列表
- 导入: Search, DropdownMenu, DropdownItem, Field, Button, PullRefresh, List, Empty, Toast
- 注册: ✅ 所有组件已正确注册
- **detail.vue** - 工单详情
- 导入: Loading, CellGroup, Cell, Tag, Tabs, Tab, Empty, Button, Toast, ActionSheet
- 注册: ✅ 所有组件已正确注册
- **pickManage.vue** - 领料管理
- 导入: NavBar, Loading, CellGroup, Cell, Empty, Tag, Button, Toast
- 注册: ✅ 所有组件已正确注册
- **pickForm.vue** - 领料表单
- 导入: NavBar, CellGroup, Cell, Field, Button, Loading, Empty, Popup, Picker, Search, List, Toast
- 注册: ✅ 所有组件已正确注册
- 特殊修改: 领料人已改为选择器(从输入框改为下拉选择)
- **pickDetail.vue** - 领料详情
- 导入: NavBar, CellGroup, Cell, Tag, Loading, Empty, Toast
- 注册: ✅ 所有组件已正确注册
#### 2. 生产订单模块 (saleOrder)
- **list.vue** - 生产订单列表
- 导入: Search, DropdownMenu, DropdownItem, Field, Button, PullRefresh, List, Empty, Toast
- 注册: ✅ 所有组件已正确注册
- **form.vue** - 生产订单表单
- 导入: NavBar, Form, Field, CellGroup, Button, Popup, Picker, DatetimePicker, Toast, Dialog, Search, List, Cell, Empty
- 注册: ✅ 所有组件已正确注册
- 特殊修改:
- 标题已改为"新增生产订单"/"编辑生产订单"
- 日期字段标签已改为"入库日期"
- 页面宽度已优化,防止横向滚动
- 产品搜索支持模糊查询和实时搜索
- **detail.vue** - 生产订单详情
- 导入: NavBar, CellGroup, Cell, Tag, Loading, Toast, Button, Dialog
- 注册: ✅ 所有需要注册的组件已正确注册
- 说明: Dialog 和 Toast 作为函数调用,无需注册
#### 3. 报工模块 (report)
- **list.vue** - 报工单列表
- 导入: Search, DropdownMenu, DropdownItem, Field, Button, PullRefresh, List, Empty, Toast
- 注册: ✅ 所有组件已正确注册
- **form.vue** - 报工单表单
- 导入: NavBar, Form, CellGroup, Cell, Field, Button, Loading, Popup, DatetimePicker, Picker, Toast
- 注册: ✅ 所有组件已正确注册
- **detail.vue** - 报工单详情
- 导入: NavBar, CellGroup, Cell, Tag, Loading, Toast, Button, Dialog
- 注册: ✅ 所有需要注册的组件已正确注册
- **selectWorkOrder.vue** - 选择工单
- 导入: NavBar, Search, PullRefresh, List, Empty, Tag, Toast, ActionSheet
- 注册: ✅ 所有组件已正确注册
#### 4. 个人中心模块 (profile)
- **index.vue** - 个人中心首页
- 导入: Image, CellGroup, Cell, Button, Toast, Dialog
- 注册: ✅ 所有需要注册的组件已正确注册
- **userInfo.vue** - 用户信息
- 导入: NavBar, CellGroup, Cell, Field, Button, Loading, Image, Popup, Picker, Toast
- 注册: ✅ 所有组件已正确注册
- **changePassword.vue** - 修改密码
- 导入: NavBar, CellGroup, Field, Button, Toast
- 注册: ✅ 所有组件已正确注册
#### 5. 首页模块 (home)
- **index.vue** - 移动端首页
- 导入: Image, Grid, GridItem, Cell, CellGroup, Toast
- 注册: ✅ 所有组件已正确注册
- 特殊修改: 今日数据统计逻辑已优化使用正确的API参数
#### 6. 布局模块 (layout)
- **MobileLayout.vue** - 移动端布局
- 导入: 无 vant 组件(使用自定义组件)
- 注册: ✅ 正确
- **components/MobileNavBar.vue** - 导航栏
- 导入: NavBar
- 注册: ✅ 正确
- **components/MobileTabBar.vue** - 底部标签栏
- 导入: Tabbar, TabbarItem
- 注册: ✅ 正确
#### 7. 公共组件 (components)
- **MobileWorkOrderCard.vue** - 工单卡片
- 导入: Tag, Button
- 注册: ✅ 正确
- **MobileSaleOrderCard.vue** - 生产订单卡片
- 导入: Tag
- 注册: ✅ 正确
- 特殊修改: 订单编号显示逻辑已优化,支持嵌套对象
- **MobileReportCard.vue** - 报工单卡片
- 导入: Tag
- 注册: ✅ 正确
## 关键修复记录
### 1. 组件注册问题
- ✅ 所有使用 `<van-empty>` 的页面都已正确导入和注册 Empty 组件
### 2. 业务逻辑优化
- ✅ 生产订单表单标题统一为"生产订单"
- ✅ 日期字段统一为"入库日期"
- ✅ 页面宽度自适应,防止横向滚动
- ✅ 领料人改为选择器
- ✅ 产品搜索支持模糊查询
- ✅ 今日数据统计使用正确的API参数
- ✅ 订单编号显示支持嵌套对象
### 3. 样式优化
- ✅ 所有表单页面添加 `overflow-x: hidden`
- ✅ 所有容器使用 `box-sizing: border-box`
- ✅ 表单元素宽度设置为 100%
## 未使用的组件
以下 Vant 组件在移动端未使用:
- van-icon
- van-divider
- van-checkbox
- van-radio
- van-switch
- van-stepper
- van-uploader
## 结论
✅ 所有移动端页面的组件都已正确导入和注册,不会出现组件未注册的错误。
✅ 所有业务逻辑和样式问题都已修复。
✅ 移动端页面可以正常使用。
## 建议
1. 定期检查新增页面的组件导入和注册
2. 保持组件导入的一致性
3. 对于函数式调用的组件(如 Toast、Dialog无需注册
4. 继续保持代码规范和最佳实践

View File

@@ -0,0 +1,303 @@
# BOM单多级优化开发文档
## 一、现状分析
### 1.1 现有系统架构
当前 `md_new_bom` 模块为**单层BOM结构**每个BOM主表记录`md_new_bom`对应一个产品的物料信息其下挂多条BOM明细`md_new_bom_item`),每条明细直接对应具体物料。
现有表字段中,`parent_id``has_children` 已预置,但未真正实现多级递归展开。
**现有数据模型:**
```
md_new_bom (1) --< md_new_bom_item (N)
└─ materialId (直接指向物料表)
```
### 1.2 关键代码现状
| 层面 | 文件 | 现状 |
|---|---|---|
| 实体-主表 | `NewBom.java` | 有 `parentId``hasChildren``children`(非持久化)字段 |
| 实体-明细 | `NewBomItem.java` | 无 `subBomId` 引用字段明细不能挂子BOM |
| Mapper | `NewBomMapper.xml` | `selectNewBomTree` 仅取一层子节点,无递归 |
| Service | `NewBomServiceImpl.java` | `calculateBomCost` 仅计算直接子项成本,未递归 |
| 前端 | `newBom/index.vue` | 已有树形表格外壳,但展开逻辑不完整 |
---
## 二、需求定义什么是本系统的多级BOM
本系统面向离散制造业多级BOM的核心诉求是**支持子装配Sub-Assembly层级**。
产品BOM的明细物料中一部分为采购/消耗的原材料,另一部分为**半成品组件**子装配件。这些组件本身也有BOM。
**目标结构(示例):**
```
产品: A (整机BOM V1.0)
├── 物料: 螺丝 M3 x 100 pcs (直接材料)
├── 物料: 外壳组件 x 1 pcs (子装配BOM)
│ ├── 物料: 外壳塑料件 x 1 (孙子层)
│ └── 物料: 卡扣 x 4 (孙子层)
└── 物料: PCB主板 x 1 (子装配BOM)
├── 物料: 芯片 xxx x 2
└── 物料: PCB板 x 1
```
---
## 三、方案设计
### 3.1 总体思路
**不动数据库表结构**`md_new_bom` 主表不变),仅做以下扩展:
1.`md_new_bom_item` 新增 `sub_bom_id` 字段标识该明细行是否引用了另一个BOM
2. 在 Service 层新增**递归成本计算**方法
3. 前端增强树形展开展示和子BOM展开功能
4. 提供**多级BOM展开视图**可一键将多层BOM展开为单层物料清单物料去重合并用量
### 3.2 数据库变更
**ALTER TABLE `md_new_bom_item`**
```sql
ALTER TABLE `md_new_bom_item`
ADD COLUMN `sub_bom_id` bigint(20) DEFAULT NULL COMMENT '引用的子BOM ID多级BOM用' AFTER `process_route`,
ADD COLUMN `is_sub_bom` tinyint(1) DEFAULT '0' COMMENT '是否子BOM项 0-否 1-是' AFTER `sub_bom_id`,
ADD COLUMN `level` int(11) DEFAULT '0' COMMENT 'BOM层级深度' AFTER `is_sub_bom`,
ADD KEY `idx_sub_bom_id` (`sub_bom_id`);
```
> **说明:** 保留原 `materialId` 字段,但当 `is_sub_bom=1` 时,`materialId` 指向子装配件的物料ID`sub_bom_id` 指向该物料对应的BOM ID。
---
## 四、后端改造
### 4.1 实体类变更
**`NewBomItem.java`** 新增字段:
```java
/** 引用的子BOM ID多级BOM用 */
private Long subBomId;
/** 是否子BOM项 0-否 1-是 */
private Boolean isSubBom;
/** BOM层级深度 */
private Integer level;
```
**`NewBom.java`** 新增非持久化字段(用于前端展开):
```java
/** 多级展开后的子层BOM列表仅在多级查询时填充 */
@TableField(exist = false)
private List<NewBom> expandedChildren;
/** 多级成本(递归计算结果) */
@TableField(exist = false)
private BigDecimal expandedMaterialCost;
```
### 4.2 Service 层新增方法
`INewBomService.java``NewBomServiceImpl.java` 中新增:
#### 4.2.1 递归成本计算
```java
/**
* 递归计算多级BOM成本支持无限层级
* @param bomId BOM主键
* @param topQuantity 顶层需求量(用于按比例缩放子级用量)
* @return 包含多级明细的成本分析
*/
Map<String, Object> calculateMultiLevelBomCost(Long bomId, BigDecimal topQuantity);
```
**核心算法:**
```
calculateMultiLevelBomCost(bomId, quantity):
bom = selectNewBomById(bomId)
totalMaterialCost = 0
totalLaborCost = bom.laborCost
totalManufacturingCost = bom.manufacturingCost
for each item in bom.bomItems:
scaleFactor = quantity / bom.baseQuantity
if item.isSubBom == true:
childCost = calculateMultiLevelBomCost(item.subBomId, item.quantity * scaleFactor)
totalMaterialCost += childCost.materialCost
else:
itemCost = item.quantity * scaleFactor * item.unitPrice * (1 + item.lossRate/100)
totalMaterialCost += itemCost
return { materialCost, laborCost, manufacturingCost, totalCost, itemDetails }
```
#### 4.2.2 完整BOM树查询
```java
/**
* 查询BOM多级展开树递归向下查询指定层级
* @param bomId 起始BOM ID
* @param maxLevel 最大展开层级默认3层防止性能问题
* @return 带完整子级结构的BOM树
*/
NewBom selectBomFullTree(Long bomId, Integer maxLevel);
```
#### 4.2.3 BOM展开清单物料去重合并
```java
/**
* 将多级BOM展开为单层物料清单含去重合并用量
* @param bomId 起始BOM ID
* @param quantity 需求量
* @return 扁平化物料列表(含各层汇总用量)
*/
List<Map<String, Object>> expandBomToFlatList(Long bomId, BigDecimal quantity);
```
**展开合并算法:**
```
expandBomToFlatList(bomId, quantity):
flatList = []
recursiveCollect(bomId, quantity, visitedBomIds):
bom = selectBomById(bomId)
if bom.id in visitedBomIds: return // 防止循环引用
add bom.id to visitedBomIds
scale = quantity / bom.baseQuantity
for each item in bom.items:
if item.isSubBom:
recursiveCollect(item.subBomId, item.quantity * scale, visitedBomIds)
else:
merge into flatList: 物料ID -> 累加用量
return flatList
```
### 4.3 Controller 层新增接口
`NewBomController.java` 新增端点:
| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/masterdata/newBom/multiLevel/{id}` | 获取BOM多级展开树 |
| `GET` | `/masterdata/newBom/cost/multiLevel/{id}` | 多级递归成本计算 |
| `GET` | `/masterdata/newBom/expand/{id}` | 展开为扁平物料清单 |
### 4.4 多级展开时需防止的问题
1. **循环引用检测:** A->B->A 的情况,在递归入口处用 `Set<Long> visitedBomIds` 记录已访问的BOM ID
2. **最大层级限制:** 默认限制 10 层,超出则提示用户"BOM层级过深"
3. **性能:** 超过 3 层时前端应显示加载状态,后端加缓存(可选)
---
## 五、前端改造
### 5.1 BOM管理页增强
文件:`mes-ui/src/views/mes/masterdata/newBom/index.vue`
**5.1.1 新增"子BOM物料"输入模式**
在新增/编辑BOM明细行时增加一个切换
- **普通物料行**(默认):选择物料,填写用量、损耗率
- **子BOM行**:选择"子装配物料" + 选择该物料对应的BOM版本
前端在选择物料时,如果该物料在 `md_new_bom` 中有已发布的BOM则自动提示可挂子BOM。
**5.1.2 明细行组件增强**
在现有的 BOM 明细表格中,每行增加:
- 列:`类型` — 显示「普通」或「子BOM」不同颜色标签区分
- 列:`引用BOM` — 当类型为子BOM时显示引用的BOM编号
- 操作:`展开`按钮 — 仅子BOM行可用点击后在该行下方嵌入子BOM明细表格
### 5.2 新增多级BOM视图页
新增 `mes-ui/src/views/mes/masterdata/newBom/multiLevel.vue`
- 输入BOM选择 + 需求量
- 展示:树形结构,多级展开/折叠
- 每行显示:层级缩进、物料编码、物料名称、用量、成本、是否关键件
- 底部显示:多级总成本汇总
- 按钮:「展开为清单」— 切换到扁平视图(物料去重合并)
### 5.3 API 层新增
文件:`mes-ui/src/api/mes/masterdata/newBom.js`
```javascript
// 多级BOM查询
export const getBomMultiLevel = (id, maxLevel) => {
return request({ url: `/masterdata/newBom/multiLevel/${id}`, params: { maxLevel } })
}
// 多级成本计算
export const getBomMultiLevelCost = (id, quantity) => {
return request({ url: `/masterdata/newBom/cost/multiLevel/${id}`, params: { quantity } })
}
// BOM展开清单
export const expandBom = (id, quantity) => {
return request({ url: `/masterdata/newBom/expand/${id}`, params: { quantity } })
}
```
---
## 六、实现计划(分三个阶段)
### 第一阶段基础设施1-2天
- [ ] 执行 `ALTER TABLE` 新增字段
- [ ] 修改 `NewBomItem.java` 实体,添加 `subBomId``isSubBom``level` 字段
- [ ] 修改 `NewBom.java` 添加非持久化展开字段
- [ ] 修改 `NewBomMapper.xml` 新增字段映射
- [ ] 单元测试验证字段变更不影响现有CRUD
### 第二阶段核心逻辑2-3天
- [ ]`INewBomService.java` 新增 3 个接口方法声明
- [ ]`NewBomServiceImpl.java` 实现 `calculateMultiLevelBomCost`(递归)
- [ ]`NewBomServiceImpl.java` 实现 `selectBomFullTree`(递归树查询)
- [ ]`NewBomServiceImpl.java` 实现 `expandBomToFlatList`(去重合并)
- [ ]`NewBomController.java` 新增 3 个API端点
- [ ] 循环引用和最大层级检测逻辑
### 第三阶段前端改造2-3天
- [ ] 前端 API 层新增 3 个接口调用
- [ ] 修改 BOM 明细行组件支持子BOM行类型
- [ ] 新增 `multiLevel.vue` 多级BOM视图页
- [ ] 树形展开/折叠交互
- [ ] 扁平化展开清单视图(物料去重合并展示)
---
## 七、风险与注意事项
1. **循环引用:** A BOM -> B BOM -> A BOM 必须被检测并阻止,递归时用 `visitedIds` 集合
2. **性能:** 深层BOM>5层成本计算较慢考虑异步计算或后台任务
3. **数据迁移:** 已有的 BOM 明细数据 `is_sub_bom=0``level=0`,无需迁移
4. **版本一致性:** 子BOM引用时限定只能引用"已发布"状态的BOM
5. **兼容旧数据:** 新字段 `sub_bom_id` 允许 NULL向下兼容
---
## 八、预期效果
| 角色 | 收益 |
|---|---|
| 工艺人员 | 一个页面管理整机BOM可挂子装配BOM无需分开维护多张独立BOM |
| 成本会计 | 一键计算整机(含所有子级)的真实材料成本 |
| 生产计划 | 展开清单可直接输出为生产领料参考清单 |
| 系统 | 复用现有表结构,改动小,风险可控 |

View File

@@ -0,0 +1,4 @@
例子xxx时间(精确到分钟)xx人xxx版本更新了xxx内容同步到了xxx分支是否具备通用性
2025-11-19 19:37周启威V1.0.12新增了日志文件同步到了dev分支具有通用性

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
LabVIEW AES解密脚本
用于解密MES接口返回的加密数据
密钥YaviiMESpassword
算法AES-128/ECB/PKCS5Padding
依赖安装:
pip install pycryptodome
使用方法:
python 2025-10-18_硬件融合接口解密脚本.py <encrypted_data>
示例:
python 2025-10-18_硬件融合接口解密脚本.py "U2FsdGVkX1+..."
返回:
{"Object":"root","Class":"password123"}
"""
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64
import sys
import json
# AES密钥必须与Java后端一致16字节
AES_KEY = b"YaviiMESpassword"
def aes_decrypt(encrypted_data):
"""
AES解密函数
参数:
encrypted_data: Base64编码的密文字符串
返回:
解密后的明文字符串
异常:
如果解密失败抛出Exception
"""
try:
# 创建AES解密器ECB模式
cipher = AES.new(AES_KEY, AES.MODE_ECB)
# Base64解码
encrypted_bytes = base64.b64decode(encrypted_data)
# AES解密
decrypted_bytes = cipher.decrypt(encrypted_bytes)
# 去除填充PKCS5/PKCS7
decrypted_bytes = unpad(decrypted_bytes, AES.block_size)
# 转换为字符串
return decrypted_bytes.decode('utf-8')
except Exception as e:
raise Exception(f"AES解密失败: {str(e)}")
def main():
"""
主函数
命令行用法python 2025-10-18_硬件融合接口解密脚本.py <encrypted_data>
"""
if len(sys.argv) < 2:
print("=" * 60)
print("LabVIEW AES解密工具")
print("=" * 60)
print()
print("用法:")
print(" python 2025-10-18_硬件融合接口解密脚本.py <encrypted_data>")
print()
print("示例:")
print(" python 2025-10-18_硬件融合接口解密脚本.py \"U2FsdGVkX1+...\"")
print()
print("参数说明:")
print(" encrypted_data: Base64编码的AES密文")
print()
print("密钥信息:")
print(" 算法: AES-128")
print(" 模式: ECB")
print(" 密钥: YaviiMESpassword")
print()
print("=" * 60)
sys.exit(1)
encrypted_data = sys.argv[1]
try:
# 解密
decrypted = aes_decrypt(encrypted_data)
# 尝试格式化JSON输出美化
try:
json_obj = json.loads(decrypted)
print(json.dumps(json_obj, ensure_ascii=False, indent=2))
except:
# 如果不是JSON直接输出
print(decrypted)
except Exception as e:
print(f"错误: {str(e)}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()