Переглянути джерело

feat(s7): FQC 成品检验任务/检验单只读列表接入真实数据源

- 新增 FqcInspBillService(api/S7FqcInspBill)task-list/result-list 两只读接口
- 任务源 qms_fqcbj(归口空表,运行时 EnsureTables 幂等建表,方案A同IPQC)
- 检验单源 qms_qcpp_inspbill + WorkOrdMaster(sczld=Batch 取SAP工单号,MIN)
- 新增 FqcInspBillDto、前端 fqcInspBill.ts,接线两既有骨架页(只读,动作按钮禁用)
- 复用 FUNC-S7-001/002 菜单;纯只读,业务表零写入
- bump version Web 2.4.245 / server 1.0.243
YY968XX 20 годин тому
батько
коміт
5427f56ef2

+ 1 - 1
Web/package.json

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

+ 99 - 0
Web/src/views/aidop/s7/api/fqcInspBill.ts

@@ -0,0 +1,99 @@
+import service from '/@/utils/request';
+import { withAidopTenantParams } from '../../api/aidopTenant';
+
+export interface FqcPaged<T> {
+	total: number;
+	page: number;
+	pageSize: number;
+	list: T[];
+}
+
+// ── S7 FQC 检验任务列表(只读,数据源 aidopdev.qms_fqcbj,归口自旧系统,空表待抽数)──
+export interface FqcTaskRow {
+	/** 主键 id */
+	id?: number;
+	/** 单据编号 FBILLNO */
+	billNo?: string;
+	/** 生产指令单 sczld */
+	productionOrderNo?: string;
+	/** 申请组织 FORGID(原样 id) */
+	applyOrgId?: number;
+	/** 申请时间 FAPPLYTIME */
+	applyTime?: string;
+	/** 物料编码 wlbm */
+	materialCode?: string;
+	/** 产品名称 cpmc */
+	productName?: string;
+	/** 产品型号 cpxh */
+	productModel?: string;
+	/** 生产批号 scph */
+	productionBatchNo?: string;
+	/** 指令单数量 sczldsl */
+	orderQty?: number;
+	/** 数量 sl */
+	qty?: number;
+	/** 申请人 FAPPLYUSER */
+	applicant?: string;
+	/** 备注 FCOMMENT */
+	remark?: string;
+	/** 检验优先级 yxj(原样,未接字典) */
+	priority?: string;
+	/** 检验开始时间 jykssj */
+	inspectStartTime?: string;
+	/** 检验完成时间 jywcsj */
+	inspectFinishTime?: string;
+	/** 检验负责人 jyfzr */
+	inspector?: string;
+	/** 检验进度 FINSPECTSTATUS(原样) */
+	inspectProgress?: string;
+}
+
+// ── S7 FQC 检验单(结果)列表(只读,数据源 aidopdev.qms_qcpp_inspbill + WorkOrdMaster)──
+export interface FqcResultRow {
+	/** 主键 id */
+	id?: number;
+	/** 单据编号 FBILLNO */
+	billNo?: string;
+	/** 检验时间 FINSPEENDDATE */
+	inspectTime?: string;
+	/** 生产批号 scph */
+	productionBatchNo?: string;
+	/** 物料编码 FMATERIALCFG */
+	materialCode?: string;
+	/** 物料名称 wlmc */
+	materialName?: string;
+	/** 规格型号 ggxh */
+	spec?: string;
+	/** 检验人 jyr */
+	inspector?: string;
+	/** 检验数量 jysj */
+	inspectQty?: number;
+	/** 来源单据编号 lydjbh */
+	sourceBillNo?: string;
+	/** 生产指令单 sczld */
+	productionOrderNo?: string;
+	/** 指令单数量 sczldsl */
+	orderQty?: number;
+	/** SAP 工单号 WorkOrdMaster.WorkOrd(sczld=Batch,MIN 取值) */
+	sapWorkOrderNo?: string;
+	/** 判定 pd(原样,未接字典) */
+	judgment?: string;
+	/** 合格数量 hgsl */
+	qualifiedQty?: number;
+	/** 不合格数量 bhgsl */
+	unqualifiedQty?: number;
+}
+
+/** FQC 检验任务列表(只读) */
+export function fetchFqcTaskList(params: any) {
+	return service
+		.get<FqcPaged<FqcTaskRow>>('/api/S7FqcInspBill/task-list', { params: withAidopTenantParams(params) })
+		.then((r) => r.data);
+}
+
+/** FQC 检验单(结果)列表(只读) */
+export function fetchFqcResultList(params: any) {
+	return service
+		.get<FqcPaged<FqcResultRow>>('/api/S7FqcInspBill/result-list', { params: withAidopTenantParams(params) })
+		.then((r) => r.data);
+}

+ 92 - 6
Web/src/views/aidop/s7/fqc/fqcInspectionResultList.vue

@@ -10,15 +10,42 @@
 			<el-form-item label="生产指令单">
 				<el-input v-model="query.productionOrderNo" clearable style="width: 200px" />
 			</el-form-item>
+			<el-form-item label="物料编码">
+				<el-input v-model="query.materialCode" clearable style="width: 200px" />
+			</el-form-item>
+			<el-form-item label="检验人">
+				<el-input v-model="query.inspector" clearable style="width: 160px" />
+			</el-form-item>
+			<el-form-item label="检验日期">
+				<el-date-picker
+					v-model="inspectDateRange"
+					type="daterange"
+					value-format="YYYY-MM-DD"
+					range-separator="-"
+					start-placeholder="开始日期"
+					end-placeholder="结束日期"
+					unlink-panels
+					style="width: 260px"
+				/>
+			</el-form-item>
 			<el-form-item>
 				<el-button type="primary" @click="doSearch">查询</el-button>
 				<el-button @click="resetQuery">重置</el-button>
 			</el-form-item>
 		</el-form>
 
