Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	Web/package.json
#	server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
YY968XX 2 недель назад
Родитель
Сommit
f4fb8e9c43

+ 16 - 1
Web/src/views/aidop/api/executableDailyPlan.ts

@@ -39,8 +39,23 @@ export interface ExecutableDailyPlanRow {
 	background?: string | null;
 }
 
+function unwrapDailyPlanPaged<T>(raw: unknown): Paged<T> {
+	const d = raw as Record<string, unknown> | null | undefined;
+	if (d && Array.isArray(d.list)) return d as Paged<T>;
+	const inner = d?.result as Record<string, unknown> | undefined;
+	if (inner && Array.isArray(inner.list)) return inner as Paged<T>;
+	return {
+		total: Number(d?.total ?? inner?.total ?? 0),
+		page: Number(d?.page ?? inner?.page ?? 1),
+		pageSize: Number(d?.pageSize ?? inner?.pageSize ?? 20),
+		list: [],
+	};
+}
+
 export function fetchExecutableDailyPlanList(params: Record<string, unknown>) {
-	return service.get<Paged<ExecutableDailyPlanRow>>('/api/Production/daily-plan/list', { params }).then((r) => r.data);
+	return service
+		.get<unknown>('/api/Production/daily-plan/list', { params })
+		.then((r) => unwrapDailyPlanPaged<ExecutableDailyPlanRow>(r.data));
 }
 
 export function releaseExecutableDailyPlan(body: { domain: string; ids: string }) {

+ 22 - 20
Web/src/views/aidop/api/workOrderScheduling.ts

@@ -1,5 +1,4 @@
 import service from '/@/utils/request';
-import axios from 'axios';
 
 /** 与工单下达页一致的外部资源接口 */
 export const EXTERNAL_RESOURCE_BASE = 'http://123.60.180.165:8087';
@@ -61,27 +60,29 @@ export function syncMaterialRequirement(domain: string) {
 	});
 }
 
-/** 优先级调整保存(GET,与 replenishment 接口约定一致) */
-export function replenishmentWorkOrdCheckChangePriority(params: {
-	workOrd: string;
+/** 优先级调整保存(GET;与工单下达 resource-examine 约定一致:须等响应体为 `ok`(忽略大小写)才算成功) */
+export async function replenishmentWorkOrdCheckChangePriority(params: {
+	workord: string;
 	qty: string | number;
-	lotSerial: string;
+	instockdate: string;
 	priority: string;
-	factory_id: string;
-	userNo: string;
-}) {
-	return axios
-		.get(`${EXTERNAL_RESOURCE_BASE}/api/business/replenishmentWorkOrdCheckChangePriority`, {
-			params: {
-				WorkOrd: params.workOrd,
-				qty: params.qty,
-				LotSerial: params.lotSerial ?? '',
-				priority: params.priority ?? '',
-				factory_id: params.factory_id,
-				UserNO: params.userNo,
-			},
-		})
-		.then((r) => r.data);
+	domain: string;
+	userAccount: string;
+	lotSerial: string;
+}): Promise<void> {
+	const qs = new URLSearchParams();
+	qs.append('workord', params.workord);
+	qs.append('qty', String(params.qty ?? ''));
+	qs.append('instockdate', params.instockdate ?? '');
+	qs.append('priority', params.priority ?? '');
+	qs.append('domain', params.domain);
+	qs.append('userAccount', params.userAccount);
+	qs.append('LotSerial', params.lotSerial ?? '');
+	const url = `${EXTERNAL_RESOURCE_BASE}/api/business/resource-examine/WorkOrdUpdateByNo?${qs.toString()}`;
+	const r = await fetch(url, { method: 'GET' });
+	if (!r.ok) throw new Error(`优先级调整接口失败(HTTP ${r.status})`);
+	const body = String((await r.text()) ?? '').trim();
+	if (body.toLowerCase() !== 'ok') throw new Error(body ? `接口返回异常:${body}` : '接口返回为空');
 }
 
 export function fetchWorkOrderSchedulingList(params: Record<string, unknown>) {
@@ -124,6 +125,7 @@ export function fetchSchedulingEditPreview(workOrd: string, domain: string) {
 			priority: string;
 			lotSerial: string;
 			urgent: number;
+			ordDate?: string | null;
 		}>('/api/Production/scheduling/edit-preview', { params: { workOrd, domain } })
 		.then((r) => r.data);
 }

+ 18 - 1
Web/src/views/aidop/production/executableDailyPlanList.vue

@@ -1,5 +1,8 @@
 <template>
-	<AidopDemoShell :title="pageTitle" subtitle="当前日期往后的日计划(PeriodSequenceDet)">
+	<AidopDemoShell
+		:title="pageTitle"
+		subtitle="默认仅「计划日期≥今天」且工单未关闭等条件;若在库里能查到但列表为空,请把「计划日期≥」调到更早(与接口 PlanDateMin 一致)。"
+	>
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
 			<el-form-item label="生产指令">
 				<el-input v-model="query.workOrds" placeholder="模糊" clearable style="width: 130px" />
@@ -32,6 +35,16 @@
 			<el-form-item label="工序">
 				<el-input v-model="query.opStdOp" placeholder="模糊" clearable style="width: 120px" />
 			</el-form-item>
+			<el-form-item label="计划日期≥">
+				<el-date-picker
+					v-model="query.planDateMin"
+					type="date"
+					value-format="YYYY-MM-DD"
+					placeholder="留空=仅今天起"
+					clearable
+					style="width: 150px"
+				/>
+			</el-form-item>
 			<el-form-item label="开工时间>=">
 				<el-date-picker
 					v-model="query.workStartFrom"
@@ -198,6 +211,8 @@ const query = reactive({
 	occupyEquipmentType: '',
 	opStdOp: '',
 	workStartFrom: '' as string,
+	/** 不传时后端用「计划日期≥今天」;选过去日期可拉出历史日计划行 */
+	planDateMin: '' as string,
 	page: 1,
 	pageSize: 20,
 });
@@ -245,6 +260,7 @@ function resetQuery() {
 	query.occupyEquipmentType = '';
 	query.opStdOp = '';
 	query.workStartFrom = '';
+	query.planDateMin = '';
 	query.page = 1;
 	loadList();
 }
@@ -262,6 +278,7 @@ async function loadList() {
 			occupyEquipmentType: query.occupyEquipmentType || undefined,
 			opStdOp: query.opStdOp || undefined,
 			workStartFrom: query.workStartFrom || undefined,
+			planDateMin: query.planDateMin || undefined,
 			page: query.page,
 			pageSize: query.pageSize,
 		});

