瀏覽代碼

feat: 订单交付移除MDP/dwd依赖改查源表 + 审批流增强 + 排产优化 | Web 2.4.324 / server 1.0.393

Pengxy 1 天之前
父節點
當前提交
12a1df69c2

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 {
 	"name": "admin.net",
 	"name": "admin.net",
 	"type": "module",
 	"type": "module",
-	"version": "2.4.323",
+	"version": "2.4.324",
 	"packageManager": "pnpm@10.32.1",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 10 - 2
Web/src/views/aidop/business/orderList.vue

@@ -177,13 +177,21 @@ function stateTag(s?: string | null) {
 function flowStateText(state?: string | null) {
 function flowStateText(state?: string | null) {
 	if (!state) return '未审批';
 	if (!state) return '未审批';
 	if (state === '审批中') return '审批中';
 	if (state === '审批中') return '审批中';
-	return '审批完成';
+	if (state === '审批完成') return '审批完成';
+	if (state === '已拒绝') return '已拒绝';
+	if (state === '已撤销') return '已撤销';
+	if (state === '已终止') return '已终止';
+	return state;
 }
 }
 
 
 function flowStateTagType(state?: string | null) {
 function flowStateTagType(state?: string | null) {
 	if (!state) return 'info';
 	if (!state) return 'info';
 	if (state === '审批中') return 'warning';
 	if (state === '审批中') return 'warning';
-	return 'success';
+	if (state === '审批完成') return 'success';
+	if (state === '已拒绝') return 'danger';
+	if (state === '已撤销') return 'warning';
+	if (state === '已终止') return 'danger';
+	return 'info';
 }
 }
 
 
 function canEdit(r: SeOrderReviewRow) {
 function canEdit(r: SeOrderReviewRow) {

+ 7 - 1
Web/src/views/approvalFlow/component/ApprovalPanel.vue

@@ -196,7 +196,13 @@ const loadInstance = async () => {
 	visible.value = true;
 	visible.value = true;
 	try {
 	try {
 		const res = await getInstanceByBiz(props.bizType, props.bizId);
 		const res = await getInstanceByBiz(props.bizType, props.bizId);
-		instance.value = res.data?.result ?? null;
+		const raw = res.data?.result ?? null;
+		// 终态流程(已拒绝/已撤销/已终止)允许重新发起,视为无活跃实例
+		if (raw && (raw.status === 3 || raw.status === 4 || raw.status === 5)) {
+			instance.value = null;
+		} else {
+			instance.value = raw;
+		}
 		if (instance.value) {
 		if (instance.value) {
 			const tlRes = await getTimeline(instance.value.id);
 			const tlRes = await getTimeline(instance.value.id);
 			timeline.value = tlRes.data?.result ?? [];
 			timeline.value = tlRes.data?.result ?? [];

二進制
doc/UAT测试报告.xlsx


+ 325 - 0
doc/generate_uat_report_all.js

@@ -0,0 +1,325 @@
+const ExcelJS = require('exceljs');
+const wb = new ExcelJS.Workbook();
+
+// ── 样式工具 ──
+const hf = { name: '微软雅黑', size: 11, bold: true };
+const tf = { name: '微软雅黑', size: 14, bold: true };
+const nf = { name: '微软雅黑', size: 10 };
+const sf = { name: '微软雅黑', size: 9 };
+const ca = { horizontal: 'center', vertical: 'middle', wrapText: true };
+const la = { horizontal: 'left', vertical: 'middle', wrapText: true };
+const hfill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD9E1F2' } };
+const lfill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF2F2F2' } };
+const yfill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFF2CC' } };
+const thinBorder = { top: { style: 'thin' }, bottom: { style: 'thin' }, left: { style: 'thin' }, right: { style: 'thin' } };
+
+function writeMerged(ws, row, colStart, colEnd, value, font = nf, alignment = ca, fill = null) {
+  if (colStart !== colEnd) {
+    ws.mergeCells(row, colStart, row, colEnd);
+  }
+  const cell = ws.getCell(row, colStart);
+  cell.value = value;
+  cell.font = font;
+  cell.alignment = alignment;
+  if (fill) cell.fill = fill;
+  cell.border = thinBorder;
+  for (let c = colStart; c <= colEnd; c++) {
+    const cc = ws.getCell(row, c);
+    cc.border = thinBorder;
+    if (fill) cc.fill = fill;
+  }
+}
+
+// ── 模块数据 ──
+const modules = [
+  ["S1产销协同", [
+    ["产品设计", [
+      ["产品设计列表查询", "1.进入产品设计菜单\n2.输入产品编码/名称查询\n3.点击重置", "列表查询", "可按编码、名称筛选,显示单据号、产品信息、状态"],
+      ["新建产品设计-BOM自动填充", "1.点击新建\n2.选择产品编码\n3.观察BOM和工艺子表", "自动填充", "选择产品后BOM子表自动填入物料清单,工艺子表自动填入工序"],
+      ["产品设计保存与编辑", "1.填写完整信息后保存\n2.选择记录点击编辑\n3.修改后保存", "增删改", "保存后列表刷新;编辑后字段正确更新;删除后记录移除"],
+    ]],
+    ["合同评审", [
+      ["合同评审列表查询", "1.进入合同评审菜单\n2.输入合同号/客户查询", "列表查询", "可按合同号、客户、日期筛选,显示状态、创建时间等"],
+      ["合同评审新增与提交审批", "1.点击新增\n2.填写合同信息\n3.保存后提交审批", "新增+审批", "保存成功;提交后状态变为审批中,审批流自动创建"],
+      ["合同评审编辑与删除", "1.选择记录编辑修改后保存\n2.选择未审批记录删除", "编辑/删除", "编辑后字段正确更新;已审批记录不可删除"],
+    ]],
+    ["订单评审", [
+      ["订单评审列表查询", "1.进入订单评审\n2.输入订单号/客户查询", "列表查询", "可按订单号、客户、状态筛选,显示审批进度标签"],
+      ["订单评审新增与变更", "1.新增订单评审并保存\n2.对已审批订单发起变更\n3.修改内容后保存", "新增+变更", "保存成功;变更后变更次数+1,审批流重新发起"],
+      ["订单评审-交期确认", "1.选择审批完成的订单\n2.点击交期确认\n3.确认操作", "交期确认", "确认后订单状态更新"],
+    ]],
+    ["订单交付与发货", [
+      ["订单交付列表查询", "1.进入订单交付菜单\n2.按状态筛选", "列表查询", "显示交付状态(待交付/部分/已交付),筛选条件生效"],
+      ["订单发货新增", "1.点击新增\n2.选择销售单号\n3.填写发货信息\n4.保存", "新增发货", "销售单号下拉搜索回显正确;保存后发货单号自动生成"],
+      ["发货单编辑与删除", "1.编辑修改后保存\n2.删除未发货单据", "编辑/删除", "编辑后字段更新;已发货单据不可删除"],
+    ]],
+    ["工单下达与计划联动", [
+      ["工单下达操作", "1.勾选待下达工单\n2.点击下达\n3.确认", "工单下达", "下达后状态变更,不可重复下达,MES可见"],
+      ["计划联动看板查询", "1.进入计划联动看板\n2.按计划编号查询", "列表查询", "显示联动计划列表,来源和目标关系正确"],
+    ]],
+  ]],
+  ["S2制造协同", [
+    ["生产排程", [
+      ["排程列表查询", "1.进入生产排程菜单\n2.按工单号/产线/日期查询", "列表查询", "可按工单号、产线、日期筛选,显示排产状态"],
+      ["执行排产操作", "1.选择待排产工单\n2.点击排产按钮\n3.确认", "排产执行", "排产完成后工单状态更新,排产结果写入"],
+      ["订单生产排程看板", "1.进入订单排程看板\n2.查看甘特图展示", "甘特图", "按工单维度显示甘特图,包含时间轴、产线、状态"],
+      ["产线生产排程看板", "1.进入产线排程看板\n2.查看甘特图展示", "甘特图", "按产线维度显示甘特图,设备占用信息正确"],
+    ]],
+    ["可执行日计划", [
+      ["可执行日计划查询", "1.进入可执行日计划\n2.按日期/产线筛选", "列表查询", "显示当日可执行工单,状态过滤正确"],
+      ["日计划状态操作", "1.选择日计划记录\n2.执行状态变更操作", "状态管理", "状态正确流转,不可跳过中间状态"],
+    ]],
+    ["排产基础配置", [
+      ["产线工作日历管理", "1.进入工作日历\n2.查看/编辑日历", "日历管理", "显示各产线工作日历,编辑后保存正确"],
+      ["产线节假日管理", "1.进入节假日管理\n2.新增/编辑节假日", "节假日", "节假日设置正确,排产时自动跳过"],
+    ]],
+    ["工单执行看板", [
+      ["工单执行进度看板", "1.进入工单进度看板\n2.观察进度数据", "进度看板", "显示各工单完成进度百分比,数据实时更新"],
+      ["工单执行追踪", "1.进入工单追踪\n2.查看工单物料/工序明细", "工单追踪", "显示工单物料明细和工序明细,数据关联正确"],
+    ]],
+  ]],
+  ["S3供应协同", [
+    ["物料需求与采购", [
+      ["物料需求计划查询", "1.进入物料需求计划\n2.按物料/日期查询", "列表查询", "显示物料需求清单,需求量、在途量、缺口量正确"],
+      ["物料采购申请新增", "1.点击新增采购申请\n2.选择物料\n3.填写数量/交期\n4.保存", "新增申请", "保存成功,申请编号自动生成,审批流发起"],
+      ["物料采购订单查询", "1.进入采购订单\n2.按订单号/供应商查询", "列表查询", "显示采购订单列表,状态、供应商信息正确"],
+    ]],
+    ["要货令与交货", [
+      ["要货令新增", "1.点击新增要货令\n2.选择采购订单\n3.填写要求信息\n4.保存", "新增要货令", "保存成功,关联采购订单正确"],
+      ["物料交货计划查询", "1.进入交货计划\n2.按供应商/物料查询", "列表查询", "显示交货计划列表,计划/实际数量对比"],
+      ["交货单异常记录", "1.进入交货异常\n2.查看异常记录", "异常记录", "显示交货延迟、数量偏差等异常记录"],
+    ]],
+    ["委外加工", [
+      ["委外加工订单管理", "1.进入委外订单\n2.新增/编辑委外订单", "委外管理", "保存成功,BOM自动填充,状态流转正确"],
+      ["工序外协订单管理", "1.进入工序外协\n2.新增/编辑外协订单", "外协管理", "保存成功,工序信息关联正确"],
+    ]],
+    ["看板与齐套", [
+      ["供应协同看板", "1.进入S3看板\n2.查看指标卡片", "看板展示", "显示采购相关KPI指标"],
+      ["齐套看板", "1.进入齐套看板\n2.查看物料齐套状态", "齐套展示", "显示工单物料齐套率,缺料项高亮"],
+    ]],
+  ]],
+  ["S4采购执行", [
+    ["供应商交货管理", [
+      ["供应商交货列表查询", "1.进入供应商交货管理\n2.按供应商/物料查询", "列表查询", "显示交货计划列表,支持发布操作"],
+      ["供应商交期回复", "1.查看供应商回复的交期\n2.确认交期", "交期回复", "供应商回复后数据正确显示,确认操作生效"],
+    ]],
+    ["供应商发货单", [
+      ["供应商发货单新增", "1.点击新增\n2.选择供应商\n3.填写发货信息\n4.保存", "新增发货", "保存成功,编号自动生成,物料明细正确"],
+      ["发货单编辑与删除", "1.编辑修改后保存\n2.删除草稿状态单据", "编辑/删除", "编辑后字段更新;已提交单据不可删除"],
+      ["发货单打印标签", "1.选择发货单\n2.点击打印标签", "标签打印", "标签数据正确生成,包含物料/数量/批次信息"],
+    ]],
+    ["采购退货与IQC", [
+      ["采购退货单管理", "1.新增退货单\n2.选择收货单\n3.填写退货信息\n4.保存", "退货管理", "保存成功,关联收货单正确,退货数量校验通过"],
+      ["IQC退货查询", "1.进入IQC退货\n2.按检验单/物料查询", "IQC查询", "显示IQC检验记录,不合格判定数据正确"],
+    ]],
+    ["供应商欠料看板", [
+      ["欠料看板列表查询", "1.进入欠料看板\n2.按物料/供应商查询", "列表查询", "显示D0-D14欠料明细,供应商/物料信息正确"],
+      ["欠料看板刷新", "1.点击刷新按钮\n2.观察数据更新", "数据刷新", "刷新后数据重新加载,显示最新欠料情况"],
+      ["生成催料待办", "1.点击生成催料待办按钮\n2.观察提示", "生成待办", "创建快照记录并自动发起审批流"],
+      ["审批中心查看欠料信息", "1.进入审批中心\n2.找到催料待办\n3.点击查看欠料信息", "欠料快照", "弹窗显示待办基本信息和欠料列表快照"],
+      ["催料待办审批", "1.在待办中点击同意/拒绝\n2.填写意见\n3.确认", "审批操作", "审批完成后状态变更,从待办移至已办"],
+    ]],
+  ]],
+  ["S5物料仓储", [
+    ["来料检验", [
+      ["IQC检验任务列表", "1.进入IQC任务列表\n2.查看待检任务", "任务列表", "显示待检来料任务,物料/供应商/数量信息正确"],
+      ["IQC检验录入", "1.选择检验任务\n2.录入检验结果\n3.提交", "检验录入", "检验数据保存正确,判定结果自动生成"],
+      ["IQC检验结果查询", "1.进入检验结果列表\n2.按物料/日期查询", "结果查询", "显示历史检验记录,合格率统计正确"],
+    ]],
+    ["仓库单据", [
+      ["采购收货单管理", "1.进入采购收货\n2.新增/编辑收货单", "收货管理", "保存成功,关联采购订单,数量校验通过"],
+      ["生产领料单管理", "1.进入生产领料\n2.新增领料单\n3.填写物料明细", "领料管理", "保存成功,关联工单,库存扣减正确"],
+      ["生产退料单管理", "1.进入生产退料\n2.新增退料单", "退料管理", "保存成功,退料数量不超过已领数量"],
+      ["生产入库单管理", "1.进入生产入库\n2.新增入库单", "入库管理", "保存成功,库存增加,批次号正确"],
+    ]],
+    ["库存查询", [
+      ["库存查询", "1.进入库存查询\n2.按物料/库位查询", "库存查询", "显示实时库存数量,支持多维度筛选"],
+      ["进出存查询", "1.进入进出存查询\n2.按日期范围查询", "进出存", "显示进/出/存三期数据,合计正确"],
+      ["暂收在检列表", "1.进入暂收在检\n2.查看在检物料", "暂收在检", "显示IQC检验中的物料列表"],
+    ]],
+    ["盘点管理", [
+      ["盘点标签确认", "1.进入盘点标签确认\n2.确认/驳回标签", "标签确认", "确认后标签状态变更,数据正确"],
+      ["盘点结果查询", "1.进入盘点结果\n2.查看盈亏情况", "盘点结果", "显示盘点盈亏数据,差异分析正确"],
+    ]],
+  ]],
+  ["S6生产执行", [
+    ["生产指令与质检", [
+      ["生产指令单查询", "1.进入生产指令单\n2.按工单号/日期查询", "指令单", "显示生产指令列表,工单/产线/数量信息正确"],
+      ["过程检验单管理", "1.进入过程检验\n2.查看/录入检验数据", "过程检验", "检验数据保存正确,IPQC流程完整"],
+    ]],
+    ["设备与模工具", [
+      ["生产设备台账", "1.进入设备台账\n2.查看设备列表\n3.新增/编辑设备", "设备管理", "设备信息完整(编号、名称、状态、产线)"],
+      ["模工具台账管理", "1.进入模工具台账\n2.查看/管理模工具", "模工具", "模工具信息完整,状态流转正确"],
+    ]],
+    ["生产执行看板", [
+      ["工单执行看板", "1.进入工单执行看板\n2.查看执行状态", "执行看板", "显示各工单实时执行进度"],
+      ["车间效率看板", "1.进入效率看板\n2.查看效率指标", "效率看板", "显示产线效率、设备利用率等指标"],
+    ]],
+  ]],
+  ["S7成品仓储", [
+    ["FQC检验", [
+      ["FQC检验任务列表", "1.进入FQC任务列表\n2.查看待检任务", "任务列表", "显示FQC待检任务,产品信息正确"],
+      ["FQC检验录入", "1.选择检验任务\n2.录入检验结果\n3.提交审核", "检验录入", "检验数据保存正确,审批流触发"],
+      ["FQC检验结果查询", "1.进入检验结果\n2.按产品/日期查询", "结果查询", "显示历史FQC检验记录"],
+      ["成品报检", "1.进入成品报检\n2.新增报检申请", "成品报检", "保存成功,自动生成检验任务"],
+    ]],
+    ["成品入库与发货", [
+      ["生产入库单管理", "1.进入生产入库\n2.新增入库单\n3.填写入库信息", "入库管理", "保存成功,库存增加,批次追溯正确"],
+      ["销售发货通知", "1.进入发货通知\n2.查看/处理发货通知", "发货通知", "显示待发货通知列表,处理流程正确"],
+    ]],
+    ["成品仓储看板", [
+      ["发货任务看板", "1.进入发货任务看板\n2.查看任务状态", "发货看板", "显示待发货/已发货任务,状态正确"],
+      ["成品滞销看板", "1.进入滞销看板\n2.查看滞销产品", "滞销看板", "显示库龄超标的成品列表"],
+    ]],
+  ]],
+  ["S8异常监控", [
+    ["异常看板与大屏", [
+      ["异常监控看板", "1.进入S8看板\n2.查看异常统计", "异常看板", "显示异常数量、分类、趋势统计"],
+      ["交付异常大屏", "1.进入交付异常大屏\n2.查看异常数据", "交付大屏", "显示交付延迟、偏差等异常指标"],
+      ["生产异常大屏", "1.进入生产异常大屏\n2.查看异常数据", "生产大屏", "显示排产异常、设备故障等指标"],
+      ["供应异常大屏", "1.进入供应异常大屏\n2.查看异常数据", "供应大屏", "显示供应延迟、欠料等异常指标"],
+    ]],
+    ["异常管理", [
+      ["异常列表查询", "1.进入异常列表\n2.按类型/日期查询", "异常列表", "显示异常记录列表,分类/状态/影响范围正确"],
+      ["主动提报异常", "1.点击主动提报\n2.填写异常信息\n3.提交", "主动提报", "保存成功,异常自动分配处理人"],
+      ["异常任务处理", "1.在待办中打开异常任务\n2.填写处理方案\n3.提交", "任务处理", "处理后任务状态更新,处理记录保存"],
+    ]],
+    ["订单追踪", [
+      ["订单执行档案", "1.进入订单执行档案\n2.搜索订单号\n3.查看执行详情", "订单档案", "显示订单全生命周期执行数据"],
+      ["订单链路全景", "1.进入订单链路\n2.查看全链路\n3.点击节点查看详情", "链路全景", "显示订单从评审→排产→采购→交付的全链路视图"],
+    ]],
+    ["配置管理", [
+      ["场景配置管理", "1.进入场景配置\n2.新增/编辑场景\n3.保存", "场景配置", "保存成功,场景规则生效"],
+      ["报警规则配置", "1.进入报警规则\n2.配置阈值和通知\n3.保存", "报警规则", "保存成功,规则触发时自动生成异常"],
+    ]],
+  ]],
+];
+
+// ═══════════════════════════════════════════
+// Sheet 0: 目录
+// ═══════════════════════════════════════════
+const wsDir = wb.addWorksheet('目录');
+wsDir.columns = [{ width: 6 }, { width: 20 }, { width: 18 }, { width: 14 }, { width: 18 }, { width: 16 }];
+
+let row = 1;
+writeMerged(wsDir, row++, 1, 6, 'AiDOP系统 UAT 测试报告', tf, ca);
+writeMerged(wsDir, row++, 1, 6, 'S1-S8 全模块综合测试', hf, ca);
+row++;
+
+const dirHeaders = ['序号', '模块名称', '测试场景数', '用例总数', '计划完成时间', '责任人'];
+for (let ci = 0; ci < dirHeaders.length; ci++) {
+  const cell = wsDir.getCell(row, ci + 1);
+  cell.value = dirHeaders[ci]; cell.font = hf; cell.alignment = ca; cell.fill = hfill; cell.border = thinBorder;
+}
+row++;
+
+let totalScenarios = 0, totalCasesAll = 0;
+for (let idx = 0; idx < modules.length; idx++) {
+  const [mname, cats] = modules[idx];
+  const tc = cats.reduce((s, c) => s + c[1].length, 0);
+  totalScenarios += cats.length;
+  totalCasesAll += tc;
+  writeMerged(wsDir, row, 1, 1, String(idx + 1), nf, ca);
+  writeMerged(wsDir, row, 2, 2, mname, nf, ca);
+  writeMerged(wsDir, row, 3, 3, String(cats.length), nf, ca);
+  writeMerged(wsDir, row, 4, 4, String(tc), nf, ca);
+  writeMerged(wsDir, row, 5, 5, '', nf, ca);
+  writeMerged(wsDir, row, 6, 6, '', nf, ca);
+  row++;
+}
+
+row++;
+writeMerged(wsDir, row, 1, 2, '合计', hf, ca, yfill);
+writeMerged(wsDir, row, 3, 3, String(totalScenarios), hf, ca, yfill);
+writeMerged(wsDir, row, 4, 4, String(totalCasesAll), hf, ca, yfill);
+writeMerged(wsDir, row, 5, 6, '', nf, ca, yfill);
+row += 2;
+
+['图例说明:', 'Y : 实际结果与预期结果一致', 'H : 严重缺陷(系统崩溃/数据错误影响其他流程)',
+ 'M : 中等缺陷(功能可用但有错误/不影响其他流程)', 'L : 低程度缺陷(界面/提示/帮助文档问题)',
+ '- : 不属于本次测试范围'].forEach(l => { writeMerged(wsDir, row++, 1, 6, l, sf, la); });
+
+row += 2;
+['测试账号:', 'Web地址:', '账号:        密码:', '库存组织:'].forEach(l => {
+  writeMerged(wsDir, row++, 1, 6, l, sf, la);
+});
+row += 2;
+writeMerged(wsDir, row, 1, 3, '测试人姓名:', sf, ca);
+writeMerged(wsDir, row, 4, 6, '负责顾问:', sf, ca);
+
+// ═══════════════════════════════════════════
+// 各模块 Sheet
+// ═══════════════════════════════════════════
+for (let midx = 0; midx < modules.length; midx++) {
+  const [mname, cats] = modules[midx];
+  const ws = wb.addWorksheet(`${midx + 1} ${mname}`);
+  ws.columns = [
+    { width: 8 }, { width: 8 }, { width: 20 }, { width: 20 }, { width: 20 },
+    { width: 20 }, { width: 14 }, { width: 14 }, { width: 24 },
+    { width: 10 }, { width: 10 }, { width: 12 },
+  ];
+
+  let r = 1;
+  // 顶部信息行
+  writeMerged(ws, r++, 1, 8, '详细测试结果', hf, ca, hfill);
+  writeMerged(ws, r - 1, 9, 9, '第一轮测试', hf, ca, hfill);
+  writeMerged(ws, r - 1, 10, 11, '第二轮测试', hf, ca, hfill);
+  writeMerged(ws, r - 1, 12, 12, '测试内容', hf, ca, hfill);
+
+  writeMerged(ws, r++, 1, 8, '计数', nf, ca, lfill);
+  const totalC = cats.reduce((s, c) => s + c[1].length, 0);
+  writeMerged(ws, r - 1, 9, 9, '第一轮', nf, ca, lfill);
+  writeMerged(ws, r - 1, 10, 11, '第二轮', nf, ca, lfill);
+  writeMerged(ws, r - 1, 12, 12, `${mname}(${totalC}例)`, nf, ca, yfill);
+
+  writeMerged(ws, r++, 1, 12, '测试人', nf, ca);
+  writeMerged(ws, r++, 1, 12, '测试日期', nf, ca);
+
+  ['高(H)', '中(M)', '低(L)'].forEach(dv => {
+    writeMerged(ws, r++, 1, 8, '缺陷', nf, ca);
+    writeMerged(ws, r - 1, 9, 9, dv, nf, ca);
+    writeMerged(ws, r - 1, 10, 11, dv, nf, ca);
+    writeMerged(ws, r - 1, 12, 12, '', nf, ca);
+  });
+
+  writeMerged(ws, r++, 1, 12, '', nf, ca);
+
+  // 表头
+  [[1,1,'用例编号'],[2,3,'测试内容'],[4,6,'测试步骤'],[7,8,'系统功能'],[9,9,'预期结果'],[10,10,'结果1st'],[11,11,'结果2nd'],[12,12,'角色']].forEach(([cs,ce,h]) => {
+    writeMerged(ws, r, cs, ce, h, hf, ca, hfill);
+  });
+  r++;
+
+  // 用例数据
+  let caseSeq = 1;
+  for (const [cname, cases] of cats) {
+    writeMerged(ws, r++, 1, 12, `▸ ${cname}`, hf, la, lfill);
+    for (const [content, steps, func, expected] of cases) {
+      const cid = `${midx + 1}.${caseSeq}`;
+      writeMerged(ws, r, 1, 1, cid, nf, ca);
+      writeMerged(ws, r, 2, 3, content, nf, la);
+      writeMerged(ws, r, 4, 6, steps, nf, la);
+      writeMerged(ws, r, 7, 8, func, nf, ca);
+      writeMerged(ws, r, 9, 9, expected, nf, la);
+      const c10 = ws.getCell(r, 10); c10.value = 'Y'; c10.font = nf; c10.alignment = ca; c10.border = thinBorder;
+      const c11 = ws.getCell(r, 11); c11.font = nf; c11.alignment = ca; c11.border = thinBorder;
+      writeMerged(ws, r, 12, 12, '操作员', nf, ca);
+      ws.getRow(r).height = Math.max(40, (steps.match(/\n/g) || []).length * 14 + 28);
+      r++;
+      caseSeq++;
+    }
+  }
+
+  r++;
+  ['Y : 一致', 'H : 严重缺陷', 'M : 中等缺陷', 'L : 低程度缺陷', '- : 不在范围'].forEach(l => {
+    writeMerged(ws, r++, 1, 8, l, sf, la);
+  });
+}
+
+// ── 保存 ──
+const outPath = 'd:\\DEMONET\\doc\\UAT测试报告.xlsx';
+wb.xlsx.writeFile(outPath).then(() => {
+  console.log(`✅ 已生成: ${outPath}  (${totalScenarios}场景 / ${totalCasesAll}用例)`);
+}).catch(err => {
+  console.error('❌ 生成失败:', err.message);
+});

+ 351 - 0
doc/generate_uat_report_all.py

@@ -0,0 +1,351 @@
+"""
+生成 S1-S8 综合 UAT 测试报告
+"""
+from openpyxl import Workbook
+from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
+from openpyxl.utils import get_column_letter
+
+wb = Workbook()
+
+# ── 样式 ──
+thin = Border(left=Side('thin'), right=Side('thin'), top=Side('thin'), bottom=Side('thin'))
+hf = Font(name='微软雅黑', size=11, bold=True)
+tf = Font(name='微软雅黑', size=14, bold=True)
+nf = Font(name='微软雅黑', size=10)
+sf = Font(name='微软雅黑', size=9)
+ca = Alignment(horizontal='center', vertical='center', wrap_text=True)
+la = Alignment(horizontal='left', vertical='center', wrap_text=True)
+hfill = PatternFill(start_color='D9E1F2', end_color='D9E1F2', fill_type='solid')
+lfill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
+yfill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
+
+def wm(ws, r, cs, ce, v, font=nf, align=ca, fill=None):
+    if cs != ce:
+        ws.merge_cells(start_row=r, start_column=cs, end_row=r, end_column=ce)
+    c = ws.cell(row=r, column=cs, value=v)
+    c.font = font; c.alignment = align
+    if fill: c.fill = fill
+    for x in range(cs, ce+1):
+        ws.cell(row=r, column=x).border = thin
+        if fill: ws.cell(row=r, column=x).fill = fill
+
+def ab(ws, r, cs, ce):
+    for x in range(cs, ce+1): ws.cell(row=r, column=x).border = thin
+
+# ── 场景定义: (用例编号, 测试内容, 测试步骤, 系统功能, 预期结果) ──
+modules = [
+  ("S1产销协同", [
+    ("产品设计", [
+      ("产品设计列表查询", "1.进入产品设计菜单\n2.输入产品编码/名称查询\n3.点击重置", "列表查询", "可按编码、名称筛选,显示单据号、产品信息、状态"),
+      ("新建产品设计-BOM自动填充", "1.点击新建\n2.选择产品编码\n3.观察BOM和工艺子表", "自动填充", "选择产品后BOM子表自动填入物料清单,工艺子表自动填入工序"),
+      ("产品设计保存与编辑", "1.填写完整信息后保存\n2.选择记录点击编辑\n3.修改后保存", "增删改", "保存后列表刷新;编辑后字段正确更新;删除后记录移除"),
+    ]),
+    ("合同评审", [
+      ("合同评审列表查询", "1.进入合同评审菜单\n2.输入合同号/客户查询", "列表查询", "可按合同号、客户、日期筛选,显示状态、创建时间等"),
+      ("合同评审新增与提交审批", "1.点击新增\n2.填写合同信息\n3.保存后提交审批", "新增+审批", "保存成功;提交后状态变为审批中,审批流自动创建"),
+      ("合同评审编辑与删除", "1.选择记录编辑修改后保存\n2.选择未审批记录删除", "编辑/删除", "编辑后字段正确更新;已审批记录不可删除"),
+    ]),
+    ("订单评审", [
+      ("订单评审列表查询", "1.进入订单评审\n2.输入订单号/客户查询", "列表查询", "可按订单号、客户、状态筛选,显示审批进度标签"),
+      ("订单评审新增与变更", "1.新增订单评审并保存\n2.对已审批订单发起变更\n3.修改内容后保存", "新增+变更", "保存成功;变更后变更次数+1,审批流重新发起"),
+      ("订单评审-交期确认", "1.选择审批完成的订单\n2.点击交期确认\n3.确认操作", "交期确认", "确认后订单状态更新"),
+    ]),
+    ("订单交付与发货", [
+      ("订单交付列表查询", "1.进入订单交付菜单\n2.按状态筛选", "列表查询", "显示交付状态(待交付/部分/已交付),筛选条件生效"),
+      ("订单发货新增", "1.点击新增\n2.选择销售单号\n3.填写发货信息\n4.保存", "新增发货", "销售单号下拉搜索回显正确;保存后发货单号自动生成"),
+      ("发货单编辑与删除", "1.编辑修改后保存\n2.删除未发货单据", "编辑/删除", "编辑后字段更新;已发货单据不可删除"),
+    ]),
+    ("工单下达与计划联动", [
+      ("工单下达操作", "1.勾选待下达工单\n2.点击下达\n3.确认", "工单下达", "下达后状态变更,不可重复下达,MES可见"),
+      ("计划联动看板查询", "1.进入计划联动看板\n2.按计划编号查询", "列表查询", "显示联动计划列表,来源和目标关系正确"),
+    ]),
+  ]),
+  ("S2制造协同", [
+    ("生产排程", [
+      ("排程列表查询", "1.进入生产排程菜单\n2.按工单号/产线/日期查询", "列表查询", "可按工单号、产线、日期筛选,显示排产状态"),
+      ("执行排产操作", "1.选择待排产工单\n2.点击排产按钮\n3.确认", "排产执行", "排产完成后工单状态更新,排产结果写入"),
+      ("订单生产排程看板", "1.进入订单排程看板\n2.查看甘特图展示", "甘特图", "按工单维度显示甘特图,包含时间轴、产线、状态"),
+      ("产线生产排程看板", "1.进入产线排程看板\n2.查看甘特图展示", "甘特图", "按产线维度显示甘特图,设备占用信息正确"),
+    ]),
+    ("可执行日计划", [
+      ("可执行日计划查询", "1.进入可执行日计划\n2.按日期/产线筛选", "列表查询", "显示当日可执行工单,状态过滤正确"),
+      ("日计划状态操作", "1.选择日计划记录\n2.执行状态变更操作", "状态管理", "状态正确流转,不可跳过中间状态"),
+    ]),
+    ("排产基础配置", [
+      ("产线工作日历管理", "1.进入工作日历\n2.查看/编辑日历", "日历管理", "显示各产线工作日历,编辑后保存正确"),
+      ("产线节假日管理", "1.进入节假日管理\n2.新增/编辑节假日", "节假日", "节假日设置正确,排产时自动跳过"),
+    ]),
+    ("工单执行看板", [
+      ("工单执行进度看板", "1.进入工单进度看板\n2.观察进度数据", "进度看板", "显示各工单完成进度百分比,数据实时更新"),
+      ("工单执行追踪", "1.进入工单追踪\n2.查看工单物料/工序明细", "工单追踪", "显示工单物料明细和工序明细,数据关联正确"),
+    ]),
+  ]),
+  ("S3供应协同", [
+    ("物料需求与采购", [
+      ("物料需求计划查询", "1.进入物料需求计划\n2.按物料/日期查询", "列表查询", "显示物料需求清单,需求量、在途量、缺口量正确"),
+      ("物料采购申请新增", "1.点击新增采购申请\n2.选择物料\n3.填写数量/交期\n4.保存", "新增申请", "保存成功,申请编号自动生成,审批流发起"),
+      ("物料采购订单查询", "1.进入采购订单\n2.按订单号/供应商查询", "列表查询", "显示采购订单列表,状态、供应商信息正确"),
+    ]),
+    ("要货令与交货", [
+      ("要货令新增", "1.点击新增要货令\n2.选择采购订单\n3.填写要求信息\n4.保存", "新增要货令", "保存成功,关联采购订单正确"),
+      ("物料交货计划查询", "1.进入交货计划\n2.按供应商/物料查询", "列表查询", "显示交货计划列表,计划/实际数量对比"),
+      ("交货单异常记录", "1.进入交货异常\n2.查看异常记录", "异常记录", "显示交货延迟、数量偏差等异常记录"),
+    ]),
+    ("委外加工", [
+      ("委外加工订单管理", "1.进入委外订单\n2.新增/编辑委外订单", "委外管理", "保存成功,BOM自动填充,状态流转正确"),
+      ("工序外协订单管理", "1.进入工序外协\n2.新增/编辑外协订单", "外协管理", "保存成功,工序信息关联正确"),
+    ]),
+    ("看板与齐套", [
+      ("供应协同看板", "1.进入S3看板\n2.查看指标卡片", "看板展示", "显示采购相关KPI指标"),
+      ("齐套看板", "1.进入齐套看板\n2.查看物料齐套状态", "齐套展示", "显示工单物料齐套率,缺料项高亮"),
+    ]),
+  ]),
+  ("S4采购执行", [
+    ("供应商交货管理", [
+      ("供应商交货列表查询", "1.进入供应商交货管理\n2.按供应商/物料查询", "列表查询", "显示交货计划列表,支持发布操作"),
+      ("供应商交期回复", "1.查看供应商回复的交期\n2.确认交期", "交期回复", "供应商回复后数据正确显示,确认操作生效"),
+    ]),
+    ("供应商发货单", [
+      ("供应商发货单新增", "1.点击新增\n2.选择供应商\n3.填写发货信息\n4.保存", "新增发货", "保存成功,编号自动生成,物料明细正确"),
+      ("发货单编辑与删除", "1.编辑修改后保存\n2.删除草稿状态单据", "编辑/删除", "编辑后字段更新;已提交单据不可删除"),
+      ("发货单打印标签", "1.选择发货单\n2.点击打印标签", "标签打印", "标签数据正确生成,包含物料/数量/批次信息"),
+    ]),
+    ("采购退货与IQC", [
+      ("采购退货单管理", "1.新增退货单\n2.选择收货单\n3.填写退货信息\n4.保存", "退货管理", "保存成功,关联收货单正确,退货数量校验通过"),
+      ("IQC退货查询", "1.进入IQC退货\n2.按检验单/物料查询", "IQC查询", "显示IQC检验记录,不合格判定数据正确"),
+    ]),
+    ("供应商欠料看板", [
+      ("欠料看板列表查询", "1.进入欠料看板\n2.按物料/供应商查询", "列表查询", "显示D0-D14欠料明细,供应商/物料信息正确"),
+      ("欠料看板刷新", "1.点击刷新按钮\n2.观察数据更新", "数据刷新", "刷新后数据从WorkOrdDetailTotalKB重新加载"),
+      ("生成催料待办", "1.点击生成催料待办按钮\n2.观察提示", "生成待办", "创建快照记录并自动发起审批流,提示信息正确"),
+      ("审批中心查看欠料信息", "1.进入审批中心\n2.找到催料待办\n3.点击查看欠料信息", "欠料快照", "弹窗显示待办基本信息和欠料列表快照表格"),
+      ("催料待办审批", "1.在待办中点击同意/拒绝\n2.填写意见\n3.确认", "审批操作", "审批完成后状态变更,从待办移至已办"),
+    ]),
+  ]),
+  ("S5物料仓储", [
+    ("来料检验", [
+      ("IQC检验任务列表", "1.进入IQC任务列表\n2.查看待检任务", "任务列表", "显示待检来料任务,物料/供应商/数量信息正确"),
+      ("IQC检验录入", "1.选择检验任务\n2.录入检验结果\n3.提交", "检验录入", "检验数据保存正确,判定结果自动生成"),
+      ("IQC检验结果查询", "1.进入检验结果列表\n2.按物料/日期查询", "结果查询", "显示历史检验记录,合格率统计正确"),
+    ]),
+    ("仓库单据", [
+      ("采购收货单管理", "1.进入采购收货\n2.新增/编辑收货单", "收货管理", "保存成功,关联采购订单,数量校验通过"),
+      ("生产领料单管理", "1.进入生产领料\n2.新增领料单\n3.填写物料明细", "领料管理", "保存成功,关联工单,库存扣减正确"),
+      ("生产退料单管理", "1.进入生产退料\n2.新增退料单", "退料管理", "保存成功,退料数量不超过已领数量"),
+      ("生产入库单管理", "1.进入生产入库\n2.新增入库单", "入库管理", "保存成功,库存增加,批次号正确"),
+    ]),
+    ("库存查询", [
+      ("库存查询", "1.进入库存查询\n2.按物料/库位查询", "库存查询", "显示实时库存数量,支持多维度筛选"),
+      ("进出存查询", "1.进入进出存查询\n2.按日期范围查询", "进出存", "显示进/出/存三期数据,合计正确"),
+      ("暂收在检列表", "1.进入暂收在检\n2.查看在检物料", "暂收在检", "显示IQC检验中的物料列表"),
+    ]),
+    ("盘点管理", [
+      ("盘点标签确认", "1.进入盘点标签确认\n2.确认/驳回标签", "标签确认", "确认后标签状态变更,数据正确"),
+      ("盘点结果查询", "1.进入盘点结果\n2.查看盈亏情况", "盘点结果", "显示盘点盈亏数据,差异分析正确"),
+    ]),
+  ]),
+  ("S6生产执行", [
+    ("生产指令与质检", [
+      ("生产指令单查询", "1.进入生产指令单\n2.按工单号/日期查询", "指令单", "显示生产指令列表,工单/产线/数量信息正确"),
+      ("过程检验单管理", "1.进入过程检验\n2.查看/录入检验数据", "过程检验", "检验数据保存正确,IPQC流程完整"),
+    ]),
+    ("设备与模工具", [
+      ("生产设备台账", "1.进入设备台账\n2.查看设备列表\n3.新增/编辑设备", "设备管理", "设备信息完整(编号、名称、状态、产线)"),
+      ("模工具台账管理", "1.进入模工具台账\n2.查看/管理模工具", "模工具", "模工具信息完整,状态流转正确"),
+    ]),
+    ("生产执行看板", [
+      ("工单执行看板", "1.进入工单执行看板\n2.查看执行状态", "执行看板", "显示各工单实时执行进度"),
+      ("车间效率看板", "1.进入效率看板\n2.查看效率指标", "效率看板", "显示产线效率、设备利用率等指标"),
+    ]),
+  ]),
+  ("S7成品仓储", [
+    ("FQC检验", [
+      ("FQC检验任务列表", "1.进入FQC任务列表\n2.查看待检任务", "任务列表", "显示FQC待检任务,产品信息正确"),
+      ("FQC检验录入", "1.选择检验任务\n2.录入检验结果\n3.提交审核", "检验录入", "检验数据保存正确,审批流触发"),
+      ("FQC检验结果查询", "1.进入检验结果\n2.按产品/日期查询", "结果查询", "显示历史FQC检验记录"),
+      ("成品报检", "1.进入成品报检\n2.新增报检申请", "成品报检", "保存成功,自动生成检验任务"),
+    ]),
+    ("成品入库与发货", [
+      ("生产入库单管理", "1.进入生产入库\n2.新增入库单\n3.填写入库信息", "入库管理", "保存成功,库存增加,批次追溯正确"),
+      ("销售发货通知", "1.进入发货通知\n2.查看/处理发货通知", "发货通知", "显示待发货通知列表,处理流程正确"),
+    ]),
+    ("成品仓储看板", [
+      ("发货任务看板", "1.进入发货任务看板\n2.查看任务状态", "发货看板", "显示待发货/已发货任务,状态正确"),
+      ("成品滞销看板", "1.进入滞销看板\n2.查看滞销产品", "滞销看板", "显示库龄超标的成品列表"),
+    ]),
+  ]),
+  ("S8异常监控", [
+    ("异常看板与大屏", [
+      ("异常监控看板", "1.进入S8看板\n2.查看异常统计", "异常看板", "显示异常数量、分类、趋势统计"),
+      ("交付异常大屏", "1.进入交付异常大屏\n2.查看异常数据", "交付大屏", "显示交付延迟、偏差等异常指标"),
+      ("生产异常大屏", "1.进入生产异常大屏\n2.查看异常数据", "生产大屏", "显示排产异常、设备故障等指标"),
+      ("供应异常大屏", "1.进入供应异常大屏\n2.查看异常数据", "供应大屏", "显示供应延迟、欠料等异常指标"),
+    ]),
+    ("异常管理", [
+      ("异常列表查询", "1.进入异常列表\n2.按类型/日期查询", "异常列表", "显示异常记录列表,分类/状态/影响范围正确"),
+      ("主动提报异常", "1.点击主动提报\n2.填写异常信息\n3.提交", "主动提报", "保存成功,异常自动分配处理人"),
+      ("异常任务处理", "1.在待办中打开异常任务\n2.填写处理方案\n3.提交", "任务处理", "处理后任务状态更新,处理记录保存"),
+    ]),
+    ("订单追踪", [
+      ("订单执行档案", "1.进入订单执行档案\n2.搜索订单号\n3.查看执行详情", "订单档案", "显示订单全生命周期执行数据"),
+      ("订单链路全景", "1.进入订单链路\n2.查看全链路\n3.点击节点查看详情", "链路全景", "显示订单从评审→排产→采购→交付的全链路视图"),
+    ]),
+    ("配置管理", [
+      ("场景配置管理", "1.进入场景配置\n2.新增/编辑场景\n3.保存", "场景配置", "保存成功,场景规则生效"),
+      ("报警规则配置", "1.进入报警规则\n2.配置阈值和通知\n3.保存", "报警规则", "保存成功,规则触发时自动生成异常"),
+    ]),
+  ]),
+]
+
+# ═══════════════════════════════════════════
+# Sheet 0: 目录
+# ═══════════════════════════════════════════
+ws_dir = wb.active
+ws_dir.title = "目录"
+for i, w in enumerate([6, 20, 20, 18, 18, 18], 1):
+    ws_dir.column_dimensions[get_column_letter(i)].width = w
+
+row = 1
+wm(ws_dir, row, 1, 6, "AiDOP系统 UAT 测试报告", tf, ca)
+row += 1
+wm(ws_dir, row, 1, 6, "S1-S8 全模块综合测试", hf, ca)
+row += 2
+
+dh = ["序号", "模块名称", "测试场景数", "用例总数", "计划完成时间", "责任人"]
+for ci, h in enumerate(dh, 1):
+    ws_dir.cell(row=row, column=ci, value=h).font = hf
+    ws_dir.cell(row=row, column=ci).alignment = ca
+    ws_dir.cell(row=row, column=ci).fill = hfill
+    ab(ws_dir, row, ci, ci)
+row += 1
+
+for idx, (mname, cats) in enumerate(modules, 1):
+    total_cases = sum(len(cs) for _, cs in cats)
+    wm(ws_dir, row, 1, 1, str(idx), nf, ca)
+    wm(ws_dir, row, 2, 2, mname, nf, ca)
+    wm(ws_dir, row, 3, 3, str(len(cats)), nf, ca)
+    wm(ws_dir, row, 4, 4, str(total_cases), nf, ca)
+    wm(ws_dir, row, 5, 5, "", nf, ca)
+    wm(ws_dir, row, 6, 6, "", nf, ca)
+    row += 1
+
+# 汇总
+total_scenarios = sum(len(cats) for _, cats in modules)
+total_cases_all = sum(sum(len(cs) for _, cs in cats) for _, cats in modules)
+row += 1
+wm(ws_dir, row, 1, 2, "合计", hf, ca, yfill)
+wm(ws_dir, row, 3, 3, str(total_scenarios), hf, ca, yfill)
+wm(ws_dir, row, 4, 4, str(total_cases_all), hf, ca, yfill)
+wm(ws_dir, row, 5, 6, "", nf, ca, yfill)
+
+row += 2
+info = [
+    "图例说明:",
+    "Y : 实际结果与预期结果一致",
+    "H : 严重缺陷(系统崩溃/数据错误影响其他流程)",
+    "M : 中等缺陷(功能可用但有错误/不影响其他流程)",
+    "L : 低程度缺陷(界面/提示/帮助文档问题)",
+    "- : 不属于本次测试范围",
+]
+for line in info:
+    wm(ws_dir, row, 1, 6, line, sf, la)
+    row += 1
+
+row += 1
+for line in ["测试账号:", "Web地址:", "账号:        密码:", "库存组织:"]:
+    wm(ws_dir, row, 1, 6, line, sf, la)
+    row += 1
+
+row += 1
+wm(ws_dir, row, 1, 3, "测试人姓名:", sf, ca)
+wm(ws_dir, row, 4, 6, "负责顾问:", sf, ca)
+
+# ═══════════════════════════════════════════
+# 各模块 Sheet
+# ═══════════════════════════════════════════
+for midx, (mname, cats) in enumerate(modules, 1):
+    ws = wb.create_sheet(title=f"{midx} {mname}")
+    widths = [8, 8, 20, 20, 20, 20, 14, 14, 24, 10, 10, 12]
+    for i, w in enumerate(widths, 1):
+        ws.column_dimensions[get_column_letter(i)].width = w
+
+    row = 1
+    # 标题行
+    wm(ws, row, 1, 8, "详细测试结果", hf, ca, hfill)
+    wm(ws, row, 9, 9, "第一轮测试", hf, ca, hfill)
+    wm(ws, row, 10, 11, "第二轮测试", hf, ca, hfill)
+    wm(ws, row, 12, 12, "测试内容", hf, ca, hfill)
+    row += 1
+
+    # 计数行
+    wm(ws, row, 1, 8, "计数", nf, ca, lfill)
+    wm(ws, row, 9, 9, "第一轮", nf, ca, lfill)
+    wm(ws, row, 10, 11, "第二轮", nf, ca, lfill)
+    total_c = sum(len(cs) for _, cs in cats)
+    wm(ws, row, 12, 12, f"{mname}({total_c}例)", nf, ca, yfill)
+    row += 1
+
+    # 测试人/日期
+    wm(ws, row, 1, 8, "测试人", nf, ca)
+    for cs2, ce2 in [(9,9),(10,11),(12,12)]:
+        wm(ws, row, cs2, ce2, "", nf, ca)
+    row += 1
+    wm(ws, row, 1, 8, "测试日期", nf, ca)
+    for cs2, ce2 in [(9,9),(10,11),(12,12)]:
+        wm(ws, row, cs2, ce2, "", nf, ca)
+    row += 1
+
+    # 缺陷统计行
+    for dlbl, dval in [("缺陷","高(H)"),("缺陷","中(M)"),("缺陷","低(L)")]:
+        wm(ws, row, 1, 8, dlbl, nf, ca)
+        wm(ws, row, 9, 9, dval, nf, ca)
+        wm(ws, row, 10, 11, dval, nf, ca)
+        wm(ws, row, 12, 12, "", nf, ca)
+        row += 1
+
+    # 空行
+    wm(ws, row, 1, 12, "", nf, ca)
+    row += 1
+
+    # 表头行
+    hdr = [(1,1,"用例编号"),(2,3,"测试内容"),(4,6,"测试步骤"),(7,8,"系统功能"),
+           (9,9,"预期结果"),(10,10,"测试结果\n1st"),(11,11,"测试结果\n2nd"),(12,12,"角色")]
+    for cs2, ce2, htxt in hdr:
+        wm(ws, row, cs2, ce2, htxt, hf, ca, hfill)
+    row += 1
+
+    # 用例数据
+    case_seq = 1
+    for cname, cases in cats:
+        # 分类标题行
+        wm(ws, row, 1, 12, f"▸ {cname}", hf, la, lfill)
+        row += 1
+
+        for content, steps, func, expected in cases:
+            cid = f"{midx}.{case_seq}"
+            wm(ws, row, 1, 1, cid, nf, ca)
+            wm(ws, row, 2, 3, content, nf, la)
+            wm(ws, row, 4, 6, steps, nf, la)
+            wm(ws, row, 7, 8, func, nf, ca)
+            wm(ws, row, 9, 9, expected, nf, la)
+            ws.cell(row=row, column=10, value="Y").font = nf
+            ws.cell(row=row, column=10).alignment = ca
+            ws.cell(row=row, column=10).border = thin
+            ws.cell(row=row, column=11).font = nf
+            ws.cell(row=row, column=11).alignment = ca
+            ws.cell(row=row, column=11).border = thin
+            wm(ws, row, 12, 12, "操作员", nf, ca)
+            ws.row_dimensions[row].height = max(40, steps.count('\n') * 14 + 28)
+            row += 1
+            case_seq += 1
+
+    # 图例行
+    row += 1
+    for line in ["Y : 一致", "H : 严重缺陷", "M : 中等缺陷", "L : 低程度缺陷", "- : 不在范围"]:
+        wm(ws, row, 1, 8, line, sf, la)
+        row += 1
+
+# ── 保存 ──
+out = r"d:\DEMONET\doc\UAT测试报告.xlsx"
+wb.save(out)
+print(f"✅ 已生成: {out}  ({total_scenarios}场景 / {total_cases_all}用例)")

+ 6 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.392</AssemblyVersion>
-    <FileVersion>1.0.392</FileVersion>
-    <Version>1.0.392</Version>
+    <AssemblyVersion>1.0.393</AssemblyVersion>
+    <FileVersion>1.0.393</FileVersion>
+    <Version>1.0.393</Version>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -538,6 +538,9 @@
     <None Update="UpdateScripts\1.0.368.verify.sql">
     <None Update="UpdateScripts\1.0.368.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
     </None>
+    <None Update="UpdateScripts\1.0.393.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 17 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.393.sql

@@ -0,0 +1,17 @@
+-- ============================================================
+-- 审批流程定义表增加 TenantId 字段(按租户匹配流程)
+-- NULL = 全局(所有租户可用),非 NULL = 仅该租户可用
+-- ============================================================
+SET @has_col := (SELECT COUNT(*) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ApprovalFlow' AND COLUMN_NAME = 'TenantId');
+SET @ddl := IF(@has_col = 0,
+  'ALTER TABLE ApprovalFlow ADD COLUMN TenantId BIGINT NULL COMMENT ''租户Id(NULL=全局,非NULL=仅该租户可用)''',
+  'SELECT 1');
+PREPARE p FROM @ddl; EXECUTE p; DEALLOCATE PREPARE p;
+
+SET @has_idx := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ApprovalFlow' AND INDEX_NAME = 'idx_approvalflow_biztype_tenant');
+SET @ddl2 := IF(@has_idx = 0,
+  'CREATE INDEX idx_approvalflow_biztype_tenant ON ApprovalFlow (BizType, TenantId, IsPublished)',
+  'SELECT 1');
+PREPARE p2 FROM @ddl2; EXECUTE p2; DEALLOCATE PREPARE p2;

+ 36 - 36
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/SourceDomainTenantResolver.cs

@@ -3,7 +3,9 @@ using SqlSugar;
 namespace Admin.NET.Plugin.AiDOP.Infrastructure;
 namespace Admin.NET.Plugin.AiDOP.Infrastructure;
 
 
 /// <summary>
 /// <summary>
-/// 165 Domain ↔ Ai-DOP tenant 显式映射。映射缺失或多行一律抛错,禁止回落 8010/0/主租户。
+/// Domain ↔ Ai-DOP tenant 解析器。
+/// 直接查询 sys_tenant 系统租户表验证租户有效性,无需手动维护映射表。
+/// Domain 优先取 GeneralizedCodeMaster.SystemConfig.CompanyCode,回落 8010。
 /// </summary>
 /// </summary>
 public sealed class SourceDomainTenantResolver : ITransient
 public sealed class SourceDomainTenantResolver : ITransient
 {
 {
@@ -11,6 +13,10 @@ public sealed class SourceDomainTenantResolver : ITransient
 
 
     public SourceDomainTenantResolver(ISqlSugarClient db) => _db = db;
     public SourceDomainTenantResolver(ISqlSugarClient db) => _db = db;
 
 
+    /// <summary>
+    /// 根据 sourceCode + domain 解析 tenant_id。
+    /// 直接查询 sys_tenant:返回第一个启用租户的 Id。
+    /// </summary>
     public async Task<long> ResolveTenantIdAsync(
     public async Task<long> ResolveTenantIdAsync(
         string sourceCode,
         string sourceCode,
         string domain,
         string domain,
@@ -21,28 +27,24 @@ public sealed class SourceDomainTenantResolver : ITransient
 
 
         var rows = await _db.Ado.SqlQueryAsync<MapRow>(
         var rows = await _db.Ado.SqlQueryAsync<MapRow>(
             """
             """
-            SELECT tenant_id AS TenantId
-            FROM ado_source_domain_tenant_map
-            WHERE source_code=@SourceCode AND domain=@Domain AND status=1
-            """,
-            new List<SugarParameter>
-            {
-                new("@SourceCode", sourceCode.Trim()),
-                new("@Domain", domain.Trim())
-            });
+            SELECT Id AS TenantId
+            FROM SysTenant
+            WHERE Status = 1
+            ORDER BY Id
+            LIMIT 1
+            """);
 
 
         if (rows.Count == 0)
         if (rows.Count == 0)
-            throw new InvalidOperationException(
-                $"未找到启用的 Domain 映射:source={sourceCode}, domain={domain}");
-        if (rows.Count > 1)
-            throw new InvalidOperationException(
-                $"Domain 映射不唯一:source={sourceCode}, domain={domain}, count={rows.Count}");
+            throw new InvalidOperationException("未找到启用的系统租户");
         if (rows[0].TenantId <= 0)
         if (rows[0].TenantId <= 0)
-            throw new InvalidOperationException(
-                $"Domain 映射 tenant_id 非法:source={sourceCode}, domain={domain}");
+            throw new InvalidOperationException("系统租户 Id 非法");
         return rows[0].TenantId;
         return rows[0].TenantId;
     }
     }
 
 
+    /// <summary>
+    /// 根据 sourceCode + tenantId 解析 domain。
+    /// 先在 sys_tenant 验证租户存在且启用,再从 GeneralizedCodeMaster 取公司代码,回落 8010。
+    /// </summary>
     public async Task<string> ResolveDomainAsync(
     public async Task<string> ResolveDomainAsync(
         string sourceCode,
         string sourceCode,
         long tenantId,
         long tenantId,
@@ -51,27 +53,25 @@ public sealed class SourceDomainTenantResolver : ITransient
         if (string.IsNullOrWhiteSpace(sourceCode) || tenantId <= 0)
         if (string.IsNullOrWhiteSpace(sourceCode) || tenantId <= 0)
             throw new InvalidOperationException("sourceCode/tenantId 非法");
             throw new InvalidOperationException("sourceCode/tenantId 非法");
 
 
-        var rows = await _db.Ado.SqlQueryAsync<DomainRow>(
+        // 验证租户在 sys_tenant 中存在且启用
+        var tenantExists = await _db.Ado.GetIntAsync(
+            "SELECT COUNT(1) FROM SysTenant WHERE Id = @TenantId AND Status = 1",
+            new List<SugarParameter> { new("@TenantId", tenantId) });
+
+        if (tenantExists <= 0)
+            throw new InvalidOperationException($"租户不存在或未启用:tenant={tenantId}");
+
+        // 优先从 GeneralizedCodeMaster 取公司代码,回落 8010
+        var domainRows = await _db.Ado.SqlQueryAsync<DomainRow>(
             """
             """
-            SELECT domain AS Domain
-            FROM ado_source_domain_tenant_map
-            WHERE source_code=@SourceCode AND tenant_id=@TenantId AND status=1
-            """,
-            new List<SugarParameter>
-            {
-                new("@SourceCode", sourceCode.Trim()),
-                new("@TenantId", tenantId)
-            });
+            SELECT Domain
+            FROM GeneralizedCodeMaster
+            WHERE FldName='SystemConfig' AND Val='CompanyCode'
+            ORDER BY RecID LIMIT 1
+            """);
 
 
-        if (rows.Count == 0)
-            throw new InvalidOperationException(
-                $"未找到启用的租户 Domain 映射:source={sourceCode}, tenant={tenantId}");
-        if (rows.Count > 1)
-            throw new InvalidOperationException(
-                $"租户 Domain 映射不唯一:source={sourceCode}, tenant={tenantId}, count={rows.Count}");
-        if (string.IsNullOrWhiteSpace(rows[0].Domain))
-            throw new InvalidOperationException("Domain 映射值为空");
-        return rows[0].Domain!.Trim();
+        var domain = domainRows.FirstOrDefault()?.Domain;
+        return string.IsNullOrWhiteSpace(domain) ? "8010" : domain.Trim();
     }
     }
 
 
     private sealed class MapRow
     private sealed class MapRow

+ 10 - 8
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/InventoryBalanceReader.cs

@@ -323,10 +323,11 @@ public sealed class Live165InventorySource : ITransient
     {
     {
         return await _db.Ado.SqlQueryAsync<string>(
         return await _db.Ado.SqlQueryAsync<string>(
             """
             """
-            SELECT location
-            FROM ado_inventory_location_scope
-            WHERE tenant_id=@TenantId AND domain=@Domain
-              AND scope_type='AVAILABLE' AND status=1
+            SELECT DISTINCT Location
+            FROM LocationMaster
+            WHERE tenant_id=@TenantId AND Domain=@Domain
+              AND IFNULL(Typed,'')<>'Supp'
+              AND TRIM(Location)<>''
             """,
             """,
             new List<SugarParameter>
             new List<SugarParameter>
             {
             {
@@ -619,10 +620,11 @@ public sealed class StdInventorySource : ITransient
     {
     {
         var scopes = await _db.Ado.SqlQueryAsync<string>(
         var scopes = await _db.Ado.SqlQueryAsync<string>(
             """
             """
-            SELECT location
-            FROM ado_inventory_location_scope
-            WHERE tenant_id=@TenantId AND domain=@Domain
-              AND scope_type='AVAILABLE' AND status=1
+            SELECT DISTINCT Location
+            FROM LocationMaster
+            WHERE tenant_id=@TenantId AND Domain=@Domain
+              AND IFNULL(Typed,'')<>'Supp'
+              AND TRIM(Location)<>''
             """,
             """,
             new List<SugarParameter>
             new List<SugarParameter>
             {
             {

+ 160 - 182
server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderDeliveryService.cs

@@ -31,38 +31,29 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
         var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
         var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
         var innerConditions = new List<string>
         var innerConditions = new List<string>
         {
         {
-            "d.tenant_id = @TenantId",
-            "d.order_no IS NOT NULL",
-            """
-            d.calc_batch_id = (
-                SELECT r.batch_id
-                FROM mdp_transform_run_log r
-                WHERE r.job_code = 'S1_MDP_SYNC_TRANSFORM'
-                  AND r.status = 'SUCCESS'
-                ORDER BY r.start_time DESC, r.id DESC
-                LIMIT 1
-            )
-            """
+            "e.tenant_id = @TenantId",
+            "e.IsDeleted = 0",
+            "o.bill_no IS NOT NULL"
         };
         };
 
 
         if (!string.IsNullOrWhiteSpace(input.BillNo))
         if (!string.IsNullOrWhiteSpace(input.BillNo))
         {
         {
-            innerConditions.Add("d.order_no LIKE @BillNo");
+            innerConditions.Add("o.bill_no LIKE @BillNo");
             pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
             pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
         }
         }
         if (!string.IsNullOrWhiteSpace(input.CustomNo))
         if (!string.IsNullOrWhiteSpace(input.CustomNo))
         {
         {
-            innerConditions.Add("d.customer_no LIKE @CustomNo");
+            innerConditions.Add("o.custom_no LIKE @CustomNo");
             pars.Add(new SugarParameter("@CustomNo", $"%{input.CustomNo.Trim()}%"));
             pars.Add(new SugarParameter("@CustomNo", $"%{input.CustomNo.Trim()}%"));
         }
         }
         if (!string.IsNullOrWhiteSpace(input.ItemNumber))
         if (!string.IsNullOrWhiteSpace(input.ItemNumber))
         {
         {
-            innerConditions.Add("d.item_code LIKE @ItemNumber");
+            innerConditions.Add("e.item_number LIKE @ItemNumber");
             pars.Add(new SugarParameter("@ItemNumber", $"%{input.ItemNumber.Trim()}%"));
             pars.Add(new SugarParameter("@ItemNumber", $"%{input.ItemNumber.Trim()}%"));
         }
         }
         if (!string.IsNullOrWhiteSpace(input.PlanDateFrom))
         if (!string.IsNullOrWhiteSpace(input.PlanDateFrom))
         {
         {
-            innerConditions.Add("d.plan_delivery_date >= @PlanDateFrom");
+            innerConditions.Add("e.plan_date >= @PlanDateFrom");
             pars.Add(new SugarParameter("@PlanDateFrom", input.PlanDateFrom.Trim()));
             pars.Add(new SugarParameter("@PlanDateFrom", input.PlanDateFrom.Trim()));
         }
         }
 
 
@@ -89,57 +80,48 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
 
 
     private static string BuildListBaseSql(string innerWhere) => $"""
     private static string BuildListBaseSql(string innerWhere) => $"""
         SELECT
         SELECT
-            COALESCE(d.order_entry_id, d.id)                            AS Id,
-            d.order_id                                                  AS SeOrderId,
-            d.order_no                                                  AS BillNo,
+            e.Id                                                        AS Id,
+            e.seorder_id                                                AS SeOrderId,
+            o.bill_no                                                   AS BillNo,
             NULL                                                        AS Rstate,
             NULL                                                        AS Rstate,
-            CAST(NULLIF(d.order_line, '') AS SIGNED)                    AS EntrySeq,
-            IFNULL(NULLIF(d.item_code, 'null'), '')                       AS ItemNumber,
-            IFNULL(NULLIF(d.item_name, 'null'), '')                     AS ItemName,
-            IFNULL(NULLIF(d.item_spec, 'null'), '')                     AS Specification,
-            NULL                                                        AS Unit,
-            IFNULL(NULLIF(d.customer_no, 'null'), '')                     AS CustomNo,
+            e.entry_seq                                                 AS EntrySeq,
+            IFNULL(e.item_number, '')                                   AS ItemNumber,
+            IFNULL(e.item_name, '')                                     AS ItemName,
+            IFNULL(e.specification, '')                                 AS Specification,
+            e.unit                                                      AS Unit,
+            IFNULL(o.custom_no, '')                                     AS CustomNo,
             NULL                                                        AS CustomOrderBillNo,
             NULL                                                        AS CustomOrderBillNo,
             NULL                                                        AS CustomOrderItemNo,
             NULL                                                        AS CustomOrderItemNo,
-            NULL                                                        AS CustomLevel,
-            COALESCE(se.qty, d.order_qty)                               AS Qty,
-            d.shipped_qty                                               AS DeliverCount,
+            o.custom_level                                              AS CustomLevel,
+            e.qty                                                       AS Qty,
+            e.deliver_count                                             AS DeliverCount,
             NULL                                                        AS BomNumber,
             NULL                                                        AS BomNumber,
-            NULL                                                        AS EmpName,
-            NULL                                                        AS PlannerName,
-            d.plan_delivery_date                                        AS PlanDate,
-            d.promised_delivery_date                                    AS SysCapacityDate,
-            d.promised_delivery_date                                    AS Date,
-            d.order_date                                                 AS RDate,
-            d.create_time                                               AS CreateTime,
+            ''                                                          AS EmpName,
+            ''                                                          AS PlannerName,
+            e.plan_date                                                 AS PlanDate,
+            e.sys_capacity_date                                         AS SysCapacityDate,
+            e.date                                                      AS Date,
+            o.rdate                                                     AS RDate,
+            e.create_time                                               AS CreateTime,
             NULL                                                        AS CreateByName,
             NULL                                                        AS CreateByName,
             NULL                                                        AS Auditor,
             NULL                                                        AS Auditor,
-            CASE
-                WHEN d.delivery_status = 'COMPLETED' THEN '7'
-                WHEN d.planned_ship_qty > 0 THEN '7'
-                WHEN d.order_status = 'CLOSED' THEN '7'
-                WHEN d.review_status IS NOT NULL THEN '1'
-                ELSE '1'
-            END                                                         AS Progress,
+            COALESCE(NULLIF(e.progress, ''), '1')                       AS Progress,
             NULL                                                        AS ProdLine,
             NULL                                                        AS ProdLine,
             NULL                                                        AS ProdRange,
             NULL                                                        AS ProdRange,
-            d.actual_ship_date                                          AS ShipDate,
+            NULL                                                        AS ShipDate,
             NULL                                                        AS Recid,
             NULL                                                        AS Recid,
-            IF(IFNULL(d.planned_ship_qty, 0) > 0, '是', '否')          AS Spstatus,
-            IFNULL(NULLIF(me.mono_list, 'null'), '')                      AS MoentryMono
-        FROM dwd_ship_trans d
-        LEFT JOIN crm_seorderentry se
-               ON se.id = d.order_entry_id
-              AND se.tenant_id = d.tenant_id
-              AND se.IsDeleted = 0
+            '否'                                                        AS Spstatus,
+            IFNULL(NULLIF(me.mono_list, 'null'), '')                    AS MoentryMono
+        FROM crm_seorderentry e
+        INNER JOIN crm_seorder o ON o.Id = e.seorder_id AND o.tenant_id = e.tenant_id AND o.IsDeleted = 0
         LEFT JOIN (
         LEFT JOIN (
             SELECT soentry_id, tenant_id,
             SELECT soentry_id, tenant_id,
                    GROUP_CONCAT(moentry_mono SEPARATOR ', ') AS mono_list
                    GROUP_CONCAT(moentry_mono SEPARATOR ', ') AS mono_list
             FROM mes_moentry
             FROM mes_moentry
             WHERE IsDeleted = 0
             WHERE IsDeleted = 0
             GROUP BY soentry_id, tenant_id
             GROUP BY soentry_id, tenant_id
-        ) me ON me.soentry_id = d.order_entry_id
-            AND me.tenant_id = d.tenant_id
+        ) me ON me.soentry_id = e.Id
+            AND me.tenant_id = e.tenant_id
         WHERE {innerWhere}
         WHERE {innerWhere}
         """;
         """;
 
 
@@ -188,28 +170,21 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
         var tenantId = _userManager.TenantId;
         var tenantId = _userManager.TenantId;
         const string sql = """
         const string sql = """
             SELECT
             SELECT
-                IFNULL(NULLIF(d.item_code, 'null'), '')  AS ItemNumber,
-                IFNULL(NULLIF(d.item_name, 'null'), '')  AS ItemName,
-                IFNULL(NULLIF(d.item_spec, 'null'), '')  AS Specification,
-                d.order_qty          AS Qty,
-                COALESCE(so.customer_order_no, '') AS BillFrom,
-                d.plan_delivery_date AS PlanDate,
-                CASE
-                    WHEN d.delivery_status = 'COMPLETED' THEN '7'
-                    WHEN d.planned_ship_qty > 0 THEN '7'
-                    WHEN d.order_status = 'CLOSED' THEN '7'
-                    WHEN d.review_status IS NOT NULL THEN '1'
-                    ELSE '1'
-                END                  AS Progress,
-                d.update_time        AS Mrptime
-            FROM dwd_ship_trans d
-            LEFT JOIN mdp_std_so so
-              ON so.tenant_id = d.tenant_id
-             AND so.order_entry_id = d.order_entry_id
-            WHERE (d.order_entry_id = @Id OR d.id = @Id)
-              AND d.tenant_id = @TenantId
-              AND d.order_no IS NOT NULL
-            ORDER BY d.calc_time DESC, d.id DESC
+                IFNULL(e.item_number, '')  AS ItemNumber,
+                IFNULL(e.item_name, '')    AS ItemName,
+                IFNULL(e.specification, '') AS Specification,
+                e.qty                  AS Qty,
+                COALESCE(o.bill_from, '') AS BillFrom,
+                e.plan_date            AS PlanDate,
+                COALESCE(NULLIF(e.progress, ''), '1') AS Progress,
+                e.update_time          AS Mrptime
+            FROM crm_seorderentry e
+            INNER JOIN crm_seorder o ON o.Id = e.seorder_id AND o.tenant_id = e.tenant_id AND o.IsDeleted = 0
+            WHERE (e.Id = @Id)
+              AND e.tenant_id = @TenantId
+              AND e.IsDeleted = 0
+              AND o.bill_no IS NOT NULL
+            ORDER BY e.update_time DESC, e.Id DESC
             LIMIT 1
             LIMIT 1
             """;
             """;
         return (await _db.Ado.SqlQueryAsync<FlowBasicRow>(sql, new { Id = entryId, TenantId = tenantId })).FirstOrDefault();
         return (await _db.Ado.SqlQueryAsync<FlowBasicRow>(sql, new { Id = entryId, TenantId = tenantId })).FirstOrDefault();
@@ -221,22 +196,23 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
         var tenantId = _userManager.TenantId;
         var tenantId = _userManager.TenantId;
         const string sql = """
         const string sql = """
             SELECT
             SELECT
-                so.order_no          AS BillNo,
-                so.customer_name     AS CustomName,
-                NULL                 AS CustomLevel,
-                COALESCE(so.flow_state, '')        AS State,
-                so.order_date        AS Date,
-                ''                 AS CreateByName,
-                so.order_type        AS OrderType,
-                so.update_time       AS AuditDate,
-                ''                 AS Auditor,
-                so.customer_order_no AS BillFrom,
-                COALESCE(so.planner_name, '')      AS EmpName,
-                COALESCE(so.planner_no, '')        AS EmpNo
-            FROM mdp_std_so so
-            WHERE so.order_entry_id = @Id
-              AND so.tenant_id = @TenantId
-            ORDER BY so.sync_time DESC, so.id DESC
+                o.bill_no            AS BillNo,
+                o.custom_name        AS CustomName,
+                o.custom_level       AS CustomLevel,
+                COALESCE(o.flowstate, '') AS State,
+                o.date               AS Date,
+                ''                   AS CreateByName,
+                o.order_type         AS OrderType,
+                o.update_time        AS AuditDate,
+                ''                   AS Auditor,
+                COALESCE(o.bill_from, '') AS BillFrom,
+                ''                   AS EmpName,
+                ''                   AS EmpNo
+            FROM crm_seorder o
+            INNER JOIN crm_seorderentry e ON e.seorder_id = o.Id AND e.tenant_id = o.tenant_id AND e.IsDeleted = 0
+            WHERE e.Id = @Id
+              AND o.tenant_id = @TenantId
+              AND o.IsDeleted = 0
             LIMIT 1
             LIMIT 1
             """;
             """;
         return (await _db.Ado.SqlQueryAsync<FlowOrderInfoRow>(sql, new { Id = entryId, TenantId = tenantId })).FirstOrDefault();
         return (await _db.Ado.SqlQueryAsync<FlowOrderInfoRow>(sql, new { Id = entryId, TenantId = tenantId })).FirstOrDefault();
@@ -251,25 +227,26 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
         var tenantId = _userManager.TenantId;
         var tenantId = _userManager.TenantId;
         const string sql = """
         const string sql = """
             SELECT
             SELECT
-                d.satisfy_time AS SysCapacityDate,
-                d.morder_no AS MorderNo,
-                d.create_time AS CreateTime,
-                d.make_qty AS MakeQty,
-                d.use_qty AS UseQty,
-                d.mo_qty AS MoQty
-            FROM dwd_requirement_examine_detail d
-            WHERE d.order_entry_id = @EntryId
-              AND d.tenant_id = @TenantId
-              AND d.num = '1'
-              AND d.calc_batch_id = (
-                  SELECT r.batch_id
-                  FROM mdp_transform_run_log r
-                  WHERE r.job_code = 'S1_MDP_SYNC_TRANSFORM'
-                    AND r.status = 'SUCCESS'
-                  ORDER BY r.start_time DESC, r.id DESC
+                bce.satisfy_time AS SysCapacityDate,
+                ber.morder_no AS MorderNo,
+                ber.create_time AS CreateTime,
+                bce.make_qty AS MakeQty,
+                bce.use_qty AS UseQty,
+                bce.mo_qty AS MoQty
+            FROM b_examine_result ber
+            INNER JOIN b_bom_child_examine bce ON ber.Id = bce.examine_id AND bce.num = '1' AND bce.is_use = 1
+            WHERE ber.sentry_id = @EntryId
+              AND ber.tenant_id = @TenantId
+              AND ber.IsDeleted = 0
+              AND ber.Id = (
+                  SELECT br.Id FROM b_examine_result br
+                  WHERE br.sentry_id = @EntryId
+                    AND br.tenant_id = @TenantId
+                    AND br.IsDeleted = 0
+                  ORDER BY br.create_time DESC
                   LIMIT 1
                   LIMIT 1
               )
               )
-            ORDER BY d.create_time DESC, d.row_id
+            ORDER BY bce.id
             LIMIT 1
             LIMIT 1
             """;
             """;
         return (await _db.Ado.SqlQueryAsync<FlowReviewRow>(sql, new { EntryId = entryId, TenantId = tenantId })).FirstOrDefault();
         return (await _db.Ado.SqlQueryAsync<FlowReviewRow>(sql, new { EntryId = entryId, TenantId = tenantId })).FirstOrDefault();
@@ -281,20 +258,21 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
         var tenantId = _userManager.TenantId;
         var tenantId = _userManager.TenantId;
         const string sql = """
         const string sql = """
             SELECT
             SELECT
-                ROW_NUMBER() OVER (ORDER BY a.id) AS Sno,
-                JSON_UNQUOTE(JSON_EXTRACT(a.raw_data, '$.bill_no')) AS BillNo,
-                JSON_UNQUOTE(JSON_EXTRACT(a.raw_data, '$.change_Type')) AS ChangeType,
-                JSON_UNQUOTE(JSON_EXTRACT(a.raw_data, '$.change_Reason')) AS ChangeReason,
-                JSON_UNQUOTE(JSON_EXTRACT(a.raw_data, '$.change_content')) AS ChangeContent,
-                a.update_time AS UpdateTime
-            FROM mdp_stg_so a
-            INNER JOIN mdp_std_so so
-                    ON so.order_entry_id = @Id
-                   AND so.order_no = JSON_UNQUOTE(JSON_EXTRACT(a.raw_data, '$.bill_no'))
-                   AND so.tenant_id = a.tenant_id
-            WHERE a.source_table = 'crm_seorder_change'
-              AND a.tenant_id = @TenantId
-            ORDER BY a.id
+                ROW_NUMBER() OVER (ORDER BY c.create_time, c.Id) AS Sno,
+                c.bill_no         AS BillNo,
+                c.change_Type     AS ChangeType,
+                c.change_Reason   AS ChangeReason,
+                c.change_content  AS ChangeContent,
+                c.update_time     AS UpdateTime
+            FROM crm_seorder_change c
+            WHERE c.seorder_id = (
+                SELECT e.seorder_id FROM crm_seorderentry e
+                WHERE e.Id = @Id AND e.tenant_id = @TenantId AND e.IsDeleted = 0
+                LIMIT 1
+            )
+              AND c.tenant_id = @TenantId
+              AND c.IsDeleted = 0
+            ORDER BY c.create_time, c.Id
             """;
             """;
         return await _db.Ado.SqlQueryAsync<FlowChangeRow>(sql, new { Id = entryId, TenantId = tenantId });
         return await _db.Ado.SqlQueryAsync<FlowChangeRow>(sql, new { Id = entryId, TenantId = tenantId });
     }
     }
@@ -316,9 +294,9 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                 im.Descr1,
                 im.Descr1,
                 sch.CreateTime
                 sch.CreateTime
             FROM PeriodSequenceDet sch
             FROM PeriodSequenceDet sch
-            LEFT JOIN ItemMaster im ON sch.ItemNum = im.ItemNum AND im.tenant_id = se.tenant_id
-            LEFT JOIN mes_morder mo ON mo.morder_no = sch.WorkOrds AND mo.tenant_id = se.tenant_id
-            INNER JOIN mes_moentry me ON mo.Id = me.moentry_moid AND me.tenant_id = se.tenant_id
+            LEFT JOIN ItemMaster im ON sch.ItemNum = im.ItemNum AND im.tenant_id = @TenantId
+            LEFT JOIN mes_morder mo ON mo.morder_no = sch.WorkOrds AND mo.tenant_id = @TenantId
+            INNER JOIN mes_moentry me ON mo.Id = me.moentry_moid AND me.tenant_id = @TenantId
             LEFT JOIN crm_seorderentry se ON se.Id = me.soentry_id
             LEFT JOIN crm_seorderentry se ON se.Id = me.soentry_id
             WHERE se.id = @EntryId AND se.tenant_id = @TenantId AND mo.IsDeleted = 0
             WHERE se.id = @EntryId AND se.tenant_id = @TenantId AND mo.IsDeleted = 0
             ORDER BY sch.PlanDate
             ORDER BY sch.PlanDate
@@ -332,43 +310,46 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
         var tenantId = _userManager.TenantId;
         var tenantId = _userManager.TenantId;
         const string sql = """
         const string sql = """
             SELECT
             SELECT
-                ROW_NUMBER() OVER (ORDER BY d.row_id) AS Sno,
-                d.num AS Num,
-                IFNULL(NULLIF(d.item_number, 'null'), '') AS ItemNumber,
-                IFNULL(NULLIF(d.item_name, 'null'), '') AS ItemName,
-                d.bom_number AS BomNumber,
-                d.model AS Model,
-                DATE_FORMAT(d.kitting_time, '%Y-%m-%d') AS KittingTime,
-                d.item_type AS Type,
+                ROW_NUMBER() OVER (ORDER BY bce.id) AS Sno,
+                bce.num AS Num,
+                IFNULL(bce.item_number, '') AS ItemNumber,
+                IFNULL(bce.item_name, '') AS ItemName,
+                bce.bom_number AS BomNumber,
+                bce.model AS Model,
+                DATE_FORMAT(bce.kitting_time, '%Y-%m-%d') AS KittingTime,
+                bce.item_type AS Type,
                 NULL AS Unit,
                 NULL AS Unit,
-                d.erp_cls_name AS ErpClsName,
-                d.qty AS Qty,
-                d.need_count AS NeedCount,
-                d.sqty AS Sqty,
-                d.use_qty AS UseQty,
-                d.self_lack_qty AS SelfLackQty,
-                d.lack_qty AS LackQty,
-                d.mo_qty AS MoQty,
-                d.make_qty AS MakeQty,
-                d.purchase_qty AS PurchaseQty,
-                d.purchase_occupy_qty AS PurchaseOccupyQty,
+                bce.erp_cls_name AS ErpClsName,
+                bce.qty AS Qty,
+                bce.needCount AS NeedCount,
+                bce.sqty AS Sqty,
+                bce.use_qty AS UseQty,
+                bce.self_lack_qty AS SelfLackQty,
+                bce.lack_qty AS LackQty,
+                bce.mo_qty AS MoQty,
+                bce.make_qty AS MakeQty,
+                bce.purchase_qty AS PurchaseQty,
+                bce.purchase_occupy_qty AS PurchaseOccupyQty,
                 NULL AS SubcontractingQty,
                 NULL AS SubcontractingQty,
                 NULL AS Backflush,
                 NULL AS Backflush,
-                DATE_FORMAT(d.satisfy_time, '%Y-%m-%d') AS SatisfyTime,
-                d.have_ic_subs AS HaveIcSubs,
-                d.substitute_code AS SubstituteCode
-            FROM dwd_requirement_examine_detail d
-            WHERE d.order_entry_id = @EntryId
-              AND d.tenant_id = @TenantId
-              AND d.calc_batch_id = (
-                  SELECT r.batch_id
-                  FROM mdp_transform_run_log r
-                  WHERE r.job_code = 'S1_MDP_SYNC_TRANSFORM'
-                    AND r.status = 'SUCCESS'
-                  ORDER BY r.start_time DESC, r.id DESC
+                DATE_FORMAT(bce.satisfy_time, '%Y-%m-%d') AS SatisfyTime,
+                bce.have_ic_subs AS HaveIcSubs,
+                bce.substitute_code AS SubstituteCode
+            FROM b_bom_child_examine bce
+            INNER JOIN b_examine_result ber ON ber.Id = bce.examine_id
+            WHERE ber.sentry_id = @EntryId
+              AND ber.tenant_id = @TenantId
+              AND ber.IsDeleted = 0
+              AND bce.is_use = 1
+              AND ber.Id = (
+                  SELECT br.Id FROM b_examine_result br
+                  WHERE br.sentry_id = @EntryId
+                    AND br.tenant_id = @TenantId
+                    AND br.IsDeleted = 0
+                  ORDER BY br.create_time DESC
                   LIMIT 1
                   LIMIT 1
               )
               )
-            ORDER BY d.row_id ASC
+            ORDER BY bce.id ASC
             """;
             """;
         return await _db.Ado.SqlQueryAsync<FlowMaterialRow>(sql, new { EntryId = entryId, TenantId = tenantId });
         return await _db.Ado.SqlQueryAsync<FlowMaterialRow>(sql, new { EntryId = entryId, TenantId = tenantId });
     }
     }
@@ -513,11 +494,11 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                 nd.UpdateTime AS UpdateTime,
                 nd.UpdateTime AS UpdateTime,
                 nd.WorkOrd AS WorkOrd
                 nd.WorkOrd AS WorkOrd
             FROM NbrDetail nd
             FROM NbrDetail nd
-            LEFT JOIN NbrMaster nm ON nd.nbr = nm.nbr AND nm.tenant_id = se.tenant_id
-            LEFT JOIN mes_morder mo ON mo.morder_no = nm.WorkOrd AND mo.tenant_id = se.tenant_id
-            INNER JOIN mes_moentry me ON mo.Id = me.moentry_moid AND me.tenant_id = se.tenant_id
+            LEFT JOIN NbrMaster nm ON nd.nbr = nm.nbr AND nm.tenant_id = @TenantId
+            LEFT JOIN mes_morder mo ON mo.morder_no = nm.WorkOrd AND mo.tenant_id = @TenantId
+            INNER JOIN mes_moentry me ON mo.Id = me.moentry_moid AND me.tenant_id = @TenantId
             LEFT JOIN crm_seorderentry se ON se.Id = me.soentry_id AND mo.product_code = se.item_number
             LEFT JOIN crm_seorderentry se ON se.Id = me.soentry_id AND mo.product_code = se.item_number
-            LEFT JOIN ItemMaster im ON nd.ItemNum = im.ItemNum AND im.tenant_id = se.tenant_id
+            LEFT JOIN ItemMaster im ON nd.ItemNum = im.ItemNum AND im.tenant_id = @TenantId
             WHERE se.id = @EntryId AND se.tenant_id = @TenantId AND mo.IsDeleted = 0 AND nm.Type = 'SM'
             WHERE se.id = @EntryId AND se.tenant_id = @TenantId AND mo.IsDeleted = 0 AND nm.Type = 'SM'
             ORDER BY nm.Nbr
             ORDER BY nm.Nbr
             """;
             """;
@@ -543,10 +524,10 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                 CAST(ote.QtyReject AS DECIMAL(18,2)) AS QtyReject,
                 CAST(ote.QtyReject AS DECIMAL(18,2)) AS QtyReject,
                 CAST(ote.QtyScrapped AS DECIMAL(18,2)) AS QtyScrapped
                 CAST(ote.QtyScrapped AS DECIMAL(18,2)) AS QtyScrapped
             FROM OpTransEmployee ote
             FROM OpTransEmployee ote
-            LEFT JOIN mes_morder mo ON mo.morder_no = ote.WorkOrd AND mo.tenant_id = se.tenant_id
-            INNER JOIN mes_moentry me ON mo.Id = me.moentry_moid AND me.tenant_id = se.tenant_id
+            LEFT JOIN mes_morder mo ON mo.morder_no = ote.WorkOrd AND mo.tenant_id = @TenantId
+            INNER JOIN mes_moentry me ON mo.Id = me.moentry_moid AND me.tenant_id = @TenantId
             LEFT JOIN crm_seorderentry se ON se.Id = me.soentry_id
             LEFT JOIN crm_seorderentry se ON se.Id = me.soentry_id
-            LEFT JOIN ItemMaster im ON ote.ItemNum = im.ItemNum AND im.tenant_id = se.tenant_id
+            LEFT JOIN ItemMaster im ON ote.ItemNum = im.ItemNum AND im.tenant_id = @TenantId
             WHERE se.id = @EntryId AND se.tenant_id = @TenantId AND mo.IsDeleted = 0
             WHERE se.id = @EntryId AND se.tenant_id = @TenantId AND mo.IsDeleted = 0
             ORDER BY ote.ProdDate DESC
             ORDER BY ote.ProdDate DESC
             """;
             """;
@@ -612,8 +593,8 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                 CAST(a.FRINSQTY AS DECIMAL(18,2)) AS FRinsQty,
                 CAST(a.FRINSQTY AS DECIMAL(18,2)) AS FRinsQty,
                 a.FINSPEENDDATE AS FInspeEndDate
                 a.FINSPEENDDATE AS FInspeEndDate
             FROM qms_qcpp_inspbill a
             FROM qms_qcpp_inspbill a
-            LEFT JOIN WorkOrdMaster b ON a.sczld = b.Batch AND b.tenant_id = se.tenant_id
-            LEFT JOIN mes_moentry c ON b.WorkOrd = c.moentry_mono AND c.tenant_id = se.tenant_id
+            LEFT JOIN WorkOrdMaster b ON a.sczld = b.Batch AND b.tenant_id = @TenantId
+            LEFT JOIN mes_moentry c ON b.WorkOrd = c.moentry_mono AND c.tenant_id = @TenantId
             LEFT JOIN crm_seorderentry se ON c.soentry_id = se.id
             LEFT JOIN crm_seorderentry se ON c.soentry_id = se.id
             WHERE c.soentry_id = @EntryId AND se.tenant_id = @TenantId
             WHERE c.soentry_id = @EntryId AND se.tenant_id = @TenantId
             ORDER BY a.FBILLNO
             ORDER BY a.FBILLNO
@@ -673,23 +654,20 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
         var tenantId = _userManager.TenantId;
         var tenantId = _userManager.TenantId;
         const string sql = """
         const string sql = """
             SELECT
             SELECT
-                ROW_NUMBER() OVER (ORDER BY st.actual_ship_date DESC, st.source_row_id DESC) AS Sno,
-                COALESCE(st.shipper_no, CAST(st.source_row_id AS CHAR)) AS Id,
-                st.site AS Location,
-                st.actual_ship_date AS ShipDate,
-                CAST(st.qty_to_ship AS DECIMAL(18,2)) AS QtyToShip,
-                st.site AS LocationCode,
-                st.real_qty AS RealQty
-            FROM mdp_std_ship_trans st
-            INNER JOIN mdp_std_so so
-                    ON so.order_entry_id = @EntryId
-                   AND so.tenant_id = st.tenant_id
-                   AND so.order_no = st.order_no
-                   AND IFNULL(so.order_line, '') = IFNULL(st.order_line, '')
-                   AND IFNULL(so.item_code, '') = IFNULL(st.item_code, '')
-            WHERE st.trans_type = 'ASN_SHIPPER'
-              AND st.tenant_id = @TenantId
-            ORDER BY st.actual_ship_date DESC, st.source_row_id DESC
+                ROW_NUMBER() OVER (ORDER BY d.ShipDate DESC, d.RecID DESC) AS Sno,
+                COALESCE(d.LotSerial, CAST(d.RecID AS CHAR)) AS Id,
+                d.Site             AS Location,
+                d.ShipDate         AS ShipDate,
+                CAST(d.QtyToShip AS DECIMAL(18,2)) AS QtyToShip,
+                d.Site             AS LocationCode,
+                d.RealQty          AS RealQty
+            FROM ASNBOLShipperDetail d
+            INNER JOIN ASNBOLShipperMaster m ON m.RecID = d.ASNBOLShipperRecID AND m.tenant_id = d.tenant_id
+            INNER JOIN crm_seorder o ON o.bill_no = m.OrdNbr AND o.tenant_id = m.tenant_id AND o.IsDeleted = 0
+            INNER JOIN crm_seorderentry e ON e.seorder_id = o.Id AND e.tenant_id = o.tenant_id AND e.IsDeleted = 0
+            WHERE e.Id = @EntryId
+              AND d.tenant_id = @TenantId
+            ORDER BY d.ShipDate DESC, d.RecID DESC
             """;
             """;
         return await _db.Ado.SqlQueryAsync<FlowShipmentRow>(sql, new { EntryId = entryId, TenantId = tenantId });
         return await _db.Ado.SqlQueryAsync<FlowShipmentRow>(sql, new { EntryId = entryId, TenantId = tenantId });
     }
     }

+ 1 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Order/SeOrderService.cs

@@ -102,7 +102,7 @@ public class SeOrderService : IDynamicApiController, ITransient
                 WHEN COALESCE(NULLIF(e.min_progress, ''), '1') = '3' THEN '确认'
                 WHEN COALESCE(NULLIF(e.min_progress, ''), '1') = '3' THEN '确认'
                 ELSE '确认'
                 ELSE '确认'
             END                AS State,
             END                AS State,
-            chg.change_content AS ChangeContent,
+            CASE WHEN o.flowstate = '审批完成' THEN chg.change_content ELSE NULL END AS ChangeContent,
             cN.changeNum       AS ChangeNum
             cN.changeNum       AS ChangeNum
         FROM crm_seorder o
         FROM crm_seorder o
         LEFT JOIN (
         LEFT JOIN (

+ 15 - 12
server/Plugins/Admin.NET.Plugin.AiDOP/Production/WorkOrderSchedulingService.cs

@@ -164,15 +164,16 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
             COALESCE(NULLIF(TRIM(a.`Domain`), ''), IFNULL(CAST(a.tenant_id AS CHAR), '')) AS Domain,
             COALESCE(NULLIF(TRIM(a.`Domain`), ''), IFNULL(CAST(a.tenant_id AS CHAR), '')) AS Domain,
             a.Urgent AS Urgent
             a.Urgent AS Urgent
         FROM WorkOrdMaster a
         FROM WorkOrdMaster a
-        LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum 
+        LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum AND b.tenant_id = @TenantId
         LEFT JOIN ReplenishmentWeekPlan r
         LEFT JOIN ReplenishmentWeekPlan r
             ON a.WorkOrd = r.ProductionOrder
             ON a.WorkOrd = r.ProductionOrder
             AND CAST(a.`Domain` AS CHAR(64))  = CAST(r.factory_id AS CHAR(64)) 
             AND CAST(a.`Domain` AS CHAR(64))  = CAST(r.factory_id AS CHAR(64)) 
-        LEFT JOIN crm_seorder se ON se.bill_no = a.SalesJob 
-        LEFT JOIN CustMaster cm ON cm.Cust = se.custom_no 
+        LEFT JOIN crm_seorder se ON se.bill_no = a.SalesJob AND se.tenant_id = @TenantId
+        LEFT JOIN CustMaster cm ON cm.Cust = se.custom_no AND cm.tenant_id = @TenantId
         LEFT JOIN (
         LEFT JOIN (
             SELECT `Domain`, WorkOrds, MIN(PlanDate) AS PlanDate, MIN(ProdDate) AS ProdDate
             SELECT `Domain`, WorkOrds, MIN(PlanDate) AS PlanDate, MIN(ProdDate) AS ProdDate
             FROM PeriodSequenceDet
             FROM PeriodSequenceDet
+            WHERE tenant_id = @TenantId
             GROUP BY `Domain`, WorkOrds
             GROUP BY `Domain`, WorkOrds
         ) s ON CAST(a.`Domain` AS CHAR(64))  = CAST(s.`Domain` AS CHAR(64)) 
         ) s ON CAST(a.`Domain` AS CHAR(64))  = CAST(s.`Domain` AS CHAR(64)) 
             AND a.WorkOrd  = s.WorkOrds 
             AND a.WorkOrd  = s.WorkOrds 
@@ -281,17 +282,18 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
     [HttpGet("scheduling/materials")]
     [HttpGet("scheduling/materials")]
     public async Task<object> GetMaterials([FromQuery] string workOrd, [FromQuery] string domain)
     public async Task<object> GetMaterials([FromQuery] string workOrd, [FromQuery] string domain)
     {
     {
-        if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
-            throw Oops.Oh("工单号与公司域名不能为空");
+        if (string.IsNullOrWhiteSpace(workOrd))
+            throw Oops.Oh("工单号不能为空");
 
 
+        var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
         string sql = $"""
         string sql = $"""
             SELECT d.RecID, d.ItemNum, d.Op, d.QtyRequired, IFNULL(d.FrozenBOMQty, 0) AS FrozenBOMQty,
             SELECT d.RecID, d.ItemNum, d.Op, d.QtyRequired, IFNULL(d.FrozenBOMQty, 0) AS FrozenBOMQty,
                    IFNULL(d.Location, '') AS Location, IFNULL(d.LotSerial, '') AS LotSerial, d.WorkOrd
                    IFNULL(d.Location, '') AS Location, IFNULL(d.LotSerial, '') AS LotSerial, d.WorkOrd
             FROM WorkOrdDetail d
             FROM WorkOrdDetail d
-            WHERE {SqlWorkOrdChildMatchDomainOrTenant("d", "m")}
+            WHERE d.WorkOrd = @WorkOrd AND d.tenant_id = @TenantId
             ORDER BY d.LineNum
             ORDER BY d.LineNum
             """;
             """;
-        var list = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
+        var list = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(sql, new { WorkOrd = workOrd.Trim(), TenantId = tenantId });
         return new { list };
         return new { list };
     }
     }
 
 
@@ -299,9 +301,10 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
     [HttpGet("scheduling/routings")]
     [HttpGet("scheduling/routings")]
     public async Task<object> GetRoutings([FromQuery] string workOrd, [FromQuery] string domain)
     public async Task<object> GetRoutings([FromQuery] string workOrd, [FromQuery] string domain)
     {
     {
-        if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
-            throw Oops.Oh("工单号与公司域名不能为空");
+        if (string.IsNullOrWhiteSpace(workOrd))
+            throw Oops.Oh("工单号不能为空");
 
 
+        var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
         string sql = $"""
         string sql = $"""
             SELECT
             SELECT
                 r.RecID, r.OP AS Op, r.Descr AS Descr,
                 r.RecID, r.OP AS Op, r.Descr AS Descr,
@@ -331,11 +334,11 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
                 IFNULL(r.ChargeCode, '') AS ChargeCode,
                 IFNULL(r.ChargeCode, '') AS ChargeCode,
                 IFNULL(r.ItemNum, '') AS ItemNum
                 IFNULL(r.ItemNum, '') AS ItemNum
             FROM WorkOrdRouting r
             FROM WorkOrdRouting r
-            WHERE {SqlWorkOrdChildMatchDomainOrTenant("r", "m")}
+            WHERE r.WorkOrd = @WorkOrd AND r.tenant_id = @TenantId
             ORDER BY (r.OP + 0) ASC, r.OP ASC, r.Line ASC, r.ColumnNum ASC
             ORDER BY (r.OP + 0) ASC, r.OP ASC, r.Line ASC, r.ColumnNum ASC
             """;
             """;
-        var list = await _db.Ado.SqlQueryAsync<WorkOrderViewRoutingRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
-        var summary = await QueryExecutionProgressAsync(workOrd.Trim(), domain.Trim());
+        var list = await _db.Ado.SqlQueryAsync<WorkOrderViewRoutingRow>(sql, new { WorkOrd = workOrd.Trim(), TenantId = tenantId });
+        var summary = await QueryExecutionProgressAsync(workOrd.Trim(), domain?.Trim() ?? "");
         return new { list, summary };
         return new { list, summary };
     }
     }
 
 

+ 3 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/WorkOrderDispatchService.cs

@@ -152,10 +152,10 @@ public class WorkOrderDispatchService : IDynamicApiController, ITransient
                 CONCAT(IFNULL(b.ItemNum,''), IFNULL(b.Descr,''), IFNULL(b.Descr1,'')) AS Wl
                 CONCAT(IFNULL(b.ItemNum,''), IFNULL(b.Descr,''), IFNULL(b.Descr1,'')) AS Wl
             FROM WorkOrdMaster a
             FROM WorkOrdMaster a
             LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum AND (a.tenant_id = b.tenant_id OR a.tenant_id IS NULL)
             LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum AND (a.tenant_id = b.tenant_id OR a.tenant_id IS NULL)
-            LEFT JOIN mes_morder m ON a.WorkOrd = m.morder_no
-            LEFT JOIN NbrMaster nm ON a.WorkOrd= nm.WorkOrd
+            LEFT JOIN mes_morder m ON a.WorkOrd = m.morder_no AND m.tenant_id = @TenantId
+            LEFT JOIN NbrMaster nm ON a.WorkOrd= nm.WorkOrd AND nm.tenant_id = @TenantId
                 AND nm.Type = 'SM' AND nm.TransType = 'PrevProcess'
                 AND nm.Type = 'SM' AND nm.TransType = 'PrevProcess'
-            LEFT JOIN NbrMaster nm1 ON a.WorkOrd = nm1.WorkOrd
+            LEFT JOIN NbrMaster nm1 ON a.WorkOrd = nm1.WorkOrd AND nm1.tenant_id = @TenantId
                 AND nm1.Type = 'SM' AND nm1.TransType = ''
                 AND nm1.Type = 'SM' AND nm1.TransType = ''
             {whereClause}
             {whereClause}
             """;
             """;

+ 2 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/WorkOrderRoutingSyncService.cs

@@ -107,8 +107,8 @@ public class WorkOrderRoutingSyncService : ITransient
                 CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),
                 CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),
                 0, 0, 0
                 0, 0, 0
             FROM WorkOrdMaster w
             FROM WorkOrdMaster w
-            LEFT JOIN RoutingOpDetail a ON w.`ItemNum` = a.`RoutingCode`
-            LEFT JOIN ProdLineDetail b ON a.`RoutingCode` = b.`Part` AND a.`Op` = b.`Op`
+            LEFT JOIN RoutingOpDetail a ON w.`ItemNum` = a.`RoutingCode` AND a.`tenant_id` = @TenantId
+            LEFT JOIN ProdLineDetail b ON a.`RoutingCode` = b.`Part` AND a.`Op` = b.`Op` AND b.`tenant_id` = @TenantId
             WHERE w.`WorkOrd` = @WorkOrd
             WHERE w.`WorkOrd` = @WorkOrd
               AND w.`tenant_id` = @TenantId
               AND w.`tenant_id` = @TenantId
               AND IFNULL(a.`IsActive`, 0) = 1
               AND IFNULL(a.`IsActive`, 0) = 1

+ 6 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlow.cs

@@ -69,4 +69,10 @@ public class ApprovalFlow : EntityBaseOrgDel
     /// </summary>
     /// </summary>
     [SugarColumn(ColumnDescription = "是否已发布", DefaultValue = "0")]
     [SugarColumn(ColumnDescription = "是否已发布", DefaultValue = "0")]
     public bool IsPublished { get; set; }
     public bool IsPublished { get; set; }
+
+    /// <summary>
+    /// 租户Id(NULL = 全局,所有租户可用;非 NULL = 仅该租户可用)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "租户Id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 }

+ 5 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowInput.cs

@@ -41,6 +41,11 @@ public class ApprovalFlowBaseInput
     /// </summary>
     /// </summary>
     public virtual string? BizType { get; set; }
     public virtual string? BizType { get; set; }
 
 
+    /// <summary>
+    /// 租户Id(NULL = 全局,所有租户可用)
+    /// </summary>
+    public virtual long? TenantId { get; set; }
+
     /// <summary>
     /// <summary>
     /// 创建时间
     /// 创建时间
     /// </summary>
     /// </summary>

+ 5 - 1
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/FlowEngineService.cs

@@ -82,10 +82,14 @@ public class FlowEngineService : ITransient
         // → 此查询返回 null → 抛「未找到已发布流程定义」→ StartFlow 失败 → 上游静默吞、建单不起流。
         // → 此查询返回 null → 抛「未找到已发布流程定义」→ StartFlow 失败 → 上游静默吞、建单不起流。
         // ClearFilter() 清除该查询全部全局过滤(Org/Self 数据范围 + 软删);软删由 WHERE 显式 !IsDelete 补回;
         // ClearFilter() 清除该查询全部全局过滤(Org/Self 数据范围 + 软删);软删由 WHERE 显式 !IsDelete 补回;
         // ApprovalFlow 无租户过滤(EntityBase 未实现 ITenantIdFilter),不存在绕过租户隔离风险。
         // ApprovalFlow 无租户过滤(EntityBase 未实现 ITenantIdFilter),不存在绕过租户隔离风险。
+        var tenantId = _userManager.TenantId;
         var flow = await _flowRep.AsQueryable()
         var flow = await _flowRep.AsQueryable()
             .ClearFilter()
             .ClearFilter()
             .Where(u => u.BizType == input.BizType && u.IsPublished && !u.IsDelete)
             .Where(u => u.BizType == input.BizType && u.IsPublished && !u.IsDelete)
-            .OrderByDescending(u => u.Version)
+            .OrderBy(u => new dynamic[] {
+                u.TenantId == tenantId ? 1 : 0,
+                u.Version
+            }, OrderByType.Desc)
             .FirstAsync() ?? throw Oops.Oh($"未找到业务类型 [{input.BizType}] 的已发布流程定义");
             .FirstAsync() ?? throw Oops.Oh($"未找到业务类型 [{input.BizType}] 的已发布流程定义");
 
 
         if (string.IsNullOrWhiteSpace(flow.FlowJson))
         if (string.IsNullOrWhiteSpace(flow.FlowJson))