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

feat(s0): restore process flow card CRUD

工序流转卡从 legacy MissedPrint 只读投影改造为 S0ProcessFlowCard 主表 CRUD:
- 新增 AdoS0ProcessFlowCardsController(增删改查 + 按物料的工艺路线联动选项)
- 删除旧 MissedPrint 只读控制器与实体投影 DTO
- 前端 ProcessFlowCardList 改为主表 CRUD,物料远程搜索联动带出物料名称/图号/BOM版本/工艺路线,组织字段后端 stamp/pin
- 流转卡号重复返回业务错误“流转卡号已存在”(AdoS0ApiErrors 映射,409 非 500)

chore: bump version Web 2.4.255 / server 1.0.253
YY968XX 2 недель назад
Родитель
Сommit
a3b22801a8

+ 1 - 1
Web/package.json

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

+ 61 - 22
Web/src/views/aidop/s0/api/s0ManufacturingApi.ts

@@ -398,30 +398,66 @@ export const s0MfgStandardOperationsApi = {
 	delete: (id: number) => service.delete(`${stdOpBase}/${id}`).then(unwrap),
 };
 
-/** 流转卡打印记录(legacy MissedPrint 只读投影)。 */
-export interface S0MfgFlowCardPrintRecordRow {
+/** 工序流转卡主数据行(数据源:S0ProcessFlowCard 主表)。 */
+export interface S0ProcessFlowCardRow {
 	id: number;
-	workOrd?: string | null;
-	op?: number | null;
-	itemNum?: string | null;
-	descr?: string | null;
-	labelFormat?: string | null;
-	labelColor?: string | null;
-	status?: string | null;
-	statusDesc?: string | null;
-	createTime?: string | null;
+	companyRefId?: string;
+	factoryRefId?: string;
+	domainCode?: string | null;
+	workOrderNo: string;
+	workOrderBatchNo?: string | null;
+	flowCardNo: string;
+	nature?: string | null;
+	itemNum: string;
+	itemName?: string | null;
+	drawingNo?: string | null;
+	bomVersion?: string | null;
+	routingCode?: string | null;
+	remarks?: string | null;
+	isActive: boolean;
+	createUser?: string | null;
+	createTime?: string;
+	updateUser?: string | null;
+	updateTime?: string | null;
 }
 
-export interface S0MfgFlowCardPrintRecordQuery {
+/** 工序流转卡新增/编辑入参。组织字段由后端 stamp/pin,前端仅透传(新增补默认、编辑保值)。 */
+export interface S0ProcessFlowCardUpsert {
+	companyRefId?: string;
+	factoryRefId?: string;
+	domainCode?: string;
+	workOrderNo: string;
+	workOrderBatchNo?: string;
+	flowCardNo: string;
+	nature?: string;
+	itemNum: string;
+	itemName?: string;
+	drawingNo?: string;
+	bomVersion?: string;
+	routingCode?: string;
+	remarks?: string;
+	isActive: boolean;
+	createUser?: string;
+	updateUser?: string;
+}
+
+export interface S0ProcessFlowCardQuery {
 	page?: number;
 	pageSize?: number;
-	keyword?: string;
-	workOrd?: string;
+	companyRefId?: string;
+	factoryRefId?: string;
+	domainCode?: string;
+	workOrderNo?: string;
+	flowCardNo?: string;
 	itemNum?: string;
-	labelColor?: string;
-	status?: string;
-	createTimeFrom?: string;
-	createTimeTo?: string;
+	isActive?: boolean;
+	keyword?: string;
+}
+
+/** 工艺路线联动选项(按物料编码过滤的 RouteCode)。 */
+export interface S0RoutingOption {
+	routeCode: string;
+	routeName: string;
 }
 
 export interface S0OrderScheduleCycleRow {
@@ -495,12 +531,15 @@ export const s0MfgPersonSkillsApi = {
 export const s0MfgWorkCentersApi = crud(`${mfg}/work-centers`);
 export const s0MfgLineMaterialsApi = crud(`${mfg}/line-materials`);
 /**
- * 流转卡打印记录 API(数据源:legacy MissedPrint 只读投影)。
- * 路由保持 process-flow-cards 不变,降低菜单/权限/前端调用迁移成本;仅暴露 list(页面只读)
+ * 工序流转卡主数据 CRUD API(数据源:S0ProcessFlowCard 主表)。
+ * 路由沿用 process-flow-cards(接口地址不变,前端/菜单/权限兼容);含工艺路线联动选项
  */
 export const s0MfgProcessFlowCardsApi = {
-	list: (params: S0MfgFlowCardPrintRecordQuery) =>
-		service.get<Paged<S0MfgFlowCardPrintRecordRow>>(`${mfg}/process-flow-cards`, { params }).then(unwrap),
+	...crud<S0ProcessFlowCardRow>(`${mfg}/process-flow-cards`),
+	routingOptions: (itemNum: string) =>
+		service
+			.get<S0RoutingOption[]>(`${mfg}/process-flow-cards/routing-options`, { params: { itemNum } })
+			.then(unwrap),
 };
 export const s0MfgOrderScheduleCyclesApi = crud(`${mfg}/order-schedule-cycles`);
 export const s0MfgWorkOrderControlsApi = crud(`${mfg}/work-order-controls`);

+ 283 - 55
Web/src/views/aidop/s0/manufacturing/ProcessFlowCardList.vue

@@ -1,52 +1,52 @@
 <template>
 	<AidopDemoShell :title="pageTitle" subtitle="S0 / Manufacturing / 工序流转卡">
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
-			<el-form-item label="关键字">
-				<el-input v-model="query.keyword" clearable placeholder="工单号 / 物料编码 / 物料描述" style="width: 220px" />
-			</el-form-item>
 			<el-form-item label="工单号">
-				<el-input v-model="query.workOrd" clearable placeholder="工单号模糊" style="width: 160px" />
+				<el-input v-model="query.workOrderNo" clearable placeholder="工单号模糊" style="width: 160px" />
+			</el-form-item>
+			<el-form-item label="流转卡号">
+				<el-input v-model="query.flowCardNo" clearable placeholder="流转卡号模糊" style="width: 160px" />
 			</el-form-item>
 			<el-form-item label="物料编码">
 				<el-input v-model="query.itemNum" clearable placeholder="物料编码模糊" style="width: 160px" />
 			</el-form-item>
-			<el-form-item label="标签颜色">
-				<el-select v-model="query.labelColor" clearable placeholder="全部" style="width: 120px">
-					<el-option label="LZK" value="LZK" />
+			<el-form-item label="启用">
+				<el-select v-model="query.isActive" clearable placeholder="全部" style="width: 110px">
+					<el-option label="是" :value="true" />
+					<el-option label="否" :value="false" />
 				</el-select>
 			</el-form-item>
-			<el-form-item label="状态">
-				<el-input v-model="query.status" clearable placeholder="状态码" style="width: 100px" />
-			</el-form-item>
-			<el-form-item label="创建时间">
-				<el-date-picker
-					v-model="query.createTimeRange"
-					type="datetimerange"
-					value-format="YYYY-MM-DDTHH:mm:ss"
-					range-separator="-"
-					start-placeholder="开始"
-					end-placeholder="结束"
-					style="width: 360px"
-				/>
-			</el-form-item>
 			<el-form-item>
 				<el-button type="primary" @click="loadList">查询</el-button>
 				<el-button @click="resetQuery">重置</el-button>
+				<el-button type="success" @click="openCreate">新增</el-button>
 			</el-form-item>
 		</el-form>
 
 		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" max-height="calc(100vh - 260px)">
-			<el-table-column prop="id" label="记录ID" width="100" show-overflow-tooltip />
-			<el-table-column prop="workOrd" label="工单号" width="140" show-overflow-tooltip />
-			<el-table-column prop="op" label="工序" width="80" align="right" />
-			<el-table-column prop="itemNum" label="物料编码" width="140" show-overflow-tooltip />
-			<el-table-column prop="descr" label="物料描述" min-width="180" show-overflow-tooltip />
-			<el-table-column prop="labelFormat" label="标签格式" width="110" show-overflow-tooltip />
-			<el-table-column prop="labelColor" label="标签颜色" width="90" />
-			<el-table-column label="状态" width="110" show-overflow-tooltip>
-				<template #default="{ row }">{{ row.statusDesc || row.status || '' }}</template>
+			<el-table-column prop="workOrderNo" label="工单" width="130" show-overflow-tooltip />
+			<el-table-column prop="workOrderBatchNo" label="工单批号" width="120" show-overflow-tooltip />
+			<el-table-column prop="flowCardNo" label="流转卡号" width="130" show-overflow-tooltip />
+			<el-table-column prop="nature" label="性质" width="100" show-overflow-tooltip />
+			<el-table-column prop="itemNum" label="物料编码" width="130" show-overflow-tooltip />
+			<el-table-column prop="itemName" label="物料名称" min-width="150" show-overflow-tooltip />
+			<el-table-column prop="drawingNo" label="图号" width="130" show-overflow-tooltip />
+			<el-table-column prop="bomVersion" label="BOM版本" width="100" show-overflow-tooltip />
+			<el-table-column prop="routingCode" label="工艺路线" width="130" show-overflow-tooltip />
+			<el-table-column label="启用" width="72" align="center">
+				<template #default="{ row }">
+					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column label="操作" width="200" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+						{{ row.isActive ? '禁用' : '启用' }}
+					</el-button>
+					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+				</template>
 			</el-table-column>
-			<el-table-column prop="createTime" label="创建时间" width="170" />
 		</el-table>
 
 		<div class="pager">
@@ -60,50 +60,187 @@
 				@size-change="loadList"
 			/>
 		</div>
+
+		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="720px" destroy-on-close @closed="resetForm">
+			<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
+				<el-row :gutter="16">
+					<el-col :span="12"><el-form-item label="工单" prop="workOrderNo"><el-input v-model="form.workOrderNo" placeholder="工单号" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="工单批号"><el-input v-model="form.workOrderBatchNo" placeholder="工单批号" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="流转卡号" prop="flowCardNo"><el-input v-model="form.flowCardNo" placeholder="同工厂内唯一" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="性质"><el-input v-model="form.nature" placeholder="性质" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="物料编码" prop="itemNum">
+						<el-select
+							v-model="form.itemNum"
+							filterable
+							remote
+							reserve-keyword
+							:remote-method="remoteSearchMaterials"
+							:loading="materialLoading"
+							placeholder="输入物料编码/名称搜索"
+							style="width: 100%"
+							@change="onMaterialChange"
+						>
+							<el-option v-for="m in materialOptions" :key="m.itemNum" :label="`${m.itemNum} ${m.descr ?? ''}`" :value="m.itemNum" />
+						</el-select>
+					</el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="物料名称"><el-input v-model="form.itemName" placeholder="选物料自动带出,可改" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="图号"><el-input v-model="form.drawingNo" placeholder="选物料自动带出,可改" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="BOM版本"><el-input v-model="form.bomVersion" placeholder="选物料自动带出,可改" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="工艺路线">
+						<el-select
+							v-if="!routingIsManual"
+							v-model="form.routingCode"
+							filterable
+							clearable
+							placeholder="按物料关联路线选择"
+							style="width: 100%"
+						>
+							<el-option v-for="r in routingOptions" :key="r.routeCode" :label="r.routeName ? `${r.routeCode} ${r.routeName}` : r.routeCode" :value="r.routeCode" />
+						</el-select>
+						<el-input v-else v-model="form.routingCode" placeholder="该物料暂无关联路线,可手工填写" />
+					</el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="启用"><el-switch v-model="form.isActive" /></el-form-item></el-col>
+					<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remarks" type="textarea" :rows="2" /></el-form-item></el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<el-button @click="dialogVisible = false">取消</el-button>
+				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+			</template>
+		</el-dialog>
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopS0MfgProcessFlowCard">
 import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
 import {
+	loadOrgList,
+	searchMaterialsForMfg,
 	s0MfgProcessFlowCardsApi,
-	type S0MfgFlowCardPrintRecordRow,
+	type OrgOption,
+	type S0ProcessFlowCardRow,
+	type S0ProcessFlowCardUpsert,
+	type S0RoutingOption,
 } from '../api/s0ManufacturingApi';
+import {
+	applyDefaultOrgScopeForCreate,
+	normalizeOrgScopeForSubmit,
+	preserveOrgScopeForEdit,
+} from '../composables/useS0MfgOrgScope';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '工序流转卡');
 
+interface MaterialOption {
+	itemNum: string;
+	descr?: string | null;
+	drawing?: string | null;
+	bomVersion?: string | null;
+}
+
 const query = reactive({
-	keyword: '',
-	workOrd: '',
+	workOrderNo: '',
+	flowCardNo: '',
 	itemNum: '',
-	labelColor: '',
-	status: '',
-	createTimeRange: [] as string[],
+	isActive: undefined as boolean | undefined,
 	page: 1,
 	pageSize: 20,
 });
-
 const loading = ref(false);
-const rows = ref<S0MfgFlowCardPrintRecordRow[]>([]);
+const rows = ref<S0ProcessFlowCardRow[]>([]);
 const total = ref(0);
 
+const dialogVisible = ref(false);
+const dialogTitle = ref('新增');
+const editingId = ref<number | null>(null);
+const saving = ref(false);
+const formRef = ref<FormInstance>();
+
+const companyOptions = ref<OrgOption[]>([]);
+const factoryOptions = ref<OrgOption[]>([]);
+const materialOptions = ref<MaterialOption[]>([]);
+const materialLoading = ref(false);
+const routingOptions = ref<S0RoutingOption[]>([]);
+const routingIsManual = computed(() => routingOptions.value.length === 0);
+
+const form = reactive<S0ProcessFlowCardUpsert>({
+	companyRefId: undefined,
+	factoryRefId: undefined,
+	domainCode: '',
+	workOrderNo: '',
+	workOrderBatchNo: '',
+	flowCardNo: '',
+	nature: '',
+	itemNum: '',
+	itemName: '',
+	drawingNo: '',
+	bomVersion: '',
+	routingCode: '',
+	remarks: '',
+	isActive: true,
+	createUser: '',
+	updateUser: '',
+});
+
+const rules: FormRules = {
+	workOrderNo: [{ required: true, message: '请填写工单', trigger: 'blur' }],
+	flowCardNo: [{ required: true, message: '请填写流转卡号', trigger: 'blur' }],
+	itemNum: [{ required: true, message: '请选择物料编码', trigger: 'change' }],
+};
+
+async function remoteSearchMaterials(keyword: string) {
+	if (!keyword) return;
+	materialLoading.value = true;
+	try {
+		const data = await searchMaterialsForMfg({ keyword, page: 1, pageSize: 50 });
+		materialOptions.value = (data.list as any[]).map((m) => ({
+			itemNum: m.itemNum,
+			descr: m.descr,
+			drawing: m.drawing,
+			bomVersion: m.bomVersion,
+		}));
+	} catch {
+		materialOptions.value = [];
+	} finally {
+		materialLoading.value = false;
+	}
+}
+
+// 用户重新选择物料时才覆盖 名称/图号/BOM版本,并重置工艺路线(旧路线属于旧物料)。
+function onMaterialChange(itemNum: string) {
+	const m = materialOptions.value.find((x) => x.itemNum === itemNum);
+	if (m) {
+		form.itemName = m.descr ?? '';
+		form.drawingNo = m.drawing ?? '';
+		form.bomVersion = m.bomVersion ?? '';
+	}
+	void loadRoutingOptions(itemNum, true);
+}
+
+async function loadRoutingOptions(itemNum: string | undefined, resetSelected: boolean) {
+	if (resetSelected) form.routingCode = '';
+	routingOptions.value = [];
+	if (!itemNum) return;
+	try {
+		routingOptions.value = await s0MfgProcessFlowCardsApi.routingOptions(itemNum);
+	} catch {
+		routingOptions.value = [];
+	}
+}
+
 async function loadList() {
 	loading.value = true;
 	try {
-		const [from, to] = query.createTimeRange || [];
 		const data = await s0MfgProcessFlowCardsApi.list({
 			page: query.page,
 			pageSize: query.pageSize,
-			keyword: query.keyword || undefined,
-			workOrd: query.workOrd || undefined,
+			workOrderNo: query.workOrderNo || undefined,
+			flowCardNo: query.flowCardNo || undefined,
 			itemNum: query.itemNum || undefined,
-			labelColor: query.labelColor || undefined,
-			status: query.status || undefined,
-			createTimeFrom: from || undefined,
-			createTimeTo: to || undefined,
+			isActive: query.isActive,
 		});
 		rows.value = data.list;
 		total.value = data.total;
@@ -116,18 +253,109 @@ async function loadList() {
 }
 
 function resetQuery() {
-	query.keyword = '';
-	query.workOrd = '';
-	query.itemNum = '';
-	query.labelColor = '';
-	query.status = '';
-	query.createTimeRange = [];
-	query.page = 1;
+	Object.assign(query, { workOrderNo: '', flowCardNo: '', itemNum: '', isActive: undefined, page: 1 });
 	void loadList();
 }
 
-onMounted(() => {
-	void loadList();
+function resetForm() {
+	editingId.value = null;
+	Object.assign(form, {
+		companyRefId: undefined, factoryRefId: undefined, domainCode: '',
+		workOrderNo: '', workOrderBatchNo: '', flowCardNo: '', nature: '',
+		itemNum: '', itemName: '', drawingNo: '', bomVersion: '', routingCode: '',
+		remarks: '', isActive: true, createUser: '', updateUser: '',
+	});
+	materialOptions.value = [];
+	routingOptions.value = [];
+	formRef.value?.clearValidate();
+}
+
+function openCreate() {
+	resetForm();
+	// 新增:后端会 stamp 默认组织,前端同步补默认便于校验/透传。
+	applyDefaultOrgScopeForCreate(form, companyOptions.value, factoryOptions.value);
+	dialogTitle.value = '新增工序流转卡';
+	dialogVisible.value = true;
+}
+
+function openEdit(row: S0ProcessFlowCardRow) {
+	resetForm();
+	editingId.value = row.id;
+	dialogTitle.value = `编辑:${row.flowCardNo}`;
+	Object.assign(form, {
+		companyRefId: row.companyRefId, factoryRefId: row.factoryRefId, domainCode: row.domainCode ?? '',
+		workOrderNo: row.workOrderNo, workOrderBatchNo: row.workOrderBatchNo ?? '', flowCardNo: row.flowCardNo,
+		nature: row.nature ?? '', itemNum: row.itemNum, itemName: row.itemName ?? '', drawingNo: row.drawingNo ?? '',
+		bomVersion: row.bomVersion ?? '', routingCode: row.routingCode ?? '', remarks: row.remarks ?? '',
+		isActive: row.isActive, createUser: row.createUser ?? '', updateUser: row.updateUser ?? '',
+	});
+	// 编辑:保留接口返回的原归属(legacy 不转雪花)。
+	preserveOrgScopeForEdit(form, row);
+	// 预置物料下拉当前值以便显示;加载工艺路线选项但不重置已保存路线(不覆盖历史值)。
+	if (row.itemNum) {
+		materialOptions.value = [{ itemNum: row.itemNum, descr: row.itemName, drawing: row.drawingNo, bomVersion: row.bomVersion }];
+		void loadRoutingOptions(row.itemNum, false);
+	}
+	dialogVisible.value = true;
+}
+
+async function submitForm() {
+	try {
+		await formRef.value?.validate();
+	} catch {
+		// 校验未通过:表单已就地提示,不继续提交。
+		return;
+	}
+	normalizeOrgScopeForSubmit(form, editingId.value ? 'edit' : 'create', companyOptions.value, factoryOptions.value);
+	saving.value = true;
+	try {
+		const payload: S0ProcessFlowCardUpsert = { ...form };
+		if (editingId.value) {
+			await s0MfgProcessFlowCardsApi.update(editingId.value, payload);
+			ElMessage.success('已保存');
+		} else {
+			await s0MfgProcessFlowCardsApi.create(payload);
+			ElMessage.success('已创建');
+		}
+		dialogVisible.value = false;
+		await loadList();
+	} catch {
+		// 保存失败(如流转卡号重复 409):错误消息由全局 axios 拦截器提示,弹窗保持打开供修改。
+	} finally {
+		saving.value = false;
+	}
+}
+
+function onDelete(row: S0ProcessFlowCardRow) {
+	ElMessageBox.confirm(`确定删除流转卡「${row.flowCardNo}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await s0MfgProcessFlowCardsApi.delete(row.id);
+			ElMessage.success('已删除');
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+function toggleActive(row: S0ProcessFlowCardRow) {
+	const next = !row.isActive;
+	ElMessageBox.confirm(`确定${next ? '启用' : '禁用'}流转卡「${row.flowCardNo}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await s0MfgProcessFlowCardsApi.toggleEnabled(row.id, { isEnabled: next });
+			ElMessage.success('操作成功');
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+async function loadOptions() {
+	const [companies, factories] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
+	companyOptions.value = companies;
+	factoryOptions.value = factories;
+}
+
+onMounted(async () => {
+	await loadOptions();
+	await loadList();
 });
 </script>
 

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET ͨ��Ȩ�޿���ƽ̨</Description>
-    <AssemblyVersion>1.0.252</AssemblyVersion>
-    <FileVersion>1.0.252</FileVersion>
-    <Version>1.0.252</Version>
+    <AssemblyVersion>1.0.253</AssemblyVersion>
+    <FileVersion>1.0.253</FileVersion>
+    <Version>1.0.253</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 0 - 74
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Manufacturing/AdoS0MfgProcessFlowCardsController.cs

@@ -1,74 +0,0 @@
-using Admin.NET.Plugin.AiDOP.Dto.S0.Manufacturing;
-using Admin.NET.Plugin.AiDOP.Entity;
-using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
-using Admin.NET.Plugin.AiDOP.Infrastructure;
-
-namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Manufacturing;
-
-/// <summary>
-/// 流转卡打印记录(legacy MissedPrint 只读投影)。
-/// 路由保留 /api/s0/manufacturing/process-flow-cards 以降低前端 / 菜单 / 权限迁移成本。
-/// 数据源由 S0ProcessFlowCard(当前 0 行)切换为 MissedPrint(200+ 行)。
-/// 写入路径仍由 S3 SupplierShipment / SupplierDelivery 服务负责,本 Controller 仅提供只读分页查询。
-/// </summary>
-[ApiController]
-[Route("api/s0/manufacturing/process-flow-cards")]
-[AllowAnonymous]
-[NonUnify]
-public class AdoS0MfgProcessFlowCardsController : ControllerBase
-{
-    private readonly SqlSugarRepository<AdoS0MissedPrintRecord> _rep;
-
-    public AdoS0MfgProcessFlowCardsController(SqlSugarRepository<AdoS0MissedPrintRecord> rep) => _rep = rep;
-
-    [HttpGet]
-    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0FlowCardPrintRecordQueryDto q)
-    {
-        var page = q.EffectivePage;
-        var pageSize = q.PageSize;
-        (page, pageSize) = PagingGuard.Normalize(page, pageSize);
-
-        var query = _rep.Context.Queryable<AdoS0MissedPrintRecord, AdoGeneralizedCodeRecord>(
-                (m, g) => new JoinQueryInfos(
-                    JoinType.Left, g.FldName == "BarcodeStatus" && g.Val == m.Status))
-            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword),
-                (m, g) => (m.WorkOrd != null && m.WorkOrd.Contains(q.Keyword!))
-                       || (m.ItemNum != null && m.ItemNum.Contains(q.Keyword!))
-                       || (m.Descr != null && m.Descr.Contains(q.Keyword!)))
-            .WhereIF(!string.IsNullOrWhiteSpace(q.WorkOrd),
-                (m, g) => m.WorkOrd != null && m.WorkOrd.Contains(q.WorkOrd!))
-            .WhereIF(!string.IsNullOrWhiteSpace(q.ItemNum),
-                (m, g) => m.ItemNum != null && m.ItemNum.Contains(q.ItemNum!))
-            .WhereIF(!string.IsNullOrWhiteSpace(q.LabelColor),
-                (m, g) => m.LabelColor == q.LabelColor)
-            .WhereIF(!string.IsNullOrWhiteSpace(q.Status),
-                (m, g) => m.Status == q.Status)
-            .WhereIF(q.CreateTimeFrom.HasValue,
-                (m, g) => m.CreateTime >= q.CreateTimeFrom!.Value)
-            .WhereIF(q.CreateTimeTo.HasValue,
-                (m, g) => m.CreateTime <= q.CreateTimeTo!.Value);
-
-        var total = await query.CountAsync();
-        var list = await query
-            .Select((m, g) => new S0MfgFlowCardPrintRecordRow
-            {
-                Id = m.Id,
-                WorkOrd = m.WorkOrd,
-                Op = m.Op,
-                ItemNum = m.ItemNum,
-                Descr = m.Descr,
-                LabelFormat = m.LabelFormat,
-                LabelColor = m.LabelColor,
-                Status = m.Status,
-                StatusDesc = g.Comments ?? m.Status,
-                CreateTime = m.CreateTime
-            })
-            .MergeTable()
-            .OrderByDescending(r => r.CreateTime)
-            .OrderByDescending(r => r.Id)
-            .Skip((page - 1) * pageSize).Take(pageSize)
-            .ToListAsync();
-
-        return Ok(new { total, page, pageSize, list });
-    }
-}

+ 209 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Manufacturing/AdoS0ProcessFlowCardsController.cs

@@ -0,0 +1,209 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Manufacturing;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Manufacturing;
+
+/// <summary>
+/// 工序流转卡主数据 CRUD(数据源:S0ProcessFlowCard 主表)。
+///
+/// 恢复主表链路:本 Controller 完全读写 S0ProcessFlowCard,替换此前误接 legacy MissedPrint 的只读投影。
+/// 路由沿用 /api/s0/manufacturing/process-flow-cards,接口地址不变 → 前端 / 菜单 / 权限链路兼容。
+///
+/// 组织兜底(后端强制,不只依赖前端 payload):
+///   - 新增:公司/工厂由后端按 S0 默认组织(SysOrg Code=S0-C001 / S0-F001)解析并 stamp,忽略 payload 组织值 → 杜绝跨厂注入;
+///   - 编辑:公司/工厂 pin 到既有记录原值,忽略 payload 组织值 → 杜绝跨公司/跨工厂篡改;
+///   - 详情/删除/启停:按 id 操作,不接受组织入参。
+/// 唯一性:依赖 DB 唯一索引 uk_S0ProcessFlowCard_factory_flowcard (factory_ref_id, flow_card_no);
+///   预检友好 409,并发竞态由唯一索引兜底(AdoS0ExceptionFilter 将重复键映射为友好 409)。
+/// </summary>
+[ApiController]
+[Route("api/s0/manufacturing/process-flow-cards")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0ProcessFlowCardsController : ControllerBase
+{
+    private const string DefaultCompanyCode = "S0-C001";
+    private const string DefaultFactoryCode = "S0-F001";
+
+    private readonly SqlSugarRepository<AdoS0ProcessFlowCard> _rep;
+    private readonly SqlSugarRepository<AdoS0MfgRoutingOpDetail> _routingRep;
+    private readonly SqlSugarRepository<SysOrg> _orgRep;
+
+    public AdoS0ProcessFlowCardsController(
+        SqlSugarRepository<AdoS0ProcessFlowCard> rep,
+        SqlSugarRepository<AdoS0MfgRoutingOpDetail> routingRep,
+        SqlSugarRepository<SysOrg> orgRep)
+    {
+        _rep = rep;
+        _routingRep = routingRep;
+        _orgRep = orgRep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0ProcessFlowCardQueryDto q)
+    {
+        var page = q.EffectivePage;
+        var pageSize = q.PageSize;
+        (page, pageSize) = PagingGuard.Normalize(page, pageSize);
+
+        var query = _rep.AsQueryable()
+            .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
+            .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.WorkOrderNo), x => x.WorkOrderNo.Contains(q.WorkOrderNo!))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.FlowCardNo), x => x.FlowCardNo.Contains(q.FlowCardNo!))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.ItemNum), x => x.ItemNum.Contains(q.ItemNum!))
+            .WhereIF(q.IsActive.HasValue, x => x.IsActive == q.IsActive!.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword),
+                x => x.WorkOrderNo.Contains(q.Keyword!)
+                    || x.FlowCardNo.Contains(q.Keyword!)
+                    || x.ItemNum.Contains(q.Keyword!)
+                    || (x.ItemName != null && x.ItemName.Contains(q.Keyword!)));
+
+        var total = await query.CountAsync();
+        var list = await query
+            .OrderByDescending(x => x.CreateTime)
+            .OrderByDescending(x => x.Id)
+            .Skip((page - 1) * pageSize).Take(pageSize)
+            .ToListAsync();
+        return Ok(new { total, page, pageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        return item == null ? AdoS0ApiErrors.NotFound("流转卡不存在") : Ok(item);
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0ProcessFlowCardUpsertDto dto)
+    {
+        var (companyId, factoryId) = await ResolveDefaultOrgAsync();
+        if (companyId <= 0 || factoryId <= 0)
+            return AdoS0ApiErrors.InvalidRequest("S0 默认组织未初始化(SysOrg S0-C001 / S0-F001 缺失),无法创建流转卡");
+
+        var flowCardNo = dto.FlowCardNo?.Trim() ?? string.Empty;
+        if (await _rep.IsAnyAsync(x => x.FactoryRefId == factoryId && x.FlowCardNo == flowCardNo))
+            return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "流转卡号已存在");
+
+        var now = DateTime.Now;
+        var entity = new AdoS0ProcessFlowCard
+        {
+            // 组织字段后端强制 stamp 默认 S0 组织,忽略 payload 组织值(杜绝跨厂注入)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
+            DomainCode = dto.DomainCode?.Trim(),
+            WorkOrderNo = dto.WorkOrderNo?.Trim() ?? string.Empty,
+            WorkOrderBatchNo = dto.WorkOrderBatchNo?.Trim(),
+            FlowCardNo = flowCardNo,
+            Nature = dto.Nature?.Trim(),
+            ItemNum = dto.ItemNum?.Trim() ?? string.Empty,
+            ItemName = dto.ItemName?.Trim(),
+            DrawingNo = dto.DrawingNo?.Trim(),
+            BomVersion = dto.BomVersion?.Trim(),
+            RoutingCode = dto.RoutingCode?.Trim(),
+            Remarks = dto.Remarks?.Trim(),
+            IsActive = dto.IsActive,
+            CreateUser = dto.CreateUser,
+            CreateTime = now,
+            UpdateUser = dto.UpdateUser,
+            UpdateTime = null
+        };
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0ProcessFlowCardUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return AdoS0ApiErrors.NotFound("流转卡不存在");
+
+        var flowCardNo = dto.FlowCardNo?.Trim() ?? string.Empty;
+        // 唯一性预检以既有记录的工厂为界(组织 pin 原值,不接受 payload 迁移工厂)。
+        if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == entity.FactoryRefId && x.FlowCardNo == flowCardNo))
+            return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "流转卡号已存在");
+
+        // 组织字段 pin 到既有原值,忽略 payload 组织值(杜绝跨公司/跨工厂篡改)。
+        entity.WorkOrderNo = dto.WorkOrderNo?.Trim() ?? string.Empty;
+        entity.WorkOrderBatchNo = dto.WorkOrderBatchNo?.Trim();
+        entity.FlowCardNo = flowCardNo;
+        entity.Nature = dto.Nature?.Trim();
+        entity.ItemNum = dto.ItemNum?.Trim() ?? string.Empty;
+        entity.ItemName = dto.ItemName?.Trim();
+        entity.DrawingNo = dto.DrawingNo?.Trim();
+        entity.BomVersion = dto.BomVersion?.Trim();
+        entity.RoutingCode = dto.RoutingCode?.Trim();
+        entity.Remarks = dto.Remarks?.Trim();
+        entity.IsActive = dto.IsActive;
+        entity.DomainCode = dto.DomainCode?.Trim();
+        entity.UpdateUser = dto.UpdateUser;
+        entity.UpdateTime = DateTime.Now;
+
+        await _rep.AsUpdateable(entity)
+            .IgnoreColumns(x => new { x.CompanyRefId, x.FactoryRefId, x.CreateUser, x.CreateTime })
+            .ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpPatch("{id:long}/toggle-enabled")]
+    public async Task<IActionResult> ToggleActiveAsync(long id, [FromBody] AdoS0ProcessFlowCardToggleActiveDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return AdoS0ApiErrors.NotFound("流转卡不存在");
+        entity.IsActive = dto.IsEnabled;
+        entity.UpdateTime = DateTime.Now;
+        await _rep.AsUpdateable(entity)
+            .UpdateColumns(x => new { x.IsActive, x.UpdateTime })
+            .ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        if (item == null) return AdoS0ApiErrors.NotFound("流转卡不存在");
+        await _rep.DeleteAsync(item);
+        return Ok(new { message = "删除成功" });
+    }
+
+    /// <summary>
+    /// 工艺路线联动选项:按所选物料编码过滤该物料关联的 RouteCode(去重)。
+    /// 用稳定物料编码 ItemNum 关联(RoutingOpDetail.MaterialCode),不用物料名称。
+    /// 返回空 → 前端允许手工填写兜底;返回非空 → 前端强制下拉。
+    /// </summary>
+    [HttpGet("routing-options")]
+    public async Task<IActionResult> GetRoutingOptionsAsync([FromQuery] string? itemNum)
+    {
+        var key = itemNum?.Trim();
+        if (string.IsNullOrWhiteSpace(key))
+            return Ok(Array.Empty<object>());
+
+        var list = await _routingRep.AsQueryable()
+            .Where(x => x.MaterialCode == key)
+            .GroupBy(x => new { x.RouteCode, x.RouteName })
+            .Select(x => new { routeCode = x.RouteCode, routeName = x.RouteName })
+            .ToListAsync();
+
+        var options = list
+            .Where(x => !string.IsNullOrWhiteSpace(x.routeCode))
+            .OrderBy(x => x.routeCode)
+            .ToList();
+        return Ok(options);
+    }
+
+    /// <summary>
+    /// 解析 S0 默认组织(公司 S0-C001 / 工厂 S0-F001)。config 级查找按 Code 全局唯一,ClearFilter 规避租户过滤。
+    /// </summary>
+    private async Task<(long CompanyId, long FactoryId)> ResolveDefaultOrgAsync()
+    {
+        var company = await _orgRep.AsQueryable().ClearFilter()
+            .Where(x => x.Code == DefaultCompanyCode).Select(x => x.Id).FirstAsync();
+        var factory = await _orgRep.AsQueryable().ClearFilter()
+            .Where(x => x.Code == DefaultFactoryCode).Select(x => x.Id).FirstAsync();
+        return (company, factory);
+    }
+}