+		<!-- 本批只读:添加按钮保留占位但禁用,不接真实写动作 -->
+		<div class="mb12 actions">
+			<el-tooltip content="本批为只读列表,暂不开放" placement="top">
+				<span><el-button disabled>添加</el-button></span>
+			</el-tooltip>
+		</div>
+
 		<el-table :data="rows" row-key="id" v-loading="loading" border stripe>
 			<el-table-column prop="billNo" label="单据编号" min-width="150" show-overflow-tooltip resizable />
-			<el-table-column prop="inspectTime" label="检验时间" min-width="170" resizable />
+			<el-table-column prop="inspectTime" label="检验时间" min-width="170" resizable>
+				<template #default="{ row }">{{ fmtDateTime(row.inspectTime) }}</template>
+			</el-table-column>
 			<el-table-column prop="productionBatchNo" label="生产批号" min-width="140" show-overflow-tooltip resizable />
 			<el-table-column prop="materialCode" label="物料编码" min-width="140" show-overflow-tooltip resizable />
 			<el-table-column prop="materialName" label="物料名称" min-width="160" show-overflow-tooltip resizable />
@@ -26,8 +53,11 @@
 			<el-table-column prop="inspector" label="检验人" min-width="120" show-overflow-tooltip resizable />
 			<el-table-column prop="inspectQty" label="检验数量" min-width="110" resizable />
 			<el-table-column prop="sourceBillNo" label="来源单据编号" min-width="150" show-overflow-tooltip resizable />
-			<el-table-column prop="productionOrderNo" label="生产指单" min-width="150" show-overflow-tooltip resizable />
+			<el-table-column prop="productionOrderNo" label="生产指单" min-width="150" show-overflow-tooltip resizable />
 			<el-table-column prop="sapWorkOrderNo" label="SAP工单号" min-width="140" show-overflow-tooltip resizable />
+			<el-table-column prop="judgment" label="判定" min-width="120" show-overflow-tooltip resizable />
+			<el-table-column prop="qualifiedQty" label="合格数量" min-width="110" resizable />
+			<el-table-column prop="unqualifiedQty" label="不合格数量" min-width="110" resizable />
 			<template #empty>
 				<el-empty description="暂无数据" />
 			</template>
@@ -40,48 +70,104 @@
 				:total="total"
 				:page-sizes="[10, 20, 50]"
 				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="loadList"
 			/>
 		</div>
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopS7FqcResultList">
-import { computed, reactive, ref } from 'vue';
+import { computed, onActivated, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import { fetchFqcResultList, type FqcResultRow } from '../api/fqcInspBill';
 
 const route = useRoute();
-const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验结果列表');
+const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验列表');
 
 const query = reactive({
 	billNo: '',
 	productionBatchNo: '',
 	productionOrderNo: '',
+	materialCode: '',
+	inspector: '',
+	inspectDateStart: '',
+	inspectDateEnd: '',
 	page: 1,
 	pageSize: 10,
 });
 
-// 本批次为只读 UI 骨架,尚无真实 FQC 检验结果接口:渲染空表,不发请求、不造数据。
+const inspectDateRange = ref<[string, string] | null>(null);
+
 const loading = ref(false);
-const rows = ref<Record<string, unknown>[]>([]);
+const rows = ref<FqcResultRow[]>([]);
 const total = ref(0);
 
+function fmtDateTime(v?: string | null) {
+	if (!v) return '';
+	const s = String(v);
+	return s.length > 19 ? s.slice(0, 19).replace('T', ' ') : s.replace('T', ' ');
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		query.inspectDateStart = inspectDateRange.value?.[0] || '';
+		query.inspectDateEnd = inspectDateRange.value?.[1] || '';
+		const data = await fetchFqcResultList({
+			billNo: query.billNo,
+			productionBatchNo: query.productionBatchNo,
+			productionOrderNo: query.productionOrderNo,
+			materialCode: query.materialCode,
+			inspector: query.inspector,
+			inspectDateStart: query.inspectDateStart,
+			inspectDateEnd: query.inspectDateEnd,
+			page: query.page,
+			pageSize: query.pageSize,
+		});
+		rows.value = data.list || [];
+		total.value = data.total || 0;
+	} catch (e: any) {
+		rows.value = [];
+		total.value = 0;
+		ElMessage.error(e?.message || '加载FQC检验单列表失败');
+	} finally {
+		loading.value = false;
+	}
+}
+
 function doSearch() {
 	query.page = 1;
+	loadList();
 }
 
 function resetQuery() {
 	query.billNo = '';
 	query.productionBatchNo = '';
 	query.productionOrderNo = '';
+	query.materialCode = '';
+	query.inspector = '';
+	inspectDateRange.value = null;
+	query.inspectDateStart = '';
+	query.inspectDateEnd = '';
 	query.page = 1;
+	loadList();
 }
+
+onMounted(() => loadList());
+onActivated(() => loadList());
 </script>
 
 <style scoped lang="scss">
 .mb12 {
 	margin-bottom: 12px;
 }