+ 24 - 17
Web/src/views/aidop/production/workOrderSchedulingList.vue

@@ -119,7 +119,7 @@
 			<WorkOrderSchedulingPriorityForm
 				v-if="editVisible && editingRow"
 				:work-ord="editingRow.workOrd ?? ''"
-				:domain="editingRow.domain ?? ''"
+				:domain="schedulingDomainForRow(editingRow)"
 				@cancel="editVisible = false"
 				@saved="onSaved"
 			/>
@@ -129,7 +129,7 @@
 			<WorkOrderSchedulingViewForm
 				v-if="viewVisible && viewingRow"
 				:work-ord="viewingRow.workOrd ?? ''"
-				:domain="viewingRow.domain ?? ''"
+				:domain="schedulingDomainForRow(viewingRow)"
 				@cancel="viewVisible = false"
 			/>
 		</el-dialog>
@@ -160,6 +160,16 @@ const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || '工单工序排产');
 const userInfoStore = useUserInfo();
 
+/** 列表行上的「公司域」:优先 WorkOrdMaster.Domain,为空时用当前租户 id(与后端 tenant_id 兜底一致)。 */
+function schedulingDomainForRow(row: WorkOrderSchedulingRow): string {
+	const d = (row.domain ?? '').trim();
+	if (d) return d;
+	const u = userInfoStore.userInfos;
+	const tid = u?.currentTenantId ?? u?.tenantId;
+	if (tid === undefined || tid === null || String(tid).trim() === '') return '';
+	return String(tid);
+}
+
 const statusOptions = [
 	{ v: 'w', l: 'w 投产' },
 	{ v: 'r', l: 'r 下达' },
@@ -332,7 +342,7 @@ async function onSyncMaterial() {
 
 async function onStatusChange(row: WorkOrderSchedulingRow, v: string) {
 	try {
-		await patchWorkOrderStatus({ id: row.id, domain: row.domain || '', status: v });
+		await patchWorkOrderStatus({ id: row.id, domain: schedulingDomainForRow(row), status: v });
 		ElMessage.success('状态已保存');
 	} catch (e: any) {
 		ElMessage.error(e?.message || '更新失败');
@@ -356,27 +366,24 @@ function onSaved() {
 }
 
 function openTraceTab(row: WorkOrderSchedulingRow) {
-	const href = router.resolve({
+	router.push({
 		path: '/aidop/production/work-order-trace',
-		query: { workOrd: row.workOrd || '', domain: row.domain || '' },
-	}).href;
-	window.open(href, '_blank');
+		query: { workOrd: row.workOrd || '', domain: schedulingDomainForRow(row) },
+	});
 }
 
 function openMaterialTab(row: WorkOrderSchedulingRow) {
-	const href = router.resolve({
+	router.push({
 		path: '/aidop/production/work-order-materials',
-		query: { workOrd: row.workOrd || '', domain: row.domain || '' },
-	}).href;
-	window.open(href, '_blank');
+		query: { workOrd: row.workOrd || '', domain: schedulingDomainForRow(row) },
+	});
 }
 
 function openRoutingTab(row: WorkOrderSchedulingRow) {
-	const href = router.resolve({
+	router.push({
 		path: '/aidop/production/work-order-routings',
-		query: { workOrd: row.workOrd || '', domain: row.domain || '' },
-	}).href;
-	window.open(href, '_blank');
+		query: { workOrd: row.workOrd || '', domain: schedulingDomainForRow(row) },
+	});
 }
 
 async function onSyncRouting(row: WorkOrderSchedulingRow) {
@@ -386,7 +393,7 @@ async function onSyncRouting(row: WorkOrderSchedulingRow) {
 		return;
 	}
 	try {
-		await syncWorkRouting({ workOrd: row.workOrd || '', domain: row.domain || '' });
+		await syncWorkRouting({ workOrd: row.workOrd || '', domain: schedulingDomainForRow(row) });
 		ElMessage.success('已执行');
 		loadList();
 	} catch (e: any) {
@@ -396,7 +403,7 @@ async function onSyncRouting(row: WorkOrderSchedulingRow) {
 
 async function onUrgent(row: WorkOrderSchedulingRow, urgent: number) {
 	try {
-		await setWorkOrderUrgent({ workOrd: row.workOrd || '', domain: row.domain || '', urgent });
+		await setWorkOrderUrgent({ workOrd: row.workOrd || '', domain: schedulingDomainForRow(row), urgent });
 		ElMessage.success(urgent === 2 ? '已设为特急' : '已设为加急');
 		loadList();
 	} catch (e: any) {

+ 10 - 4
Web/src/views/aidop/production/workOrderSchedulingPriorityForm.vue

@@ -6,6 +6,9 @@
 		<el-form-item label="物料编号">
 			<el-input v-model="form.itemNum" disabled />
 		</el-form-item>
+		<el-form-item label="订单日期">
+			<el-input v-model="form.ordDate" disabled />
+		</el-form-item>
 		<el-form-item label="工单数量">
 			<el-input-number v-model="form.qtyOrded" :min="0" :precision="4" style="width: 100%" />
 		</el-form-item>
@@ -52,6 +55,7 @@ const saving = ref(false);
 const form = reactive({
 	workOrd: '',
 	itemNum: '',
+	ordDate: '' as string,
 	qtyOrded: 0 as number,
 	priority: '',
 	lotSerial: '',
@@ -64,6 +68,7 @@ async function load() {
 		const d = await fetchSchedulingEditPreview(props.workOrd, props.domain);
 		form.workOrd = d.workOrd;
 		form.itemNum = d.itemNum ?? '';
+		form.ordDate = d.ordDate ? String(d.ordDate).slice(0, 10) : '';
 		form.qtyOrded = Number(d.qtyOrded ?? 0);
 		form.priority = d.priority ?? '';
 		form.lotSerial = d.lotSerial ?? '';
@@ -84,12 +89,13 @@ async function onSave() {
 	saving.value = true;
 	try {
 		await replenishmentWorkOrdCheckChangePriority({
-			workOrd: props.workOrd,
+			workord: props.workOrd,
 			qty: form.qtyOrded,
-			lotSerial: form.lotSerial ?? '',
+			instockdate: form.ordDate,
 			priority: String(form.priority ?? ''),
-			factory_id: props.domain,
-			userNo,
+			domain: props.domain,
+			userAccount: userNo,
+			lotSerial: form.lotSerial ?? '',
 		});
 		ElMessage.success('保存成功');
 		emit('saved');

+ 6 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Production/Dto/ExecutableDailyPlanDto.cs

@@ -36,6 +36,12 @@ public class ExecutableDailyPlanListInput
     /// <summary>开工时间 &gt;=(ScheduleResultOpMaster.WorkStartTime)</summary>
     public string? WorkStartFrom { get; set; }
 
+    /// <summary>
+    /// 计划日期下限(含)。格式 yyyy-MM-dd;不传时与列表说明一致:<c>DATE(p.PlanDate) &gt;= CURDATE()</c>(仅今天及以后)。
+    /// 若在库里能查到行但页面为空,多为历史计划日期:可在此选较早日期以拉取数据。
+    /// </summary>
+    public string? PlanDateMin { get; set; }
+
     /// <summary>排序字段</summary>
     public string? OrderBy { get; set; }
 

+ 2 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Production/Entity/PeriodSequenceDet.cs

@@ -112,13 +112,13 @@ public class PeriodSequenceDet
     [SugarColumn(ColumnName = "BusinessID", IsNullable = true)]
     public long? BusinessID { get; set; }
 
-    [SugarColumn(ColumnName = "CreateUser", Length = 8, IsNullable = true)]
+    [SugarColumn(ColumnName = "CreateUser", Length = 80, IsNullable = true)]
     public string? CreateUser { get; set; }
 
     [SugarColumn(ColumnName = "CreateTime", IsNullable = true)]
     public DateTime? CreateTime { get; set; }
 
-    [SugarColumn(ColumnName = "UpdateUser", Length = 8, IsNullable = true)]
+    [SugarColumn(ColumnName = "UpdateUser", Length = 80, IsNullable = true)]
     public string? UpdateUser { get; set; }
 
     [SugarColumn(ColumnName = "UpdateTime", IsNullable = true)]

+ 19 - 8
server/Plugins/Admin.NET.Plugin.AiDOP/Production/ExecutableDailyPlanService.cs

@@ -30,9 +30,19 @@ public class ExecutableDailyPlanService : IDynamicApiController, ITransient
         {
             "p.OrdQty > 0",
             "p.IsActive = 1",
-            "DATE(p.PlanDate) >= CURDATE()",
             "LOWER(IFNULL(w.Status,'')) <> 'c'"
         };
+        if (!string.IsNullOrWhiteSpace(input.PlanDateMin))
+        {
+            var pdm = input.PlanDateMin.Trim();
+            if (pdm.Length > 10) pdm = pdm[..10];
+            innerWhere.Add("DATE(p.PlanDate) >= @PlanDateMin");
+            pars.Add(new SugarParameter("@PlanDateMin", pdm));
+        }
+        else
+        {
+            innerWhere.Add("DATE(p.PlanDate) >= CURDATE()");
+        }
         if (!string.IsNullOrWhiteSpace(input.Domain))
         {
             innerWhere.Add("p.`Domain` = @Domain");
@@ -228,14 +238,14 @@ public class ExecutableDailyPlanService : IDynamicApiController, ITransient
             w.Eff AS Eff,
             p.`Domain` AS Domain
         FROM PeriodSequenceDet p
-        LEFT JOIN LineMaster line ON p.Line = line.Line AND p.`Domain` = line.`Domain`
-        LEFT JOIN ItemMaster i ON p.ItemNum = i.ItemNum AND p.`Domain` = i.`Domain`
-        LEFT JOIN WorkOrdMaster w ON p.WorkOrds = w.WorkOrd AND p.`Domain` = w.`Domain`
+        LEFT JOIN LineMaster line ON p.Line = line.Line 
+        LEFT JOIN ItemMaster i ON p.ItemNum = i.ItemNum 
+        LEFT JOIN WorkOrdMaster w ON p.WorkOrds = w.WorkOrd 
         LEFT JOIN WorkOrdRouting r ON p.Op = r.OP AND p.ItemNum = r.ItemNum AND p.WorkOrds = r.WorkOrd
         LEFT JOIN mes_morder m ON w.WorkOrd = m.morder_no AND CAST(w.`Domain` AS CHAR(64)) = CAST(m.factory_id AS CHAR(64))
-        LEFT JOIN ProdLineDetail pd ON p.`Domain` = pd.`Domain` AND p.ItemNum = pd.Part AND p.Line = pd.Line AND p.Op = pd.Op
-        LEFT JOIN ScheduleResultOpMaster sm ON sm.`Domain` = p.`Domain` AND p.UDate2 = sm.WorkActivateTime AND p.ItemNum = sm.ItemNum AND sm.WorkOrd = p.WorkOrds AND sm.Op = p.Op
-        LEFT JOIN WorkCtrMaster wm ON wm.WorkCtr = sm.WorkCtr AND wm.`Domain` = sm.`Domain`
+        LEFT JOIN ProdLineDetail pd ON p.ItemNum = pd.Part AND p.Line = pd.Line AND p.Op = pd.Op
+        LEFT JOIN ScheduleResultOpMaster sm ON p.UDate2 = sm.WorkActivateTime AND p.ItemNum = sm.ItemNum AND sm.WorkOrd = p.WorkOrds AND sm.Op = p.Op
+        LEFT JOIN WorkCtrMaster wm ON wm.WorkCtr = sm.WorkCtr 
         LEFT JOIN (
             SELECT SUM(CompQty) AS CompQty, SUM(RejectQty) AS RejectQty, PlanDate, `Period` AS UInt2, Op, WorkOrds, ItemNum
             FROM PeriodSequenceDet
@@ -284,7 +294,8 @@ public class ExecutableDailyPlanService : IDynamicApiController, ITransient
         public int? Op { get; set; }
         public DateTime? PlanDate { get; set; }
         public decimal? OrdQty { get; set; }
-        public decimal? RunCrew { get; set; }
+        /// <summary>ProdLineDetail.RunCrew 为班组/编码类字符串,勿用 decimal。</summary>
+        public string? RunCrew { get; set; }
         public string? Status { get; set; }
         public string? Batch { get; set; }
         public string? PStatus { get; set; }

+ 166 - 72
server/Plugins/Admin.NET.Plugin.AiDOP/Production/WorkOrderSchedulingService.cs

@@ -19,6 +19,63 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
         _userManager = userManager;
     }
 
+    /// <summary>
+    /// 主表 WorkOrdMaster 按工单号 +「公司域或租户 id」匹配:<c>domain</c> 可为原 <c>Domain</c> 列,或与 <c>tenant_id</c> 相同的字符串(列表已做 COALESCE 回传)。
+    /// </summary>
+    private const string SqlWorkOrdMasterMatchDomainOrTenant = """
+        WorkOrd = @WorkOrd AND (
+            TRIM(IFNULL(`Domain`, '')) = TRIM(@Domain)
+            OR (
+                TRIM(IFNULL(`Domain`, '')) = ''
+                AND TRIM(@Domain) <> ''
+                AND CAST(IFNULL(tenant_id, 0) AS CHAR) = TRIM(@Domain)
+            )
+        )
+        """;
+
+    /// <summary>带表别名的 WorkOrdMaster 匹配(参数 <c>alias</c> 为表别名,如 a、w)。</summary>
+    private static string SqlWorkOrdMasterMatchDomainOrTenantAliased(string alias) => $"""
+        {alias}.WorkOrd = @WorkOrd AND (
+            TRIM(IFNULL({alias}.`Domain`, '')) = TRIM(@Domain)
+            OR (
+                TRIM(IFNULL({alias}.`Domain`, '')) = ''
+                AND TRIM(@Domain) <> ''
+                AND CAST(IFNULL({alias}.tenant_id, 0) AS CHAR) = TRIM(@Domain)
+            )
+        )
+        """;
+
+    /// <summary>子表(工序/明细)按工单号 + 域或主表租户匹配。</summary>
+    private static string SqlWorkOrdChildMatchDomainOrTenant(string childAlias, string masterAlias) => $"""
+        {childAlias}.WorkOrd = @WorkOrd AND (
+            TRIM(IFNULL({childAlias}.`Domain`, '')) = TRIM(@Domain)
+            OR EXISTS (
+                SELECT 1 FROM WorkOrdMaster {masterAlias}
+                WHERE {masterAlias}.WorkOrd = {childAlias}.WorkOrd
+                  AND (
+                      TRIM(IFNULL({masterAlias}.`Domain`, '')) = TRIM(@Domain)
+                      OR (
+                          TRIM(IFNULL({masterAlias}.`Domain`, '')) = ''
+                          AND TRIM(@Domain) <> ''
+                          AND CAST(IFNULL({masterAlias}.tenant_id, 0) AS CHAR) = TRIM(@Domain)
+                      )
+                  )
+            )
+        )
+        """;
+
+    /// <summary>按主键 +「公司域或租户 id」匹配(状态更新等)。</summary>
+    private const string SqlWorkOrdMasterMatchRecIdAndDomainOrTenant = """
+        RecID = @Id AND (
+            TRIM(IFNULL(`Domain`, '')) = TRIM(@Domain)
+            OR (
+                TRIM(IFNULL(`Domain`, '')) = ''
+                AND TRIM(@Domain) <> ''
+                AND CAST(IFNULL(tenant_id, 0) AS CHAR) = TRIM(@Domain)
+            )
+        )
+        """;
+
     // ══════════════════════════════════════════════════════════════
     // 列表 GET /api/Production/scheduling/list
     // ══════════════════════════════════════════════════════════════
@@ -27,7 +84,8 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
     [HttpGet("scheduling/list")]
     public async Task<object> GetList([FromQuery] WorkOrderSchedulingListInput input)
     {
-        const string C = "utf8mb4_general_ci";
+        //const string C = "utf8mb4_general_ci";
+        const string C = "utf8mb4_0900_ai_ci";
         var pars = new List<SugarParameter>();
 
         var innerWhere = new List<string>
@@ -37,17 +95,17 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
 
         if (!string.IsNullOrWhiteSpace(input.WorkOrd))
         {
-            innerWhere.Add($"(a.WorkOrd COLLATE {C}) LIKE @WorkOrd");
+            innerWhere.Add($"a.WorkOrd LIKE @WorkOrd");
             pars.Add(new SugarParameter("@WorkOrd", $"%{input.WorkOrd.Trim()}%"));
         }
         if (!string.IsNullOrWhiteSpace(input.LotSerial))
         {
-            innerWhere.Add($"(a.LotSerial COLLATE {C}) LIKE @LotSerial");
+            innerWhere.Add($"a.LotSerial LIKE @LotSerial");
             pars.Add(new SugarParameter("@LotSerial", $"%{input.LotSerial.Trim()}%"));
         }
         if (!string.IsNullOrWhiteSpace(input.ItemNum))
         {
-            innerWhere.Add($"(a.ItemNum COLLATE {C}) LIKE @ItemNum");
+            innerWhere.Add($"a.ItemNum LIKE @ItemNum");
             pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
         }
         if (!string.IsNullOrWhiteSpace(input.StartDateFrom))
@@ -89,29 +147,29 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
             s.PlanDate AS PlanDate,
             s.ProdDate AS ProdDate,
             LOWER(a.Status) AS Status,
-            a.`Domain` AS Domain,
+            COALESCE(NULLIF(TRIM(a.`Domain`), ''), IFNULL(CAST(a.tenant_id AS CHAR), '')) AS Domain,
             a.Urgent AS Urgent
         FROM WorkOrdMaster a
-        LEFT JOIN ItemMaster b ON a.ItemNum COLLATE {C} = b.ItemNum COLLATE {C}
+        LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum 
         LEFT JOIN ReplenishmentWeekPlan r
-            ON a.WorkOrd COLLATE {C} = r.ProductionOrder COLLATE {C}
-            AND CAST(a.`Domain` AS CHAR(64)) COLLATE {C} = CAST(r.factory_id AS CHAR(64)) COLLATE {C}
-        LEFT JOIN crm_seorder se ON se.bill_no COLLATE {C} = a.SalesJob COLLATE {C}
-        LEFT JOIN CustMaster cm ON cm.Cust COLLATE {C} = se.custom_no COLLATE {C}
+            ON a.WorkOrd = r.ProductionOrder
+            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 (
             SELECT `Domain`, WorkOrds, MIN(PlanDate) AS PlanDate, MIN(ProdDate) AS ProdDate
             FROM PeriodSequenceDet
             GROUP BY `Domain`, WorkOrds
-        ) s ON CAST(a.`Domain` AS CHAR(64)) COLLATE {C} = CAST(s.`Domain` AS CHAR(64)) COLLATE {C}
-            AND a.WorkOrd COLLATE {C} = s.WorkOrds COLLATE {C}
+        ) s ON CAST(a.`Domain` AS CHAR(64))  = CAST(s.`Domain` AS CHAR(64)) 
+            AND a.WorkOrd  = s.WorkOrds 
         LEFT JOIN (
             SELECT morder_no, MAX(create_time) AS checktime
             FROM b_examine_result
             GROUP BY morder_no
-        ) exm ON exm.morder_no COLLATE {C} = a.WorkOrd COLLATE {C}
+        ) exm ON exm.morder_no = a.WorkOrd 
         LEFT JOIN WorkOrdInStorage ins
-            ON a.WorkOrd COLLATE {C} = ins.WorkOrd COLLATE {C}
-            AND ins.Remark COLLATE {C} = '工单预留' COLLATE {C}
+            ON a.WorkOrd  = ins.WorkOrd 
+            AND ins.Remark  = '工单预留' 
         WHERE {innerWhere}
         """;
 
@@ -125,10 +183,17 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
         if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
             throw Oops.Oh("工单号与公司域名不能为空");
 
-        const string sql = """
-            SELECT WorkOrd, ItemNum, QtyOrded, Priority, LotSerial, IFNULL(Urgent, 0) AS Urgent
-            FROM WorkOrdMaster
-            WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
+        string sql = $"""
+            SELECT
+                a.WorkOrd,
+                a.ItemNum,
+                a.QtyOrded,
+                a.Priority,
+                a.LotSerial,
+                IFNULL(a.Urgent, 0) AS Urgent,
+                DATE(a.OrdDate) AS OrdDate
+            FROM WorkOrdMaster a
+            WHERE {SqlWorkOrdMasterMatchDomainOrTenantAliased("a")}
             LIMIT 1
             """;
         var row = (await _db.Ado.SqlQueryAsync<WorkOrderEditPreviewRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() }))
@@ -146,7 +211,7 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
         if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
             throw Oops.Oh("工单号与公司域名不能为空");
 
-        const string masterSql = """
+        string masterSql = $"""
             SELECT
                 a.WorkOrd,
                 a.QtyOrded,
@@ -160,13 +225,13 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
                 IFNULL(a.Remark, '') AS Remark
             FROM WorkOrdMaster a
             LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
-            WHERE a.WorkOrd = @WorkOrd AND a.`Domain` = @Domain
+            WHERE {SqlWorkOrdMasterMatchDomainOrTenantAliased("a")}
             LIMIT 1
             """;
         var master = (await _db.Ado.SqlQueryAsync<WorkOrderViewMasterRow>(masterSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() }))
             .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
 
-        const string routingSql = """
+        string routingSql = $"""
             SELECT
                 r.OP AS Op,
                 r.Descr AS Descr,
@@ -178,16 +243,16 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
                 IFNULL(r.QtyScrap, 0) AS QtyScrap,
                 r.Status AS Status
             FROM WorkOrdRouting r
-            WHERE r.WorkOrd = @WorkOrd AND r.`Domain` = @Domain
-            ORDER BY r.Line, r.ColumnNum
+            WHERE {SqlWorkOrdChildMatchDomainOrTenant("r", "m")}
+            ORDER BY (r.OP + 0) ASC, r.OP ASC, r.Line ASC, r.ColumnNum ASC
             """;
         var routings = await _db.Ado.SqlQueryAsync<WorkOrderViewRoutingRow>(routingSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
 
-        const string detailSql = """
-            SELECT ItemNum, Op, QtyRequired, IFNULL(FrozenBOMQty, 0) AS FrozenBOMQty
-            FROM WorkOrdDetail
-            WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
-            ORDER BY LineNum
+        string detailSql = $"""
+            SELECT d.ItemNum, d.Op, d.QtyRequired, IFNULL(d.FrozenBOMQty, 0) AS FrozenBOMQty
+            FROM WorkOrdDetail d
+            WHERE {SqlWorkOrdChildMatchDomainOrTenant("d", "m")}
+            ORDER BY d.LineNum
             """;
         var details = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(detailSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
 
@@ -204,11 +269,11 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
         if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
             throw Oops.Oh("工单号与公司域名不能为空");
 
-        const string sql = """
-            SELECT ItemNum, Op, QtyRequired, IFNULL(FrozenBOMQty, 0) AS FrozenBOMQty
-            FROM WorkOrdDetail
-            WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
-            ORDER BY LineNum
+        string sql = $"""
+            SELECT d.ItemNum, d.Op, d.QtyRequired, IFNULL(d.FrozenBOMQty, 0) AS FrozenBOMQty
+            FROM WorkOrdDetail d
+            WHERE {SqlWorkOrdChildMatchDomainOrTenant("d", "m")}
+            ORDER BY d.LineNum
             """;
         var list = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
         return new { list };
@@ -221,7 +286,7 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
         if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
             throw Oops.Oh("工单号与公司域名不能为空");
 
-        const string sql = """
+        string sql = $"""
             SELECT
                 r.OP AS Op,
                 r.Descr AS Descr,
@@ -233,8 +298,8 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
                 IFNULL(r.QtyScrap, 0) AS QtyScrap,
                 r.Status AS Status
             FROM WorkOrdRouting r
-            WHERE r.WorkOrd = @WorkOrd AND r.`Domain` = @Domain
-            ORDER BY r.Line, r.ColumnNum
+            WHERE {SqlWorkOrdChildMatchDomainOrTenant("r", "m")}
+            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() });
         return new { list };
@@ -253,7 +318,7 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
         var w = workOrd.Trim();
         var d = domain.Trim();
 
-        const string masterSql = """
+        string masterSql = $"""
             SELECT
                 a.WorkOrd,
                 a.QtyOrded,
@@ -266,7 +331,7 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
                 LOWER(a.Status) AS Status
             FROM WorkOrdMaster a
             LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
-            WHERE a.WorkOrd = @WorkOrd AND a.`Domain` = @Domain
+            WHERE {SqlWorkOrdMasterMatchDomainOrTenantAliased("a")}
             LIMIT 1
             """;
         var master = (await _db.Ado.SqlQueryAsync<WorkOrderTraceMasterRow>(masterSql, new { WorkOrd = w, Domain = d }))
@@ -562,7 +627,7 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
         };
 
         var n = await _db.Ado.ExecuteCommandAsync(
-            """
+            $"""
             UPDATE WorkOrdMaster
             SET QtyOrded = COALESCE(@QtyOrded, QtyOrded),
                 Priority = COALESCE(@Priority, Priority),
@@ -570,7 +635,7 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
                 Urgent = COALESCE(@Urgent, Urgent),
                 UpdateUser = @UpdateUser,
                 UpdateTime = @UpdateTime
-            WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
+            WHERE {SqlWorkOrdMasterMatchDomainOrTenant}
             """,
             pars);
 
@@ -598,12 +663,12 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
             new("@Domain", input.Domain.Trim())
         };
         var n = await _db.Ado.ExecuteCommandAsync(
-            """
+            $"""
             UPDATE WorkOrdMaster
             SET Status = @Status,
                 UpdateUser = @UpdateUser,
                 UpdateTime = @UpdateTime
-            WHERE RecID = @Id AND `Domain` = @Domain
+            WHERE {SqlWorkOrdMasterMatchRecIdAndDomainOrTenant}
             """,
             pars);
         if (n == 0)
@@ -633,50 +698,78 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
     {
         var workOrd = input.WorkOrd.Trim();
         var domain = input.Domain.Trim();
+        var createUser = (_userManager.Account ?? "system").Trim();
+        if (createUser.Length > 24)
+            createUser = createUser[..24];
+
         var pars = new List<SugarParameter>
         {
             new("@WorkOrd", workOrd),
-            new("@Domain", domain)
+            new("@Domain", domain),
+            new("@CreateUser", createUser)
         };
 
         await _db.Ado.BeginTranAsync();
         try
         {
             await _db.Ado.ExecuteCommandAsync(
-                """
-                DELETE FROM WorkOrdRouting
-                WHERE WorkOrd = @WorkOrd AND CAST(`Domain` AS CHAR(64)) COLLATE utf8mb4_general_ci = CAST(@Domain AS CHAR(64)) COLLATE utf8mb4_general_ci
+                $"""
+                DELETE FROM WorkOrdRouting rr
+                WHERE {SqlWorkOrdChildMatchDomainOrTenant("rr", "m")}
                 """,
                 pars);
 
+            // 业务口径:由 RoutingOpDetail + ProdLineDetail 重算工单工艺路线(MySQL)。
+            // 与历史脚本一致:DELETE 仍按工单 + 域/租户匹配,避免误删其它域数据。
             await _db.Ado.ExecuteCommandAsync(
-                """
+                $"""
                 INSERT INTO WorkOrdRouting (
-                    `Domain`, Descr, MilestoneOp, WorkOrd, OP, ParentOp, RunTime, ItemNum, QtyOrded, OverlapUnits, Status, IsActive, CommentIndex, CreateTime, StdOp, PackingQty, WorkOrdMasterRecID
+                    `Descr`, `Domain`, `ChargeCode`, `Machine`, `RunCrew`, `MilestoneOp`, `OP`, `StdOp`, `ItemNum`, `WorkCtr`,
+                    `Ufld1`, `Ufld3`, `Setup`, `MachinesperOp`, `Labor`, `RunTime`, `MachBdnRate`, `WorkCode`, `StdSetupTime`, `Engineer`,
+                    `WorkOrd`, `WorkOrdMasterRecID`, `ERPfld1`, `QtyOrded`, `ProcessOut`, `ProcessOutDay`, `ProcessOutSupp`,
+                    `IsActive`, `Status`, `ProdLine`, `CreateTime`, `CreateUser`, `tenant_id`,`CommentIndex`,`WaitTime`
                 )
                 SELECT
-                    w.`Domain`,
-                    r.Descr,
-                    r.MilestoneOp,
-                    w.WorkOrd,
-                    r.OP,
-                    r.ParentOp,
-                    r.RunTime,
-                    w.ItemNum,
-                    w.QtyOrded,
-                    r.OverlapUnits,
-                    w.Status,
-                    1,
-                    r.CommentIndex,
-                    NOW(),
-                    r.StdOp,
-                    r.PackingQty,
-                    w.RecID
+                    a.`Descr`,
+                    LEFT(TRIM(COALESCE(NULLIF(TRIM(IFNULL(w.`Domain`, '')), ''), NULLIF(TRIM(IFNULL(a.`Domain`, '')), ''), ' ')), 8),
+                    '',
+                    b.`InternalEquipmentCode`,
+                    IFNULL(b.`StandardStaffCount`, 0),
+                    CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),
+                    IFNULL(a.`Op`, 0),
+                    a.`StdOp`,
+                    a.`RoutingCode`,
+                    LEFT(IFNULL(b.`Site`, ''), 8),
+                    '',
+                    '',
+                    IFNULL(a.`UDeci1`, 0),
+                    IFNULL(a.`UDeci2`, 0),
+                    IFNULL(a.`UDeci3`, 0),
+                    IFNULL(a.`UDeci3`, 0) / 3600.0,
+                    IFNULL(b.`Rate`, 0),
+                    b.`OpType`,
+                    IFNULL(b.`SetupTime`, 0),
+                    b.`SkillNo`,
+                    w.`WorkOrd`,
+                    w.`RecID`,
+                    w.`ERPfld1`,
+                    IFNULL(w.`QtyOrded`, 0),
+                    CAST(IFNULL(a.`UDeci5`, 0) AS SIGNED),
+                    IFNULL(a.`ProcessOutDay`, 0),
+                    a.`ProcessOutSupp`,
+                    b'1',
+                    'r',
+                    LEFT(IFNULL(b.`Line`, ''), 8),
+                    NOW(3),
+                    @CreateUser,
+                    w.`tenant_id`,CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),0
                 FROM WorkOrdMaster w
-                LEFT JOIN RoutingOpDetail r ON w.ItemNum COLLATE utf8mb4_general_ci = r.RoutingCode COLLATE utf8mb4_general_ci
-                WHERE w.WorkOrd = @WorkOrd
-                  AND CAST(w.`Domain` AS CHAR(64)) COLLATE utf8mb4_general_ci = CAST(@Domain AS CHAR(64)) COLLATE utf8mb4_general_ci
-                  AND r.MilestoneOp IS NOT NULL
+                LEFT JOIN RoutingOpDetail a ON w.`ItemNum` = a.`RoutingCode`
+                LEFT JOIN ProdLineDetail b ON a.`RoutingCode` = b.`Part` AND a.`Op` = b.`Op`
+                WHERE w.`WorkOrd` = @WorkOrd
+                  AND {SqlWorkOrdMasterMatchDomainOrTenantAliased("w")}
+                  AND IFNULL(a.`IsActive`, 0) = 1
+                  AND a.`MilestoneOp` IS NOT NULL
                 """,
                 pars);
 
@@ -700,12 +793,12 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
     {
         var account = _userManager.Account ?? "system";
         var n = await _db.Ado.ExecuteCommandAsync(
-            """
+            $"""
             UPDATE WorkOrdMaster
             SET Urgent = @Urgent,
                 UpdateUser = @UpdateUser,
                 UpdateTime = @UpdateTime
-            WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
+            WHERE {SqlWorkOrdMasterMatchDomainOrTenant}
             """,
             new List<SugarParameter>
             {
@@ -750,6 +843,7 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
         public string? Priority { get; set; }
         public string? LotSerial { get; set; }
         public int Urgent { get; set; }
+        public DateTime? OrdDate { get; set; }
     }
 
     private sealed class WorkOrderViewMasterRow

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

@@ -92,13 +92,13 @@ public class WorkOrdDetail
     [SugarColumn(ColumnName = "BusinessID", IsNullable = true)]
     public long? BusinessID { get; set; }
 
-    [SugarColumn(ColumnName = "CreateUser", Length = 8, IsNullable = true)]
+    [SugarColumn(ColumnName = "CreateUser", Length = 80, IsNullable = true)]
     public string? CreateUser { get; set; }
 
     [SugarColumn(ColumnName = "CreateTime", IsNullable = true)]
     public DateTime? CreateTime { get; set; }
 
-    [SugarColumn(ColumnName = "UpdateUser", Length = 8, IsNullable = true)]
+    [SugarColumn(ColumnName = "UpdateUser", Length = 80, IsNullable = true)]
     public string? UpdateUser { get; set; }
 
     [SugarColumn(ColumnName = "UpdateTime", IsNullable = true)]

+ 10 - 10
server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/WorkOrdRouting.cs

@@ -20,7 +20,7 @@ public class WorkOrdRouting
     [SugarColumn(ColumnName = "WorkOrd", Length = 64, IsNullable = true)]
     public string? WorkOrd { get; set; }
 
-    [SugarColumn(ColumnName = "OP", Length = 6, IsNullable = true)]
+    [SugarColumn(ColumnName = "OP", Length = 6, IsNullable = false)]
     public int OP { get; set; }
 
     [SugarColumn(ColumnName = "ItemNum", Length = 30, IsNullable = true)]
@@ -38,11 +38,11 @@ public class WorkOrdRouting
     [SugarColumn(ColumnName = "Descr", Length = 256, IsNullable = true)]
     public string? Descr { get; set; }
 
-    [SugarColumn(ColumnName = "QtyOrded", IsNullable = true)]
-    public decimal? QtyOrded { get; set; }
+    [SugarColumn(ColumnName = "QtyOrded", IsNullable = false)]
+    public decimal QtyOrded { get; set; }
 
-    [SugarColumn(ColumnName = "QtyComplete", IsNullable = true)]
-    public decimal? QtyComplete { get; set; }
+    [SugarColumn(ColumnName = "QtyComplete", IsNullable = false)]
+    public decimal QtyComplete { get; set; }
 
     [SugarColumn(ColumnName = "CumRejected", IsNullable = true)]
     public decimal? CumRejected { get; set; }
@@ -71,7 +71,7 @@ public class WorkOrdRouting
     [SugarColumn(ColumnName = "ActSetupTime", IsNullable = true)]
     public decimal? ActSetupTime { get; set; }
 
-    [SugarColumn(ColumnName = "RunCrew", IsNullable = true)]
+    [SugarColumn(ColumnName = "RunCrew", IsNullable = false)]
     public decimal RunCrew { get; set; }
 
     [SugarColumn(ColumnName = "SetupCrew", IsNullable = true)]
@@ -89,7 +89,7 @@ public class WorkOrdRouting
     [SugarColumn(ColumnName = "Priority", IsNullable = true)]
     public int? Priority { get; set; }
 
-    [SugarColumn(ColumnName = "Machine", Length = 8, IsNullable = true)]
+    [SugarColumn(ColumnName = "Machine", Length = 800, IsNullable = true)]
     public string? Machine { get; set; }
 
     [SugarColumn(ColumnName = "Labor", IsNullable = true)]
@@ -110,7 +110,7 @@ public class WorkOrdRouting
     [SugarColumn(ColumnName = "NextOp1", Length = 6, IsNullable = true)]
     public string? NextOp1 { get; set; }
 
-    [SugarColumn(ColumnName = "ParentOp", Length = 6, IsNullable = true)]
+    [SugarColumn(ColumnName = "ParentOp", Length = 6, IsNullable = false)]
     public int ParentOp { get; set; }
 
     [SugarColumn(ColumnName = "IsCheckOp", IsNullable = true)]
@@ -131,13 +131,13 @@ public class WorkOrdRouting
     [SugarColumn(ColumnName = "BusinessID", IsNullable = true)]
     public long? BusinessID { get; set; }
 
-    [SugarColumn(ColumnName = "CreateUser", Length = 8, IsNullable = true)]
+    [SugarColumn(ColumnName = "CreateUser", Length = 80, IsNullable = true)]
     public string? CreateUser { get; set; }
 
     [SugarColumn(ColumnName = "CreateTime", IsNullable = true)]
     public DateTime? CreateTime { get; set; }
 
-    [SugarColumn(ColumnName = "UpdateUser", Length = 8, IsNullable = true)]
+    [SugarColumn(ColumnName = "UpdateUser", Length = 80, IsNullable = true)]
     public string? UpdateUser { get; set; }
 
     [SugarColumn(ColumnName = "UpdateTime", IsNullable = true)]