+ 0 - 28
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Manufacturing/AdoS0ManufacturingDtos.cs

@@ -1007,32 +1007,4 @@ public class AdoS0OrderScheduleCycleToggleActiveDto
     public bool IsEnabled { get; set; }
 }
 
-// =========================
-// 流转卡打印记录(legacy MissedPrint 只读投影)
-// =========================
-
-public class AdoS0FlowCardPrintRecordQueryDto : AdoS0MfgPagedQueryBase
-{
-    public string? WorkOrd { get; set; }
-    public string? ItemNum { get; set; }
-    public string? LabelColor { get; set; }
-    public string? Status { get; set; }
-    public DateTime? CreateTimeFrom { get; set; }
-    public DateTime? CreateTimeTo { get; set; }
-}
-
-public class S0MfgFlowCardPrintRecordRow
-{
-    public long Id { get; set; }
-    public string? WorkOrd { get; set; }
-    public int? Op { get; set; }
-    public string? ItemNum { get; set; }
-    public string? Descr { get; set; }
-    public string? LabelFormat { get; set; }
-    public string? LabelColor { get; set; }
-    public string? Status { get; set; }
-    public string? StatusDesc { get; set; }
-    public DateTime? CreateTime { get; set; }
-}
-
 #endregion

+ 0 - 38
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0MissedPrintRecord.cs