+.actions {
+	display: flex;
+	gap: 8px;
+}
 .pager {
 	margin-top: 12px;
 	display: flex;

+ 104 - 18
Web/src/views/aidop/s7/fqc/fqcInspectionTaskList.vue

@@ -1,8 +1,8 @@
 <template>
 	<AidopDemoShell :title="pageTitle" subtitle="只读列表">
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
-			<el-form-item label="单据编号">
-				<el-input v-model="query.billNo" clearable style="width: 200px" />
+			<el-form-item label="生产指令单">
+				<el-input v-model="query.productionOrderNo" clearable style="width: 200px" />
 			</el-form-item>
 			<el-form-item label="生产批号">
 				<el-input v-model="query.productionBatchNo" clearable style="width: 200px" />
@@ -11,16 +11,29 @@
 				<el-input v-model="query.materialCode" clearable style="width: 200px" />
 			</el-form-item>
 			<el-form-item label="申请时间">
-				<el-date-picker v-model="query.applyTime" type="date" value-format="YYYY-MM-DD" style="width: 180px" />
+				<el-date-picker
+					v-model="applyTimeRange"
+					type="daterange"
+					value-format="YYYY-MM-DD"
+					range-separator="-"
+					start-placeholder="开始日期"
+					end-placeholder="结束日期"
+					unlink-panels
+					style="width: 260px"
+				/>
 			</el-form-item>
 			<el-form-item label="检验进度">
-				<el-input v-model="query.inspectProgress" clearable style="width: 200px" />
+				<el-select v-model="query.inspectProgress" placeholder="全部" clearable style="width: 160px">
+					<el-option label="未检验" value="未检验" />
+					<el-option label="检验中" value="检验中" />
+					<el-option label="检验完成" value="检验完成" />
+				</el-select>
 			</el-form-item>
 			<el-form-item label="检验优先级">
-				<el-select v-model="query.priority" placeholder="请选择" clearable style="width: 200px" />
+				<el-input v-model="query.priority" clearable style="width: 160px" />
 			</el-form-item>
 			<el-form-item label="检验负责人">
-				<el-select v-model="query.inspector" placeholder="请选择" clearable style="width: 200px" />
+				<el-input v-model="query.inspector" clearable style="width: 160px" />
 			</el-form-item>
 			<el-form-item>
 				<el-button type="primary" @click="doSearch">查询</el-button>
@@ -28,9 +41,27 @@
 			</el-form-item>
 		</el-form>
 
+		<!-- 本批只读:动作按钮保留占位但禁用,不接真实写动作 -->
+		<div class="mb12 actions">
+			<el-tooltip content="本批为只读列表,暂不开放" placement="top">
+				<span><el-button disabled>检验完成</el-button></span>
+			</el-tooltip>
+			<el-tooltip content="本批为只读列表,暂不开放" placement="top">
+				<span><el-button disabled>认领</el-button></span>
+			</el-tooltip>
+			<el-tooltip content="本批为只读列表,暂不开放" placement="top">
+				<span><el-button disabled>检验员调配</el-button></span>
+			</el-tooltip>
+			<el-tooltip content="本批为只读列表,暂不开放" placement="top">
+				<span><el-button disabled>优先级调整</el-button></span>
+			</el-tooltip>
+		</div>
+
 		<el-table :data="rows" row-key="id" v-loading="loading" border stripe>
 			<el-table-column prop="productionOrderNo" label="生产指令单" min-width="150" show-overflow-tooltip resizable />
-			<el-table-column prop="applyTime" label="申请时间" min-width="170" resizable />
+			<el-table-column prop="applyTime" label="申请时间" min-width="170" resizable>
+				<template #default="{ row }">{{ fmtDateTime(row.applyTime) }}</template>
+			</el-table-column>
 			<el-table-column prop="materialCode" label="物料编码" min-width="140" show-overflow-tooltip resizable />
 			<el-table-column prop="productName" label="产品名称" min-width="160" show-overflow-tooltip resizable />
 			<el-table-column prop="productModel" label="产品型号" min-width="140" show-overflow-tooltip resizable />
@@ -39,8 +70,12 @@
 			<el-table-column prop="applicant" label="申请人" min-width="120" show-overflow-tooltip resizable />
 			<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip resizable />
 			<el-table-column prop="priority" label="检验优先级" min-width="110" resizable />
-			<el-table-column prop="inspectStartTime" label="检验开始时间" min-width="170" resizable />
-			<el-table-column prop="inspectFinishTime" label="检验完成时间" min-width="170" resizable />
+			<el-table-column prop="inspectStartTime" label="检验开始时间" min-width="170" resizable>
+				<template #default="{ row }">{{ fmtDateTime(row.inspectStartTime) }}</template>
+			</el-table-column>
+			<el-table-column prop="inspectFinishTime" label="检验完成时间" min-width="170" resizable>
+				<template #default="{ row }">{{ fmtDateTime(row.inspectFinishTime) }}</template>
+			</el-table-column>
 			<el-table-column prop="inspector" label="检验负责人" min-width="120" show-overflow-tooltip resizable />
 			<el-table-column prop="inspectProgress" label="检验进度" min-width="110" resizable />
 			<template #empty>
@@ -55,56 +90,107 @@
 				:total="total"
 				:page-sizes="[10, 20, 50]"
 				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="loadList"
 			/>
 		</div>
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopS7FqcTaskList">
-import { computed, reactive, ref } from 'vue';
+import { computed, onActivated, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import { fetchFqcTaskList, type FqcTaskRow } from '../api/fqcInspBill';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验任务列表');
 
 const query = reactive({
-	billNo: '',
+	productionOrderNo: '',
 	productionBatchNo: '',
 	materialCode: '',
-	applyTime: '',
-	inspectProgress: '未检验',
+	applyTimeStart: '',
+	applyTimeEnd: '',
+	inspectProgress: '',
 	priority: '',
 	inspector: '',
 	page: 1,
 	pageSize: 10,
 });
 
-// 本批次为只读 UI 骨架,尚无真实 FQC 检验任务接口:渲染空表,不发请求、不造数据。
+const applyTimeRange = ref<[string, string] | null>(null);
+
 const loading = ref(false);
-const rows = ref<Record<string, unknown>[]>([]);
+const rows = ref<FqcTaskRow[]>([]);
 const total = ref(0);
 
+function fmtDateTime(v?: string | null) {
+	if (!v) return '';
+	const s = String(v);
+	return s.length > 19 ? s.slice(0, 19).replace('T', ' ') : s.replace('T', ' ');
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		query.applyTimeStart = applyTimeRange.value?.[0] || '';
+		query.applyTimeEnd = applyTimeRange.value?.[1] || '';
+		const data = await fetchFqcTaskList({
+			productionOrderNo: query.productionOrderNo,
+			productionBatchNo: query.productionBatchNo,
+			materialCode: query.materialCode,
+			applyTimeStart: query.applyTimeStart,
+			applyTimeEnd: query.applyTimeEnd,
+			inspectProgress: query.inspectProgress,
+			priority: query.priority,
+			inspector: query.inspector,
+			page: query.page,
+			pageSize: query.pageSize,
+		});
+		rows.value = data.list || [];
+		total.value = data.total || 0;
+	} catch (e: any) {
+		rows.value = [];
+		total.value = 0;
+		ElMessage.error(e?.message || '加载FQC检验任务列表失败');
+	} finally {
+		loading.value = false;
+	}
+}
+
 function doSearch() {
 	query.page = 1;
+	loadList();
 }
 
 function resetQuery() {
-	query.billNo = '';
+	query.productionOrderNo = '';
 	query.productionBatchNo = '';
 	query.materialCode = '';
-	query.applyTime = '';
-	query.inspectProgress = '未检验';
+	applyTimeRange.value = null;
+	query.applyTimeStart = '';
+	query.applyTimeEnd = '';
+	query.inspectProgress = '';
 	query.priority = '';
 	query.inspector = '';
 	query.page = 1;
+	loadList();
 }
+
+onMounted(() => loadList());
+onActivated(() => loadList());
 </script>
 
 <style scoped lang="scss">
 .mb12 {
 	margin-bottom: 12px;
 }
+.actions {
+	display: flex;
+	gap: 8px;
+}
 .pager {
 	margin-top: 12px;
 	display: flex;

+ 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.242</AssemblyVersion>
-    <FileVersion>1.0.242</FileVersion>
-    <Version>1.0.242</Version>
+    <AssemblyVersion>1.0.243</AssemblyVersion>
+    <FileVersion>1.0.243</FileVersion>
+    <Version>1.0.243</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 213 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/Dto/FqcInspBillDto.cs

@@ -0,0 +1,213 @@
+namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse.Dto;
+
+// ══════════════════════════════════════════════════════════════════════════
+// S7 FQC 成品质量:检验任务列表 / 检验单(结果)列表 只读 DTO
+//
+// 数据源(均 aidopdev,只读):
+//   - 检验任务:qms_fqcbj(37 列,归口自旧系统 dopdemo,空表待上游抽数)
+//   - 检验单  :qms_qcpp_inspbill(主表)+ WorkOrdMaster(取 SAP 工单号,sczld=Batch)
+// 两表均无 tenant_id:前端 withAidopTenantParams 注入的 TenantId 仅透传、本查询不据其过滤。
+// 字段修正:物料名称=wlmc(非 lmc);检验数量=jysj(非 ysj);SAP工单号=WorkOrdMaster.WorkOrd。
+// ══════════════════════════════════════════════════════════════════════════
+
+/// <summary>FQC 检验任务列表 查询入参(只读,源 qms_fqcbj)。</summary>
+public class FqcTaskListInput
+{
+    /// <summary>页码(从 1 开始)</summary>
+    public int Page { get; set; } = 1;
+
+    /// <summary>每页条数(上限 200)</summary>
+    public int PageSize { get; set; } = 10;
+
+    /// <summary>排序字段(前端 prop,白名单校验)</summary>
+    public string? OrderBy { get; set; }
+
+    /// <summary>排序方向 asc/desc(默认 desc)</summary>
+    public string? OrderDir { get; set; }
+
+    /// <summary>生产指令单(qms_fqcbj.sczld,模糊)</summary>
+    public string? ProductionOrderNo { get; set; }
+
+    /// <summary>生产批号(qms_fqcbj.scph,模糊)</summary>
+    public string? ProductionBatchNo { get; set; }
+
+    /// <summary>物料编码(qms_fqcbj.wlbm,模糊)</summary>
+    public string? MaterialCode { get; set; }
+
+    /// <summary>申请时间起(qms_fqcbj.FAPPLYTIME,含当日,yyyy-MM-dd)</summary>
+    public string? ApplyTimeStart { get; set; }
+
+    /// <summary>申请时间止(qms_fqcbj.FAPPLYTIME,含当日,yyyy-MM-dd)</summary>
+    public string? ApplyTimeEnd { get; set; }
+
+    /// <summary>检验进度(qms_fqcbj.FINSPECTSTATUS,精确)</summary>
+    public string? InspectProgress { get; set; }
+
+    /// <summary>检验优先级(qms_fqcbj.yxj,模糊;字典未接,原样匹配)</summary>
+    public string? Priority { get; set; }
+
+    /// <summary>检验负责人(qms_fqcbj.jyfzr,模糊)</summary>
+    public string? Inspector { get; set; }
+
+    /// <summary>租户 ID(前端注入;qms_fqcbj 无租户列,本查询忽略)</summary>
+    public long? TenantId { get; set; }
+}
+
+/// <summary>FQC 检验任务列表 行(只读)。prop 对齐前端列;源列见注释(qms_fqcbj)。</summary>
+public class FqcTaskListRow
+{
+    /// <summary>主键(qms_fqcbj.id)</summary>
+    public long Id { get; set; }
+
+    /// <summary>单据编号(qms_fqcbj.FBILLNO)</summary>
+    public string? BillNo { get; set; }
+
+    /// <summary>生产指令单(qms_fqcbj.sczld)</summary>
+    public string? ProductionOrderNo { get; set; }
+
+    /// <summary>申请组织(qms_fqcbj.FORGID,原样 id)</summary>
+    public long? ApplyOrgId { get; set; }
+
+    /// <summary>申请时间(qms_fqcbj.FAPPLYTIME)</summary>
+    public DateTime? ApplyTime { get; set; }
+
+    /// <summary>物料编码(qms_fqcbj.wlbm)</summary>
+    public string? MaterialCode { get; set; }
+
+    /// <summary>产品名称(qms_fqcbj.cpmc)</summary>
+    public string? ProductName { get; set; }
+
+    /// <summary>产品型号(qms_fqcbj.cpxh)</summary>
+    public string? ProductModel { get; set; }
+
+    /// <summary>生产批号(qms_fqcbj.scph)</summary>
+    public string? ProductionBatchNo { get; set; }
+
+    /// <summary>指令单数量(qms_fqcbj.sczldsl)</summary>
+    public decimal? OrderQty { get; set; }
+
+    /// <summary>数量(qms_fqcbj.sl)</summary>
+    public decimal? Qty { get; set; }
+
+    /// <summary>申请人(qms_fqcbj.FAPPLYUSER)</summary>
+    public string? Applicant { get; set; }
+
+    /// <summary>备注(qms_fqcbj.FCOMMENT)</summary>
+    public string? Remark { get; set; }
+
+    /// <summary>检验优先级(qms_fqcbj.yxj,原样,未接字典)</summary>
+    public string? Priority { get; set; }
+
+    /// <summary>检验开始时间(qms_fqcbj.jykssj)</summary>
+    public DateTime? InspectStartTime { get; set; }
+
+    /// <summary>检验完成时间(qms_fqcbj.jywcsj)</summary>
+    public DateTime? InspectFinishTime { get; set; }
+
+    /// <summary>检验负责人(qms_fqcbj.jyfzr)</summary>
+    public string? Inspector { get; set; }
+
+    /// <summary>检验进度(qms_fqcbj.FINSPECTSTATUS,原样)</summary>
+    public string? InspectProgress { get; set; }
+}
+
+/// <summary>FQC 检验单(结果)列表 查询入参(只读,源 qms_qcpp_inspbill)。</summary>
+public class FqcResultListInput
+{
+    /// <summary>页码(从 1 开始)</summary>
+    public int Page { get; set; } = 1;
+
+    /// <summary>每页条数(上限 200)</summary>
+    public int PageSize { get; set; } = 10;
+
+    /// <summary>排序字段(前端 prop,白名单校验)</summary>
+    public string? OrderBy { get; set; }
+
+    /// <summary>排序方向 asc/desc(默认 desc)</summary>
+    public string? OrderDir { get; set; }
+
+    /// <summary>单据编号(qms_qcpp_inspbill.FBILLNO,模糊)</summary>
+    public string? BillNo { get; set; }
+
+    /// <summary>生产批号(qms_qcpp_inspbill.scph,模糊)</summary>
+    public string? ProductionBatchNo { get; set; }
+
+    /// <summary>物料编码(qms_qcpp_inspbill.FMATERIALCFG,模糊)</summary>
+    public string? MaterialCode { get; set; }
+
+    /// <summary>物料名称(qms_qcpp_inspbill.wlmc,模糊)</summary>
+    public string? MaterialName { get; set; }
+
+    /// <summary>检验人(qms_qcpp_inspbill.jyr,模糊)</summary>
+    public string? Inspector { get; set; }
+
+    /// <summary>来源单据编号(qms_qcpp_inspbill.lydjbh,模糊)</summary>
+    public string? SourceBillNo { get; set; }
+
+    /// <summary>生产指令单(qms_qcpp_inspbill.sczld,模糊)</summary>
+    public string? ProductionOrderNo { get; set; }
+
+    /// <summary>检验日期起(qms_qcpp_inspbill.FINSPEENDDATE,含当日,yyyy-MM-dd)</summary>
+    public string? InspectDateStart { get; set; }
+
+    /// <summary>检验日期止(qms_qcpp_inspbill.FINSPEENDDATE,含当日,yyyy-MM-dd)</summary>
+    public string? InspectDateEnd { get; set; }
+
+    /// <summary>判定(qms_qcpp_inspbill.pd,原样精确)</summary>
+    public string? Judgment { get; set; }
+
+    /// <summary>租户 ID(前端注入;qms_qcpp_inspbill 无租户列,本查询忽略)</summary>
+    public long? TenantId { get; set; }
+}
+
+/// <summary>FQC 检验单(结果)列表 行(只读)。prop 对齐前端列;源列见注释。</summary>
+public class FqcResultListRow
+{
+    /// <summary>主键(qms_qcpp_inspbill.id)</summary>
+    public long Id { get; set; }
+
+    /// <summary>单据编号(qms_qcpp_inspbill.FBILLNO)</summary>
+    public string? BillNo { get; set; }
+
+    /// <summary>检验时间(qms_qcpp_inspbill.FINSPEENDDATE)</summary>
+    public DateTime? InspectTime { get; set; }
+
+    /// <summary>生产批号(qms_qcpp_inspbill.scph)</summary>
+    public string? ProductionBatchNo { get; set; }
+
+    /// <summary>物料编码(qms_qcpp_inspbill.FMATERIALCFG)</summary>
+    public string? MaterialCode { get; set; }
+
+    /// <summary>物料名称(qms_qcpp_inspbill.wlmc)</summary>
+    public string? MaterialName { get; set; }
+
+    /// <summary>规格型号(qms_qcpp_inspbill.ggxh)</summary>
+    public string? Spec { get; set; }
+
+    /// <summary>检验人(qms_qcpp_inspbill.jyr)</summary>
+    public string? Inspector { get; set; }
+
+    /// <summary>检验数量(qms_qcpp_inspbill.jysj)</summary>
+    public decimal? InspectQty { get; set; }
+
+    /// <summary>来源单据编号(qms_qcpp_inspbill.lydjbh)</summary>
+    public string? SourceBillNo { get; set; }
+
+    /// <summary>生产指令单(qms_qcpp_inspbill.sczld)</summary>
+    public string? ProductionOrderNo { get; set; }
+
+    /// <summary>指令单数量(qms_qcpp_inspbill.sczldsl)</summary>
+    public decimal? OrderQty { get; set; }
+
+    /// <summary>SAP 工单号(WorkOrdMaster.WorkOrd,经 sczld=Batch,MIN 取确定值)</summary>
+    public string? SapWorkOrderNo { get; set; }
+
+    /// <summary>判定(qms_qcpp_inspbill.pd,原样,字典未接)</summary>
+    public string? Judgment { get; set; }
+
+    /// <summary>合格数量(qms_qcpp_inspbill.hgsl)</summary>
+    public decimal? QualifiedQty { get; set; }
+
+    /// <summary>不合格数量(qms_qcpp_inspbill.bhgsl)</summary>
+    public decimal? UnqualifiedQty { get; set; }
+}

+ 299 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillService.cs

@@ -0,0 +1,299 @@
+using Admin.NET.Plugin.AiDOP.FinishedWarehouse.Dto;
+
+namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
+
+/// <summary>
+/// S7 FQC 成品质量:检验任务列表 / 检验单(结果)列表 只读 list 服务。
+///
+/// 数据源(均 aidopdev,只读):
+///   - task-list  :qms_fqcbj(37 列,归口自旧系统 dopdemo,空表待上游抽数)。yxj/FINSPECTSTATUS 原样透出,
+///                  rf_dictionary 优先级字典未接(CTO 拍板舍弃)。默认排序 FAPPLYTIME DESC, id DESC。
+///   - result-list:qms_qcpp_inspbill 主表;SAP 工单号经 (SELECT MIN(wo.WorkOrd) FROM WorkOrdMaster wo
+///                  WHERE wo.Batch = a.sczld) 标量子查询取得(Batch 非唯一仅因 NULL;非空 Batch 现 1:1,
+///                  MIN 保确定性、杜绝行倍增)。默认排序 FINSPEENDDATE DESC, id DESC。
+///
+/// 归口说明(方案 A,复刻已闭环 S6 IPQC IpqcInspectionMdpSyncService.EnsureTablesAsync):
+///   qms_fqcbj 原仅存于旧系统 dopdemo(0 行)、aidopdev 无此表,且 dopdemo 非运行时连接。
+///   EnsureFqcTaskTableAsync 在 aidopdev 建同构空表(CREATE TABLE IF NOT EXISTS,DDL 例外、仅一次、零 DML),
+///   后续由上游一次性抽数填充,届时无需改代码即随查询流通。id 不设 AUTO_INCREMENT(抽数将携旧系统主键)。
+///
+/// 只读边界:纯 SELECT + 一次性 DDL 建空表;绝不 INSERT/UPDATE/DELETE 任何业务表、不写库存/仓储/生产状态、
+///   不接 ApprovalFlow、不做详情/子表(qms_qcpp_inspbillst 留下一批)/录入/审核/处置。两表无 tenant_id,不做租户过滤。
+/// </summary>
+[ApiDescriptionSettings(Order = 330, Description = "FQC检验单列表")]
+[Route("api/S7FqcInspBill")]
+[AllowAnonymous]
+[NonUnify]
+public class FqcInspBillService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+
+    /// <summary>归口空表建表守卫:每进程仅尝试一次 CREATE TABLE IF NOT EXISTS。</summary>
+    private static int _taskTableEnsured;
+
+    public FqcInspBillService(ISqlSugarClient db)
+    {
+        _db = db;
+    }
+
+    /// <summary>检验任务列表 排序白名单(前端 prop → 物理列,杜绝 orderBy 注入)。</summary>
+    private static readonly Dictionary<string, string> TaskOrderWhitelist = new(StringComparer.OrdinalIgnoreCase)
+    {
+        ["applyTime"] = "t.FAPPLYTIME",
+        ["productionBatchNo"] = "t.scph",
+        ["productionOrderNo"] = "t.sczld",
+        ["materialCode"] = "t.wlbm",
+        ["priority"] = "t.yxj",
+        ["inspectStartTime"] = "t.jykssj",
+        ["inspectFinishTime"] = "t.jywcsj",
+        ["inspectProgress"] = "t.FINSPECTSTATUS",
+    };
+
+    /// <summary>检验单列表 排序白名单(前端 prop → 物理列)。</summary>
+    private static readonly Dictionary<string, string> ResultOrderWhitelist = new(StringComparer.OrdinalIgnoreCase)
+    {
+        ["inspectTime"] = "a.FINSPEENDDATE",
+        ["billNo"] = "a.FBILLNO",
+        ["productionBatchNo"] = "a.scph",
+        ["materialCode"] = "a.FMATERIALCFG",
+        ["inspector"] = "a.jyr",
+        ["inspectQty"] = "a.jysj",
+        ["productionOrderNo"] = "a.sczld",
+    };
+
+    /// <summary>
+    /// FQC 检验任务列表(只读分页查询,源 qms_fqcbj)。
+    /// </summary>
+    [DisplayName("FQC检验任务列表")]
+    [HttpGet("task-list")]
+    public async Task<object> GetTaskList([FromQuery] FqcTaskListInput input)
+    {
+        await EnsureFqcTaskTableAsync();
+
+        var page = input.Page <= 0 ? 1 : input.Page;
+        var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
+        var offset = (page - 1) * pageSize;
+
+        // qms_fqcbj 无 tenant_id:不做租户过滤(TenantId 接收即忽略)。
+        var where = new List<string> { "1=1" };
+        var pars = new List<SugarParameter>();
+
+        void AddLike(string? v, string col, string p)
+        {
+            if (string.IsNullOrWhiteSpace(v)) return;
+            where.Add($"{col} LIKE @{p}");
+            pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%"));
+        }
+
+        AddLike(input.ProductionOrderNo, "t.sczld", "ProductionOrderNo");
+        AddLike(input.ProductionBatchNo, "t.scph", "ProductionBatchNo");
+        AddLike(input.MaterialCode, "t.wlbm", "MaterialCode");
+        AddLike(input.Priority, "t.yxj", "Priority");
+        AddLike(input.Inspector, "t.jyfzr", "Inspector");
+
+        if (!string.IsNullOrWhiteSpace(input.InspectProgress))
+        {
+            where.Add("t.FINSPECTSTATUS = @InspectProgress");
+            pars.Add(new SugarParameter("@InspectProgress", input.InspectProgress.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.ApplyTimeStart))
+        {
+            where.Add("t.FAPPLYTIME >= @ApplyTimeStart");
+            pars.Add(new SugarParameter("@ApplyTimeStart", $"{input.ApplyTimeStart.Trim()} 00:00:00"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.ApplyTimeEnd))
+        {
+            where.Add("t.FAPPLYTIME <= @ApplyTimeEnd");
+            pars.Add(new SugarParameter("@ApplyTimeEnd", $"{input.ApplyTimeEnd.Trim()} 23:59:59"));
+        }
+
+        var whereSql = string.Join(" AND ", where);
+        var orderSql = ResolveOrder(input.OrderBy, input.OrderDir, TaskOrderWhitelist, "t.FAPPLYTIME DESC, t.id DESC", "t.id");
+
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM qms_fqcbj t WHERE {whereSql}", pars);
+
+        var list = await _db.Ado.SqlQueryAsync<FqcTaskListRow>(
+            $"""
+            SELECT
+                t.id            AS Id,
+                t.FBILLNO       AS BillNo,
+                t.sczld         AS ProductionOrderNo,
+                t.FORGID        AS ApplyOrgId,
+                t.FAPPLYTIME    AS ApplyTime,
+                t.wlbm          AS MaterialCode,
+                t.cpmc          AS ProductName,
+                t.cpxh          AS ProductModel,
+                t.scph          AS ProductionBatchNo,
+                t.sczldsl       AS OrderQty,
+                t.sl            AS Qty,
+                t.FAPPLYUSER    AS Applicant,
+                t.FCOMMENT      AS Remark,
+                t.yxj           AS Priority,
+                t.jykssj        AS InspectStartTime,
+                t.jywcsj        AS InspectFinishTime,
+                t.jyfzr         AS Inspector,
+                t.FINSPECTSTATUS AS InspectProgress
+            FROM qms_fqcbj t
+            WHERE {whereSql}
+            ORDER BY {orderSql}
+            LIMIT {pageSize} OFFSET {offset}
+            """,
+            pars);
+
+        return new { total, page, pageSize, list };
+    }
+
+    /// <summary>
+    /// FQC 检验单(结果)列表(只读分页查询,源 qms_qcpp_inspbill + WorkOrdMaster 取 SAP 工单号)。
+    /// </summary>
+    [DisplayName("FQC检验单结果列表")]
+    [HttpGet("result-list")]
+    public async Task<object> GetResultList([FromQuery] FqcResultListInput input)
+    {
+        var page = input.Page <= 0 ? 1 : input.Page;
+        var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
+        var offset = (page - 1) * pageSize;
+
+        // qms_qcpp_inspbill 无 tenant_id:不做租户过滤(TenantId 接收即忽略)。
+        var where = new List<string> { "1=1" };
+        var pars = new List<SugarParameter>();
+
+        void AddLike(string? v, string col, string p)
+        {
+            if (string.IsNullOrWhiteSpace(v)) return;
+            where.Add($"{col} LIKE @{p}");
+            pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%"));
+        }
+
+        AddLike(input.BillNo, "a.FBILLNO", "BillNo");
+        AddLike(input.ProductionBatchNo, "a.scph", "ProductionBatchNo");
+        AddLike(input.MaterialCode, "a.FMATERIALCFG", "MaterialCode");
+        AddLike(input.MaterialName, "a.wlmc", "MaterialName");
+        AddLike(input.Inspector, "a.jyr", "Inspector");
+        AddLike(input.SourceBillNo, "a.lydjbh", "SourceBillNo");
+        AddLike(input.ProductionOrderNo, "a.sczld", "ProductionOrderNo");
+
+        if (!string.IsNullOrWhiteSpace(input.Judgment))
+        {
+            where.Add("a.pd = @Judgment");
+            pars.Add(new SugarParameter("@Judgment", input.Judgment.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.InspectDateStart))
+        {
+            where.Add("a.FINSPEENDDATE >= @InspectDateStart");
+            pars.Add(new SugarParameter("@InspectDateStart", $"{input.InspectDateStart.Trim()} 00:00:00"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.InspectDateEnd))
+        {
+            where.Add("a.FINSPEENDDATE <= @InspectDateEnd");
+            pars.Add(new SugarParameter("@InspectDateEnd", $"{input.InspectDateEnd.Trim()} 23:59:59"));
+        }
+
+        var whereSql = string.Join(" AND ", where);
+        var orderSql = ResolveOrder(input.OrderBy, input.OrderDir, ResultOrderWhitelist, "a.FINSPEENDDATE DESC, a.id DESC", "a.id");
+
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM qms_qcpp_inspbill a WHERE {whereSql}", pars);
+
+        var list = await _db.Ado.SqlQueryAsync<FqcResultListRow>(
+            $"""
+            SELECT
+                a.id            AS Id,
+                a.FBILLNO       AS BillNo,
+                a.FINSPEENDDATE AS InspectTime,
+                a.scph          AS ProductionBatchNo,
+                a.FMATERIALCFG  AS MaterialCode,
+                a.wlmc          AS MaterialName,
+                a.ggxh          AS Spec,
+                a.jyr           AS Inspector,
+                a.jysj          AS InspectQty,
+                a.lydjbh        AS SourceBillNo,
+                a.sczld         AS ProductionOrderNo,
+                a.sczldsl       AS OrderQty,
+                (SELECT MIN(wo.WorkOrd) FROM WorkOrdMaster wo WHERE wo.Batch = a.sczld) AS SapWorkOrderNo,
+                a.pd            AS Judgment,
+                a.hgsl          AS QualifiedQty,
+                a.bhgsl         AS UnqualifiedQty
+            FROM qms_qcpp_inspbill a
+            WHERE {whereSql}
+            ORDER BY {orderSql}
+            LIMIT {pageSize} OFFSET {offset}
+            """,
+            pars);
+
+        return new { total, page, pageSize, list };
+    }
+
+    /// <summary>
+    /// 排序解析:白名单命中则 "col dir, tie",否则回落默认;方向仅 asc/desc,非法回落 desc。
+    /// </summary>
+    private static string ResolveOrder(string? orderBy, string? orderDir, Dictionary<string, string> whitelist, string fallback, string tie)
+    {
+        if (!string.IsNullOrWhiteSpace(orderBy) && whitelist.TryGetValue(orderBy.Trim(), out var col))
+        {
+            var dir = string.Equals(orderDir?.Trim(), "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
+            return $"{col} {dir}, {tie} DESC";
+        }
+        return fallback;
+    }
+
+    /// <summary>
+    /// 归口空表建表(方案 A,DDL 例外、每进程一次、零 DML):
+    /// aidopdev 无 qms_fqcbj 时 CREATE TABLE IF NOT EXISTS 建同构 dopdemo 的 37 列空表,待上游抽数填充。
+    /// 仅建表,不写任何数据行。
+    /// </summary>
+    private async Task EnsureFqcTaskTableAsync()
+    {
+        if (Interlocked.Exchange(ref _taskTableEnsured, 1) == 1) return;
+        try
+        {
+            await _db.Ado.ExecuteCommandAsync(
+                """
+                CREATE TABLE IF NOT EXISTS qms_fqcbj (
+                    id BIGINT NOT NULL PRIMARY KEY COMMENT '主表id',
+                    FBILLNO VARCHAR(80) NULL COMMENT '单据编号',
+                    FBILLSTATUS VARCHAR(50) NULL COMMENT '单据状态',
+                    FCREATORID BIGINT NULL,
+                    FMODIFIERID BIGINT NULL,
+                    FAUDITORID BIGINT NULL,
+                    FAUDITDATE DATETIME NULL,
+                    FMODIFYTIME DATETIME NULL,
+                    FCREATETIME DATETIME NULL,
+                    FORGID BIGINT NULL COMMENT '申请组织',
+                    FQUALITYORG BIGINT NULL,
+                    FAPPLYUSER VARCHAR(50) NULL COMMENT '申请人',
+                    FAPPLYTIME DATETIME NULL COMMENT '申请时间',
+                    FBILLTYPE VARCHAR(50) NULL COMMENT '单据类型',
+                    FBIZTYPE BIGINT NULL COMMENT '业务类型',
+                    FINSPECORGID BIGINT NULL,
+                    FCOMMENT TEXT NULL COMMENT '备注',
+                    FAUTHORIZEOBJID BIGINT NULL,
+                    FINTERFACEID VARCHAR(50) NULL,
+                    cpmc VARCHAR(255) NULL COMMENT '产品名称',
+                    cpxh VARCHAR(255) NULL COMMENT '产品型号',
+                    scph VARCHAR(255) NULL COMMENT '生产批号',
+                    sl DECIMAL(10,0) NULL COMMENT '数量',
+                    jfbh TEXT NULL COMMENT '击发编号',
+                    wlbm VARCHAR(255) NULL COMMENT '物料编码',
+                    jyfzr VARCHAR(255) NULL COMMENT '检验负责人',
+                    yxj VARCHAR(255) NULL COMMENT '检验优先级',
+                    jykssj DATETIME NULL COMMENT '检验开始时间',
+                    jywcsj DATETIME NULL COMMENT '检验完成时间',
+                    mjpc VARCHAR(255) NULL,
+                    tsyq VARCHAR(255) NULL,
+                    FINSPECTSTATUS VARCHAR(255) NULL COMMENT '检验进度',
+                    sczld VARCHAR(255) NULL COMMENT '生产指令单',
+                    sczldsl DECIMAL(10,0) NULL COMMENT '指令单数量',
+                    lydjbh VARCHAR(100) NULL COMMENT '来源单据编号',
+                    ISscrap INT NULL,
+                    CirculationCard TEXT NULL
+                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='FQC检验任务(报检) 归口自旧系统dopdemo,空表待上游抽数'
+                """);
+        }
+        catch
+        {
+            // 建表失败(如权限/并发)不吞成静默通过:复位守卫,让后续调用可再尝试,并向上抛出真实错误。
+            Interlocked.Exchange(ref _taskTableEnsured, 0);
+            throw;
+        }
+    }
+}