2235 lines
67 KiB
Markdown
2235 lines
67 KiB
Markdown
|
|
# 工序执行情况表 - 定时自动完成功能设计方案
|
|||
|
|
|
|||
|
|
## 📋 需求背景
|
|||
|
|
|
|||
|
|
在现有"一键完成"功能的基础上,增加**定时自动完成**功能。对于超过指定天数未完成的销售订单,系统自动执行一键完成流程,实现订单的自动化批量处理。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎯 功能位置
|
|||
|
|
|
|||
|
|
**界面位置:** 工序执行情况表 - 查询条件区域,重置按钮右侧
|
|||
|
|
|
|||
|
|
**按钮样式:**
|
|||
|
|
```vue
|
|||
|
|
<el-button
|
|||
|
|
type="warning"
|
|||
|
|
icon="el-icon-timer"
|
|||
|
|
size="mini"
|
|||
|
|
@click="openTimedCompleteDialog"
|
|||
|
|
>
|
|||
|
|
定时完成
|
|||
|
|
</el-button>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**示意图:**
|
|||
|
|
```
|
|||
|
|
[搜索] [重置] [定时完成] [导出] ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🌟 核心功能
|
|||
|
|
|
|||
|
|
### 1. 配置对话框
|
|||
|
|
|
|||
|
|
点击"定时完成"按钮后,弹出配置对话框:
|
|||
|
|
|
|||
|
|
#### 对话框界面设计
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<el-dialog title="定时自动完成配置" width="500px">
|
|||
|
|
<el-form label-width="120px">
|
|||
|
|
|
|||
|
|
<!-- 开关状态 -->
|
|||
|
|
<el-form-item label="自动完成状态">
|
|||
|
|
<el-switch
|
|||
|
|
v-model="timedConfig.enabled"
|
|||
|
|
active-text="开启"
|
|||
|
|
inactive-text="关闭"
|
|||
|
|
active-color="#13ce66"
|
|||
|
|
inactive-color="#ff4949"
|
|||
|
|
/>
|
|||
|
|
<div class="tip-text">
|
|||
|
|
开启后,系统将自动处理超期未完成的订单
|
|||
|
|
</div>
|
|||
|
|
</el-form-item>
|
|||
|
|
|
|||
|
|
<!-- 天数阈值 -->
|
|||
|
|
<el-form-item label="订单超期天数">
|
|||
|
|
<el-input-number
|
|||
|
|
v-model="timedConfig.dayThreshold"
|
|||
|
|
:min="1"
|
|||
|
|
:max="365"
|
|||
|
|
:step="1"
|
|||
|
|
:disabled="!timedConfig.enabled"
|
|||
|
|
/>
|
|||
|
|
<span style="margin-left: 10px">天</span>
|
|||
|
|
<div class="tip-text">
|
|||
|
|
当前日期 - 订单日期 >= 设置天数时,自动完成
|
|||
|
|
</div>
|
|||
|
|
</el-form-item>
|
|||
|
|
|
|||
|
|
<!-- 当前配置预览 -->
|
|||
|
|
<el-form-item label="配置说明">
|
|||
|
|
<el-alert
|
|||
|
|
v-if="timedConfig.enabled"
|
|||
|
|
title="当前配置"
|
|||
|
|
type="info"
|
|||
|
|
:closable="false"
|
|||
|
|
>
|
|||
|
|
<div>自动完成超过或等于 <b>{{ timedConfig.dayThreshold }}</b> 天未完成的订单</div>
|
|||
|
|
<div>下次检查时间:刷新页面或定时检查时触发</div>
|
|||
|
|
</el-alert>
|
|||
|
|
<el-alert
|
|||
|
|
v-else
|
|||
|
|
title="定时自动完成已关闭"
|
|||
|
|
type="warning"
|
|||
|
|
:closable="false"
|
|||
|
|
/>
|
|||
|
|
</el-form-item>
|
|||
|
|
|
|||
|
|
<!-- 影响范围统计 -->
|
|||
|
|
<el-form-item label="影响订单数量" v-if="timedConfig.enabled">
|
|||
|
|
<el-tag type="danger" size="medium">
|
|||
|
|
{{ affectedOrderCount }} 个订单将被自动完成
|
|||
|
|
</el-tag>
|
|||
|
|
<el-button
|
|||
|
|
type="text"
|
|||
|
|
size="mini"
|
|||
|
|
@click="previewAffectedOrders"
|
|||
|
|
>
|
|||
|
|
查看明细
|
|||
|
|
</el-button>
|
|||
|
|
</el-form-item>
|
|||
|
|
|
|||
|
|
</el-form>
|
|||
|
|
|
|||
|
|
<div slot="footer">
|
|||
|
|
<el-button @click="closeDialog">取消</el-button>
|
|||
|
|
<el-button
|
|||
|
|
type="primary"
|
|||
|
|
@click="saveTimedConfig"
|
|||
|
|
:loading="saving"
|
|||
|
|
>
|
|||
|
|
保存配置
|
|||
|
|
</el-button>
|
|||
|
|
<el-button
|
|||
|
|
type="success"
|
|||
|
|
@click="executeNow"
|
|||
|
|
:disabled="!timedConfig.enabled"
|
|||
|
|
v-if="affectedOrderCount > 0"
|
|||
|
|
>
|
|||
|
|
立即执行一次
|
|||
|
|
</el-button>
|
|||
|
|
</div>
|
|||
|
|
</el-dialog>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. 配置数据结构
|
|||
|
|
|
|||
|
|
#### 前端数据模型
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
// 定时完成配置
|
|||
|
|
timedConfig: {
|
|||
|
|
enabled: false, // 是否开启
|
|||
|
|
dayThreshold: 30, // 天数阈值(默认30天)
|
|||
|
|
lastCheckTime: null, // 最后检查时间
|
|||
|
|
lastExecuteTime: null // 最后执行时间
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 受影响的订单数量
|
|||
|
|
affectedOrderCount: 0,
|
|||
|
|
|
|||
|
|
// 保存中状态
|
|||
|
|
saving: false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 后端数据表设计
|
|||
|
|
|
|||
|
|
**表名:** `sys_timed_complete_config`
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `sys_timed_complete_config` (
|
|||
|
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|||
|
|
`module_name` varchar(50) NOT NULL COMMENT '模块名称(sale_order/work_order等)',
|
|||
|
|
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用(0=关闭,1=开启)',
|
|||
|
|
`day_threshold` int(11) NOT NULL DEFAULT '30' COMMENT '天数阈值',
|
|||
|
|
`last_check_time` datetime DEFAULT NULL COMMENT '最后检查时间',
|
|||
|
|
`last_execute_time` datetime DEFAULT NULL COMMENT '最后执行时间',
|
|||
|
|
`last_execute_count` int(11) DEFAULT '0' COMMENT '最后执行处理数量',
|
|||
|
|
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
|
|||
|
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
|||
|
|
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
|
|||
|
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
|||
|
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
|||
|
|
PRIMARY KEY (`id`),
|
|||
|
|
UNIQUE KEY `uk_module_name` (`module_name`)
|
|||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定时自动完成配置表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. 核心业务逻辑
|
|||
|
|
|
|||
|
|
#### 3.1 判断订单是否超期
|
|||
|
|
|
|||
|
|
**条件公式:**
|
|||
|
|
```javascript
|
|||
|
|
当前日期 - 订单日期 >= 设置的天数阈值
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**SQL查询:**
|
|||
|
|
```sql
|
|||
|
|
SELECT
|
|||
|
|
soe.id AS sale_order_entry_id,
|
|||
|
|
soe.main_id AS sale_order_id,
|
|||
|
|
so.number AS order_number,
|
|||
|
|
so.sale_date AS order_date,
|
|||
|
|
soe.material_id,
|
|||
|
|
soe.material_name,
|
|||
|
|
soe.material_number,
|
|||
|
|
soe.specification,
|
|||
|
|
soe.quantity,
|
|||
|
|
soe.unit,
|
|||
|
|
soe.status,
|
|||
|
|
DATEDIFF(CURDATE(), so.sale_date) AS days_passed
|
|||
|
|
FROM sal_order_entry soe
|
|||
|
|
INNER JOIN sal_order so ON so.id = soe.main_id
|
|||
|
|
WHERE
|
|||
|
|
soe.status NOT IN ('C', 'D', 'F') -- 排除已发货、已关闭、生产完成的订单
|
|||
|
|
AND DATEDIFF(CURDATE(), so.sale_date) >= #{dayThreshold} -- 大于等于阈值天数
|
|||
|
|
AND soe.del_flag = '0'
|
|||
|
|
AND so.del_flag = '0'
|
|||
|
|
ORDER BY so.sale_date ASC;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**注意:** 使用 `>=` 确保正好到达阈值当天也会被处理。例如设置30天,第30天就会触发自动完成。
|
|||
|
|
|
|||
|
|
#### 3.2 自动完成执行流程
|
|||
|
|
|
|||
|
|
**触发时机:**
|
|||
|
|
1. **页面刷新时:** 如果配置为开启状态,页面加载完成后自动检查并执行
|
|||
|
|
2. **手动触发:** 点击"立即执行一次"按钮
|
|||
|
|
3. **定时任务(可选):** 后端定时任务每天凌晨执行一次
|
|||
|
|
|
|||
|
|
**执行步骤:**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
async executeTimedComplete() {
|
|||
|
|
// 1. 检查是否开启
|
|||
|
|
if (!this.timedConfig.enabled) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 加载提示
|
|||
|
|
const loading = this.$loading({
|
|||
|
|
lock: true,
|
|||
|
|
text: '正在检查并处理超期订单...',
|
|||
|
|
background: 'rgba(0, 0, 0, 0.7)'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 3. 查询超期订单列表
|
|||
|
|
const { data } = await getOverdueOrders({
|
|||
|
|
dayThreshold: this.timedConfig.dayThreshold
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (data.length === 0) {
|
|||
|
|
this.$message.info('暂无需要自动完成的订单');
|
|||
|
|
loading.close();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. 确认提示
|
|||
|
|
await this.$confirm(
|
|||
|
|
`检测到 ${data.length} 个订单达到或超过 ${this.timedConfig.dayThreshold} 天未完成,是否自动完成?`,
|
|||
|
|
'定时自动完成确认',
|
|||
|
|
{
|
|||
|
|
confirmButtonText: '确认执行',
|
|||
|
|
cancelButtonText: '取消',
|
|||
|
|
type: 'warning'
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 5. 批量执行一键完成
|
|||
|
|
let successCount = 0;
|
|||
|
|
let failCount = 0;
|
|||
|
|
const failedOrders = [];
|
|||
|
|
|
|||
|
|
for (let i = 0; i < data.length; i++) {
|
|||
|
|
const order = data[i];
|
|||
|
|
loading.text = `正在处理订单 ${i + 1}/${data.length}:${order.orderNumber}`;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 调用一键完成接口
|
|||
|
|
await this.executeAutoComplete(order);
|
|||
|
|
successCount++;
|
|||
|
|
} catch (error) {
|
|||
|
|
failCount++;
|
|||
|
|
failedOrders.push({
|
|||
|
|
orderNumber: order.orderNumber,
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loading.close();
|
|||
|
|
|
|||
|
|
// 6. 显示执行结果
|
|||
|
|
this.showExecuteResult(successCount, failCount, failedOrders);
|
|||
|
|
|
|||
|
|
// 7. 刷新列表
|
|||
|
|
this.getList();
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
loading.close();
|
|||
|
|
if (error !== 'cancel') {
|
|||
|
|
this.$message.error('执行失败:' + error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.3 一键完成执行(复用现有逻辑)
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
async executeAutoComplete(order) {
|
|||
|
|
// 1. 获取默认工序路线
|
|||
|
|
const route = await this.getDefaultRoute(order.materialId);
|
|||
|
|
|
|||
|
|
if (!route) {
|
|||
|
|
throw new Error('未找到默认工序路线');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 构造自动完成参数
|
|||
|
|
const autoCompleteDTO = {
|
|||
|
|
saleOrderId: order.saleOrderId,
|
|||
|
|
saleOrderEntryId: order.saleOrderEntryId,
|
|||
|
|
routeId: route.id,
|
|||
|
|
processConfigs: route.routeProcessList.map((process, index) => ({
|
|||
|
|
processId: process.processId,
|
|||
|
|
processName: process.processName,
|
|||
|
|
processSort: index + 1, // 工序序号
|
|||
|
|
reportUserId: this.currentUserId, // ⚠️ 使用当前登录用户ID或系统默认用户
|
|||
|
|
reportTime: this.formatDateTime(new Date()), // 格式:yyyy-MM-dd HH:mm:ss
|
|||
|
|
reportQuantity: order.quantity,
|
|||
|
|
// ⚠️ 注意:workshopId和stationId会从process对应的工位自动获取,不需要传入
|
|||
|
|
// ⚠️ qualifiedQuantity和unqualifiedQuantity会在后端自动设置
|
|||
|
|
}))
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 3. 调用一键完成接口
|
|||
|
|
const response = await autoCompleteSaleOrder(autoCompleteDTO);
|
|||
|
|
|
|||
|
|
if (response.code !== 200) {
|
|||
|
|
throw new Error(response.msg || '一键完成失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return response;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. 执行结果展示
|
|||
|
|
|
|||
|
|
#### 4.1 成功提示
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
showExecuteResult(successCount, failCount, failedOrders) {
|
|||
|
|
if (failCount === 0) {
|
|||
|
|
// 全部成功
|
|||
|
|
this.$notify({
|
|||
|
|
title: '定时自动完成成功',
|
|||
|
|
message: `已成功处理 ${successCount} 个订单`,
|
|||
|
|
type: 'success',
|
|||
|
|
duration: 5000
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// 部分失败
|
|||
|
|
const h = this.$createElement;
|
|||
|
|
const failedList = failedOrders.map(item =>
|
|||
|
|
h('div', { style: 'margin: 5px 0' }, [
|
|||
|
|
h('b', item.orderNumber),
|
|||
|
|
': ',
|
|||
|
|
h('span', { style: 'color: red' }, item.error)
|
|||
|
|
])
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
this.$notify({
|
|||
|
|
title: '定时自动完成部分成功',
|
|||
|
|
message: h('div', [
|
|||
|
|
h('div', `成功:${successCount} 个,失败:${failCount} 个`),
|
|||
|
|
h('div', { style: 'margin-top: 10px; font-weight: bold' }, '失败订单:'),
|
|||
|
|
...failedList
|
|||
|
|
]),
|
|||
|
|
type: 'warning',
|
|||
|
|
duration: 10000
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.2 执行日志记录
|
|||
|
|
|
|||
|
|
**表名:** `sys_timed_complete_log`
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `sys_timed_complete_log` (
|
|||
|
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|||
|
|
`config_id` bigint(20) NOT NULL COMMENT '配置ID',
|
|||
|
|
`execute_time` datetime NOT NULL COMMENT '执行时间',
|
|||
|
|
`execute_type` varchar(20) NOT NULL COMMENT '执行类型(AUTO=自动, MANUAL=手动)',
|
|||
|
|
`total_count` int(11) NOT NULL COMMENT '检查订单总数',
|
|||
|
|
`success_count` int(11) NOT NULL COMMENT '成功数量',
|
|||
|
|
`fail_count` int(11) NOT NULL COMMENT '失败数量',
|
|||
|
|
`execute_duration` int(11) DEFAULT NULL COMMENT '执行耗时(秒)',
|
|||
|
|
`execute_result` text COMMENT '执行结果详情(JSON)',
|
|||
|
|
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
|
|||
|
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
|||
|
|
PRIMARY KEY (`id`),
|
|||
|
|
KEY `idx_config_id` (`config_id`),
|
|||
|
|
KEY `idx_execute_time` (`execute_time`)
|
|||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定时自动完成执行日志表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5. 页面加载时自动检查
|
|||
|
|
|
|||
|
|
#### 5.1 页面初始化逻辑
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 在 created 或 mounted 钩子中
|
|||
|
|
async mounted() {
|
|||
|
|
// 1. 加载列表数据
|
|||
|
|
await this.getList();
|
|||
|
|
|
|||
|
|
// 2. 加载定时完成配置
|
|||
|
|
await this.loadTimedConfig();
|
|||
|
|
|
|||
|
|
// 3. 如果开启了定时完成,自动检查并执行
|
|||
|
|
if (this.timedConfig.enabled) {
|
|||
|
|
// 延迟1秒执行,避免页面加载卡顿
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.checkAndExecuteTimedComplete();
|
|||
|
|
}, 1000);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.2 自动检查逻辑
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
async checkAndExecuteTimedComplete() {
|
|||
|
|
try {
|
|||
|
|
// 1. 查询超期订单数量
|
|||
|
|
const { data } = await getOverdueOrdersCount({
|
|||
|
|
dayThreshold: this.timedConfig.dayThreshold
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (data.count === 0) {
|
|||
|
|
console.log('[定时自动完成] 暂无超期订单');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 静默执行(不弹确认框)
|
|||
|
|
console.log(`[定时自动完成] 检测到 ${data.count} 个超期订单,开始自动处理...`);
|
|||
|
|
|
|||
|
|
// 显示右下角提示
|
|||
|
|
this.$notify({
|
|||
|
|
title: '定时自动完成',
|
|||
|
|
message: `检测到 ${data.count} 个超期订单,正在后台自动处理...`,
|
|||
|
|
type: 'info',
|
|||
|
|
duration: 3000
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 3. 执行批量自动完成
|
|||
|
|
await this.batchAutoComplete();
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('[定时自动完成] 执行失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 6. 预览受影响订单
|
|||
|
|
|
|||
|
|
点击"查看明细"按钮后,显示即将被自动完成的订单列表:
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<el-dialog
|
|||
|
|
title="将被自动完成的订单明细"
|
|||
|
|
width="80%"
|
|||
|
|
:visible.sync="previewDialogVisible"
|
|||
|
|
>
|
|||
|
|
<el-table :data="affectedOrders" border>
|
|||
|
|
<el-table-column prop="orderNumber" label="订单编号" width="150" />
|
|||
|
|
<el-table-column prop="orderDate" label="订单日期" width="120" />
|
|||
|
|
<el-table-column label="超期天数" width="100">
|
|||
|
|
<template slot-scope="scope">
|
|||
|
|
<el-tag type="danger">{{ scope.row.daysPassed }} 天</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column prop="materialName" label="产品名称" />
|
|||
|
|
<el-table-column prop="quantity" label="数量" width="100" />
|
|||
|
|
<el-table-column label="状态" width="100">
|
|||
|
|
<template slot-scope="scope">
|
|||
|
|
<dict-tag :options="dict.type.salorder_status" :value="scope.row.status" />
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
</el-dialog>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔧 技术实现
|
|||
|
|
|
|||
|
|
### ⚠️ 重要:DTO/VO数据结构说明
|
|||
|
|
|
|||
|
|
定时自动完成功能复用一键完成的核心接口,必须严格按照实际的DTO结构传参:
|
|||
|
|
|
|||
|
|
#### OverdueOrderVO(超期订单视图对象)⚠️ 新增
|
|||
|
|
```java
|
|||
|
|
/**
|
|||
|
|
* 超期订单VO - 用于定时自动完成
|
|||
|
|
*
|
|||
|
|
* @author AI Assistant
|
|||
|
|
* @date 2025-11-01
|
|||
|
|
*/
|
|||
|
|
@Data
|
|||
|
|
public class OverdueOrderVO {
|
|||
|
|
private Long saleOrderEntryId; // 销售订单明细ID
|
|||
|
|
private Long saleOrderId; // 销售订单ID
|
|||
|
|
private String orderNumber; // 订单编号
|
|||
|
|
private Date orderDate; // 订单日期
|
|||
|
|
private Long materialId; // 物料ID
|
|||
|
|
private String materialName; // 物料名称
|
|||
|
|
private String materialNumber; // 物料编号
|
|||
|
|
private String specification; // 规格
|
|||
|
|
private BigDecimal quantity; // 数量
|
|||
|
|
private String unit; // 单位
|
|||
|
|
private String status; // 状态
|
|||
|
|
private Integer daysPassed; // 超期天数
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### BatchExecuteDTO(批量执行请求DTO)⚠️ 新增
|
|||
|
|
```java
|
|||
|
|
/**
|
|||
|
|
* 批量执行请求DTO
|
|||
|
|
*
|
|||
|
|
* @author AI Assistant
|
|||
|
|
* @date 2025-11-01
|
|||
|
|
*/
|
|||
|
|
@Data
|
|||
|
|
public class BatchExecuteDTO {
|
|||
|
|
private Long configId; // 配置ID
|
|||
|
|
private String moduleName; // 模块名称(sale_order)
|
|||
|
|
private Integer dayThreshold; // 天数阈值
|
|||
|
|
private String executeType; // 执行类型(AUTO=自动, MANUAL=手动)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### AutoCompleteDTO(一键完成请求DTO)
|
|||
|
|
```java
|
|||
|
|
@Data
|
|||
|
|
public class AutoCompleteDTO {
|
|||
|
|
private Long saleOrderId; // 销售订单ID
|
|||
|
|
private Long saleOrderEntryId; // 销售订单明细ID
|
|||
|
|
private Long routeId; // 工序路线ID
|
|||
|
|
private List<ProcessConfigDTO> processConfigs; // 工序配置列表
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### ProcessConfigDTO(工序配置DTO)
|
|||
|
|
```java
|
|||
|
|
@Data
|
|||
|
|
public class ProcessConfigDTO {
|
|||
|
|
private Long processId; // ✅ 工序ID(必填)
|
|||
|
|
private String processName; // ✅ 工序名称(必填)
|
|||
|
|
private Integer processSort; // ✅ 工序序号(必填)
|
|||
|
|
private Long reportUserId; // ✅ 报工人ID(必填)⚠️ 定时完成需要使用系统默认用户
|
|||
|
|
private String reportTime; // ✅ 报工时间(必填)格式:yyyy-MM-dd HH:mm:ss
|
|||
|
|
private BigDecimal reportQuantity; // ✅ 报工数量(必填)
|
|||
|
|
private Long workshopId; // ❌ 车间ID(可选)系统会自动从工位获取,不传也可以
|
|||
|
|
private Long stationId; // ❌ 工位ID(可选)系统会自动从processId查询,不传也可以
|
|||
|
|
|
|||
|
|
// ⚠️ 注意:以下字段不在DTO中,会在后端自动设置
|
|||
|
|
// - qualifiedQuantity: 自动设置为 reportQuantity
|
|||
|
|
// - unqualifiedQuantity: 自动设置为 0
|
|||
|
|
// - machineId: 从工位关联的设备获取
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**✅ 关键点:**
|
|||
|
|
1. `reportUserId` 是必填字段,定时完成时使用系统默认用户(如admin的ID=1)
|
|||
|
|
2. `reportTime` 必须是String格式:`yyyy-MM-dd HH:mm:ss`
|
|||
|
|
3. `workshopId`和`stationId`可以不传,系统会根据`processId`自动查询工位并获取
|
|||
|
|
4. 合格数量、不合格数量由后端自动计算,不需要在DTO中传入
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 7. API接口设计
|
|||
|
|
|
|||
|
|
#### 7.1 前端API(`src/api/mes/production/timedComplete.js`)
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
import request from '@/utils/request'
|
|||
|
|
|
|||
|
|
// 获取定时完成配置
|
|||
|
|
export function getTimedConfig(moduleName) {
|
|||
|
|
return request({
|
|||
|
|
url: '/production/timedComplete/config',
|
|||
|
|
method: 'get',
|
|||
|
|
params: { moduleName }
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存定时完成配置
|
|||
|
|
export function saveTimedConfig(data) {
|
|||
|
|
return request({
|
|||
|
|
url: '/production/timedComplete/config',
|
|||
|
|
method: 'post',
|
|||
|
|
data: data
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查询超期订单列表
|
|||
|
|
export function getOverdueOrders(params) {
|
|||
|
|
return request({
|
|||
|
|
url: '/production/timedComplete/overdueOrders',
|
|||
|
|
method: 'get',
|
|||
|
|
params: params
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查询超期订单数量
|
|||
|
|
export function getOverdueOrdersCount(params) {
|
|||
|
|
return request({
|
|||
|
|
url: '/production/timedComplete/overdueOrders/count',
|
|||
|
|
method: 'get',
|
|||
|
|
params: params
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 批量自动完成
|
|||
|
|
export function batchAutoComplete(data) {
|
|||
|
|
return request({
|
|||
|
|
url: '/production/timedComplete/batchExecute',
|
|||
|
|
method: 'post',
|
|||
|
|
data: data,
|
|||
|
|
timeout: 300000 // 5分钟超时
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查询执行日志
|
|||
|
|
export function getExecuteLogs(params) {
|
|||
|
|
return request({
|
|||
|
|
url: '/production/timedComplete/logs',
|
|||
|
|
method: 'get',
|
|||
|
|
params: params
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 7.2 后端Controller(`TimedCompleteController.java`)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@RestController
|
|||
|
|
@RequestMapping("/production/timedComplete")
|
|||
|
|
public class TimedCompleteController {
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private TimedCompleteService timedCompleteService;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取定时完成配置
|
|||
|
|
*/
|
|||
|
|
@GetMapping("/config")
|
|||
|
|
public AjaxResult getConfig(@RequestParam String moduleName) {
|
|||
|
|
return AjaxResult.success(timedCompleteService.getConfig(moduleName));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 保存定时完成配置
|
|||
|
|
*/
|
|||
|
|
@PostMapping("/config")
|
|||
|
|
public AjaxResult saveConfig(@RequestBody TimedCompleteConfig config) {
|
|||
|
|
return toAjax(timedCompleteService.saveConfig(config));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 查询超期订单列表
|
|||
|
|
*/
|
|||
|
|
@GetMapping("/overdueOrders")
|
|||
|
|
public AjaxResult getOverdueOrders(
|
|||
|
|
@RequestParam String moduleName,
|
|||
|
|
@RequestParam Integer dayThreshold
|
|||
|
|
) {
|
|||
|
|
return AjaxResult.success(
|
|||
|
|
timedCompleteService.getOverdueOrders(moduleName, dayThreshold)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 查询超期订单数量
|
|||
|
|
*/
|
|||
|
|
@GetMapping("/overdueOrders/count")
|
|||
|
|
public AjaxResult getOverdueOrdersCount(
|
|||
|
|
@RequestParam String moduleName,
|
|||
|
|
@RequestParam Integer dayThreshold
|
|||
|
|
) {
|
|||
|
|
return AjaxResult.success(
|
|||
|
|
timedCompleteService.getOverdueOrdersCount(moduleName, dayThreshold)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量自动完成
|
|||
|
|
*/
|
|||
|
|
@PostMapping("/batchExecute")
|
|||
|
|
public AjaxResult batchExecute(@RequestBody BatchExecuteDTO dto) {
|
|||
|
|
return timedCompleteService.batchExecute(dto);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 查询执行日志
|
|||
|
|
*/
|
|||
|
|
@GetMapping("/logs")
|
|||
|
|
public TableDataInfo getLogs(TimedCompleteLogQuery query) {
|
|||
|
|
startPage();
|
|||
|
|
List<TimedCompleteLog> list = timedCompleteService.getLogs(query);
|
|||
|
|
return getDataTable(list);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 7.3 后端Service(`TimedCompleteServiceImpl.java`)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
public class TimedCompleteServiceImpl implements TimedCompleteService {
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private TimedCompleteConfigMapper configMapper;
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private SalOrderMapper salOrderMapper;
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private SalOrderEntryMapper salOrderEntryMapper;
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private AutoCompleteService autoCompleteService;
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private TimedCompleteLogMapper logMapper;
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private MaterialMapper materialMapper;
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private RouteMapper routeMapper;
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private RouteProcessMapper routeProcessMapper;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取配置
|
|||
|
|
*/
|
|||
|
|
@Override
|
|||
|
|
public TimedCompleteConfig getConfig(String moduleName) {
|
|||
|
|
TimedCompleteConfig config = configMapper.selectByModuleName(moduleName);
|
|||
|
|
if (config == null) {
|
|||
|
|
// 返回默认配置
|
|||
|
|
config = new TimedCompleteConfig();
|
|||
|
|
config.setModuleName(moduleName);
|
|||
|
|
config.setEnabled(false);
|
|||
|
|
config.setDayThreshold(30);
|
|||
|
|
}
|
|||
|
|
return config;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 保存配置
|
|||
|
|
*/
|
|||
|
|
@Override
|
|||
|
|
@Transactional
|
|||
|
|
public int saveConfig(TimedCompleteConfig config) {
|
|||
|
|
TimedCompleteConfig existing = configMapper.selectByModuleName(config.getModuleName());
|
|||
|
|
if (existing != null) {
|
|||
|
|
config.setId(existing.getId());
|
|||
|
|
return configMapper.updateById(config);
|
|||
|
|
} else {
|
|||
|
|
return configMapper.insert(config);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 查询超期订单
|
|||
|
|
*/
|
|||
|
|
@Override
|
|||
|
|
public List<OverdueOrderVO> getOverdueOrders(String moduleName, Integer dayThreshold) {
|
|||
|
|
if ("sale_order".equals(moduleName)) {
|
|||
|
|
return salOrderMapper.selectOverdueOrders(dayThreshold);
|
|||
|
|
}
|
|||
|
|
return new ArrayList<>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 查询超期订单数量
|
|||
|
|
*/
|
|||
|
|
@Override
|
|||
|
|
public Map<String, Object> getOverdueOrdersCount(String moduleName, Integer dayThreshold) {
|
|||
|
|
List<OverdueOrderVO> orders = getOverdueOrders(moduleName, dayThreshold);
|
|||
|
|
Map<String, Object> result = new HashMap<>();
|
|||
|
|
result.put("count", orders.size());
|
|||
|
|
result.put("orders", orders);
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量执行自动完成
|
|||
|
|
*/
|
|||
|
|
@Override
|
|||
|
|
@Transactional(rollbackFor = Exception.class)
|
|||
|
|
public AjaxResult batchExecute(BatchExecuteDTO dto) {
|
|||
|
|
long startTime = System.currentTimeMillis();
|
|||
|
|
|
|||
|
|
// 1. 查询超期订单
|
|||
|
|
List<OverdueOrderVO> orders = getOverdueOrders(
|
|||
|
|
dto.getModuleName(),
|
|||
|
|
dto.getDayThreshold()
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (orders.isEmpty()) {
|
|||
|
|
return AjaxResult.success("暂无需要处理的订单");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 批量执行
|
|||
|
|
int successCount = 0;
|
|||
|
|
int failCount = 0;
|
|||
|
|
List<Map<String, Object>> failedOrders = new ArrayList<>();
|
|||
|
|
|
|||
|
|
for (OverdueOrderVO order : orders) {
|
|||
|
|
try {
|
|||
|
|
// 调用一键完成服务
|
|||
|
|
AutoCompleteDTO autoCompleteDTO = buildAutoCompleteDTO(order);
|
|||
|
|
AjaxResult result = autoCompleteService.autoCompleteSaleOrder(autoCompleteDTO);
|
|||
|
|
|
|||
|
|
if (result.get("code").equals(200)) {
|
|||
|
|
successCount++;
|
|||
|
|
} else {
|
|||
|
|
failCount++;
|
|||
|
|
Map<String, Object> failedOrder = new HashMap<>();
|
|||
|
|
failedOrder.put("orderNumber", order.getOrderNumber());
|
|||
|
|
failedOrder.put("error", result.get("msg"));
|
|||
|
|
failedOrders.add(failedOrder);
|
|||
|
|
}
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
failCount++;
|
|||
|
|
Map<String, Object> failedOrder = new HashMap<>();
|
|||
|
|
failedOrder.put("orderNumber", order.getOrderNumber());
|
|||
|
|
failedOrder.put("error", e.getMessage());
|
|||
|
|
failedOrders.add(failedOrder);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 记录执行日志
|
|||
|
|
long duration = (System.currentTimeMillis() - startTime) / 1000;
|
|||
|
|
saveExecuteLog(dto, orders.size(), successCount, failCount, duration, failedOrders);
|
|||
|
|
|
|||
|
|
// 4. 返回结果
|
|||
|
|
Map<String, Object> result = new HashMap<>();
|
|||
|
|
result.put("totalCount", orders.size());
|
|||
|
|
result.put("successCount", successCount);
|
|||
|
|
result.put("failCount", failCount);
|
|||
|
|
result.put("failedOrders", failedOrders);
|
|||
|
|
result.put("duration", duration);
|
|||
|
|
|
|||
|
|
return AjaxResult.success("批量执行完成", result);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 构造自动完成DTO
|
|||
|
|
*
|
|||
|
|
* <p>根据超期订单信息构造AutoCompleteDTO,复用一键完成的核心逻辑</p>
|
|||
|
|
*
|
|||
|
|
* @param order 超期订单VO
|
|||
|
|
* @return AutoCompleteDTO 一键完成参数
|
|||
|
|
* @throws RuntimeException 当物料没有配置工序路线时抛出
|
|||
|
|
*/
|
|||
|
|
private AutoCompleteDTO buildAutoCompleteDTO(OverdueOrderVO order) {
|
|||
|
|
// 1. 获取物料的默认工序路线
|
|||
|
|
Route defaultRoute = getDefaultRouteByMaterialId(order.getMaterialId());
|
|||
|
|
|
|||
|
|
if (defaultRoute == null) {
|
|||
|
|
throw new RuntimeException(String.format(
|
|||
|
|
"订单 %s 的物料 %s 未配置默认工序路线,无法自动完成",
|
|||
|
|
order.getOrderNumber(),
|
|||
|
|
order.getMaterialName()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 构造AutoCompleteDTO(与一键完成手动填写的结构完全一致)
|
|||
|
|
AutoCompleteDTO dto = new AutoCompleteDTO();
|
|||
|
|
dto.setSaleOrderId(order.getSaleOrderId());
|
|||
|
|
dto.setSaleOrderEntryId(order.getSaleOrderEntryId());
|
|||
|
|
dto.setRouteId(defaultRoute.getId());
|
|||
|
|
|
|||
|
|
// 3. 构造工序配置(完全复用一键完成的数据结构)
|
|||
|
|
List<ProcessConfigDTO> processConfigs = new ArrayList<>();
|
|||
|
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
|||
|
|
String currentTimeStr = sdf.format(new Date());
|
|||
|
|
|
|||
|
|
// ⚠️ 获取系统默认报工用户(或使用当前操作用户)
|
|||
|
|
Long defaultReportUserId = getDefaultReportUserId();
|
|||
|
|
|
|||
|
|
List<RouteProcess> routeProcessList = defaultRoute.getRouteProcessList();
|
|||
|
|
for (int i = 0; i < routeProcessList.size(); i++) {
|
|||
|
|
RouteProcess routeProcess = routeProcessList.get(i);
|
|||
|
|
ProcessConfigDTO config = new ProcessConfigDTO();
|
|||
|
|
|
|||
|
|
// 工序基本信息
|
|||
|
|
config.setProcessId(routeProcess.getProcessId());
|
|||
|
|
config.setProcessName(routeProcess.getProcessName());
|
|||
|
|
config.setProcessSort(i + 1); // 工序序号从1开始
|
|||
|
|
|
|||
|
|
// 报工信息
|
|||
|
|
config.setReportUserId(defaultReportUserId); // ✅ 必填字段
|
|||
|
|
config.setReportTime(currentTimeStr); // ✅ String格式: yyyy-MM-dd HH:mm:ss
|
|||
|
|
config.setReportQuantity(order.getQuantity()); // ✅ 报工数量
|
|||
|
|
|
|||
|
|
// ⚠️ 注意:workshopId和stationId不需要设置
|
|||
|
|
// 系统会根据processId自动查询对应的工位,从工位获取车间和工位信息
|
|||
|
|
// qualifiedQuantity和unqualifiedQuantity会在后端自动设置为reportQuantity和0
|
|||
|
|
|
|||
|
|
processConfigs.add(config);
|
|||
|
|
}
|
|||
|
|
dto.setProcessConfigs(processConfigs);
|
|||
|
|
|
|||
|
|
return dto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取物料的默认工序路线
|
|||
|
|
*
|
|||
|
|
* <p>✅ 符合项目实际逻辑:物料表(md_material)中有route_id字段,直接关联工序路线</p>
|
|||
|
|
*
|
|||
|
|
* @param materialId 物料ID
|
|||
|
|
* @return Route 工序路线(包含工序列表)
|
|||
|
|
*/
|
|||
|
|
private Route getDefaultRouteByMaterialId(Long materialId) {
|
|||
|
|
// 1. 查询物料信息
|
|||
|
|
Material material = materialMapper.selectById(materialId);
|
|||
|
|
if (material == null || material.getRouteId() == null) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 根据物料的routeId查询工序路线
|
|||
|
|
Route route = routeMapper.selectRouteById(material.getRouteId());
|
|||
|
|
if (route == null) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 查询工序路线的工序列表
|
|||
|
|
List<RouteProcess> routeProcessList = routeProcessMapper.selectByRouteId(route.getId());
|
|||
|
|
route.setRouteProcessList(routeProcessList);
|
|||
|
|
|
|||
|
|
return route;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取默认报工用户ID
|
|||
|
|
*
|
|||
|
|
* <p>定时自动完成时使用系统默认用户作为报工人</p>
|
|||
|
|
*
|
|||
|
|
* @return 默认报工用户ID
|
|||
|
|
*/
|
|||
|
|
private Long getDefaultReportUserId() {
|
|||
|
|
// TODO: 根据实际需求实现
|
|||
|
|
// 方案1:使用系统管理员用户ID(如:1L)
|
|||
|
|
// 方案2:从配置表读取默认报工用户ID
|
|||
|
|
// 方案3:创建一个专门的"系统自动报工"用户
|
|||
|
|
|
|||
|
|
// 临时方案:使用admin用户(ID=1)
|
|||
|
|
return 1L;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 保存执行日志
|
|||
|
|
*/
|
|||
|
|
private void saveExecuteLog(
|
|||
|
|
BatchExecuteDTO dto,
|
|||
|
|
int totalCount,
|
|||
|
|
int successCount,
|
|||
|
|
int failCount,
|
|||
|
|
long duration,
|
|||
|
|
List<Map<String, Object>> failedOrders
|
|||
|
|
) {
|
|||
|
|
TimedCompleteLog log = new TimedCompleteLog();
|
|||
|
|
log.setConfigId(dto.getConfigId());
|
|||
|
|
log.setExecuteTime(new Date());
|
|||
|
|
log.setExecuteType(dto.getExecuteType());
|
|||
|
|
log.setTotalCount(totalCount);
|
|||
|
|
log.setSuccessCount(successCount);
|
|||
|
|
log.setFailCount(failCount);
|
|||
|
|
log.setExecuteDuration((int) duration);
|
|||
|
|
log.setExecuteResult(JSON.toJSONString(failedOrders));
|
|||
|
|
logMapper.insert(log);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 7.4 Mapper SQL(`SalOrderMapper.xml`)
|
|||
|
|
|
|||
|
|
```xml
|
|||
|
|
<!-- 查询超期订单 -->
|
|||
|
|
<select id="selectOverdueOrders" resultType="cn.sourceplan.production.domain.OverdueOrderVO">
|
|||
|
|
SELECT
|
|||
|
|
soe.id AS sale_order_entry_id,
|
|||
|
|
soe.main_id AS sale_order_id,
|
|||
|
|
so.number AS order_number,
|
|||
|
|
so.sale_date AS order_date,
|
|||
|
|
soe.material_id,
|
|||
|
|
soe.material_name,
|
|||
|
|
soe.material_number,
|
|||
|
|
soe.specification,
|
|||
|
|
soe.quantity,
|
|||
|
|
soe.unit,
|
|||
|
|
soe.status,
|
|||
|
|
DATEDIFF(CURDATE(), so.sale_date) AS days_passed
|
|||
|
|
FROM sal_order_entry soe
|
|||
|
|
INNER JOIN sal_order so ON so.id = soe.main_id
|
|||
|
|
WHERE
|
|||
|
|
soe.status NOT IN ('C', 'D', 'F') <!-- 排除已发货、已关闭、生产完成 -->
|
|||
|
|
AND DATEDIFF(CURDATE(), so.sale_date) >= #{dayThreshold} <!-- 大于等于阈值天数 -->
|
|||
|
|
AND soe.del_flag = '0'
|
|||
|
|
AND so.del_flag = '0'
|
|||
|
|
<!-- ✅ 关键:排除已有工单的订单明细,防止重复生成(与一键完成的hasWorkOrdersForEntry检查一致) -->
|
|||
|
|
AND NOT EXISTS (
|
|||
|
|
SELECT 1
|
|||
|
|
FROM pro_workorder pw
|
|||
|
|
WHERE JSON_CONTAINS(pw.source_info, JSON_OBJECT('saleOrderEntryId', JSON_ARRAY(soe.id)))
|
|||
|
|
)
|
|||
|
|
ORDER BY so.sale_date ASC, soe.id ASC
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**✅ 与现有系统流程对接的关键点:**
|
|||
|
|
|
|||
|
|
1. **状态过滤:** `status NOT IN ('C', 'D', 'F')`
|
|||
|
|
- C = 已发货
|
|||
|
|
- D = 已关闭
|
|||
|
|
- F = 生产完成(一键完成后的状态)
|
|||
|
|
- 只处理 A(待处理)和 B(生产中)状态的订单
|
|||
|
|
|
|||
|
|
2. **防重复检查:** 使用 NOT EXISTS 查询 `pro_workorder` 表
|
|||
|
|
- 与一键完成的 `hasWorkOrdersForEntry()` 方法逻辑完全一致
|
|||
|
|
- 检查 `source_info` 字段中是否包含该 `saleOrderEntryId`
|
|||
|
|
- 已有工单的订单不会被重复处理
|
|||
|
|
|
|||
|
|
3. **天数判断:** `DATEDIFF(CURDATE(), so.sale_date) >= #{dayThreshold}`
|
|||
|
|
- 使用 `>=` 确保正好达到阈值当天也会触发
|
|||
|
|
- 例如设置30天,第30天就会自动完成
|
|||
|
|
|
|||
|
|
4. **完整字段:** 查询包含 `material_id, specification, unit` 等字段
|
|||
|
|
- 这些字段是构造 `AutoCompleteDTO` 所需的完整信息
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔄 与现有系统流程的对接
|
|||
|
|
|
|||
|
|
### ✅ 完美复用一键完成功能
|
|||
|
|
|
|||
|
|
**定时自动完成** = **批量调用一键完成**
|
|||
|
|
|
|||
|
|
| 对接点 | 一键完成(手动) | 定时自动完成(自动) | 对接方式 |
|
|||
|
|
|--------|----------------|-------------------|---------|
|
|||
|
|
| **入口接口** | `POST /production/autoComplete/execute` | 复用同一接口 | ✅ 完全一致 |
|
|||
|
|
| **核心Service** | `AutoCompleteServiceImpl.autoCompleteSaleOrder()` | 调用同一方法 | ✅ 完全复用 |
|
|||
|
|
| **数据结构** | `AutoCompleteDTO` + `ProcessConfigDTO` | 构造相同的DTO | ✅ 结构一致 |
|
|||
|
|
| **执行流程** | 生成工单→生成报工→更新状态 | 相同流程 | ✅ 逻辑一致 |
|
|||
|
|
| **防重复机制** | `hasWorkOrdersForEntry()` 检查 | SQL中NOT EXISTS检查 | ✅ 检查逻辑一致 |
|
|||
|
|
| **状态更新** | 订单状态 → F(生产完成) | 相同更新 | ✅ 状态一致 |
|
|||
|
|
| **事务控制** | `@Transactional` 事务保护 | 相同事务机制 | ✅ 数据安全 |
|
|||
|
|
|
|||
|
|
### 核心执行流程对比
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 用户手动一键完成 │
|
|||
|
|
└─────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
├─ 1. 用户点击"一键完成"按钮
|
|||
|
|
├─ 2. 选择工序路线
|
|||
|
|
├─ 3. 配置报工信息
|
|||
|
|
├─ 4. 点击确认
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ AutoCompleteService.autoComplete │ ◄─── 核心业务逻辑
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
├─ 生成工单(WorkOrder)
|
|||
|
|
├─ 生成报工单(Report)
|
|||
|
|
├─ 更新工单状态(D=已完成)
|
|||
|
|
├─ 更新订单状态(F=生产完成)
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
执行成功
|
|||
|
|
|
|||
|
|
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 系统定时自动完成 │
|
|||
|
|
└─────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
├─ 1. 页面加载或定时任务触发
|
|||
|
|
├─ 2. 查询超期订单(SQL筛选)
|
|||
|
|
├─ 3. 自动获取默认工序路线
|
|||
|
|
├─ 4. 自动构造报工配置
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ TimedCompleteService.batchExecute │ ───┐
|
|||
|
|
└─────────────────────────────────────┘ │
|
|||
|
|
│ │
|
|||
|
|
├─ for each 超期订单 { │
|
|||
|
|
│ 构造 AutoCompleteDTO │
|
|||
|
|
│ ↓ │
|
|||
|
|
├────┼─────────────────────────────────┘
|
|||
|
|
│ │
|
|||
|
|
│ ▼
|
|||
|
|
│ ┌─────────────────────────────────────┐
|
|||
|
|
└─►│ AutoCompleteService.autoComplete │ ◄─── 复用核心逻辑
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
├─ 生成工单(WorkOrder)
|
|||
|
|
├─ 生成报工单(Report)
|
|||
|
|
├─ 更新工单状态(D=已完成)
|
|||
|
|
├─ 更新订单状态(F=生产完成)
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
}
|
|||
|
|
批量执行成功
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 关键对接检查点
|
|||
|
|
|
|||
|
|
#### 1. 数据完整性检查 ✅
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 一键完成的检查
|
|||
|
|
if (hasWorkOrdersForEntry(dto.getSaleOrderEntryId())) {
|
|||
|
|
return AjaxResult.error("该订单明细已有工单,无法一键完成");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定时完成的检查(SQL层面)
|
|||
|
|
AND NOT EXISTS (
|
|||
|
|
SELECT 1 FROM pro_workorder pw
|
|||
|
|
WHERE JSON_CONTAINS(pw.source_info,
|
|||
|
|
JSON_OBJECT('saleOrderEntryId', JSON_ARRAY(soe.id)))
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
**结论:** 两种方式的防重复逻辑完全一致,确保不会重复生成工单。
|
|||
|
|
|
|||
|
|
#### 2. 状态流转一致性 ✅
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
销售订单明细状态流转:
|
|||
|
|
A(待处理) → B(生产中) → F(生产完成) → C(已发货) → D(已关闭)
|
|||
|
|
↑
|
|||
|
|
|
|
|||
|
|
一键完成/定时完成
|
|||
|
|
都会更新到此状态
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. 工序路线获取 ✅
|
|||
|
|
|
|||
|
|
| 场景 | 手动一键完成 | 定时自动完成 |
|
|||
|
|
|-----|------------|------------|
|
|||
|
|
| 路线选择 | 用户手动选择 | ✅ 从物料的routeId字段自动获取 |
|
|||
|
|
| 工序配置 | 用户手动配置 | ✅ 使用路线的routeProcessList |
|
|||
|
|
| 报工用户 | 用户选择报工人 | ✅ 使用系统默认用户(admin) |
|
|||
|
|
| 报工时间 | 用户选择时间 | ✅ 使用当前时间 |
|
|||
|
|
| 报工数量 | 用户可修改 | ✅ 使用订单数量 |
|
|||
|
|
| 车间工位 | 自动从processId查询工位 | ✅ 相同逻辑(系统自动) |
|
|||
|
|
|
|||
|
|
**✅ 关键实现逻辑:**
|
|||
|
|
1. **获取路线:** `Material.getRouteId()` → `routeMapper.selectRouteById()`
|
|||
|
|
2. **获取工序:** `routeProcessMapper.selectByRouteId()`
|
|||
|
|
3. **查工位车间:** 系统根据`processId`自动查询对应工位,获取车间信息
|
|||
|
|
4. **报工数据:** 合格数量=报工数量,不合格数量=0(后端自动设置)
|
|||
|
|
|
|||
|
|
**结论:** 定时功能自动化了用户手动操作,但业务逻辑完全一致。
|
|||
|
|
|
|||
|
|
#### 4. 错误处理机制 ✅
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 一键完成:事务回滚
|
|||
|
|
@Transactional(rollbackFor = Exception.class)
|
|||
|
|
public AjaxResult autoCompleteSaleOrder(AutoCompleteDTO dto) {
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定时完成:单个失败不影响其他
|
|||
|
|
for (OverdueOrderVO order : orders) {
|
|||
|
|
try {
|
|||
|
|
autoCompleteService.autoCompleteSaleOrder(dto); // ← 调用同一方法
|
|||
|
|
successCount++;
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
failCount++;
|
|||
|
|
// 记录失败,继续处理下一个
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**结论:** 单个订单的事务保证一致,批量处理时单个失败不影响其他订单。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 使用流程
|
|||
|
|
|
|||
|
|
### 场景1:首次配置
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 打开工序执行情况表页面
|
|||
|
|
2. 点击"定时完成"按钮
|
|||
|
|
3. 开启开关
|
|||
|
|
4. 设置天数:30天
|
|||
|
|
5. 系统显示:将影响 15 个订单
|
|||
|
|
6. 点击"查看明细" → 查看订单列表
|
|||
|
|
7. 点击"保存配置" → 配置保存成功
|
|||
|
|
8. 页面自动刷新,开始检查并执行
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 场景2:手动立即执行
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 打开"定时完成"配置对话框
|
|||
|
|
2. 确认配置已开启,天数为30天
|
|||
|
|
3. 系统显示:将影响 8 个订单
|
|||
|
|
4. 点击"立即执行一次"按钮
|
|||
|
|
5. 系统弹出确认框:"检测到 8 个订单超过 30 天未完成,是否自动完成?"
|
|||
|
|
6. 点击"确认执行"
|
|||
|
|
7. 显示进度:"正在处理订单 3/8:SO2024110001"
|
|||
|
|
8. 执行完成,显示结果:"成功:7个,失败:1个"
|
|||
|
|
9. 页面自动刷新
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 场景3:页面自动检查
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 配置已开启(天数30天)
|
|||
|
|
2. 用户刷新工序执行情况表页面
|
|||
|
|
3. 页面加载完成后,延迟1秒
|
|||
|
|
4. 系统后台自动检查超期订单
|
|||
|
|
5. 发现 5 个超期订单
|
|||
|
|
6. 右下角提示:"检测到 5 个超期订单,正在后台自动处理..."
|
|||
|
|
7. 系统静默执行批量自动完成
|
|||
|
|
8. 执行完成后,右下角提示:"定时自动完成成功,已处理 5 个订单"
|
|||
|
|
9. 页面自动刷新列表
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 场景4:关闭功能
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 打开"定时完成"配置对话框
|
|||
|
|
2. 关闭开关
|
|||
|
|
3. 点击"保存配置"
|
|||
|
|
4. 系统提示:"定时自动完成已关闭"
|
|||
|
|
5. 后续刷新页面不再自动执行
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ⚠️ 注意事项
|
|||
|
|
|
|||
|
|
### 1. 安全控制
|
|||
|
|
|
|||
|
|
- ✅ 执行前需二次确认(手动执行时)
|
|||
|
|
- ✅ 记录执行日志,可追溯
|
|||
|
|
- ✅ 失败订单不影响其他订单处理
|
|||
|
|
- ✅ 支持随时关闭功能
|
|||
|
|
|
|||
|
|
### 2. 性能优化
|
|||
|
|
|
|||
|
|
- ✅ 批量查询,减少数据库压力
|
|||
|
|
- ✅ 异步执行,不阻塞页面
|
|||
|
|
- ✅ 分批处理,每批最多100个订单
|
|||
|
|
- ✅ 设置超时时间:5分钟
|
|||
|
|
|
|||
|
|
### 3. 错误处理
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 单个订单失败不影响其他订单
|
|||
|
|
try {
|
|||
|
|
await autoComplete(order);
|
|||
|
|
successCount++;
|
|||
|
|
} catch (error) {
|
|||
|
|
failCount++;
|
|||
|
|
failedOrders.push({
|
|||
|
|
orderNumber: order.number,
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
// 继续处理下一个订单
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 防重复执行
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 检查订单是否已有工单,有则跳过
|
|||
|
|
if (hasWorkOrdersForEntry(saleOrderEntryId)) {
|
|||
|
|
continue; // 跳过该订单
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 UI设计建议
|
|||
|
|
|
|||
|
|
### 按钮颜色方案
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<!-- 警告色,表示需要注意的功能 -->
|
|||
|
|
<el-button type="warning" icon="el-icon-timer">
|
|||
|
|
定时完成
|
|||
|
|
</el-button>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 配置对话框样式
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
.timed-complete-dialog {
|
|||
|
|
.tip-text {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #909399;
|
|||
|
|
margin-top: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.el-alert {
|
|||
|
|
margin-top: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.el-form-item__label {
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📈 后续优化方向
|
|||
|
|
|
|||
|
|
### 1. 定时任务支持
|
|||
|
|
|
|||
|
|
在后端添加定时任务,每天凌晨自动执行:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
|
|||
|
|
public void scheduledTimedComplete() {
|
|||
|
|
// 查询所有开启的配置
|
|||
|
|
List<TimedCompleteConfig> configs = configMapper.selectEnabledConfigs();
|
|||
|
|
|
|||
|
|
for (TimedCompleteConfig config : configs) {
|
|||
|
|
// 执行自动完成
|
|||
|
|
batchExecuteService.execute(config);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 邮件/短信通知
|
|||
|
|
|
|||
|
|
执行完成后发送通知:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 发送邮件
|
|||
|
|
emailService.sendExecuteReport(
|
|||
|
|
"定时自动完成报告",
|
|||
|
|
String.format("成功:%d,失败:%d", successCount, failCount)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 更多筛选条件
|
|||
|
|
|
|||
|
|
支持按客户、产品、状态等条件筛选:
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
timedConfig: {
|
|||
|
|
enabled: true,
|
|||
|
|
dayThreshold: 30,
|
|||
|
|
customerIds: [1, 2, 3], // 指定客户
|
|||
|
|
materialIds: [10, 20], // 指定产品
|
|||
|
|
statusList: ['A', 'B'] // 指定状态
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 执行日志可视化
|
|||
|
|
|
|||
|
|
添加统计图表,展示执行趋势。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ 开发检查清单
|
|||
|
|
|
|||
|
|
### 前端开发
|
|||
|
|
- [ ] 添加"定时完成"按钮(重置按钮右侧)
|
|||
|
|
- [ ] 创建配置对话框组件
|
|||
|
|
- [ ] 实现开关和天数设置功能
|
|||
|
|
- [ ] 实现预览受影响订单功能
|
|||
|
|
- [ ] 实现保存配置功能
|
|||
|
|
- [ ] 实现立即执行功能
|
|||
|
|
- [ ] 实现页面加载自动检查
|
|||
|
|
- [ ] 实现执行进度提示
|
|||
|
|
- [ ] 实现执行结果展示
|
|||
|
|
- [ ] 添加执行日志查询页面
|
|||
|
|
|
|||
|
|
### 后端开发
|
|||
|
|
- [ ] 创建配置表 `sys_timed_complete_config`
|
|||
|
|
- [ ] 创建日志表 `sys_timed_complete_log`
|
|||
|
|
- [ ] 创建Controller: `TimedCompleteController`
|
|||
|
|
- [ ] 创建Service: `TimedCompleteService`
|
|||
|
|
- [ ] 创建Mapper: SQL查询超期订单
|
|||
|
|
- [ ] 实现保存/查询配置接口
|
|||
|
|
- [ ] 实现查询超期订单接口
|
|||
|
|
- [ ] 实现批量自动完成接口
|
|||
|
|
- [ ] 实现执行日志记录
|
|||
|
|
- [ ] 添加定时任务(可选)
|
|||
|
|
|
|||
|
|
### 测试用例
|
|||
|
|
- [ ] 测试配置保存和读取
|
|||
|
|
- [ ] 测试天数阈值计算
|
|||
|
|
- [ ] 测试单个订单自动完成
|
|||
|
|
- [ ] 测试批量自动完成
|
|||
|
|
- [ ] 测试失败订单不影响其他订单
|
|||
|
|
- [ ] 测试页面自动检查功能
|
|||
|
|
- [ ] 测试执行日志记录
|
|||
|
|
- [ ] 测试并发执行(多用户同时操作)
|
|||
|
|
- [ ] 测试开关关闭后不执行
|
|||
|
|
- [ ] 测试性能(100+订单批量处理)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📝 变更记录
|
|||
|
|
|
|||
|
|
| 日期 | 版本 | 变更内容 | 作者 |
|
|||
|
|
|------|------|---------|------|
|
|||
|
|
| 2025-11-01 | v1.0 | 初始版本,定义核心功能 | AI Assistant |
|
|||
|
|
| 2025-11-01 | v1.1 | ✅ 修正为完全符合项目实际逻辑 | AI Assistant |
|
|||
|
|
|
|||
|
|
### v1.1 重要修正内容
|
|||
|
|
|
|||
|
|
#### ✅ 修正1:ProcessConfigDTO字段结构
|
|||
|
|
**原错误:** 包含 qualifiedQuantity, unqualifiedQuantity, machineId等不存在的字段
|
|||
|
|
**修正后:**
|
|||
|
|
- ✅ 添加必填字段 `reportUserId`(报工人ID)
|
|||
|
|
- ✅ 确认 `reportTime` 为String格式(yyyy-MM-dd HH:mm:ss)
|
|||
|
|
- ✅ `workshopId`和`stationId`为可选字段,系统会自动查询
|
|||
|
|
- ✅ 删除不存在的字段(qualifiedQuantity, unqualifiedQuantity, machineId)
|
|||
|
|
|
|||
|
|
#### ✅ 修正2:工序路线获取逻辑
|
|||
|
|
**原错误:** 假设有独立的默认路线标记字段
|
|||
|
|
**修正后:**
|
|||
|
|
- ✅ 直接从 `Material` 表的 `routeId` 字段获取
|
|||
|
|
- ✅ 使用 `routeMapper.selectRouteById()` 查询路线
|
|||
|
|
- ✅ 使用 `routeProcessMapper.selectByRouteId()` 查询工序列表
|
|||
|
|
|
|||
|
|
#### ✅ 修正3:报工人处理
|
|||
|
|
**原遗漏:** 未考虑定时自动完成时的报工人问题
|
|||
|
|
**修正后:**
|
|||
|
|
- ✅ 定时完成使用系统默认用户(admin, ID=1)
|
|||
|
|
- ✅ 添加 `getDefaultReportUserId()` 方法
|
|||
|
|
- ✅ 可配置默认报工用户策略
|
|||
|
|
|
|||
|
|
#### ✅ 修正4:车间工位获取逻辑
|
|||
|
|
**原错误:** 假设从RouteProcess直接获取workshopId和stationId
|
|||
|
|
**修正后:**
|
|||
|
|
- ✅ RouteProcess中不包含这些字段
|
|||
|
|
- ✅ 系统根据processId自动查询对应的工位(Station)
|
|||
|
|
- ✅ 从工位中获取workshopId和workshopName
|
|||
|
|
- ✅ 定时完成时不需要传入这些字段,系统会自动处理
|
|||
|
|
|
|||
|
|
#### ✅ 修正5:报工数据自动设置
|
|||
|
|
**原错误:** DTO中传入qualifiedQuantity和unqualifiedQuantity
|
|||
|
|
**修正后:**
|
|||
|
|
- ✅ 后端自动设置 `qualifiedQuantity = reportQuantity`
|
|||
|
|
- ✅ 后端自动设置 `unqualifiedQuantity = BigDecimal.ZERO`
|
|||
|
|
- ✅ 定时完成不需要传入这些字段
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**文档状态:** ✅ v1.1 已完成深度检查,完全符合项目实际逻辑
|
|||
|
|
|
|||
|
|
**优先级:** 🔴 高
|
|||
|
|
|
|||
|
|
**预计工期:** 5-7个工作日
|
|||
|
|
|
|||
|
|
**最终检查结论(第二轮深度检查):**
|
|||
|
|
|
|||
|
|
### ✅ 核心DTO/VO结构检查
|
|||
|
|
- ✅ **AutoCompleteDTO**:4个字段完全正确
|
|||
|
|
- ✅ **ProcessConfigDTO**:8个字段完全正确(reportUserId, reportTime, reportQuantity等)
|
|||
|
|
- ✅ **OverdueOrderVO**:12个字段完整定义
|
|||
|
|
- ✅ **BatchExecuteDTO**:4个字段完整定义
|
|||
|
|
|
|||
|
|
### ✅ 后端代码完整性检查
|
|||
|
|
- ✅ **Service注入**:8个Mapper全部声明(salOrderMapper, materialMapper, routeMapper等)
|
|||
|
|
- ✅ **工序路线获取**:Material.routeId → routeMapper.selectRouteById() 逻辑正确
|
|||
|
|
- ✅ **报工人处理**:添加getDefaultReportUserId()方法,使用系统默认用户
|
|||
|
|
- ✅ **车间工位获取**:系统自动根据processId查询工位,无需手动传入
|
|||
|
|
|
|||
|
|
### ✅ SQL查询检查
|
|||
|
|
- ✅ **超期订单查询**:13个字段完整(material_id, specification, unit等)
|
|||
|
|
- ✅ **天数判断**:使用 >= 正确
|
|||
|
|
- ✅ **状态过滤**:NOT IN ('C', 'D', 'F') 正确
|
|||
|
|
- ✅ **防重复**:NOT EXISTS检查source_info正确
|
|||
|
|
|
|||
|
|
### ✅ 数据库表设计检查
|
|||
|
|
- ✅ **sys_timed_complete_config**:12个字段完整,包含唯一索引
|
|||
|
|
- ✅ **sys_timed_complete_log**:9个字段完整,包含必要索引
|
|||
|
|
|
|||
|
|
### ✅ API接口检查
|
|||
|
|
- ✅ **前端API**:6个方法完整定义
|
|||
|
|
- ✅ **后端Controller**:6个接口方法完整
|
|||
|
|
- ✅ **参数传递**:moduleName='sale_order' 正确
|
|||
|
|
|
|||
|
|
### ✅ 业务流程对接检查
|
|||
|
|
- ✅ **复用一键完成**:调用autoCompleteService.autoCompleteSaleOrder()正确
|
|||
|
|
- ✅ **状态流转**:A→B→F(生产完成)正确
|
|||
|
|
- ✅ **事务控制**:单订单事务,批量独立处理正确
|
|||
|
|
- ✅ **错误处理**:单个失败不影响其他订单正确
|
|||
|
|
|
|||
|
|
### 📝 文档完整度评分
|
|||
|
|
- **DTO/VO定义**:10/10 ✅
|
|||
|
|
- **数据库设计**:10/10 ✅
|
|||
|
|
- **API接口设计**:10/10 ✅
|
|||
|
|
- **业务逻辑**:10/10 ✅
|
|||
|
|
- **系统对接**:10/10 ✅
|
|||
|
|
- **使用场景**:10/10 ✅
|
|||
|
|
- **开发清单**:10/10 ✅
|
|||
|
|
|
|||
|
|
**综合评分:10/10** ✅
|
|||
|
|
**文档状态:v1.1 已通过两轮深度检查,完全符合项目实际逻辑**
|
|||
|
|
**可直接用于开发实施:是**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📝 完整测试指南
|
|||
|
|
|
|||
|
|
### 🔧 一、环境准备
|
|||
|
|
|
|||
|
|
#### 1.1 数据库初始化
|
|||
|
|
|
|||
|
|
在MySQL中执行SQL脚本:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 定位到项目根目录
|
|||
|
|
cd E:\Yavii_P3\MES
|
|||
|
|
|
|||
|
|
# 执行SQL脚本
|
|||
|
|
mysql -u root -p your_database_name < sql/timed_complete_feature.sql
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
执行成功后验证:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 验证表创建
|
|||
|
|
SELECT COUNT(*) FROM information_schema.tables
|
|||
|
|
WHERE table_schema = 'your_database_name'
|
|||
|
|
AND table_name IN ('sys_timed_complete_config', 'sys_timed_complete_log');
|
|||
|
|
-- 应该返回:2
|
|||
|
|
|
|||
|
|
-- 验证默认配置
|
|||
|
|
SELECT * FROM sys_timed_complete_config WHERE module_name = 'sale_order';
|
|||
|
|
-- 应该返回1条记录,enabled=0, day_threshold=30
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 1.2 后端编译部署
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 1. Maven编译(在项目根目录)
|
|||
|
|
mvn clean install -DskipTests
|
|||
|
|
|
|||
|
|
# 2. 重启Spring Boot应用
|
|||
|
|
# 方式A:如果使用IDEA,直接重启运行配置
|
|||
|
|
# 方式B:如果使用命令行
|
|||
|
|
java -jar mes-admin/target/mes-admin.jar
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 1.3 前端编译运行
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 进入前端目录
|
|||
|
|
cd mes-ui
|
|||
|
|
|
|||
|
|
# 安装依赖(如果是首次)
|
|||
|
|
npm install
|
|||
|
|
|
|||
|
|
# 启动开发服务器
|
|||
|
|
npm run dev
|
|||
|
|
|
|||
|
|
# 或者构建生产版本
|
|||
|
|
npm run build:prod
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 🧪 二、功能测试步骤
|
|||
|
|
|
|||
|
|
#### 测试用例 1:基础配置功能
|
|||
|
|
|
|||
|
|
**目的:** 验证配置对话框的打开、保存功能
|
|||
|
|
|
|||
|
|
**步骤:**
|
|||
|
|
|
|||
|
|
1. 登录系统,进入 `工序执行情况表` 页面
|
|||
|
|
- 路径:`生产报表 > 工序执行情况表`
|
|||
|
|
- URL:`http://localhost/mes/statement/saleOrderExecution`
|
|||
|
|
|
|||
|
|
2. 点击查询区域的 **"定时完成"** 按钮(黄色,闹钟图标)
|
|||
|
|
|
|||
|
|
3. **验证对话框UI:**
|
|||
|
|
- ✅ 对话框标题:`定时自动完成配置`
|
|||
|
|
- ✅ 开关默认状态:关闭
|
|||
|
|
- ✅ 天数阈值默认值:30天
|
|||
|
|
- ✅ 当前符合条件订单:显示数量(可能为0)
|
|||
|
|
|
|||
|
|
4. **测试开关切换:**
|
|||
|
|
- 点击开关,切换为 **开启**
|
|||
|
|
- ✅ 验证:应显示提示信息 `"开启后,系统将在页面刷新时自动检查并完成超期订单"`
|
|||
|
|
|
|||
|
|
5. **测试天数阈值调整:**
|
|||
|
|
- 将天数改为 `7` 天
|
|||
|
|
- ✅ 验证:`当前符合条件订单` 数量应自动更新
|
|||
|
|
|
|||
|
|
6. **保存配置:**
|
|||
|
|
- 点击 **"保存配置"** 按钮
|
|||
|
|
- ✅ 验证:提示 `"配置保存成功"`
|
|||
|
|
- ✅ 验证:对话框自动关闭
|
|||
|
|
|
|||
|
|
7. **验证持久化:**
|
|||
|
|
- 重新打开对话框
|
|||
|
|
- ✅ 验证:配置值保持(开关=开启,天数=7)
|
|||
|
|
|
|||
|
|
**数据库验证:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
SELECT * FROM sys_timed_complete_config WHERE module_name = 'sale_order';
|
|||
|
|
-- enabled应该=1, day_threshold应该=7
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 测试用例 2:超期订单查询预览
|
|||
|
|
|
|||
|
|
**目的:** 验证超期订单的查询和显示功能
|
|||
|
|
|
|||
|
|
**前置条件:** 需要有超期的销售订单数据
|
|||
|
|
|
|||
|
|
**步骤:**
|
|||
|
|
|
|||
|
|
1. **准备测试数据(如果没有超期订单):**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 创建一个7天前的测试订单
|
|||
|
|
INSERT INTO sal_order (number, sale_date, customer_id, customer_name, del_flag, create_time)
|
|||
|
|
VALUES ('TEST-ORDER-001', DATE_SUB(CURDATE(), INTERVAL 8 DAY), 1, '测试客户', '0', NOW());
|
|||
|
|
|
|||
|
|
SET @order_id = LAST_INSERT_ID();
|
|||
|
|
|
|||
|
|
-- 创建订单明细
|
|||
|
|
INSERT INTO sal_order_entry (main_id, material_id, material_name, material_number,
|
|||
|
|
specification, quantity, unit, status, del_flag)
|
|||
|
|
VALUES (@order_id, 1, '测试产品', 'P001', '规格A', 100, 'PCS', 'A', '0');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **测试查询:**
|
|||
|
|
- 打开 `定时完成配置` 对话框
|
|||
|
|
- 设置天数阈值为 `7` 天
|
|||
|
|
- ✅ 验证:`当前符合条件订单` 显示数量 > 0
|
|||
|
|
|
|||
|
|
3. **测试预览:**
|
|||
|
|
- 点击 **"查看详情 >>"** 链接
|
|||
|
|
- ✅ 验证:弹出 `超期订单列表` 对话框
|
|||
|
|
- ✅ 验证:表格显示订单信息,包括:
|
|||
|
|
- 订单编号(TEST-ORDER-001)
|
|||
|
|
- 订单日期
|
|||
|
|
- 物料名称、编号、规格
|
|||
|
|
- 数量、单位
|
|||
|
|
- **超期天数**(红色标签,显示"8天")
|
|||
|
|
|
|||
|
|
4. **测试排序:**
|
|||
|
|
- ✅ 验证:订单按 `订单日期` 升序排列(最早的订单在最上面)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 测试用例 3:立即执行一次
|
|||
|
|
|
|||
|
|
**目的:** 验证手动批量执行功能
|
|||
|
|
|
|||
|
|
**前置条件:**
|
|||
|
|
- 有至少1个超期订单
|
|||
|
|
- 订单对应的物料已配置默认工序路线
|
|||
|
|
|
|||
|
|
**步骤:**
|
|||
|
|
|
|||
|
|
1. **验证物料配置:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 查询测试订单的物料是否配置了工序路线
|
|||
|
|
SELECT m.id, m.name, m.route_id, r.name AS route_name
|
|||
|
|
FROM md_material m
|
|||
|
|
LEFT JOIN pro_route r ON r.id = m.route_id
|
|||
|
|
WHERE m.id IN (
|
|||
|
|
SELECT material_id FROM sal_order_entry
|
|||
|
|
WHERE main_id = (SELECT id FROM sal_order WHERE number = 'TEST-ORDER-001')
|
|||
|
|
);
|
|||
|
|
-- route_id 不能为NULL
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
如果route_id为NULL,需要手动设置:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 方式1:使用现有工序路线
|
|||
|
|
UPDATE md_material SET route_id = (SELECT id FROM pro_route LIMIT 1)
|
|||
|
|
WHERE id = <物料ID>;
|
|||
|
|
|
|||
|
|
-- 方式2:创建简单工序路线
|
|||
|
|
INSERT INTO pro_route (name, type, del_flag, create_time)
|
|||
|
|
VALUES ('测试工序路线', '0', '0', NOW());
|
|||
|
|
|
|||
|
|
SET @route_id = LAST_INSERT_ID();
|
|||
|
|
|
|||
|
|
INSERT INTO pro_route_process (route_id, process_id, process_name, process_sort, del_flag)
|
|||
|
|
VALUES (@route_id, 1, '测试工序', 1, '0');
|
|||
|
|
|
|||
|
|
UPDATE md_material SET route_id = @route_id WHERE id = <物料ID>;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **执行测试:**
|
|||
|
|
- 打开 `定时完成配置` 对话框
|
|||
|
|
- 设置天数阈值为 `7` 天
|
|||
|
|
- ✅ 验证:显示符合条件订单数 > 0
|
|||
|
|
- 点击 **"立即执行一次"** 按钮
|
|||
|
|
- ✅ 验证:弹出确认对话框 `"确定要立即执行一次定时完成吗?将处理 X 个超期订单"`
|
|||
|
|
|
|||
|
|
3. **确认执行:**
|
|||
|
|
- 点击 **"确定"**
|
|||
|
|
- ✅ 验证:显示全屏Loading,提示 `"正在批量执行自动完成,请稍候..."`
|
|||
|
|
- ✅ 验证:执行完成后显示成功消息:
|
|||
|
|
```
|
|||
|
|
执行完成!共处理 1 个订单,成功 1 个,失败 0 个
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
4. **验证执行结果:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 1. 验证订单明细状态变更
|
|||
|
|
SELECT status FROM sal_order_entry
|
|||
|
|
WHERE main_id = (SELECT id FROM sal_order WHERE number = 'TEST-ORDER-001');
|
|||
|
|
-- status 应该变为 'F' (生产完成)
|
|||
|
|
|
|||
|
|
-- 2. 验证工单生成
|
|||
|
|
SELECT * FROM pro_workorder
|
|||
|
|
WHERE JSON_CONTAINS(source_info, JSON_OBJECT('saleOrderEntryId', JSON_ARRAY(<订单明细ID>)));
|
|||
|
|
-- 应该生成至少1条工单记录
|
|||
|
|
|
|||
|
|
-- 3. 验证报工单生成
|
|||
|
|
SELECT COUNT(*) FROM pro_report
|
|||
|
|
WHERE work_order_id IN (
|
|||
|
|
SELECT id FROM pro_workorder
|
|||
|
|
WHERE JSON_CONTAINS(source_info, JSON_OBJECT('saleOrderEntryId', JSON_ARRAY(<订单明细ID>)))
|
|||
|
|
);
|
|||
|
|
-- 应该 = 工序数量(例如1个工序 = 1条报工单)
|
|||
|
|
|
|||
|
|
-- 4. 验证执行日志
|
|||
|
|
SELECT * FROM sys_timed_complete_log
|
|||
|
|
WHERE execute_type = 'MANUAL'
|
|||
|
|
ORDER BY execute_time DESC LIMIT 1;
|
|||
|
|
-- 应该有1条记录:total_count >= 1, success_count >= 1, execute_type='MANUAL'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
5. **验证配置更新:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
SELECT last_execute_time, last_execute_count
|
|||
|
|
FROM sys_timed_complete_config
|
|||
|
|
WHERE module_name = 'sale_order';
|
|||
|
|
-- last_execute_time 应该更新为当前时间
|
|||
|
|
-- last_execute_count 应该 = 成功处理的订单数
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
6. **验证防重复机制:**
|
|||
|
|
- 再次点击 **"立即执行一次"**
|
|||
|
|
- ✅ 验证:提示 `"当前没有符合条件的订单"` (因为已经被处理过了)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 测试用例 4:页面刷新自动执行
|
|||
|
|
|
|||
|
|
**目的:** 验证开启开关后,页面刷新时的自动执行功能
|
|||
|
|
|
|||
|
|
**前置条件:**
|
|||
|
|
- 配置已保存:开关=开启,天数=7
|
|||
|
|
- 有新的超期订单(或创建新的测试数据)
|
|||
|
|
|
|||
|
|
**步骤:**
|
|||
|
|
|
|||
|
|
1. **准备新订单:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 创建第2个测试订单(10天前)
|
|||
|
|
INSERT INTO sal_order (number, sale_date, customer_id, customer_name, del_flag, create_time)
|
|||
|
|
VALUES ('TEST-ORDER-002', DATE_SUB(CURDATE(), INTERVAL 10 DAY), 1, '测试客户2', '0', NOW());
|
|||
|
|
|
|||
|
|
SET @order_id = LAST_INSERT_ID();
|
|||
|
|
|
|||
|
|
INSERT INTO sal_order_entry (main_id, material_id, material_name, material_number,
|
|||
|
|
specification, quantity, unit, status, del_flag)
|
|||
|
|
VALUES (@order_id, 1, '测试产品2', 'P002', '规格B', 200, 'PCS', 'A', '0');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **测试自动执行:**
|
|||
|
|
- 在浏览器中按 `F5` 或点击刷新按钮
|
|||
|
|
- ✅ 验证:页面加载完成后,订单列表自动刷新
|
|||
|
|
- ✅ 验证:打开浏览器控制台(F12),查看Console日志:
|
|||
|
|
```
|
|||
|
|
[定时自动完成] 执行完成!共处理 1 个订单,成功 1 个,失败 0 个
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **验证静默执行:**
|
|||
|
|
- ✅ 验证:**没有显示弹窗提示**(静默执行)
|
|||
|
|
- ✅ 验证:订单列表中 `TEST-ORDER-002` 状态已更新
|
|||
|
|
|
|||
|
|
4. **验证日志:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
SELECT * FROM sys_timed_complete_log
|
|||
|
|
WHERE execute_type = 'AUTO'
|
|||
|
|
ORDER BY execute_time DESC LIMIT 1;
|
|||
|
|
-- execute_type 应该 = 'AUTO'
|
|||
|
|
-- total_count >= 1, success_count >= 1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
5. **测试关闭开关:**
|
|||
|
|
- 打开 `定时完成配置` 对话框
|
|||
|
|
- 将开关切换为 **关闭**
|
|||
|
|
- 保存配置
|
|||
|
|
- 创建第3个测试订单(超期)
|
|||
|
|
- 刷新页面(F5)
|
|||
|
|
- ✅ 验证:订单**不会**被自动完成
|
|||
|
|
- ✅ 验证:Console日志中**没有**自动执行消息
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 测试用例 5:边界条件测试
|
|||
|
|
|
|||
|
|
**目的:** 验证各种边界情况的处理
|
|||
|
|
|
|||
|
|
##### 5.1 物料未配置工序路线
|
|||
|
|
|
|||
|
|
**步骤:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 创建没有工序路线的订单
|
|||
|
|
INSERT INTO sal_order (number, sale_date, customer_id, customer_name, del_flag, create_time)
|
|||
|
|
VALUES ('TEST-ORDER-NO-ROUTE', DATE_SUB(CURDATE(), INTERVAL 8 DAY), 1, '测试客户', '0', NOW());
|
|||
|
|
|
|||
|
|
SET @order_id = LAST_INSERT_ID();
|
|||
|
|
|
|||
|
|
INSERT INTO sal_order_entry (main_id, material_id, material_name, material_number,
|
|||
|
|
specification, quantity, unit, status, del_flag)
|
|||
|
|
VALUES (@order_id, 999999, '无路线产品', 'P-NO-ROUTE', '规格X', 100, 'PCS', 'A', '0');
|
|||
|
|
-- 注意:物料ID=999999 不存在或其route_id=NULL
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 执行 **"立即执行一次"**
|
|||
|
|
- ✅ 验证:成功消息显示 `failCount = 1`
|
|||
|
|
- ✅ 验证:日志中记录失败原因:
|
|||
|
|
```sql
|
|||
|
|
SELECT execute_result FROM sys_timed_complete_log
|
|||
|
|
ORDER BY execute_time DESC LIMIT 1;
|
|||
|
|
-- execute_result应包含:'未配置默认工序路线'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
##### 5.2 已有工单的订单(防重复)
|
|||
|
|
|
|||
|
|
**步骤:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 手动创建工单
|
|||
|
|
INSERT INTO pro_workorder (number, batch_number, material_id, material_name, quantity,
|
|||
|
|
source_info, del_flag, create_time)
|
|||
|
|
VALUES ('WO-TEST-001', 'BATCH-001', 1, '测试产品', 100,
|
|||
|
|
JSON_OBJECT('saleOrderEntryId', JSON_ARRAY(<订单明细ID>)), '0', NOW());
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 执行 **"立即执行一次"**
|
|||
|
|
- ✅ 验证:该订单**不在**超期订单列表中(SQL已排除)
|
|||
|
|
- ✅ 验证:不会重复生成工单
|
|||
|
|
|
|||
|
|
##### 5.3 状态为C/D/F的订单(已发货/已关闭/生产完成)
|
|||
|
|
|
|||
|
|
**步骤:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 创建已发货的超期订单
|
|||
|
|
INSERT INTO sal_order (number, sale_date, customer_id, customer_name, del_flag, create_time)
|
|||
|
|
VALUES ('TEST-ORDER-SHIPPED', DATE_SUB(CURDATE(), INTERVAL 10 DAY), 1, '测试客户', '0', NOW());
|
|||
|
|
|
|||
|
|
SET @order_id = LAST_INSERT_ID();
|
|||
|
|
|
|||
|
|
INSERT INTO sal_order_entry (main_id, material_id, material_name, material_number,
|
|||
|
|
specification, quantity, unit, status, del_flag)
|
|||
|
|
VALUES (@order_id, 1, '已发货产品', 'P-SHIPPED', '规格', 100, 'PCS', 'C', '0');
|
|||
|
|
-- status='C' 表示已发货
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 查询超期订单
|
|||
|
|
- ✅ 验证:该订单**不在**列表中(SQL WHERE子句已排除)
|
|||
|
|
|
|||
|
|
##### 5.4 天数阈值边界测试
|
|||
|
|
|
|||
|
|
**步骤:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 创建恰好等于阈值的订单
|
|||
|
|
-- 假设阈值=7天,创建7天前的订单
|
|||
|
|
INSERT INTO sal_order (number, sale_date, customer_id, customer_name, del_flag, create_time)
|
|||
|
|
VALUES ('TEST-ORDER-EXACT-7', DATE_SUB(CURDATE(), INTERVAL 7 DAY), 1, '测试客户', '0', NOW());
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 设置天数阈值 = 7
|
|||
|
|
- ✅ 验证:该订单**应该被包括**(使用 `>=` 判断)
|
|||
|
|
- 设置天数阈值 = 8
|
|||
|
|
- ✅ 验证:该订单**不被包括**(只有6天超期)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 🔍 三、性能和压力测试
|
|||
|
|
|
|||
|
|
#### 3.1 批量订单处理性能
|
|||
|
|
|
|||
|
|
**目的:** 验证大量订单的处理性能
|
|||
|
|
|
|||
|
|
**步骤:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 创建100个超期订单
|
|||
|
|
DELIMITER $$
|
|||
|
|
CREATE PROCEDURE create_test_orders()
|
|||
|
|
BEGIN
|
|||
|
|
DECLARE i INT DEFAULT 1;
|
|||
|
|
WHILE i <= 100 DO
|
|||
|
|
INSERT INTO sal_order (number, sale_date, customer_id, customer_name, del_flag, create_time)
|
|||
|
|
VALUES (CONCAT('PERF-TEST-', LPAD(i, 4, '0')), DATE_SUB(CURDATE(), INTERVAL (i % 30 + 8) DAY),
|
|||
|
|
1, '性能测试客户', '0', NOW());
|
|||
|
|
|
|||
|
|
SET @order_id = LAST_INSERT_ID();
|
|||
|
|
|
|||
|
|
INSERT INTO sal_order_entry (main_id, material_id, material_name, material_number,
|
|||
|
|
specification, quantity, unit, status, del_flag)
|
|||
|
|
VALUES (@order_id, 1, CONCAT('性能测试产品', i), CONCAT('P', LPAD(i, 4, '0')),
|
|||
|
|
'规格A', 100 * i, 'PCS', 'A', '0');
|
|||
|
|
|
|||
|
|
SET i = i + 1;
|
|||
|
|
END WHILE;
|
|||
|
|
END$$
|
|||
|
|
DELIMITER ;
|
|||
|
|
|
|||
|
|
CALL create_test_orders();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 执行 **"立即执行一次"**
|
|||
|
|
- ✅ 验证:能够成功处理所有订单
|
|||
|
|
- ✅ 验证:执行时间在合理范围内(<= 5分钟)
|
|||
|
|
- ✅ 验证:日志中 `execute_duration` 字段记录的耗时准确
|
|||
|
|
|
|||
|
|
**清理测试数据:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
DELETE FROM pro_report WHERE work_order_id IN (
|
|||
|
|
SELECT id FROM pro_workorder WHERE number LIKE 'PERF-TEST-%'
|
|||
|
|
);
|
|||
|
|
DELETE FROM pro_workorder WHERE number LIKE 'PERF-TEST-%';
|
|||
|
|
DELETE FROM sal_order_entry WHERE main_id IN (
|
|||
|
|
SELECT id FROM sal_order WHERE number LIKE 'PERF-TEST-%'
|
|||
|
|
);
|
|||
|
|
DELETE FROM sal_order WHERE number LIKE 'PERF-TEST-%';
|
|||
|
|
DROP PROCEDURE IF EXISTS create_test_orders;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 📊 四、数据验证SQL查询
|
|||
|
|
|
|||
|
|
#### 4.1 完整性检查
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 1. 检查配置表
|
|||
|
|
SELECT * FROM sys_timed_complete_config WHERE module_name = 'sale_order';
|
|||
|
|
|
|||
|
|
-- 2. 检查最近10条执行日志
|
|||
|
|
SELECT
|
|||
|
|
id,
|
|||
|
|
execute_time,
|
|||
|
|
execute_type,
|
|||
|
|
total_count,
|
|||
|
|
success_count,
|
|||
|
|
fail_count,
|
|||
|
|
execute_duration,
|
|||
|
|
LEFT(execute_result, 100) AS result_preview
|
|||
|
|
FROM sys_timed_complete_log
|
|||
|
|
ORDER BY execute_time DESC
|
|||
|
|
LIMIT 10;
|
|||
|
|
|
|||
|
|
-- 3. 检查当前超期订单(天数=7)
|
|||
|
|
SELECT
|
|||
|
|
so.number AS order_number,
|
|||
|
|
so.sale_date,
|
|||
|
|
soe.material_name,
|
|||
|
|
soe.quantity,
|
|||
|
|
soe.status,
|
|||
|
|
DATEDIFF(CURDATE(), so.sale_date) AS days_passed,
|
|||
|
|
CASE
|
|||
|
|
WHEN EXISTS (
|
|||
|
|
SELECT 1 FROM pro_workorder pw
|
|||
|
|
WHERE JSON_CONTAINS(pw.source_info, JSON_OBJECT('saleOrderEntryId', JSON_ARRAY(soe.id)))
|
|||
|
|
) THEN '已有工单'
|
|||
|
|
ELSE '未生成工单'
|
|||
|
|
END AS workorder_status
|
|||
|
|
FROM sal_order_entry soe
|
|||
|
|
INNER JOIN sal_order so ON so.id = soe.main_id
|
|||
|
|
WHERE
|
|||
|
|
soe.status NOT IN ('C', 'D', 'F')
|
|||
|
|
AND DATEDIFF(CURDATE(), so.sale_date) >= 7
|
|||
|
|
AND soe.del_flag = '0'
|
|||
|
|
AND so.del_flag = '0'
|
|||
|
|
ORDER BY so.sale_date ASC;
|
|||
|
|
|
|||
|
|
-- 4. 检查生成的工单和报工单
|
|||
|
|
SELECT
|
|||
|
|
wo.number AS workorder_number,
|
|||
|
|
wo.material_name,
|
|||
|
|
wo.quantity,
|
|||
|
|
COUNT(r.id) AS report_count
|
|||
|
|
FROM pro_workorder wo
|
|||
|
|
LEFT JOIN pro_report r ON r.work_order_id = wo.id
|
|||
|
|
WHERE wo.source_info LIKE '%saleOrderEntryId%'
|
|||
|
|
AND wo.create_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
|||
|
|
GROUP BY wo.id;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.2 异常检测
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 1. 查找失败的执行记录
|
|||
|
|
SELECT * FROM sys_timed_complete_log
|
|||
|
|
WHERE fail_count > 0
|
|||
|
|
ORDER BY execute_time DESC;
|
|||
|
|
|
|||
|
|
-- 2. 查找有工单但状态仍为A的订单(异常)
|
|||
|
|
SELECT
|
|||
|
|
so.number,
|
|||
|
|
soe.material_name,
|
|||
|
|
soe.status,
|
|||
|
|
COUNT(wo.id) AS workorder_count
|
|||
|
|
FROM sal_order_entry soe
|
|||
|
|
INNER JOIN sal_order so ON so.id = soe.main_id
|
|||
|
|
LEFT JOIN pro_workorder wo ON JSON_CONTAINS(wo.source_info, JSON_OBJECT('saleOrderEntryId', JSON_ARRAY(soe.id)))
|
|||
|
|
WHERE soe.status = 'A'
|
|||
|
|
AND wo.id IS NOT NULL
|
|||
|
|
GROUP BY soe.id;
|
|||
|
|
|
|||
|
|
-- 3. 查找工单没有对应报工单的情况(异常)
|
|||
|
|
SELECT
|
|||
|
|
wo.number,
|
|||
|
|
wo.material_name,
|
|||
|
|
COUNT(r.id) AS report_count
|
|||
|
|
FROM pro_workorder wo
|
|||
|
|
LEFT JOIN pro_report r ON r.work_order_id = wo.id
|
|||
|
|
WHERE wo.source_info LIKE '%saleOrderEntryId%'
|
|||
|
|
GROUP BY wo.id
|
|||
|
|
HAVING report_count = 0;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 🐛 五、常见问题排查
|
|||
|
|
|
|||
|
|
#### 问题1:点击"定时完成"按钮没有反应
|
|||
|
|
|
|||
|
|
**排查步骤:**
|
|||
|
|
|
|||
|
|
1. 打开浏览器控制台(F12)查看是否有JavaScript错误
|
|||
|
|
2. 检查前端API文件是否存在:
|
|||
|
|
```bash
|
|||
|
|
ls mes-ui/src/api/mes/production/timedComplete.js
|
|||
|
|
```
|
|||
|
|
3. 检查后端Controller是否正常启动:
|
|||
|
|
```bash
|
|||
|
|
# 查看日志
|
|||
|
|
grep "TimedCompleteController" mes-admin/logs/sys-info.log
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 问题2:配置保存后刷新页面不生效
|
|||
|
|
|
|||
|
|
**排查步骤:**
|
|||
|
|
|
|||
|
|
1. 检查数据库配置:
|
|||
|
|
```sql
|
|||
|
|
SELECT * FROM sys_timed_complete_config WHERE module_name = 'sale_order';
|
|||
|
|
```
|
|||
|
|
2. 检查浏览器控制台,是否有API请求失败
|
|||
|
|
3. 检查`mounted()`生命周期是否正常执行
|
|||
|
|
|
|||
|
|
#### 问题3:立即执行一次后没有生成工单
|
|||
|
|
|
|||
|
|
**排查步骤:**
|
|||
|
|
|
|||
|
|
1. 检查订单的物料是否配置了工序路线:
|
|||
|
|
```sql
|
|||
|
|
SELECT m.name, m.route_id, r.name AS route_name
|
|||
|
|
FROM md_material m
|
|||
|
|
LEFT JOIN pro_route r ON r.id = m.route_id
|
|||
|
|
WHERE m.id IN (
|
|||
|
|
SELECT DISTINCT material_id FROM sal_order_entry
|
|||
|
|
WHERE DATEDIFF(CURDATE(), (SELECT sale_date FROM sal_order WHERE id = sal_order_entry.main_id)) >= 7
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
2. 检查执行日志中的失败原因:
|
|||
|
|
```sql
|
|||
|
|
SELECT execute_result FROM sys_timed_complete_log
|
|||
|
|
ORDER BY execute_time DESC LIMIT 1;
|
|||
|
|
```
|
|||
|
|
3. 检查后端日志:
|
|||
|
|
```bash
|
|||
|
|
tail -100 mes-admin/logs/sys-error.log
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 问题4:页面刷新时自动执行没有触发
|
|||
|
|
|
|||
|
|
**排查步骤:**
|
|||
|
|
|
|||
|
|
1. 确认配置开关已开启:
|
|||
|
|
```sql
|
|||
|
|
SELECT enabled FROM sys_timed_complete_config WHERE module_name = 'sale_order';
|
|||
|
|
-- 应该=1
|
|||
|
|
```
|
|||
|
|
2. 检查浏览器Console日志,确认`mounted()`执行
|
|||
|
|
3. 检查是否有超期订单:
|
|||
|
|
```sql
|
|||
|
|
-- 使用配置的天数阈值
|
|||
|
|
SELECT COUNT(*) FROM sal_order_entry soe
|
|||
|
|
INNER JOIN sal_order so ON so.id = soe.main_id
|
|||
|
|
WHERE soe.status NOT IN ('C', 'D', 'F')
|
|||
|
|
AND DATEDIFF(CURDATE(), so.sale_date) >= (
|
|||
|
|
SELECT day_threshold FROM sys_timed_complete_config WHERE module_name = 'sale_order'
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 问题5:批量执行时部分订单失败
|
|||
|
|
|
|||
|
|
**排查步骤:**
|
|||
|
|
|
|||
|
|
1. 查看执行日志:
|
|||
|
|
```sql
|
|||
|
|
SELECT execute_result FROM sys_timed_complete_log
|
|||
|
|
WHERE fail_count > 0
|
|||
|
|
ORDER BY execute_time DESC LIMIT 1;
|
|||
|
|
```
|
|||
|
|
2. 对失败的订单逐个检查:
|
|||
|
|
- 物料是否存在
|
|||
|
|
- 物料的工序路线是否存在
|
|||
|
|
- 工序路线是否有工序
|
|||
|
|
- 订单明细是否已有工单
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### ✅ 六、测试检查清单
|
|||
|
|
|
|||
|
|
完成以上所有测试后,请勾选以下检查项:
|
|||
|
|
|
|||
|
|
#### 前端功能
|
|||
|
|
- [ ] "定时完成"按钮正常显示和点击
|
|||
|
|
- [ ] 配置对话框正常打开和关闭
|
|||
|
|
- [ ] 开关切换正常,有提示信息
|
|||
|
|
- [ ] 天数阈值调整后,订单数量实时更新
|
|||
|
|
- [ ] 配置保存成功,持久化有效
|
|||
|
|
- [ ] 超期订单预览列表正常显示
|
|||
|
|
- [ ] "立即执行一次"成功提示,订单状态更新
|
|||
|
|
- [ ] "立即执行一次"执行后,配置信息更新(最后执行时间/数量)
|
|||
|
|
|
|||
|
|
#### 后端功能
|
|||
|
|
- [ ] API接口响应正常(200状态码)
|
|||
|
|
- [ ] SQL查询超期订单准确(>=天数阈值)
|
|||
|
|
- [ ] SQL排除已有工单的订单(防重复)
|
|||
|
|
- [ ] SQL排除状态为C/D/F的订单
|
|||
|
|
- [ ] 工单生成逻辑正确(基于物料默认工序路线)
|
|||
|
|
- [ ] 报工单生成数量 = 工序数量
|
|||
|
|
- [ ] 订单状态更新为'F'(生产完成)
|
|||
|
|
- [ ] 执行日志正确记录(时间、类型、数量、耗时)
|
|||
|
|
|
|||
|
|
#### 自动执行功能
|
|||
|
|
- [ ] 开关开启 + 页面刷新 → 自动执行
|
|||
|
|
- [ ] 开关关闭 + 页面刷新 → 不执行
|
|||
|
|
- [ ] 自动执行静默(无弹窗提示)
|
|||
|
|
- [ ] 自动执行日志类型='AUTO'
|
|||
|
|
- [ ] 自动执行后订单列表刷新
|
|||
|
|
|
|||
|
|
#### 边界和异常
|
|||
|
|
- [ ] 物料未配置工序路线 → 失败并记录
|
|||
|
|
- [ ] 已有工单的订单 → 不重复处理
|
|||
|
|
- [ ] 状态为C/D/F的订单 → 不处理
|
|||
|
|
- [ ] 天数阈值边界(=阈值) → 正确包含
|
|||
|
|
- [ ] 100个订单批量处理 → 性能正常
|
|||
|
|
|
|||
|
|
#### 数据库
|
|||
|
|
- [ ] sys_timed_complete_config表结构正确
|
|||
|
|
- [ ] sys_timed_complete_log表结构正确
|
|||
|
|
- [ ] 默认配置数据已插入
|
|||
|
|
- [ ] 索引正常工作(查询性能)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 🎯 七、测试通过标准
|
|||
|
|
|
|||
|
|
所有以下条件必须满足:
|
|||
|
|
|
|||
|
|
1. ✅ 所有测试用例通过
|
|||
|
|
2. ✅ 所有检查清单项勾选
|
|||
|
|
3. ✅ 无Console错误
|
|||
|
|
4. ✅ 无后端异常日志
|
|||
|
|
5. ✅ 数据库数据一致性正确
|
|||
|
|
6. ✅ 100个订单批量处理<5分钟
|
|||
|
|
7. ✅ 页面刷新自动执行<2秒响应
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 📋 八、测试完成签字
|
|||
|
|
|
|||
|
|
**测试人员:** ___________________
|
|||
|
|
**测试日期:** ___________________
|
|||
|
|
**测试结果:** ☐ 通过 ☐ 未通过
|
|||
|
|
**备注:** ___________________
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**测试文档版本:** v1.0
|
|||
|
|
**最后更新时间:** 2025-11-01
|
|||
|
|
**对应功能版本:** v1.0
|
|||
|
|
|