@@ -1,38 +0,0 @@
-namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
-
-/// <summary>
-/// 工序流转卡打印记录(legacy MissedPrint 只读投影)。
-/// 列表展示仅取 9 个字段;MissedPrint 80+ 余字段由 S3 SupplierShipment / SupplierDelivery 写入路径负责,本 Entity 不投影。
-/// 标 IgnoreTable 避免 SqlSugar CodeFirst 介入 legacy 表 schema。
-/// </summary>
-[IgnoreTable]
-[SugarTable("MissedPrint", "工序流转卡打印记录(legacy MissedPrint 只读投影)")]
-public class AdoS0MissedPrintRecord
-{
-    [SugarColumn(ColumnName = "RecID", IsPrimaryKey = true, ColumnDataType = "bigint")]
-    public long Id { get; set; }
-
-    [SugarColumn(ColumnName = "WorkOrd", Length = 100, IsNullable = true)]
-    public string? WorkOrd { get; set; }
-
-    [SugarColumn(ColumnName = "Op", IsNullable = true)]
-    public int? Op { get; set; }
-
-    [SugarColumn(ColumnName = "ItemNum", Length = 100, IsNullable = true)]
-    public string? ItemNum { get; set; }
-
-    [SugarColumn(ColumnName = "Descr", Length = 255, IsNullable = true)]
-    public string? Descr { get; set; }
-
-    [SugarColumn(ColumnName = "LabelFormat", Length = 50, IsNullable = true)]
-    public string? LabelFormat { get; set; }
-
-    [SugarColumn(ColumnName = "LabelColor", Length = 20, IsNullable = true)]
-    public string? LabelColor { get; set; }
-
-    [SugarColumn(ColumnName = "Status", Length = 10, IsNullable = true)]
-    public string? Status { get; set; }
-
-    [SugarColumn(ColumnName = "CreateTime", IsNullable = true)]
-    public DateTime? CreateTime { get; set; }
-}

+ 1 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AdoS0ApiErrors.cs

@@ -23,6 +23,7 @@ public static class AdoS0ApiErrors
         ("/api/s0/manufacturing/production-lines", AdoS0ErrorCodes.DuplicateCode, "生产线编码已存在"),
         ("/api/s0/manufacturing/routings", AdoS0ErrorCodes.DuplicateCode, "工艺路线编码已存在"),
         ("/api/s0/manufacturing/work-order-controls", AdoS0ErrorCodes.DuplicateCode, "工单控制参数编码已存在"),
+        ("/api/s0/manufacturing/process-flow-cards", AdoS0ErrorCodes.DuplicateCode, "流转卡号已存在"),
         ("/api/s0/manufacturing/person-skills", AdoS0ErrorCodes.DuplicateCode, "人员技能编码已存在"),
         ("/api/s0/manufacturing/line-posts", AdoS0ErrorCodes.DuplicateCode, "线体岗位编码已存在"),
         ("/api/s0/manufacturing/work-centers", AdoS0ErrorCodes.DuplicateCode, "工作中心编码已存在"),