Kaynağa Gözat

S0模块&单元测试

YY968XX 4 gün önce
ebeveyn
işleme
337385db5a
99 değiştirilmiş dosya ile 4235 ekleme ve 3 silme
  1. 62 0
      Web/src/views/aidop/s0/api/s0QualityApi.spec.ts
  2. 65 0
      Web/src/views/aidop/s0/api/s0QualityApi.ts
  3. 97 0
      Web/src/views/aidop/s0/api/s0SalesApi.spec.ts
  4. 71 0
      Web/src/views/aidop/s0/api/s0SupplyApi.spec.ts
  5. 50 0
      Web/src/views/aidop/s0/api/s0WarehouseApi.spec.ts
  6. 8 0
      Web/src/views/aidop/s0/quality/InspectionBasisList.vue
  7. 8 0
      Web/src/views/aidop/s0/quality/InspectionFrequencyList.vue
  8. 8 0
      Web/src/views/aidop/s0/quality/InspectionInstrumentList.vue
  9. 8 0
      Web/src/views/aidop/s0/quality/InspectionItemList.vue
  10. 8 0
      Web/src/views/aidop/s0/quality/InspectionMethodList.vue
  11. 8 0
      Web/src/views/aidop/s0/quality/InspectionPlanList.vue
  12. 8 0
      Web/src/views/aidop/s0/quality/InspectionStandardList.vue
  13. 8 0
      Web/src/views/aidop/s0/quality/ProcessInspectionSpecList.vue
  14. 146 0
      Web/src/views/aidop/s0/quality/QualityDictionaryPage.vue
  15. 8 0
      Web/src/views/aidop/s0/quality/RawInspectionSpecList.vue
  16. 8 0
      Web/src/views/aidop/s0/quality/RawWhitelistList.vue
  17. 8 0
      Web/src/views/aidop/s0/quality/SamplingSchemeList.vue
  18. 244 0
      Web/src/views/aidop/s0/quality/components/QualityAggregateCrudPage.vue
  19. 184 0
      Web/src/views/aidop/s0/quality/components/QualitySimpleCrudPage.vue
  20. 376 0
      Web/src/views/aidop/s0/quality/qualityConfigs.ts
  21. 147 0
      doc/plan/S0/20240410/S0列表迁移修正方案提示词.md
  22. BIN
      doc/plan/S0/20240410/质量建模/原材料检验规范1.png
  23. BIN
      doc/plan/S0/20240410/质量建模/原材料检验规范2.png
  24. 1 0
      doc/plan/S0/20240410/质量建模/原材料检验规范3.sql
  25. BIN
      doc/plan/S0/20240410/质量建模/原材料检验规范4.png
  26. BIN
      doc/plan/S0/20240410/质量建模/原材料检验规范5.png
  27. BIN
      doc/plan/S0/20240410/质量建模/原材料白名单1.png
  28. BIN
      doc/plan/S0/20240410/质量建模/原材料白名单2.png
  29. 1 0
      doc/plan/S0/20240410/质量建模/原材料白名单3.sql
  30. BIN
      doc/plan/S0/20240410/质量建模/原材料白名单4.png
  31. BIN
      doc/plan/S0/20240410/质量建模/原材料白名单5.png
  32. BIN
      doc/plan/S0/20240410/质量建模/抽样方案1.png
  33. 1 0
      doc/plan/S0/20240410/质量建模/抽样方案2.sql
  34. BIN
      doc/plan/S0/20240410/质量建模/抽样方案3.png
  35. BIN
      doc/plan/S0/20240410/质量建模/抽样方案4.png
  36. BIN
      doc/plan/S0/20240410/质量建模/抽样方案5.png
  37. BIN
      doc/plan/S0/20240410/质量建模/检验仪器1.png
  38. 1 0
      doc/plan/S0/20240410/质量建模/检验仪器2.sql
  39. BIN
      doc/plan/S0/20240410/质量建模/检验仪器3.png
  40. BIN
      doc/plan/S0/20240410/质量建模/检验仪器4.png
  41. BIN
      doc/plan/S0/20240410/质量建模/检验仪器5.png
  42. BIN
      doc/plan/S0/20240410/质量建模/检验依据1.png
  43. 1 0
      doc/plan/S0/20240410/质量建模/检验依据2.sql
  44. BIN
      doc/plan/S0/20240410/质量建模/检验依据3.png
  45. BIN
      doc/plan/S0/20240410/质量建模/检验依据4.png
  46. BIN
      doc/plan/S0/20240410/质量建模/检验依据5.png
  47. BIN
      doc/plan/S0/20240410/质量建模/检验方案1.png
  48. 0 0
      doc/plan/S0/20240410/质量建模/检验方案2.sql
  49. BIN
      doc/plan/S0/20240410/质量建模/检验方案3.png
  50. BIN
      doc/plan/S0/20240410/质量建模/检验方案4.png
  51. BIN
      doc/plan/S0/20240410/质量建模/检验方案5.png
  52. BIN
      doc/plan/S0/20240410/质量建模/检验方法1.png
  53. 1 0
      doc/plan/S0/20240410/质量建模/检验方法2.sql
  54. BIN
      doc/plan/S0/20240410/质量建模/检验方法3.png
  55. BIN
      doc/plan/S0/20240410/质量建模/检验方法4.png
  56. BIN
      doc/plan/S0/20240410/质量建模/检验方法5.png
  57. BIN
      doc/plan/S0/20240410/质量建模/检验标准1.png
  58. 1 0
      doc/plan/S0/20240410/质量建模/检验标准2.sql
  59. BIN
      doc/plan/S0/20240410/质量建模/检验标准3.png
  60. BIN
      doc/plan/S0/20240410/质量建模/检验标准4.png
  61. BIN
      doc/plan/S0/20240410/质量建模/检验标准5.png
  62. BIN
      doc/plan/S0/20240410/质量建模/检验项目1.png
  63. 1 0
      doc/plan/S0/20240410/质量建模/检验项目2.sql
  64. BIN
      doc/plan/S0/20240410/质量建模/检验项目3.png
  65. BIN
      doc/plan/S0/20240410/质量建模/检验项目4.png
  66. BIN
      doc/plan/S0/20240410/质量建模/检验项目5.png
  67. BIN
      doc/plan/S0/20240410/质量建模/检验频率1.png
  68. 1 0
      doc/plan/S0/20240410/质量建模/检验频率2.sql
  69. BIN
      doc/plan/S0/20240410/质量建模/检验频率3.png
  70. BIN
      doc/plan/S0/20240410/质量建模/检验频率4.png
  71. BIN
      doc/plan/S0/20240410/质量建模/检验频率5.png
  72. BIN
      doc/plan/S0/20240410/质量建模/质量建模(字典).png
  73. BIN
      doc/plan/S0/20240410/质量建模/过程检验规范1.png
  74. BIN
      doc/plan/S0/20240410/质量建模/过程检验规范2.png
  75. 1 0
      doc/plan/S0/20240410/质量建模/过程检验规范3.sql
  76. BIN
      doc/plan/S0/20240410/质量建模/过程检验规范4.png
  77. BIN
      doc/plan/S0/20240410/质量建模/过程检验规范5.png
  78. 37 0
      doc/plan/S0/20240410/质量方案/原材料检验规范方案.md
  79. 34 0
      doc/plan/S0/20240410/质量方案/原材料白名单方案.md
  80. 30 0
      doc/plan/S0/20240410/质量方案/抽样方案方案.md
  81. 30 0
      doc/plan/S0/20240410/质量方案/检验仪器方案.md
  82. 35 0
      doc/plan/S0/20240410/质量方案/检验依据方案.md
  83. 34 0
      doc/plan/S0/20240410/质量方案/检验方案方案.md
  84. 30 0
      doc/plan/S0/20240410/质量方案/检验方法方案.md
  85. 35 0
      doc/plan/S0/20240410/质量方案/检验标准方案.md
  86. 30 0
      doc/plan/S0/20240410/质量方案/检验项目方案.md
  87. 30 0
      doc/plan/S0/20240410/质量方案/检验频率方案.md
  88. 47 0
      doc/plan/S0/20240410/质量方案/质量字典方案.md
  89. 35 0
      doc/plan/S0/20240410/质量方案/过程检验规范方案.md
  90. 24 0
      server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Manufacturing/AdoS0ProductStructureRulesTests.cs
  91. 57 0
      server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Quality/AdoS0QualityDtosValidationTests.cs
  92. 732 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Quality/AdoS0QualityAggregateControllers.cs
  93. 451 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Quality/AdoS0QualityBaseDataControllers.cs
  94. 42 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Quality/AdoS0QualityDictionaryController.cs
  95. 271 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Quality/AdoS0QualityDtos.cs
  96. 49 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoSmartOpsEntities.cs
  97. 605 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Quality/AdoS0QualityEntities.cs
  98. 57 0
      server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs
  99. 0 3
      server/Plugins/Admin.NET.Plugin.AiDOP/Startup.cs

+ 62 - 0
Web/src/views/aidop/s0/api/s0QualityApi.spec.ts

@@ -0,0 +1,62 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const getMock = vi.fn();
+const postMock = vi.fn();
+const putMock = vi.fn();
+const deleteMock = vi.fn();
+
+vi.mock('/@/utils/request', () => ({
+	default: {
+		get: (...args: unknown[]) => getMock(...args),
+		post: (...args: unknown[]) => postMock(...args),
+		put: (...args: unknown[]) => putMock(...args),
+		delete: (...args: unknown[]) => deleteMock(...args),
+		patch: vi.fn(),
+	},
+}));
+
+import {
+	s0QualityDictionaryApi,
+	s0RawWhitelistsApi,
+	s0InspectionPlansApi,
+} from './s0QualityApi';
+
+describe('s0QualityApi', () => {
+	beforeEach(() => {
+		getMock.mockReset();
+		postMock.mockReset();
+		putMock.mockReset();
+		deleteMock.mockReset();
+	});
+
+	it('dictionary nodes calls expected endpoint and unwraps response', async () => {
+		const payload = [{ key: 'k1', label: 'L1', route: '/x', kind: 'simple' }];
+		getMock.mockResolvedValue({ data: payload });
+
+		const result = await s0QualityDictionaryApi.nodes();
+
+		expect(getMock).toHaveBeenCalledWith('/api/s0/quality/dictionary/nodes');
+		expect(result).toEqual(payload);
+	});
+
+	it('simple crud list forwards params to raw-whitelists endpoint', async () => {
+		getMock.mockResolvedValue({ data: { total: 0, page: 1, pageSize: 20, list: [] } });
+
+		await s0RawWhitelistsApi.list({ keyword: 'abc', page: 2 });
+
+		expect(getMock).toHaveBeenCalledWith('/api/s0/quality/raw-whitelists', {
+			params: { keyword: 'abc', page: 2 },
+		});
+	});
+
+	it('aggregate crud create posts body and returns unwrapped aggregate data', async () => {
+		const body = { number: 'IP-001', name: '检验方案', items: [] };
+		const payload = { master: { id: 1 }, items: [] };
+		postMock.mockResolvedValue({ data: payload });
+
+		const result = await s0InspectionPlansApi.create(body);
+
+		expect(postMock).toHaveBeenCalledWith('/api/s0/quality/inspection-plans', body);
+		expect(result).toEqual(payload);
+	});
+});

+ 65 - 0
Web/src/views/aidop/s0/api/s0QualityApi.ts

@@ -0,0 +1,65 @@
+import service from '/@/utils/request';
+
+function unwrap<T>(res: { data: T }): T {
+	return res.data;
+}
+
+export interface Paged<T> {
+	total: number;
+	page: number;
+	pageSize: number;
+	list: T[];
+}
+
+export interface OptionItem {
+	value: number;
+	label: string;
+}
+
+export interface AggregateDetail<TMaster = Record<string, any>, TItem = Record<string, any>> {
+	master: TMaster;
+	items: TItem[];
+}
+
+export type GenericRecord = Record<string, any>;
+
+function buildSimpleCrudApi(base: string) {
+	return {
+		list: (params: GenericRecord) => service.get<Paged<GenericRecord>>(base, { params }).then(unwrap),
+		get: (id: number) => service.get<GenericRecord>(`${base}/${id}`).then(unwrap),
+		create: (data: GenericRecord) => service.post<GenericRecord>(base, data).then(unwrap),
+		update: (id: number, data: GenericRecord) => service.put<GenericRecord>(`${base}/${id}`, data).then(unwrap),
+		delete: (id: number) => service.delete(`${base}/${id}`).then(unwrap),
+		options: () => service.get<OptionItem[]>(`${base}/options`).then(unwrap),
+	};
+}
+
+function buildAggregateCrudApi(base: string) {
+	return {
+		list: (params: GenericRecord) => service.get<Paged<GenericRecord>>(base, { params }).then(unwrap),
+		get: (id: number) => service.get<AggregateDetail>(`${base}/${id}`).then(unwrap),
+		create: (data: GenericRecord) => service.post<AggregateDetail>(base, data).then(unwrap),
+		update: (id: number, data: GenericRecord) => service.put<AggregateDetail>(`${base}/${id}`, data).then(unwrap),
+		delete: (id: number) => service.delete(`${base}/${id}`).then(unwrap),
+	};
+}
+
+export const s0QualityDictionaryApi = {
+	nodes: () =>
+		service
+			.get<Array<{ key: string; label: string; route: string; kind: string }>>('/api/s0/quality/dictionary/nodes')
+			.then(unwrap),
+};
+
+export const s0RawWhitelistsApi = buildSimpleCrudApi('/api/s0/quality/raw-whitelists');
+export const s0SamplingSchemesApi = buildSimpleCrudApi('/api/s0/quality/sampling-schemes');
+export const s0InspectionInstrumentsApi = buildSimpleCrudApi('/api/s0/quality/instruments');
+export const s0InspectionMethodsApi = buildSimpleCrudApi('/api/s0/quality/inspection-methods');
+export const s0InspectionItemsApi = buildSimpleCrudApi('/api/s0/quality/inspection-items');
+export const s0InspectionFrequenciesApi = buildSimpleCrudApi('/api/s0/quality/inspection-frequencies');
+
+export const s0InspectionBasesApi = buildAggregateCrudApi('/api/s0/quality/inspection-bases');
+export const s0InspectionStandardsApi = buildAggregateCrudApi('/api/s0/quality/inspection-standards');
+export const s0InspectionPlansApi = buildAggregateCrudApi('/api/s0/quality/inspection-plans');
+export const s0RawInspectionSpecsApi = buildAggregateCrudApi('/api/s0/quality/raw-inspection-specs');
+export const s0ProcessInspectionSpecsApi = buildAggregateCrudApi('/api/s0/quality/process-inspection-specs');

+ 97 - 0
Web/src/views/aidop/s0/api/s0SalesApi.spec.ts

@@ -0,0 +1,97 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const getMock = vi.fn();
+const postMock = vi.fn();
+const putMock = vi.fn();
+const deleteMock = vi.fn();
+const patchMock = vi.fn();
+
+const dictListMock = vi.fn();
+const orgListMock = vi.fn();
+
+vi.mock('/@/utils/request', () => ({
+	default: {
+		get: (...args: unknown[]) => getMock(...args),
+		post: (...args: unknown[]) => postMock(...args),
+		put: (...args: unknown[]) => putMock(...args),
+		delete: (...args: unknown[]) => deleteMock(...args),
+		patch: (...args: unknown[]) => patchMock(...args),
+	},
+}));
+
+vi.mock('/@/api-services', () => ({
+	SysDictDataApi: vi.fn().mockImplementation(() => ({
+		apiSysDictDataDataListCodeGet: (...args: unknown[]) => dictListMock(...args),
+	})),
+	SysOrgApi: vi.fn().mockImplementation(() => ({
+		apiSysOrgListGet: (...args: unknown[]) => orgListMock(...args),
+	})),
+}));
+
+import { loadDictOptions, loadOrgList } from './s0SalesApi';
+
+describe('s0SalesApi load helpers', () => {
+	beforeEach(() => {
+		getMock.mockReset();
+		postMock.mockReset();
+		putMock.mockReset();
+		deleteMock.mockReset();
+		patchMock.mockReset();
+		dictListMock.mockReset();
+		orgListMock.mockReset();
+	});
+
+	it('loadDictOptions maps api result to {label, value}', async () => {
+		dictListMock.mockResolvedValue({
+			data: {
+				result: [{ label: '启用', value: '1' }, { label: null, value: null }],
+			},
+		});
+
+		const result = await loadDictOptions('enable_status');
+
+		expect(dictListMock).toHaveBeenCalledWith('enable_status');
+		expect(result).toEqual([
+			{ label: '启用', value: '1' },
+			{ label: '', value: '' },
+		]);
+	});
+
+	it('loadDictOptions returns empty list when api throws', async () => {
+		dictListMock.mockRejectedValue(new Error('network'));
+		const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+		const result = await loadDictOptions('x');
+
+		expect(result).toEqual([]);
+		expect(warnSpy).toHaveBeenCalled();
+		warnSpy.mockRestore();
+	});
+
+	it('loadOrgList maps org rows and fills id fallback with 0', async () => {
+		orgListMock.mockResolvedValue({
+			data: {
+				result: [{ id: 12, pid: 1, name: '研发', code: 'DEV', type: 'dept' }, { id: null }],
+			},
+		});
+
+		const result = await loadOrgList('dept');
+
+		expect(orgListMock).toHaveBeenCalledWith(0, undefined, undefined, 'dept');
+		expect(result).toEqual([
+			{ id: 12, pid: 1, name: '研发', code: 'DEV', type: 'dept' },
+			{ id: 0, pid: undefined, name: undefined, code: undefined, type: undefined },
+		]);
+	});
+
+	it('loadOrgList returns empty list when api throws', async () => {
+		orgListMock.mockRejectedValue(new Error('boom'));
+		const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+		const result = await loadOrgList();
+
+		expect(result).toEqual([]);
+		expect(warnSpy).toHaveBeenCalled();
+		warnSpy.mockRestore();
+	});
+});

+ 71 - 0
Web/src/views/aidop/s0/api/s0SupplyApi.spec.ts

@@ -0,0 +1,71 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const getMock = vi.fn();
+const postMock = vi.fn();
+const putMock = vi.fn();
+const deleteMock = vi.fn();
+const patchMock = vi.fn();
+
+const dictListMock = vi.fn();
+const orgListMock = vi.fn();
+
+vi.mock('/@/utils/request', () => ({
+	default: {
+		get: (...args: unknown[]) => getMock(...args),
+		post: (...args: unknown[]) => postMock(...args),
+		put: (...args: unknown[]) => putMock(...args),
+		delete: (...args: unknown[]) => deleteMock(...args),
+		patch: (...args: unknown[]) => patchMock(...args),
+	},
+}));
+
+vi.mock('/@/api-services', () => ({
+	SysDictDataApi: vi.fn().mockImplementation(() => ({
+		apiSysDictDataDataListCodeGet: (...args: unknown[]) => dictListMock(...args),
+	})),
+	SysOrgApi: vi.fn().mockImplementation(() => ({
+		apiSysOrgListGet: (...args: unknown[]) => orgListMock(...args),
+	})),
+}));
+
+import { loadDictOptions, loadOrgList, s0SrmPurchasesApi, s0SuppliersApi } from './s0SupplyApi';
+
+describe('s0SupplyApi', () => {
+	beforeEach(() => {
+		getMock.mockReset();
+		postMock.mockReset();
+		putMock.mockReset();
+		deleteMock.mockReset();
+		patchMock.mockReset();
+		dictListMock.mockReset();
+		orgListMock.mockReset();
+	});
+
+	it('suppliers toggleEnabled calls patch endpoint and unwraps', async () => {
+		patchMock.mockResolvedValue({ data: { id: 7, isActive: false } });
+		const result = await s0SuppliersApi.toggleEnabled(7, { isActive: false });
+		expect(patchMock).toHaveBeenCalledWith('/api/s0/supply/suppliers/7/toggle-enabled', { isActive: false });
+		expect(result).toEqual({ id: 7, isActive: false });
+	});
+
+	it('srm purchases list forwards params', async () => {
+		getMock.mockResolvedValue({ data: { total: 0, page: 1, pageSize: 20, list: [] } });
+		await s0SrmPurchasesApi.list({ page: 2, keyword: 'abc' });
+		expect(getMock).toHaveBeenCalledWith('/api/s0/supply/srm-purchases', { params: { page: 2, keyword: 'abc' } });
+	});
+
+	it('loadDictOptions maps and falls back empty strings', async () => {
+		dictListMock.mockResolvedValue({ data: { result: [{ label: '人民币', value: 'CNY' }, { label: null, value: null }] } });
+		const result = await loadDictOptions('currency');
+		expect(result).toEqual([{ label: '人民币', value: 'CNY' }, { label: '', value: '' }]);
+	});
+
+	it('loadOrgList returns [] on error', async () => {
+		orgListMock.mockRejectedValue(new Error('network'));
+		const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+		const result = await loadOrgList('factory');
+		expect(result).toEqual([]);
+		expect(warnSpy).toHaveBeenCalled();
+		warnSpy.mockRestore();
+	});
+});

+ 50 - 0
Web/src/views/aidop/s0/api/s0WarehouseApi.spec.ts

@@ -0,0 +1,50 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const getMock = vi.fn();
+const postMock = vi.fn();
+const putMock = vi.fn();
+const deleteMock = vi.fn();
+
+vi.mock('/@/utils/request', () => ({
+	default: {
+		get: (...args: unknown[]) => getMock(...args),
+		post: (...args: unknown[]) => postMock(...args),
+		put: (...args: unknown[]) => putMock(...args),
+		delete: (...args: unknown[]) => deleteMock(...args),
+		patch: vi.fn(),
+	},
+}));
+
+import { s0DepartmentsApi, s0TaskAssignmentsApi } from './s0WarehouseApi';
+
+describe('s0WarehouseApi', () => {
+	beforeEach(() => {
+		getMock.mockReset();
+		postMock.mockReset();
+		putMock.mockReset();
+		deleteMock.mockReset();
+	});
+
+	it('departments list forwards query params', async () => {
+		getMock.mockResolvedValue({ data: { total: 0, page: 1, pageSize: 20, list: [] } });
+		await s0DepartmentsApi.list({ page: 3, department: 'D01' });
+		expect(getMock).toHaveBeenCalledWith('/api/s0/warehouse/departments', {
+			params: { page: 3, department: 'D01' },
+		});
+	});
+
+	it('departments create posts body and unwraps', async () => {
+		const body = { companyRefId: 1, factoryRefId: 2, department: 'D01', isActive: true };
+		postMock.mockResolvedValue({ data: { id: 101, ...body } });
+		const result = await s0DepartmentsApi.create(body);
+		expect(postMock).toHaveBeenCalledWith('/api/s0/warehouse/departments', body);
+		expect(result).toEqual({ id: 101, ...body });
+	});
+
+	it('task assignments delete calls endpoint', async () => {
+		deleteMock.mockResolvedValue({ data: { ok: true } });
+		const result = await s0TaskAssignmentsApi.delete(99);
+		expect(deleteMock).toHaveBeenCalledWith('/api/s0/warehouse/task-assignments/99');
+		expect(result).toEqual({ ok: true });
+	});
+});

+ 8 - 0
Web/src/views/aidop/s0/quality/InspectionBasisList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualityAggregateCrudPage :config="inspectionBasisConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyInspectionBasis">
+import QualityAggregateCrudPage from './components/QualityAggregateCrudPage.vue';
+import { inspectionBasisConfig } from './qualityConfigs';
+</script>

+ 8 - 0
Web/src/views/aidop/s0/quality/InspectionFrequencyList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualitySimpleCrudPage :config="inspectionFrequencyConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyInspectionFrequency">
+import QualitySimpleCrudPage from './components/QualitySimpleCrudPage.vue';
+import { inspectionFrequencyConfig } from './qualityConfigs';
+</script>

+ 8 - 0
Web/src/views/aidop/s0/quality/InspectionInstrumentList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualitySimpleCrudPage :config="inspectionInstrumentConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyInstrument">
+import QualitySimpleCrudPage from './components/QualitySimpleCrudPage.vue';
+import { inspectionInstrumentConfig } from './qualityConfigs';
+</script>

+ 8 - 0
Web/src/views/aidop/s0/quality/InspectionItemList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualitySimpleCrudPage :config="inspectionItemConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyInspectionItem">
+import QualitySimpleCrudPage from './components/QualitySimpleCrudPage.vue';
+import { inspectionItemConfig } from './qualityConfigs';
+</script>

+ 8 - 0
Web/src/views/aidop/s0/quality/InspectionMethodList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualitySimpleCrudPage :config="inspectionMethodConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyInspectionMethod">
+import QualitySimpleCrudPage from './components/QualitySimpleCrudPage.vue';
+import { inspectionMethodConfig } from './qualityConfigs';
+</script>

+ 8 - 0
Web/src/views/aidop/s0/quality/InspectionPlanList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualityAggregateCrudPage :config="inspectionPlanConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyInspectionPlan">
+import QualityAggregateCrudPage from './components/QualityAggregateCrudPage.vue';
+import { inspectionPlanConfig } from './qualityConfigs';
+</script>

+ 8 - 0
Web/src/views/aidop/s0/quality/InspectionStandardList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualityAggregateCrudPage :config="inspectionStandardConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyInspectionStandard">
+import QualityAggregateCrudPage from './components/QualityAggregateCrudPage.vue';
+import { inspectionStandardConfig } from './qualityConfigs';
+</script>

+ 8 - 0
Web/src/views/aidop/s0/quality/ProcessInspectionSpecList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualityAggregateCrudPage :config="processInspectionSpecConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyProcessInspectionSpec">
+import QualityAggregateCrudPage from './components/QualityAggregateCrudPage.vue';
+import { processInspectionSpecConfig } from './qualityConfigs';
+</script>

+ 146 - 0
Web/src/views/aidop/s0/quality/QualityDictionaryPage.vue

@@ -0,0 +1,146 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="S0 / 质量建模 / 质量字典">
+		<div class="dictionary-layout" v-loading="loading">
+			<div class="dictionary-tree">
+				<div class="panel-title">QMS 字典树</div>
+				<el-scrollbar height="560px">
+					<div
+						v-for="node in nodes"
+						:key="node.key"
+						class="tree-item"
+						:class="{ active: activeKey === node.key }"
+						@click="selectNode(node)"
+					>
+						<span>{{ node.label }}</span>
+						<el-tag size="small" :type="node.kind === 'page' ? 'success' : 'info'">{{ node.kind === 'page' ? '页面' : '字典' }}</el-tag>
+					</div>
+				</el-scrollbar>
+			</div>
+			<div class="dictionary-content">
+				<div class="panel-title">内容区</div>
+				<div v-if="!activeNode" class="empty-state">点击左边字典项进行操作</div>
+				<div v-else class="content-card">
+					<div class="node-title">{{ activeNode.label }}</div>
+					<div class="node-meta">类型:{{ activeNode.kind === 'page' ? '已落地页面入口' : '统一字典资源入口' }}</div>
+					<div v-if="activeNode.route" class="action-row">
+						<el-button type="primary" @click="router.push(activeNode.route)">进入维护页</el-button>
+					</div>
+					<div v-else class="empty-state small">当前按质量字典导航页处理,不单独新建业务实体表。</div>
+				</div>
+			</div>
+		</div>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS0QlyDictionary">
+import { computed, onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import { s0QualityDictionaryApi } from '../api/s0QualityApi';
+
+const route = useRoute();
+const router = useRouter();
+const pageTitle = computed(() => (route.meta?.title as string) || '质量字典');
+
+const loading = ref(false);
+const nodes = ref<Array<{ key: string; label: string; route: string; kind: string }>>([]);
+const activeKey = ref('');
+
+const activeNode = computed(() => nodes.value.find((item) => item.key === activeKey.value));
+
+function selectNode(node: { key: string }) {
+	activeKey.value = node.key;
+}
+
+async function loadNodes() {
+	loading.value = true;
+	try {
+		nodes.value = await s0QualityDictionaryApi.nodes();
+		activeKey.value = '';
+	} finally {
+		loading.value = false;
+	}
+}
+
+onMounted(() => {
+	void loadNodes();
+});
+</script>
+
+<style scoped lang="scss">
+@import '/@/views/aidop/styles/aidop-demo.scss';
+
+.dictionary-layout {
+	display: grid;
+	grid-template-columns: 320px minmax(0, 1fr);
+	gap: 16px;
+}
+
+.dictionary-tree,
+.dictionary-content {
+	border: 1px solid var(--el-border-color);
+	border-radius: 8px;
+	background: var(--el-bg-color);
+	padding: 16px;
+	min-height: 620px;
+}
+
+.panel-title {
+	margin-bottom: 12px;
+	font-size: 15px;
+	font-weight: 600;
+}
+
+.tree-item {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 10px 12px;
+	border-radius: 6px;
+	cursor: pointer;
+}
+
+.tree-item:hover,
+.tree-item.active {
+	background: var(--el-color-primary-light-9);
+	color: var(--el-color-primary);
+}
+
+.empty-state {
+	height: 100%;
+	min-height: 440px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	color: var(--el-text-color-secondary);
+	font-size: 14px;
+}
+
+.empty-state.small {
+	min-height: auto;
+	justify-content: flex-start;
+}
+
+.content-card {
+	padding: 24px;
+	border-radius: 8px;
+	background: linear-gradient(180deg, #f7fbff 0%, #ffffff 100%);
+	border: 1px solid #d8e6f5;
+}
+
+.node-title {
+	font-size: 20px;
+	font-weight: 600;
+	margin-bottom: 8px;
+}
+
+.node-meta {
+	color: var(--el-text-color-secondary);
+	margin-bottom: 16px;
+}
+
+.action-row {
+	display: flex;
+	gap: 12px;
+}
+</style>

+ 8 - 0
Web/src/views/aidop/s0/quality/RawInspectionSpecList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualityAggregateCrudPage :config="rawInspectionSpecConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyRawInspectionSpec">
+import QualityAggregateCrudPage from './components/QualityAggregateCrudPage.vue';
+import { rawInspectionSpecConfig } from './qualityConfigs';
+</script>

+ 8 - 0
Web/src/views/aidop/s0/quality/RawWhitelistList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualitySimpleCrudPage :config="rawWhitelistConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlyRawWhitelist">
+import QualitySimpleCrudPage from './components/QualitySimpleCrudPage.vue';
+import { rawWhitelistConfig } from './qualityConfigs';
+</script>

+ 8 - 0
Web/src/views/aidop/s0/quality/SamplingSchemeList.vue

@@ -0,0 +1,8 @@
+<template>
+	<QualitySimpleCrudPage :config="samplingSchemeConfig" />
+</template>
+
+<script setup lang="ts" name="aidopS0QlySamplingScheme">
+import QualitySimpleCrudPage from './components/QualitySimpleCrudPage.vue';
+import { samplingSchemeConfig } from './qualityConfigs';
+</script>

+ 244 - 0
Web/src/views/aidop/s0/quality/components/QualityAggregateCrudPage.vue

@@ -0,0 +1,244 @@
+<template>
+	<AidopDemoShell :title="pageTitle" :subtitle="config.subtitle">
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="关键字">
+				<el-input v-model="query.keyword" :placeholder="config.queryPlaceholder" clearable style="width: 260px" />
+			</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">新增{{ config.entityLabel }}</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table :data="rows" v-loading="loading" border stripe>
+			<el-table-column v-for="column in config.listColumns" :key="column.prop" :prop="column.prop" :label="column.label" :width="column.width" :min-width="column.minWidth" show-overflow-tooltip />
+			<el-table-column label="操作" width="180" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+
+		<div class="pager">
+			<el-pagination
+				v-model:current-page="query.page"
+				v-model:page-size="query.pageSize"
+				:total="total"
+				:page-sizes="[20, 50, 100]"
+				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="loadList"
+			/>
+		</div>
+
+		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="1200px" destroy-on-close @closed="resetForm">
+			<el-form ref="formRef" :model="master" :rules="rules" label-width="120px" class="head-grid">
+				<el-form-item v-for="field in config.headFields" :key="field.prop" :label="field.label" :prop="field.prop">
+					<el-input-number
+						v-if="field.type === 'number'"
+						v-model="master[field.prop]"
+						controls-position="right"
+						style="width: 100%"
+					/>
+					<el-input
+						v-else
+						v-model="master[field.prop]"
+						:type="field.type === 'textarea' ? 'textarea' : 'text'"
+						:rows="field.type === 'textarea' ? 3 : undefined"
+					/>
+				</el-form-item>
+			</el-form>
+
+			<div class="detail-toolbar">
+				<div class="detail-title">明细</div>
+				<el-button type="primary" plain @click="addItem">新增行</el-button>
+			</div>
+
+			<el-table :data="items" border stripe class="detail-table">
+				<el-table-column type="index" label="#" width="50" />
+				<el-table-column v-for="column in config.detailColumns" :key="column.prop" :label="column.label" :width="column.type === 'number' ? 140 : undefined" :min-width="column.type === 'number' ? undefined : 150">
+					<template #default="{ row }">
+						<el-input-number
+							v-if="column.type === 'number'"
+							v-model="row[column.prop]"
+							controls-position="right"
+							style="width: 100%"
+						/>
+						<el-input v-else v-model="row[column.prop]" />
+					</template>
+				</el-table-column>
+				<el-table-column label="操作" width="80" fixed="right" align="center">
+					<template #default="{ $index }">
+						<el-button link type="danger" @click="removeItem($index)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+
+			<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">
+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 type { QualityAggregatePageConfig } from '../qualityConfigs';
+
+const props = defineProps<{ config: QualityAggregatePageConfig }>();
+
+const route = useRoute();
+const pageTitle = computed(() => (route.meta?.title as string) || props.config.entityLabel);
+
+const query = reactive({ keyword: '', page: 1, pageSize: 20 });
+const rows = ref<Record<string, any>[]>([]);
+const total = ref(0);
+const loading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref('');
+const editingId = ref<number | null>(null);
+const saving = ref(false);
+const formRef = ref<FormInstance>();
+const master = reactive<Record<string, any>>({});
+const items = ref<Record<string, any>[]>([]);
+
+const rules = computed<FormRules>(() =>
+	props.config.headFields.reduce<FormRules>((acc, field) => {
+		if (field.required) acc[field.prop] = [{ required: true, message: `请填写${field.label}`, trigger: 'blur' }];
+		return acc;
+	}, {})
+);
+
+function resetForm() {
+	editingId.value = null;
+	Object.keys(master).forEach((key) => delete master[key]);
+	Object.assign(master, structuredClone(props.config.initialMaster));
+	items.value = [];
+	formRef.value?.clearValidate();
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await props.config.api.list({
+			keyword: query.keyword || undefined,
+			page: query.page,
+			pageSize: query.pageSize,
+		});
+		rows.value = data.list ?? [];
+		total.value = data.total ?? 0;
+	} catch {
+		rows.value = [];
+		total.value = 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function resetQuery() {
+	query.keyword = '';
+	query.page = 1;
+	void loadList();
+}
+
+function addItem() {
+	items.value.push(props.config.createEmptyItem());
+}
+
+function removeItem(index: number) {
+	items.value.splice(index, 1);
+}
+
+function openCreate() {
+	resetForm();
+	dialogTitle.value = `新增${props.config.entityLabel}`;
+	addItem();
+	dialogVisible.value = true;
+}
+
+async function openEdit(row: Record<string, any>) {
+	resetForm();
+	editingId.value = Number(row.id);
+	dialogTitle.value = `编辑${props.config.entityLabel}`;
+	const detail = await props.config.api.get(editingId.value);
+	Object.assign(master, structuredClone(detail.master ?? {}));
+	items.value = structuredClone(detail.items ?? []);
+	dialogVisible.value = true;
+}
+
+async function submitForm() {
+	await formRef.value?.validate();
+	saving.value = true;
+	try {
+		const payload = { ...master, items: items.value.map((item) => ({ ...item })) };
+		if (editingId.value) {
+			await props.config.api.update(editingId.value, payload);
+			ElMessage.success('已保存');
+		} else {
+			await props.config.api.create(payload);
+			ElMessage.success('已创建');
+		}
+		dialogVisible.value = false;
+		await loadList();
+	} finally {
+		saving.value = false;
+	}
+}
+
+function onDelete(row: Record<string, any>) {
+	ElMessageBox.confirm(`确定删除${props.config.entityLabel}「${row[props.config.listColumns[0]?.prop] ?? row.id}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await props.config.api.delete(Number(row.id));
+			ElMessage.success('已删除');
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+onMounted(() => {
+	resetForm();
+	void loadList();
+});
+</script>
+
+<style scoped lang="scss">
+@import '/@/views/aidop/styles/aidop-demo.scss';
+
+.mb12 {
+	margin-bottom: 12px;
+}
+
+.pager {
+	margin-top: 12px;
+	display: flex;
+	justify-content: flex-end;
+}
+
+.head-grid {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	column-gap: 12px;
+}
+
+.detail-toolbar {
+	margin: 8px 0 12px;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+}
+
+.detail-title {
+	font-size: 14px;
+	font-weight: 600;
+}
+
+.detail-table {
+	margin-bottom: 8px;
+}
+</style>

+ 184 - 0
Web/src/views/aidop/s0/quality/components/QualitySimpleCrudPage.vue

@@ -0,0 +1,184 @@
+<template>
+	<AidopDemoShell :title="pageTitle" :subtitle="config.subtitle">
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="关键字">
+				<el-input v-model="query.keyword" :placeholder="config.queryPlaceholder" clearable style="width: 240px" />
+			</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">新增{{ config.entityLabel }}</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table :data="rows" v-loading="loading" border stripe>
+			<el-table-column v-for="column in config.columns" :key="column.prop" :prop="column.prop" :label="column.label" :width="column.width" :min-width="column.minWidth" show-overflow-tooltip />
+			<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+
+		<div class="pager">
+			<el-pagination
+				v-model:current-page="query.page"
+				v-model:page-size="query.pageSize"
+				:total="total"
+				:page-sizes="[20, 50, 100]"
+				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@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="120px">
+				<el-form-item v-for="field in config.fields" :key="field.prop" :label="field.label" :prop="field.prop">
+					<el-input-number
+						v-if="field.type === 'number'"
+						v-model="form[field.prop]"
+						controls-position="right"
+						style="width: 100%"
+					/>
+					<el-input
+						v-else
+						v-model="form[field.prop]"
+						:type="field.type === 'textarea' ? 'textarea' : 'text'"
+						:rows="field.type === 'textarea' ? 3 : undefined"
+					/>
+				</el-form-item>
+			</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">
+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 type { QualitySimplePageConfig } from '../qualityConfigs';
+
+const props = defineProps<{ config: QualitySimplePageConfig }>();
+
+const route = useRoute();
+const pageTitle = computed(() => (route.meta?.title as string) || props.config.entityLabel);
+
+const query = reactive({ keyword: '', page: 1, pageSize: 20 });
+const rows = ref<Record<string, any>[]>([]);
+const total = ref(0);
+const loading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref('');
+const editingId = ref<number | null>(null);
+const saving = ref(false);
+const formRef = ref<FormInstance>();
+const form = reactive<Record<string, any>>({});
+
+const rules = computed<FormRules>(() =>
+	props.config.fields.reduce<FormRules>((acc, field) => {
+		if (field.required) acc[field.prop] = [{ required: true, message: `请填写${field.label}`, trigger: 'blur' }];
+		return acc;
+	}, {})
+);
+
+function resetForm() {
+	editingId.value = null;
+	Object.keys(form).forEach((key) => delete form[key]);
+	Object.assign(form, structuredClone(props.config.initialForm));
+	formRef.value?.clearValidate();
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await props.config.api.list({
+			keyword: query.keyword || undefined,
+			page: query.page,
+			pageSize: query.pageSize,
+		});
+		rows.value = data.list ?? [];
+		total.value = data.total ?? 0;
+	} catch {
+		rows.value = [];
+		total.value = 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function resetQuery() {
+	query.keyword = '';
+	query.page = 1;
+	void loadList();
+}
+
+function openCreate() {
+	resetForm();
+	dialogTitle.value = `新增${props.config.entityLabel}`;
+	dialogVisible.value = true;
+}
+
+async function openEdit(row: Record<string, any>) {
+	resetForm();
+	editingId.value = Number(row.id);
+	dialogTitle.value = `编辑${props.config.entityLabel}`;
+	const detail = await props.config.api.get(editingId.value);
+	Object.assign(form, structuredClone(detail));
+	dialogVisible.value = true;
+}
+
+async function submitForm() {
+	await formRef.value?.validate();
+	saving.value = true;
+	try {
+		const payload = { ...form };
+		if (editingId.value) {
+			await props.config.api.update(editingId.value, payload);
+			ElMessage.success('已保存');
+		} else {
+			await props.config.api.create(payload);
+			ElMessage.success('已创建');
+		}
+		dialogVisible.value = false;
+		await loadList();
+	} finally {
+		saving.value = false;
+	}
+}
+
+function onDelete(row: Record<string, any>) {
+	ElMessageBox.confirm(`确定删除${props.config.entityLabel}「${row[props.config.columns[0]?.prop] ?? row.id}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await props.config.api.delete(Number(row.id));
+			ElMessage.success('已删除');
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+onMounted(() => {
+	resetForm();
+	void loadList();
+});
+</script>
+
+<style scoped lang="scss">
+@import '/@/views/aidop/styles/aidop-demo.scss';
+
+.mb12 {
+	margin-bottom: 12px;
+}
+
+.pager {
+	margin-top: 12px;
+	display: flex;
+	justify-content: flex-end;
+}
+</style>

+ 376 - 0
Web/src/views/aidop/s0/quality/qualityConfigs.ts

@@ -0,0 +1,376 @@
+import {
+	s0InspectionBasesApi,
+	s0InspectionFrequenciesApi,
+	s0InspectionInstrumentsApi,
+	s0InspectionItemsApi,
+	s0InspectionMethodsApi,
+	s0InspectionPlansApi,
+	s0InspectionStandardsApi,
+	s0ProcessInspectionSpecsApi,
+	s0RawInspectionSpecsApi,
+	s0RawWhitelistsApi,
+	s0SamplingSchemesApi,
+	type GenericRecord,
+} from '../api/s0QualityApi';
+
+export interface QualityColumnDef {
+	prop: string;
+	label: string;
+	width?: number;
+	minWidth?: number;
+}
+
+export interface QualityFieldDef {
+	prop: string;
+	label: string;
+	type?: 'input' | 'textarea' | 'number';
+	required?: boolean;
+}
+
+export interface QualitySimplePageConfig {
+	subtitle: string;
+	entityLabel: string;
+	queryPlaceholder: string;
+	columns: QualityColumnDef[];
+	fields: QualityFieldDef[];
+	initialForm: GenericRecord;
+	api: {
+		list(params: GenericRecord): Promise<any>;
+		get(id: number): Promise<any>;
+		create(data: GenericRecord): Promise<any>;
+		update(id: number, data: GenericRecord): Promise<any>;
+		delete(id: number): Promise<any>;
+	};
+}
+
+export interface QualityAggregatePageConfig {
+	subtitle: string;
+	entityLabel: string;
+	queryPlaceholder: string;
+	listColumns: QualityColumnDef[];
+	headFields: QualityFieldDef[];
+	detailColumns: QualityFieldDef[];
+	initialMaster: GenericRecord;
+	createEmptyItem: () => GenericRecord;
+	api: {
+		list(params: GenericRecord): Promise<any>;
+		get(id: number): Promise<any>;
+		create(data: GenericRecord): Promise<any>;
+		update(id: number, data: GenericRecord): Promise<any>;
+		delete(id: number): Promise<any>;
+	};
+}
+
+export const rawWhitelistConfig: QualitySimplePageConfig = {
+	subtitle: 'S0 / 质量建模 / 原材料白名单',
+	entityLabel: '原材料白名单',
+	queryPlaceholder: '供应商编码/名称',
+	api: s0RawWhitelistsApi,
+	columns: [
+		{ prop: 'supplierCode', label: '供应商编码', width: 220 },
+		{ prop: 'supplierName', label: '供应商名称', minWidth: 260 },
+	],
+	fields: [
+		{ prop: 'supplierCode', label: '供应商编码', required: true },
+		{ prop: 'supplierName', label: '供应商名称', required: true },
+	],
+	initialForm: { supplierCode: '', supplierName: '' },
+};
+
+export const samplingSchemeConfig: QualitySimplePageConfig = {
+	subtitle: 'S0 / 质量建模 / 抽样方案',
+	entityLabel: '抽样方案',
+	queryPlaceholder: '编号/名称',
+	api: s0SamplingSchemesApi,
+	columns: [
+		{ prop: 'number', label: '编号', width: 180 },
+		{ prop: 'name', label: '名称', minWidth: 220 },
+		{ prop: 'samplingType', label: '抽样类型', width: 140 },
+		{ prop: 'inspectionLevel', label: '检验水准', width: 140 },
+		{ prop: 'aqlValue', label: 'AQL值', width: 120 },
+		{ prop: 'comment', label: '备注', minWidth: 180 },
+	],
+	fields: [
+		{ prop: 'number', label: '编号', required: true },
+		{ prop: 'name', label: '名称', required: true },
+		{ prop: 'samplingType', label: '抽样类型' },
+		{ prop: 'inspectionLevel', label: '检验水准' },
+		{ prop: 'strictness', label: '严格程度' },
+		{ prop: 'aqlValue', label: 'AQL值' },
+		{ prop: 'inspectionType', label: '检验类型' },
+		{ prop: 'inspectOrgId', label: '检验组织', type: 'number' },
+		{ prop: 'inspectUserId', label: '检验员' },
+		{ prop: 'status', label: '状态' },
+		{ prop: 'enableStatus', label: '启用' },
+		{ prop: 'comment', label: '备注', type: 'textarea' },
+	],
+	initialForm: { number: '', name: '', samplingType: '', inspectionLevel: '', strictness: '', aqlValue: '', inspectionType: '', inspectOrgId: undefined, inspectUserId: '', status: '', enableStatus: '', comment: '' },
+};
+
+export const inspectionInstrumentConfig: QualitySimplePageConfig = {
+	subtitle: 'S0 / 质量建模 / 检验仪器',
+	entityLabel: '检验仪器',
+	queryPlaceholder: '编号/名称/型号',
+	api: s0InspectionInstrumentsApi,
+	columns: [
+		{ prop: 'number', label: '编号', width: 180 },
+		{ prop: 'name', label: '名称', minWidth: 220 },
+		{ prop: 'model', label: '型号', width: 160 },
+		{ prop: 'specification', label: '规格', width: 160 },
+		{ prop: 'manufacturer', label: '生产厂家', minWidth: 180 },
+	],
+	fields: [
+		{ prop: 'number', label: '编号', required: true },
+		{ prop: 'name', label: '名称', required: true },
+		{ prop: 'model', label: '型号' },
+		{ prop: 'specification', label: '规格' },
+		{ prop: 'manufacturer', label: '生产厂家' },
+		{ prop: 'status', label: '状态' },
+		{ prop: 'enableStatus', label: '启用' },
+		{ prop: 'comment', label: '备注', type: 'textarea' },
+	],
+	initialForm: { number: '', name: '', model: '', specification: '', manufacturer: '', status: '', enableStatus: '', comment: '' },
+};
+
+export const inspectionMethodConfig: QualitySimplePageConfig = {
+	subtitle: 'S0 / 质量建模 / 检验方法',
+	entityLabel: '检验方法',
+	queryPlaceholder: '编号/名称',
+	api: s0InspectionMethodsApi,
+	columns: [
+		{ prop: 'number', label: '编号', width: 180 },
+		{ prop: 'name', label: '名称', minWidth: 240 },
+		{ prop: 'controlStrategy', label: '控制策略', width: 180 },
+		{ prop: 'comment', label: '备注', minWidth: 200 },
+	],
+	fields: [
+		{ prop: 'number', label: '编号', required: true },
+		{ prop: 'name', label: '名称', required: true },
+		{ prop: 'controlStrategy', label: '控制策略' },
+		{ prop: 'status', label: '状态' },
+		{ prop: 'enableStatus', label: '启用' },
+		{ prop: 'comment', label: '备注', type: 'textarea' },
+	],
+	initialForm: { number: '', name: '', controlStrategy: '', status: '', enableStatus: '', comment: '' },
+};
+
+export const inspectionItemConfig: QualitySimplePageConfig = {
+	subtitle: 'S0 / 质量建模 / 检验项目',
+	entityLabel: '检验项目',
+	queryPlaceholder: '编号/名称',
+	api: s0InspectionItemsApi,
+	columns: [
+		{ prop: 'number', label: '编号', width: 180 },
+		{ prop: 'name', label: '名称', minWidth: 220 },
+		{ prop: 'checkMethodId', label: '检验方法', width: 120 },
+		{ prop: 'checkBasisId', label: '检验依据', width: 120 },
+		{ prop: 'checkInstructId', label: '检验指导书', width: 140 },
+	],
+	fields: [
+		{ prop: 'number', label: '编号', required: true },
+		{ prop: 'name', label: '名称', required: true },
+		{ prop: 'checkMethodId', label: '检验方法', type: 'number' },
+		{ prop: 'checkBasisId', label: '检验依据', type: 'number' },
+		{ prop: 'checkInstructId', label: '检验指导书', type: 'number' },
+		{ prop: 'radioGroupField', label: '单选组1' },
+		{ prop: 'radioGroupField1', label: '单选组2' },
+		{ prop: 'metricType', label: '指标类型', type: 'number' },
+		{ prop: 'status', label: '状态' },
+		{ prop: 'enableStatus', label: '启用' },
+		{ prop: 'comment', label: '备注', type: 'textarea' },
+	],
+	initialForm: { number: '', name: '', checkMethodId: undefined, checkBasisId: undefined, checkInstructId: undefined, radioGroupField: '', radioGroupField1: '', metricType: undefined, status: '', enableStatus: '', comment: '' },
+};
+
+export const inspectionFrequencyConfig: QualitySimplePageConfig = {
+	subtitle: 'S0 / 质量建模 / 检验频率',
+	entityLabel: '检验频率',
+	queryPlaceholder: '编号/名称',
+	api: s0InspectionFrequenciesApi,
+	columns: [
+		{ prop: 'number', label: '编号', width: 180 },
+		{ prop: 'name', label: '名称', minWidth: 220 },
+		{ prop: 'remark', label: '备注', minWidth: 240 },
+	],
+	fields: [
+		{ prop: 'number', label: '编号', required: true },
+		{ prop: 'name', label: '名称', required: true },
+		{ prop: 'remark', label: '备注', type: 'textarea' },
+		{ prop: 'status', label: '状态' },
+		{ prop: 'enableStatus', label: '启用' },
+	],
+	initialForm: { number: '', name: '', remark: '', status: '', enableStatus: '' },
+};
+
+export const inspectionBasisConfig: QualityAggregatePageConfig = {
+	subtitle: 'S0 / 质量建模 / 检验依据',
+	entityLabel: '检验依据',
+	queryPlaceholder: '编号/名称',
+	api: s0InspectionBasesApi,
+	listColumns: [
+		{ prop: 'number', label: '编号', width: 180 },
+		{ prop: 'name', label: '名称', minWidth: 220 },
+		{ prop: 'comment', label: '备注', minWidth: 220 },
+	],
+	headFields: [
+		{ prop: 'number', label: '编号', required: true },
+		{ prop: 'name', label: '名称', required: true },
+		{ prop: 'controlStrategy', label: '控制策略' },
+		{ prop: 'createOrgId', label: '创建组织', type: 'number' },
+		{ prop: 'useOrgId', label: '使用组织', type: 'number' },
+		{ prop: 'status', label: '状态' },
+		{ prop: 'enableStatus', label: '启用' },
+		{ prop: 'comment', label: '备注', type: 'textarea' },
+	],
+	detailColumns: [
+		{ prop: 'seq', label: '序号', type: 'number' },
+		{ prop: 'documentNumber', label: '文档编号' },
+		{ prop: 'documentName', label: '文档名称' },
+		{ prop: 'attachment', label: '附件' },
+	],
+	initialMaster: { number: '', name: '', controlStrategy: '', createOrgId: undefined, useOrgId: undefined, status: '', enableStatus: '', comment: '' },
+	createEmptyItem: () => ({ seq: undefined, documentNumber: '', documentName: '', attachment: '' }),
+};
+
+export const inspectionStandardConfig: QualityAggregatePageConfig = {
+	subtitle: 'S0 / 质量建模 / 检验标准',
+	entityLabel: '检验标准',
+	queryPlaceholder: '编号/名称',
+	api: s0InspectionStandardsApi,
+	listColumns: [
+		{ prop: 'number', label: '编号', width: 180 },
+		{ prop: 'name', label: '名称', minWidth: 220 },
+		{ prop: 'comment', label: '备注', minWidth: 220 },
+	],
+	headFields: [
+		{ prop: 'number', label: '编号', required: true },
+		{ prop: 'name', label: '名称', required: true },
+		{ prop: 'controlStrategy', label: '控制策略' },
+		{ prop: 'createOrgId', label: '创建组织', type: 'number' },
+		{ prop: 'useOrgId', label: '使用组织', type: 'number' },
+		{ prop: 'status', label: '状态' },
+		{ prop: 'enableStatus', label: '启用' },
+		{ prop: 'comment', label: '备注', type: 'textarea' },
+	],
+	detailColumns: [
+		{ prop: 'seq', label: '序号', type: 'number' },
+		{ prop: 'checkItems', label: '检验项目' },
+		{ prop: 'checkContent', label: '检验内容' },
+		{ prop: 'normType', label: '比较符' },
+		{ prop: 'specValue', label: '标准' },
+		{ prop: 'topValue', label: '上限', type: 'number' },
+		{ prop: 'downValue', label: '下限', type: 'number' },
+		{ prop: 'checkBasisId', label: '检验依据', type: 'number' },
+	],
+	initialMaster: { number: '', name: '', controlStrategy: '', createOrgId: undefined, useOrgId: undefined, status: '', enableStatus: '', comment: '' },
+	createEmptyItem: () => ({ seq: undefined, checkItems: '', checkContent: '', normType: '', specValue: '', topValue: undefined, downValue: undefined, checkBasisId: undefined, checkMethodId: undefined, checkFrequencyId: undefined, checkInstructId: undefined, unit: '', keyQuality: undefined }),
+};
+
+export const inspectionPlanConfig: QualityAggregatePageConfig = {
+	subtitle: 'S0 / 质量建模 / 检验方案',
+	entityLabel: '检验方案',
+	queryPlaceholder: '编号/名称',
+	api: s0InspectionPlansApi,
+	listColumns: [
+		{ prop: 'number', label: '编号', width: 180 },
+		{ prop: 'name', label: '名称', minWidth: 220 },
+		{ prop: 'bizTypeId', label: '业务类型', width: 160 },
+		{ prop: 'comment', label: '备注', minWidth: 220 },
+	],
+	headFields: [
+		{ prop: 'number', label: '编号', required: true },
+		{ prop: 'name', label: '名称', required: true },
+		{ prop: 'bizTypeId', label: '检验业务类型' },
+		{ prop: 'controlStrategy', label: '控制策略' },
+		{ prop: 'createOrgId', label: '创建组织', type: 'number' },
+		{ prop: 'useOrgId', label: '使用组织', type: 'number' },
+		{ prop: 'status', label: '状态' },
+		{ prop: 'enableStatus', label: '启用' },
+		{ prop: 'comment', label: '备注', type: 'textarea' },
+	],
+	detailColumns: [
+		{ prop: 'seq', label: '序号', type: 'number' },
+		{ prop: 'setupType', label: '设置类型' },
+		{ prop: 'materialCode', label: '物料编码' },
+		{ prop: 'materialName', label: '物料名称' },
+		{ prop: 'supplierId', label: '供应商' },
+		{ prop: 'samplingSchemeId', label: '抽样方案', type: 'number' },
+		{ prop: 'inspectionStandardId', label: '检验标准', type: 'number' },
+		{ prop: 'inspectionFrequencyId', label: '检验频率', type: 'number' },
+	],
+	initialMaster: { number: '', name: '', bizTypeId: '', controlStrategy: '', createOrgId: undefined, useOrgId: undefined, status: '', enableStatus: '', comment: '' },
+	createEmptyItem: () => ({ seq: undefined, setupType: '', materialCode: '', materialName: '', materialTypeId: undefined, supplierId: '', samplingSchemeId: undefined, inspectionStandardId: undefined, inspectOrgId: undefined, inspectUserId: undefined, qRouteId: undefined, operationNo: '', operationId: undefined, inspectionFrequencyId: undefined, processSeq: '', inspectionType: undefined }),
+};
+
+export const rawInspectionSpecConfig: QualityAggregatePageConfig = {
+	subtitle: 'S0 / 质量建模 / 原材料检验规范',
+	entityLabel: '原材料检验规范',
+	queryPlaceholder: '文件编号/原材料名称/物料编码',
+	api: s0RawInspectionSpecsApi,
+	listColumns: [
+		{ prop: 'fileNumber', label: '文件编号', width: 180 },
+		{ prop: 'rawMaterialName', label: '原材料名称', minWidth: 220 },
+		{ prop: 'materialCode', label: '物料编码', width: 180 },
+		{ prop: 'versionNo', label: '版本', width: 100 },
+	],
+	headFields: [
+		{ prop: 'fileNumber', label: '文件编号', required: true },
+		{ prop: 'versionNo', label: '版本' },
+		{ prop: 'drawingNo', label: '图号' },
+		{ prop: 'rawMaterialName', label: '原材料名称' },
+		{ prop: 'materialCode', label: '物料编码' },
+		{ prop: 'effectiveDate', label: '生效日期' },
+		{ prop: 'drawingVersion', label: '图纸版本' },
+		{ prop: 'materialGrade', label: '材质/材料牌号' },
+		{ prop: 'cavityOrMold', label: '穴/模' },
+		{ prop: 'attachment', label: '附件', type: 'textarea' },
+		{ prop: 'fileName', label: '附件文件名' },
+		{ prop: 'title', label: '附件标题' },
+	],
+	detailColumns: [
+		{ prop: 'seq', label: '序号', type: 'number' },
+		{ prop: 'inspectionItem', label: '检验项目' },
+		{ prop: 'inspectionStandard', label: '检验标准' },
+		{ prop: 'inspectionMethod', label: '检验工具/方法' },
+		{ prop: 'samplingScheme', label: '抽样方案' },
+		{ prop: 'upperLimit', label: '上限' },
+		{ prop: 'lowerLimit', label: '下限' },
+	],
+	initialMaster: { fileNumber: '', versionNo: '', drawingNo: '', rawMaterialName: '', materialCode: '', effectiveDate: '', drawingVersion: '', materialGrade: '', cavityOrMold: '', attachment: '', fileName: '', title: '' },
+	createEmptyItem: () => ({ seq: undefined, inspectionItem: '', inspectionStandard: '', inspectionMethod: '', imageCategory: '', samplingScheme: '', remark: '', attachment: '', upperLimit: '', lowerLimit: '' }),
+};
+
+export const processInspectionSpecConfig: QualityAggregatePageConfig = {
+	subtitle: 'S0 / 质量建模 / 过程检验规范',
+	entityLabel: '过程检验规范',
+	queryPlaceholder: '文件编号/适用型号/物料编码',
+	api: s0ProcessInspectionSpecsApi,
+	listColumns: [
+		{ prop: 'fileNumber', label: '文件编号', width: 180 },
+		{ prop: 'applicableModel', label: '适用型号', minWidth: 220 },
+		{ prop: 'materialCode', label: '物料编码', width: 180 },
+		{ prop: 'versionNo', label: '版本', width: 100 },
+	],
+	headFields: [
+		{ prop: 'applicableModel', label: '适用型号' },
+		{ prop: 'fileNumber', label: '文件编号', required: true },
+		{ prop: 'versionNo', label: '版本' },
+		{ prop: 'effectiveDate', label: '生效日期' },
+		{ prop: 'attachment', label: '附件', type: 'textarea' },
+		{ prop: 'materialCode', label: '物料编码' },
+		{ prop: 'attachment2', label: '附件2', type: 'textarea' },
+		{ prop: 'version', label: '版本号', type: 'number' },
+	],
+	detailColumns: [
+		{ prop: 'operationCode', label: '工序代号' },
+		{ prop: 'operationName', label: '工序名称' },
+		{ prop: 'inspectionItem', label: '检验项目' },
+		{ prop: 'inspectionMethod', label: '检验方法' },
+		{ prop: 'inspectionSpec', label: '检验规格' },
+		{ prop: 'inspectionFrequency', label: '检验频次' },
+		{ prop: 'upperLimit', label: '上限' },
+		{ prop: 'lowerLimit', label: '下限' },
+	],
+	initialMaster: { applicableModel: '', fileNumber: '', versionNo: '', effectiveDate: '', attachment: '', materialCode: '', attachment2: '', version: undefined },
+	createEmptyItem: () => ({ operationCode: '', operationName: '', inspectionItem: '', inspectionMethod: '', inspectionSpec: '', imageCategory: '', inspectionFrequency: '', technicalStandard: '', peelingForce: undefined, upperLimit: '', lowerLimit: '' }),
+};

+ 147 - 0
doc/plan/S0/20240409/S0列表迁移修正方案提示词.md → doc/plan/S0/20240410/S0列表迁移修正方案提示词.md

@@ -442,6 +442,18 @@
 | 12 | 仓储 | 物料状态任务指派 | 主表 `wms_rwzp` + 页面含头信息和子表明细区 + 流程动作 | B | 按聚合头表处理;保持任务指派单据语义,头信息和物料明细整体提交 | 待确认 | 虽然表单属性指向单表,但页面明显是主子结构和流程单据,不宜降级成普通单表列表 |
 | 13 | 仓储 | 物料职责维护 | 主表 `EmpWorkDutyMaster` + 左联 `EmployeeMaster` / `LocationMaster` / `LineMaster` 补展示 + 区间物料编码职责维护 | D | 保留物料职责模块边界,但整体向 `EmpWorkDutyMaster` 语义收敛;查询层补雇员/库位/产线/职责展示字段 | 待确认 | 本质是职责分配维护表,不是查询模型;多表关联主要用于展示 |
 | 14 | 仓储 | 零件包装规格列表 | 主表 `ItemPackMaster` + 左联 `ItemMaster` 补物料名称型号 | D | 保留零件包装规格模块边界,但整体向 `ItemPackMaster` 语义收敛;查询层补物料展示字段 | 待确认 | 本质是包装规格主数据,不是查询模型;多表关联主要用于展示 |
+| 15 | 质量 | 原材料检验规范 | 主表 `qms_jygf` + 页面含附件与子表明细 | B | 按聚合头表处理;主表向 `qms_jygf` 语义收敛,检验明细纳入整体保存 | 待确认 | 页面存在附件导入和子表区,不应按普通单表页处理 |
+| 16 | 质量 | 原材料白名单 | 单表 `qms_lymjbmd` + 主键 `id` + 表单维护 | D | 保留原材料白名单模块边界,但整体向 `qms_lymjbmd` 语义收敛 | 待确认 | 本质是免检白名单主数据,不是查询模型 |
+| 17 | 质量 | 抽样方案 | 单表 `qms_sampscheme` + 主键 `id` + 表单维护 | D | 保留抽样方案模块边界,但整体向 `qms_sampscheme` 语义收敛 | 待确认 | 本质是抽样方案主数据,不是查询模型 |
+| 18 | 质量 | 检验仪器 | 单表 `qms_inspectioninstru` + 主键 `id` + 表单维护 | D | 保留检验仪器模块边界,但整体向 `qms_inspectioninstru` 语义收敛 | 待确认 | 本质是检验仪器主数据,不是查询模型 |
+| 19 | 质量 | 检验依据 | 主表 `qms_inspectioncrit` + 页面含技术文档明细子表 | B | 按聚合头表处理;主表向 `qms_inspectioncrit` 语义收敛,技术文档纳入整体保存 | 待确认 | 页面存在“技术文档”子表,不应按单表页处理 |
+| 20 | 质量 | 检验方案 | 主表 `qms_inspectpro` + 页面含方案设置子表 | B | 按聚合头表处理;主表向 `qms_inspectpro` 语义收敛,方案设置明细纳入整体保存 | 待确认 | 页面存在“检验方案设置”子表,不应按单表页处理 |
+| 21 | 质量 | 检验方法 | 单表 `qms_inspection_method` + 主键 `id` + 表单维护 | D | 保留检验方法模块边界,但整体向 `qms_inspection_method` 语义收敛 | 待确认 | 本质是检验方法主数据,不是查询模型 |
+| 22 | 质量 | 检验标准 | 主表 `qms_inspectionstd` + 页面含检验项目子表 | B | 按聚合头表处理;主表向 `qms_inspectionstd` 语义收敛,检验项目明细纳入整体保存 | 待确认 | 页面存在“检验项目”子表,不应按单表页处理 |
+| 23 | 质量 | 检验项目 | 单表 `qms_inspectionitems` + 主键 `id` + 表单维护 | D | 保留检验项目模块边界,但整体向 `qms_inspectionitems` 语义收敛 | 待确认 | 本质是检验项目主数据,不是查询模型 |
+| 24 | 质量 | 检验频率 | 单表 `qms_inspectionfreq` + 主键 `id` + 表单维护 | D | 保留检验频率模块边界,但整体向 `qms_inspectionfreq` 语义收敛 | 待确认 | 本质是检验频率主数据,不是查询模型 |
+| 25 | 质量 | 过程检验规范 | 主表 `qms_gcjygf` + 页面含附件与子表明细 | B | 按聚合头表处理;主表向 `qms_gcjygf` 语义收敛,检验明细纳入整体保存 | 待确认 | 页面存在附件导入和子表区,不应按普通单表页处理 |
+| 26 | 质量 | 质量字典 | QMS 左侧字典树导航页 + 多类字典入口聚合 | E | 不新建业务实体表;作为质量字典导航页复刻,复用下层字典资源 | 待确认 | 这是导航/入口页,不是单一业务表单 |
 
 ### 11.2 已登记明细
 
@@ -2621,3 +2633,138 @@ WHERE Domain = ?;
 - 我会把结果继续记录到这份文档里,作为持续迁移台账。
 
 如果后续你直接发 SQL、字段截图文字版、页面按钮、详情保存逻辑中的任意一种,我都可以开始判定,不要求你一次把信息补全。
+
+---
+
+## 14. 20240410 质量建模已登记明细
+
+### 14.1 原材料检验规范
+
+- 来源模块:质量
+- 原表名:`qms_jygf`
+- 主键:`id`
+- 初判分类:B `聚合头表`
+- 判断依据:页面含附件导入和子表区,明显不是单表 CRUD
+- 建议落地:主表按 `qms_jygf` 复刻,附件和检验明细纳入整体保存
+- 源 SQL:`select * from qms_jygf`
+- 关联修改方案:`质量方案/原材料检验规范方案.md`
+
+### 14.2 原材料白名单
+
+- 来源模块:质量
+- 原表名:`qms_lymjbmd`
+- 主键:`id`
+- 初判分类:D `实体查询页`
+- 判断依据:表单属性截图显示单表维护,无子表区
+- 建议落地:整体向 `qms_lymjbmd` 语义收敛
+- 源 SQL:`select * from qms_lymjbmd`
+- 关联修改方案:`质量方案/原材料白名单方案.md`
+
+### 14.3 抽样方案
+
+- 来源模块:质量
+- 原表名:`qms_sampscheme`
+- 主键:`id`
+- 初判分类:D `实体查询页`
+- 判断依据:表单属性截图显示单表维护,无子表区
+- 建议落地:整体向 `qms_sampscheme` 语义收敛
+- 源 SQL:`select * from qms_sampscheme`
+- 关联修改方案:`质量方案/抽样方案方案.md`
+
+### 14.4 检验仪器
+
+- 来源模块:质量
+- 原表名:`qms_inspectioninstru`
+- 主键:`id`
+- 初判分类:D `实体查询页`
+- 判断依据:表单属性截图显示单表维护,无子表区
+- 建议落地:整体向 `qms_inspectioninstru` 语义收敛
+- 源 SQL:`select * from qms_inspectioninstru`
+- 关联修改方案:`质量方案/检验仪器方案.md`
+
+### 14.5 检验依据
+
+- 来源模块:质量
+- 原表名:`qms_inspectioncrit`
+- 主键:`id`
+- 初判分类:B `聚合头表`
+- 判断依据:页面存在“技术文档”明细区和附件列
+- 建议落地:主表按 `qms_inspectioncrit` 复刻,技术文档作为子表整体保存
+- 源 SQL:`select * from qms_inspectioncrit`
+- 关联修改方案:`质量方案/检验依据方案.md`
+
+### 14.6 检验方案
+
+- 来源模块:质量
+- 原表名:`qms_inspectpro`
+- 主键:`id`
+- 初判分类:B `聚合头表`
+- 判断依据:页面存在“检验方案设置”子表
+- 建议落地:主表按 `qms_inspectpro` 复刻,方案设置明细纳入整体保存
+- 源 SQL:当前 SQL 文件为空,先按页面结构识别
+- 关联修改方案:`质量方案/检验方案方案.md`
+
+### 14.7 检验方法
+
+- 来源模块:质量
+- 原表名:`qms_inspection_method`
+- 主键:`id`
+- 初判分类:D `实体查询页`
+- 判断依据:表单属性截图显示单表维护,无子表区
+- 建议落地:整体向 `qms_inspection_method` 语义收敛
+- 源 SQL:`select * from qms_inspection_method`
+- 关联修改方案:`质量方案/检验方法方案.md`
+
+### 14.8 检验标准
+
+- 来源模块:质量
+- 原表名:`qms_inspectionstd`
+- 主键:`id`
+- 初判分类:B `聚合头表`
+- 判断依据:页面存在“检验项目”子表区
+- 建议落地:主表按 `qms_inspectionstd` 复刻,检验项目明细纳入整体保存
+- 源 SQL:`select * from qms_inspectionstd`
+- 关联修改方案:`质量方案/检验标准方案.md`
+
+### 14.9 检验项目
+
+- 来源模块:质量
+- 原表名:`qms_inspectionitems`
+- 主键:`id`
+- 初判分类:D `实体查询页`
+- 判断依据:表单属性截图显示单表维护,无子表区
+- 建议落地:整体向 `qms_inspectionitems` 语义收敛
+- 源 SQL:`select * from qms_inspectionitems`
+- 关联修改方案:`质量方案/检验项目方案.md`
+
+### 14.10 检验频率
+
+- 来源模块:质量
+- 原表名:`qms_inspectionfreq`
+- 主键:`id`
+- 初判分类:D `实体查询页`
+- 判断依据:页面结构为标准基础信息表单,无子表区
+- 建议落地:整体向 `qms_inspectionfreq` 语义收敛
+- 源 SQL:`select * from qms_inspectionfreq`
+- 关联修改方案:`质量方案/检验频率方案.md`
+
+### 14.11 过程检验规范
+
+- 来源模块:质量
+- 原表名:`qms_gcjygf`
+- 主键:`id`
+- 初判分类:B `聚合头表`
+- 判断依据:页面含附件导入和子表区,明显不是单表 CRUD
+- 建议落地:主表按 `qms_gcjygf` 复刻,附件和检验明细纳入整体保存
+- 源 SQL:`select * from qms_gcjygf`
+- 关联修改方案:`质量方案/过程检验规范方案.md`
+
+### 14.12 质量字典
+
+- 来源模块:质量
+- 页面名称:`质量基础`
+- 初判分类:E `查询模型`
+- 判断依据:这是左侧字典树导航页,不是单一业务表单
+- 建议落地:不新建业务实体表;复刻质量字典导航树,并将下层字典项接到统一字典资源
+- 字典范围:`单据类型`、`业务类型`、`客诉`、`检验优先级`、`产品不合格联络单`、`8D`、`NCR`、`项目管理`、`生化`、`CAPA`、`生产记录清单`、`检验状态`、`AQL接受水平`、`AQL值`
+- 关联修改方案:`质量方案/质量字典方案.md`

BIN
doc/plan/S0/20240410/质量建模/原材料检验规范1.png


BIN
doc/plan/S0/20240410/质量建模/原材料检验规范2.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/原材料检验规范3.sql

@@ -0,0 +1 @@
+select * from qms_jygf

BIN
doc/plan/S0/20240410/质量建模/原材料检验规范4.png


BIN
doc/plan/S0/20240410/质量建模/原材料检验规范5.png


BIN
doc/plan/S0/20240410/质量建模/原材料白名单1.png


BIN
doc/plan/S0/20240410/质量建模/原材料白名单2.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/原材料白名单3.sql

@@ -0,0 +1 @@
+select * from qms_lymjbmd

BIN
doc/plan/S0/20240410/质量建模/原材料白名单4.png


BIN
doc/plan/S0/20240410/质量建模/原材料白名单5.png


BIN
doc/plan/S0/20240410/质量建模/抽样方案1.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/抽样方案2.sql

@@ -0,0 +1 @@
+select * from [qms_sampscheme]image.png

BIN
doc/plan/S0/20240410/质量建模/抽样方案3.png


BIN
doc/plan/S0/20240410/质量建模/抽样方案4.png


BIN
doc/plan/S0/20240410/质量建模/抽样方案5.png


BIN
doc/plan/S0/20240410/质量建模/检验仪器1.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/检验仪器2.sql

@@ -0,0 +1 @@
+select * from qms_inspectioninstruimage.png

BIN
doc/plan/S0/20240410/质量建模/检验仪器3.png


BIN
doc/plan/S0/20240410/质量建模/检验仪器4.png


BIN
doc/plan/S0/20240410/质量建模/检验仪器5.png


BIN
doc/plan/S0/20240410/质量建模/检验依据1.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/检验依据2.sql

@@ -0,0 +1 @@
+select * from [qms_inspectioncrit]image.png

BIN
doc/plan/S0/20240410/质量建模/检验依据3.png


BIN
doc/plan/S0/20240410/质量建模/检验依据4.png


BIN
doc/plan/S0/20240410/质量建模/检验依据5.png


BIN
doc/plan/S0/20240410/质量建模/检验方案1.png


+ 0 - 0
doc/plan/S0/20240410/质量建模/检验方案2.sql


BIN
doc/plan/S0/20240410/质量建模/检验方案3.png


BIN
doc/plan/S0/20240410/质量建模/检验方案4.png


BIN
doc/plan/S0/20240410/质量建模/检验方案5.png


BIN
doc/plan/S0/20240410/质量建模/检验方法1.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/检验方法2.sql

@@ -0,0 +1 @@
+select * from [qms_inspection_method]

BIN
doc/plan/S0/20240410/质量建模/检验方法3.png


BIN
doc/plan/S0/20240410/质量建模/检验方法4.png


BIN
doc/plan/S0/20240410/质量建模/检验方法5.png


BIN
doc/plan/S0/20240410/质量建模/检验标准1.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/检验标准2.sql

@@ -0,0 +1 @@
+select * from [qms_inspectionstd]

BIN
doc/plan/S0/20240410/质量建模/检验标准3.png


BIN
doc/plan/S0/20240410/质量建模/检验标准4.png


BIN
doc/plan/S0/20240410/质量建模/检验标准5.png


BIN
doc/plan/S0/20240410/质量建模/检验项目1.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/检验项目2.sql

@@ -0,0 +1 @@
+select * from [qms_inspectionitems]

BIN
doc/plan/S0/20240410/质量建模/检验项目3.png


BIN
doc/plan/S0/20240410/质量建模/检验项目4.png


BIN
doc/plan/S0/20240410/质量建模/检验项目5.png


BIN
doc/plan/S0/20240410/质量建模/检验频率1.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/检验频率2.sql

@@ -0,0 +1 @@
+select * from [qms_inspectionfreq]image.png

BIN
doc/plan/S0/20240410/质量建模/检验频率3.png


BIN
doc/plan/S0/20240410/质量建模/检验频率4.png


BIN
doc/plan/S0/20240410/质量建模/检验频率5.png


BIN
doc/plan/S0/20240410/质量建模/质量建模(字典).png


BIN
doc/plan/S0/20240410/质量建模/过程检验规范1.png


BIN
doc/plan/S0/20240410/质量建模/过程检验规范2.png


+ 1 - 0
doc/plan/S0/20240410/质量建模/过程检验规范3.sql

@@ -0,0 +1 @@
+select * from qms_gcjygf

BIN
doc/plan/S0/20240410/质量建模/过程检验规范4.png


BIN
doc/plan/S0/20240410/质量建模/过程检验规范5.png


+ 37 - 0
doc/plan/S0/20240410/质量方案/原材料检验规范方案.md

@@ -0,0 +1,37 @@
+# 原材料检验规范方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `原材料检验规范`。
+
+## 1. 结论
+
+这条按 `qms_jygf` 聚合头表处理,分类为 `B 聚合头表`。
+
+- 主表向 `qms_jygf` 语义收敛
+- 附件与检验明细纳入整体保存
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_jygf
+```
+
+### 2.2 页面特征
+
+- 数据表:`qms_jygf - 检验规范`
+- 主键:`id`
+- 页面包含附件导入区
+- 页面包含子表区
+
+## 3. 落地要求
+
+- 保持文件编号、版本、图号、原材料名称、物料编码、生效日期、图纸版本、材质/材料牌号、穴/模、附件等头字段语义
+- 子表作为检验明细整体维护
+- 不降级成普通单表页
+
+## 4. 待确认项
+
+- 子表真实物理表名与主外键
+- 附件存储方式

+ 34 - 0
doc/plan/S0/20240410/质量方案/原材料白名单方案.md

@@ -0,0 +1,34 @@
+# 原材料白名单方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `原材料白名单`。
+
+## 1. 结论
+
+这条按 `qms_lymjbmd` 单表主数据处理,分类为 `D 实体查询页`。
+
+- 保留原材料白名单模块边界
+- 整体向 `qms_lymjbmd` 语义收敛
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_lymjbmd
+```
+
+### 2.2 页面特征
+
+- 数据表:`qms_lymjbmd`
+- 主键:`id`
+- 页面是标准列表 + 表单维护
+
+## 3. 落地要求
+
+- 不新建“白名单列表专用实体表”
+- 保持白名单对象的源表字段语义
+
+## 4. 待确认项
+
+- 字段清单和值域

+ 30 - 0
doc/plan/S0/20240410/质量方案/抽样方案方案.md

@@ -0,0 +1,30 @@
+# 抽样方案方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `抽样方案`。
+
+## 1. 结论
+
+这条按 `qms_sampscheme` 单表主数据处理,分类为 `D 实体查询页`。
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_sampscheme
+```
+
+### 2.2 页面特征
+
+- 数据表:`qms_sampscheme - 抽样方案`
+- 主键:`id`
+- 页面无子表区
+
+## 3. 落地要求
+
+- 整体向 `qms_sampscheme` 语义收敛
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 4. 待确认项
+
+- 抽样方案字段清单

+ 30 - 0
doc/plan/S0/20240410/质量方案/检验仪器方案.md

@@ -0,0 +1,30 @@
+# 检验仪器方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `检验仪器`。
+
+## 1. 结论
+
+这条按 `qms_inspectioninstru` 单表主数据处理,分类为 `D 实体查询页`。
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_inspectioninstru
+```
+
+### 2.2 页面特征
+
+- 数据表:`qms_inspectioninstru`
+- 主键:`id`
+- 页面无子表区
+
+## 3. 落地要求
+
+- 整体向 `qms_inspectioninstru` 语义收敛
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 4. 待确认项
+
+- 仪器编号、名称、校准周期等字段清单

+ 35 - 0
doc/plan/S0/20240410/质量方案/检验依据方案.md

@@ -0,0 +1,35 @@
+# 检验依据方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `检验依据`。
+
+## 1. 结论
+
+这条按 `qms_inspectioncrit` 聚合头表处理,分类为 `B 聚合头表`。
+
+- 主表向 `qms_inspectioncrit` 语义收敛
+- 技术文档纳入整体保存
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_inspectioncrit
+```
+
+### 2.2 页面特征
+
+- 主表:`qms_inspectioncrit`
+- 页面包含“技术文档”子表
+- 子表字段包含文档编号、文档名称、附件
+
+## 3. 落地要求
+
+- 基本信息按名称、分类、创建组织、备注复刻
+- 技术文档作为子表整体保存
+
+## 4. 待确认项
+
+- 子表真实物理表名
+- 附件存储方式

+ 34 - 0
doc/plan/S0/20240410/质量方案/检验方案方案.md

@@ -0,0 +1,34 @@
+# 检验方案方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `检验方案`。
+
+## 1. 结论
+
+这条按 `qms_inspectpro` 聚合头表处理,分类为 `B 聚合头表`。
+
+- 主表向 `qms_inspectpro` 语义收敛
+- 方案设置明细纳入整体保存
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+当前 SQL 文件为空,待补原始 SQL。
+
+### 2.2 页面特征
+
+- 主表:`qms_inspectpro`
+- 主键:`id`
+- 页面存在“检验方案设置”子表
+- 子表列包含物料编码、物料名称、供应商、抽样方案
+
+## 3. 落地要求
+
+- 头字段按编号、名称、分类、检验业务类型、备注复刻
+- 方案设置子表整体保存
+
+## 4. 待确认项
+
+- 源 SQL
+- 子表真实物理表名

+ 30 - 0
doc/plan/S0/20240410/质量方案/检验方法方案.md

@@ -0,0 +1,30 @@
+# 检验方法方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `检验方法`。
+
+## 1. 结论
+
+这条按 `qms_inspection_method` 单表主数据处理,分类为 `D 实体查询页`。
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_inspection_method
+```
+
+### 2.2 页面特征
+
+- 数据表:`qms_inspection_method`
+- 主键:`id`
+- 页面无子表区
+
+## 3. 落地要求
+
+- 整体向 `qms_inspection_method` 语义收敛
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 4. 待确认项
+
+- 方法分类和值域

+ 35 - 0
doc/plan/S0/20240410/质量方案/检验标准方案.md

@@ -0,0 +1,35 @@
+# 检验标准方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `检验标准`。
+
+## 1. 结论
+
+这条按 `qms_inspectionstd` 聚合头表处理,分类为 `B 聚合头表`。
+
+- 主表向 `qms_inspectionstd` 语义收敛
+- 检验项目明细纳入整体保存
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_inspectionstd
+```
+
+### 2.2 页面特征
+
+- 主表:`qms_inspectionstd`
+- 页面包含“检验项目”子表
+- 子表列包含检验项目、检验内容、比较符、标准、上限、下限、检验依据
+
+## 3. 落地要求
+
+- 基本信息按编号、名称、分类、备注复刻
+- 检验项目子表整体保存
+
+## 4. 待确认项
+
+- 子表真实物理表名
+- 检验依据字段是否引用 `qms_inspectioncrit`

+ 30 - 0
doc/plan/S0/20240410/质量方案/检验项目方案.md

@@ -0,0 +1,30 @@
+# 检验项目方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `检验项目`。
+
+## 1. 结论
+
+这条按 `qms_inspectionitems` 单表主数据处理,分类为 `D 实体查询页`。
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_inspectionitems
+```
+
+### 2.2 页面特征
+
+- 数据表:`qms_inspectionitems`
+- 主键:`id`
+- 页面无子表区
+
+## 3. 落地要求
+
+- 整体向 `qms_inspectionitems` 语义收敛
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 4. 待确认项
+
+- 检验项目字段与单位、比较符、默认标准的关系

+ 30 - 0
doc/plan/S0/20240410/质量方案/检验频率方案.md

@@ -0,0 +1,30 @@
+# 检验频率方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `检验频率`。
+
+## 1. 结论
+
+这条按 `qms_inspectionfreq` 单表主数据处理,分类为 `D 实体查询页`。
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_inspectionfreq
+```
+
+### 2.2 页面特征
+
+- 数据表:`qms_inspectionfreq`
+- 主键:`id`
+- 页面无子表区
+
+## 3. 落地要求
+
+- 整体向 `qms_inspectionfreq` 语义收敛
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 4. 待确认项
+
+- 名称、分类、创建组织、备注等字段清单

+ 47 - 0
doc/plan/S0/20240410/质量方案/质量字典方案.md

@@ -0,0 +1,47 @@
+# 质量字典方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `质量字典` 导航页。
+
+## 1. 结论
+
+这条按质量字典导航页处理,分类为 `E 查询模型`。
+
+- 不新建“质量字典”业务实体表
+- 复刻左侧树形导航和右侧空白态
+- 下层字典项接入统一字典资源或既有字典表
+
+## 2. 源平台信息
+
+### 2.1 页面特征
+
+- 页面标题:`质量基础`
+- 左侧为 QMS 字典树
+- 右侧为空白占位,提示“点击左边字典项进行操作”
+
+### 2.2 当前识别到的字典项
+
+- 单据类型
+- 业务类型
+- 客诉
+- 检验优先级
+- 产品不合格联络单
+- 8D
+- NCR
+- 项目管理
+- 生化
+- CAPA
+- 生产记录清单
+- 检验状态
+- AQL接受水平
+- AQL值
+
+## 3. 落地要求
+
+- 保持“左树右内容”结构
+- 左树节点继续使用中文业务名称
+- 点击节点后跳到对应字典维护页或在右侧承载对应维护内容
+
+## 4. 待确认项
+
+- 左树数据源是否已有统一字典表
+- 每个字典项是否复用统一页面还是拆独立页

+ 35 - 0
doc/plan/S0/20240410/质量方案/过程检验规范方案.md

@@ -0,0 +1,35 @@
+# 过程检验规范方案
+
+> 适用于当前开发阶段“以复刻原平台为优先目标”的场景。本方案覆盖质量建模中的 `过程检验规范`。
+
+## 1. 结论
+
+这条按 `qms_gcjygf` 聚合头表处理,分类为 `B 聚合头表`。
+
+- 主表向 `qms_gcjygf` 语义收敛
+- 附件与检验明细纳入整体保存
+- 页面中文化,接旧路由、旧菜单、旧权限
+
+## 2. 源平台信息
+
+### 2.1 源 SQL
+
+```sql
+select * from qms_gcjygf
+```
+
+### 2.2 页面特征
+
+- 数据表:`qms_gcjygf`
+- 主键:`id`
+- 页面包含附件区和子表区
+
+## 3. 落地要求
+
+- 保持文件编号、版本、生效日期、适用型号、物料编码、附件等头字段语义
+- 子表作为检验明细整体维护
+
+## 4. 待确认项
+
+- 子表真实物理表名
+- 附件2字段的业务含义

+ 24 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Manufacturing/AdoS0ProductStructureRulesTests.cs

@@ -29,4 +29,28 @@ public class AdoS0ProductStructureRulesTests
     {
         Assert.Null(AdoS0ProductStructureRules.ValidateUpsert(BaseDto()));
     }
+
+    [Fact]
+    public void ValidateUpsert_NegativeQty_ReturnsError()
+    {
+        var dto = BaseDto();
+        dto.Qty = -1m;
+        Assert.Equal("标准用量不能为负", AdoS0ProductStructureRules.ValidateUpsert(dto));
+    }
+
+    [Fact]
+    public void ValidateUpsert_NegativeScrap_ReturnsError()
+    {
+        var dto = BaseDto();
+        dto.Scrap = -0.01m;
+        Assert.Equal("损耗率不能为负", AdoS0ProductStructureRules.ValidateUpsert(dto));
+    }
+
+    [Fact]
+    public void ValidateUpsert_NegativeQtyConsumed_ReturnsError()
+    {
+        var dto = BaseDto();
+        dto.QtyConsumed = -0.5m;
+        Assert.Equal("固定损耗量不能为负", AdoS0ProductStructureRules.ValidateUpsert(dto));
+    }
 }

+ 57 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Quality/AdoS0QualityDtosValidationTests.cs

@@ -0,0 +1,57 @@
+using System.ComponentModel.DataAnnotations;
+using Admin.NET.Plugin.AiDOP.Dto.S0.Quality;
+using Xunit;
+
+namespace Admin.NET.Plugin.AiDOP.Tests.S0.Quality;
+
+public class AdoS0QualityDtosValidationTests
+{
+    private static IReadOnlyList<ValidationResult> Validate(object model)
+    {
+        var context = new ValidationContext(model);
+        var results = new List<ValidationResult>();
+        Validator.TryValidateObject(model, context, results, validateAllProperties: true);
+        return results;
+    }
+
+    [Fact]
+    public void RawWhitelist_WhenSupplierFieldsMissing_ReturnsRequiredErrors()
+    {
+        var dto = new AdoS0QmsRawWhitelistUpsertDto
+        {
+            SupplierCode = "",
+            SupplierName = ""
+        };
+
+        var results = Validate(dto);
+        Assert.Contains(results, r => r.ErrorMessage == "供应商编码不能为空");
+        Assert.Contains(results, r => r.ErrorMessage == "供应商名称不能为空");
+    }
+
+    [Theory]
+    [InlineData(typeof(AdoS0QmsSamplingSchemeUpsertDto))]
+    [InlineData(typeof(AdoS0QmsInspectionInstrumentUpsertDto))]
+    [InlineData(typeof(AdoS0QmsInspectionMethodUpsertDto))]
+    [InlineData(typeof(AdoS0QmsInspectionItemUpsertDto))]
+    [InlineData(typeof(AdoS0QmsInspectionFrequencyUpsertDto))]
+    [InlineData(typeof(AdoS0QmsInspectionBasisUpsertDto))]
+    [InlineData(typeof(AdoS0QmsInspectionStandardUpsertDto))]
+    [InlineData(typeof(AdoS0QmsInspectionPlanUpsertDto))]
+    public void NumberNameDtos_WhenDefaultsUsed_ReturnRequiredErrors(Type dtoType)
+    {
+        var dto = Activator.CreateInstance(dtoType)!;
+        var results = Validate(dto);
+        Assert.Contains(results, r => r.ErrorMessage == "编号不能为空");
+        Assert.Contains(results, r => r.ErrorMessage == "名称不能为空");
+    }
+
+    [Theory]
+    [InlineData(typeof(AdoS0QmsRawInspectionSpecUpsertDto))]
+    [InlineData(typeof(AdoS0QmsProcessInspectionSpecUpsertDto))]
+    public void FileNumberDtos_WhenDefaultsUsed_ReturnRequiredError(Type dtoType)
+    {
+        var dto = Activator.CreateInstance(dtoType)!;
+        var results = Validate(dto);
+        Assert.Contains(results, r => r.ErrorMessage == "文件编号不能为空");
+    }
+}

+ 732 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Quality/AdoS0QualityAggregateControllers.cs

@@ -0,0 +1,732 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Quality;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Quality;
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+using static Admin.NET.Plugin.AiDOP.Controllers.S0.Quality.AdoS0QmsControllerHelpers;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Quality;
+
+[ApiController]
+[Route("api/s0/quality/inspection-bases")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsInspectionBasesController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsInspectionBasis> _rep;
+    private readonly SqlSugarRepository<AdoS0QmsInspectionBasisEntry> _entryRep;
+
+    public AdoS0QmsInspectionBasesController(
+        SqlSugarRepository<AdoS0QmsInspectionBasis> rep,
+        SqlSugarRepository<AdoS0QmsInspectionBasisEntry> entryRep)
+    {
+        _rep = rep;
+        _entryRep = entryRep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.Number != null && x.Number.Contains(q.Keyword!)) ||
+                (x.Name != null && x.Name.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetDetailAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Seq).OrderBy(x => x.Id).ToListAsync();
+        return Ok(new { master, items });
+    }
+
+    [HttpGet("options")]
+    public async Task<IActionResult> GetOptionsAsync() => Ok(await _rep.AsQueryable()
+        .OrderBy(x => x.Number)
+        .Select(x => new AdoS0QualitySimpleOptionDto { Value = x.Id, Label = (x.Number ?? "") + " / " + (x.Name ?? "") })
+        .ToListAsync());
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsInspectionBasisUpsertDto dto)
+    {
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            var master = new AdoS0QmsInspectionBasis();
+            ApplyInspectionBasis(master, dto, true);
+            await _rep.AsInsertable(master).ExecuteReturnEntityAsync();
+            await SyncInspectionBasisEntriesAsync(master.Id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(master.Id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsInspectionBasisUpsertDto dto)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            ApplyInspectionBasis(master, dto, false);
+            await _rep.AsUpdateable(master).ExecuteCommandAsync();
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await SyncInspectionBasisEntriesAsync(id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await _rep.DeleteAsync(master);
+            await db.Ado.CommitTranAsync();
+            return Ok(new { message = "删除成功" });
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    private static void ApplyInspectionBasis(AdoS0QmsInspectionBasis entity, AdoS0QmsInspectionBasisUpsertDto dto, bool isCreate)
+    {
+        entity.Number = dto.Number.Trim();
+        entity.Name = dto.Name.Trim();
+        entity.ControlStrategy = NullIfWhiteSpace(dto.ControlStrategy);
+        entity.CreateOrgId = dto.CreateOrgId;
+        entity.UseOrgId = dto.UseOrgId;
+        entity.Comment = NullIfWhiteSpace(dto.Comment);
+        entity.Status = NullIfWhiteSpace(dto.Status);
+        entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus);
+        if (isCreate) entity.CreateTime = DateTime.Now;
+        entity.ModifyTime = DateTime.Now;
+    }
+
+    private async Task SyncInspectionBasisEntriesAsync(long masterId, IEnumerable<AdoS0QmsInspectionBasisEntryDto> items)
+    {
+        var seq = 1L;
+        foreach (var item in items)
+        {
+            var entity = new AdoS0QmsInspectionBasisEntry
+            {
+                MasterId = masterId,
+                Seq = item.Seq ?? seq++,
+                DocumentNumber = NullIfWhiteSpace(item.DocumentNumber),
+                DocumentName = NullIfWhiteSpace(item.DocumentName),
+                Attachment = NullIfWhiteSpace(item.Attachment)
+            };
+            await _entryRep.AsInsertable(entity).ExecuteCommandAsync();
+        }
+    }
+}
+
+[ApiController]
+[Route("api/s0/quality/inspection-standards")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsInspectionStandardsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsInspectionStandard> _rep;
+    private readonly SqlSugarRepository<AdoS0QmsInspectionStandardEntry> _entryRep;
+
+    public AdoS0QmsInspectionStandardsController(
+        SqlSugarRepository<AdoS0QmsInspectionStandard> rep,
+        SqlSugarRepository<AdoS0QmsInspectionStandardEntry> entryRep)
+    {
+        _rep = rep;
+        _entryRep = entryRep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.Number != null && x.Number.Contains(q.Keyword!)) ||
+                (x.Name != null && x.Name.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetDetailAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Seq).OrderBy(x => x.Id).ToListAsync();
+        return Ok(new { master, items });
+    }
+
+    [HttpGet("options")]
+    public async Task<IActionResult> GetOptionsAsync() => Ok(await _rep.AsQueryable()
+        .OrderBy(x => x.Number)
+        .Select(x => new AdoS0QualitySimpleOptionDto { Value = x.Id, Label = (x.Number ?? "") + " / " + (x.Name ?? "") })
+        .ToListAsync());
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsInspectionStandardUpsertDto dto)
+    {
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            var master = new AdoS0QmsInspectionStandard();
+            ApplyInspectionStandard(master, dto);
+            await _rep.AsInsertable(master).ExecuteReturnEntityAsync();
+            await SyncInspectionStandardEntriesAsync(master.Id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(master.Id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsInspectionStandardUpsertDto dto)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            ApplyInspectionStandard(master, dto);
+            await _rep.AsUpdateable(master).ExecuteCommandAsync();
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await SyncInspectionStandardEntriesAsync(id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await _rep.DeleteAsync(master);
+            await db.Ado.CommitTranAsync();
+            return Ok(new { message = "删除成功" });
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    private static void ApplyInspectionStandard(AdoS0QmsInspectionStandard entity, AdoS0QmsInspectionStandardUpsertDto dto)
+    {
+        entity.Number = dto.Number.Trim();
+        entity.Name = dto.Name.Trim();
+        entity.Comment = NullIfWhiteSpace(dto.Comment);
+        entity.ControlStrategy = NullIfWhiteSpace(dto.ControlStrategy);
+        entity.CreateOrgId = dto.CreateOrgId;
+        entity.UseOrgId = dto.UseOrgId;
+        entity.Status = NullIfWhiteSpace(dto.Status);
+        entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus);
+    }
+
+    private async Task SyncInspectionStandardEntriesAsync(long masterId, IEnumerable<AdoS0QmsInspectionStandardEntryDto> items)
+    {
+        var seq = 1L;
+        foreach (var item in items)
+        {
+            var entity = new AdoS0QmsInspectionStandardEntry
+            {
+                MasterId = masterId,
+                Seq = item.Seq ?? seq++,
+                CheckItems = NullIfWhiteSpace(item.CheckItems),
+                CheckContent = NullIfWhiteSpace(item.CheckContent),
+                NormType = NullIfWhiteSpace(item.NormType),
+                SpecValue = NullIfWhiteSpace(item.SpecValue),
+                TopValue = item.TopValue,
+                DownValue = item.DownValue,
+                CheckBasisId = item.CheckBasisId,
+                CheckMethodId = item.CheckMethodId,
+                CheckFrequencyId = item.CheckFrequencyId,
+                CheckInstructId = item.CheckInstructId,
+                Unit = NullIfWhiteSpace(item.Unit),
+                KeyQuality = item.KeyQuality
+            };
+            await _entryRep.AsInsertable(entity).ExecuteCommandAsync();
+        }
+    }
+}
+
+[ApiController]
+[Route("api/s0/quality/inspection-plans")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsInspectionPlansController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsInspectionPlan> _rep;
+    private readonly SqlSugarRepository<AdoS0QmsInspectionPlanEntry> _entryRep;
+
+    public AdoS0QmsInspectionPlansController(
+        SqlSugarRepository<AdoS0QmsInspectionPlan> rep,
+        SqlSugarRepository<AdoS0QmsInspectionPlanEntry> entryRep)
+    {
+        _rep = rep;
+        _entryRep = entryRep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.Number != null && x.Number.Contains(q.Keyword!)) ||
+                (x.Name != null && x.Name.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetDetailAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Seq).OrderBy(x => x.Id).ToListAsync();
+        return Ok(new { master, items });
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsInspectionPlanUpsertDto dto)
+    {
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            var master = new AdoS0QmsInspectionPlan();
+            ApplyInspectionPlan(master, dto, true);
+            await _rep.AsInsertable(master).ExecuteReturnEntityAsync();
+            await SyncInspectionPlanEntriesAsync(master.Id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(master.Id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsInspectionPlanUpsertDto dto)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            ApplyInspectionPlan(master, dto, false);
+            await _rep.AsUpdateable(master).ExecuteCommandAsync();
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await SyncInspectionPlanEntriesAsync(id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await _rep.DeleteAsync(master);
+            await db.Ado.CommitTranAsync();
+            return Ok(new { message = "删除成功" });
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    private static void ApplyInspectionPlan(AdoS0QmsInspectionPlan entity, AdoS0QmsInspectionPlanUpsertDto dto, bool isCreate)
+    {
+        entity.Number = dto.Number.Trim();
+        entity.Name = dto.Name.Trim();
+        entity.BizTypeId = NullIfWhiteSpace(dto.BizTypeId);
+        entity.Comment = NullIfWhiteSpace(dto.Comment);
+        entity.ControlStrategy = NullIfWhiteSpace(dto.ControlStrategy);
+        entity.CreateOrgId = dto.CreateOrgId;
+        entity.UseOrgId = dto.UseOrgId;
+        entity.Status = NullIfWhiteSpace(dto.Status);
+        entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus);
+        if (isCreate) entity.CreateTime = DateTime.Now;
+        entity.ModifyTime = DateTime.Now;
+    }
+
+    private async Task SyncInspectionPlanEntriesAsync(long masterId, IEnumerable<AdoS0QmsInspectionPlanEntryDto> items)
+    {
+        var seq = 1;
+        foreach (var item in items)
+        {
+            var entity = new AdoS0QmsInspectionPlanEntry
+            {
+                MasterId = masterId,
+                Seq = item.Seq ?? seq++,
+                SetupType = NullIfWhiteSpace(item.SetupType),
+                MaterialCode = NullIfWhiteSpace(item.MaterialCode),
+                MaterialName = NullIfWhiteSpace(item.MaterialName),
+                MaterialTypeId = item.MaterialTypeId,
+                SupplierId = NullIfWhiteSpace(item.SupplierId),
+                SamplingSchemeId = item.SamplingSchemeId,
+                InspectionStandardId = item.InspectionStandardId,
+                InspectOrgId = item.InspectOrgId,
+                InspectUserId = item.InspectUserId,
+                QRouteId = item.QRouteId,
+                OperationNo = NullIfWhiteSpace(item.OperationNo),
+                OperationId = item.OperationId,
+                InspectionFrequencyId = item.InspectionFrequencyId,
+                ProcessSeq = NullIfWhiteSpace(item.ProcessSeq),
+                InspectionType = item.InspectionType
+            };
+            await _entryRep.AsInsertable(entity).ExecuteCommandAsync();
+        }
+    }
+}
+
+[ApiController]
+[Route("api/s0/quality/raw-inspection-specs")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsRawInspectionSpecsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsRawInspectionSpec> _rep;
+    private readonly SqlSugarRepository<AdoS0QmsRawInspectionSpecEntry> _entryRep;
+
+    public AdoS0QmsRawInspectionSpecsController(
+        SqlSugarRepository<AdoS0QmsRawInspectionSpec> rep,
+        SqlSugarRepository<AdoS0QmsRawInspectionSpecEntry> entryRep)
+    {
+        _rep = rep;
+        _entryRep = entryRep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.FileNumber != null && x.FileNumber.Contains(q.Keyword!)) ||
+                (x.RawMaterialName != null && x.RawMaterialName.Contains(q.Keyword!)) ||
+                (x.MaterialCode != null && x.MaterialCode.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderByDescending(x => x.Id).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetDetailAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Seq).OrderBy(x => x.Id).ToListAsync();
+        return Ok(new { master, items });
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsRawInspectionSpecUpsertDto dto)
+    {
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            var master = new AdoS0QmsRawInspectionSpec();
+            ApplyRawInspectionSpec(master, dto);
+            await _rep.AsInsertable(master).ExecuteReturnEntityAsync();
+            await SyncRawInspectionSpecEntriesAsync(master.Id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(master.Id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsRawInspectionSpecUpsertDto dto)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            ApplyRawInspectionSpec(master, dto);
+            await _rep.AsUpdateable(master).ExecuteCommandAsync();
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await SyncRawInspectionSpecEntriesAsync(id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await _rep.DeleteAsync(master);
+            await db.Ado.CommitTranAsync();
+            return Ok(new { message = "删除成功" });
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    private static void ApplyRawInspectionSpec(AdoS0QmsRawInspectionSpec entity, AdoS0QmsRawInspectionSpecUpsertDto dto)
+    {
+        entity.FileNumber = dto.FileNumber.Trim();
+        entity.VersionNo = NullIfWhiteSpace(dto.VersionNo);
+        entity.DrawingNo = NullIfWhiteSpace(dto.DrawingNo);
+        entity.RawMaterialName = NullIfWhiteSpace(dto.RawMaterialName);
+        entity.MaterialCode = NullIfWhiteSpace(dto.MaterialCode);
+        entity.EffectiveDate = NullIfWhiteSpace(dto.EffectiveDate);
+        entity.DrawingVersion = NullIfWhiteSpace(dto.DrawingVersion);
+        entity.MaterialGrade = NullIfWhiteSpace(dto.MaterialGrade);
+        entity.CavityOrMold = NullIfWhiteSpace(dto.CavityOrMold);
+        entity.Attachment = NullIfWhiteSpace(dto.Attachment);
+        entity.FileName = NullIfWhiteSpace(dto.FileName);
+        entity.Title = NullIfWhiteSpace(dto.Title);
+    }
+
+    private async Task SyncRawInspectionSpecEntriesAsync(long masterId, IEnumerable<AdoS0QmsRawInspectionSpecEntryDto> items)
+    {
+        var seq = 1;
+        foreach (var item in items)
+        {
+            var entity = new AdoS0QmsRawInspectionSpecEntry
+            {
+                MasterId = masterId,
+                Seq = item.Seq ?? seq++,
+                InspectionItem = NullIfWhiteSpace(item.InspectionItem),
+                InspectionStandard = NullIfWhiteSpace(item.InspectionStandard),
+                InspectionMethod = NullIfWhiteSpace(item.InspectionMethod),
+                ImageCategory = NullIfWhiteSpace(item.ImageCategory),
+                SamplingScheme = NullIfWhiteSpace(item.SamplingScheme),
+                Remark = NullIfWhiteSpace(item.Remark),
+                Attachment = NullIfWhiteSpace(item.Attachment),
+                UpperLimit = NullIfWhiteSpace(item.UpperLimit),
+                LowerLimit = NullIfWhiteSpace(item.LowerLimit)
+            };
+            await _entryRep.AsInsertable(entity).ExecuteCommandAsync();
+        }
+    }
+}
+
+[ApiController]
+[Route("api/s0/quality/process-inspection-specs")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsProcessInspectionSpecsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsProcessInspectionSpec> _rep;
+    private readonly SqlSugarRepository<AdoS0QmsProcessInspectionSpecEntry> _entryRep;
+
+    public AdoS0QmsProcessInspectionSpecsController(
+        SqlSugarRepository<AdoS0QmsProcessInspectionSpec> rep,
+        SqlSugarRepository<AdoS0QmsProcessInspectionSpecEntry> entryRep)
+    {
+        _rep = rep;
+        _entryRep = entryRep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.FileNumber != null && x.FileNumber.Contains(q.Keyword!)) ||
+                (x.ApplicableModel != null && x.ApplicableModel.Contains(q.Keyword!)) ||
+                (x.MaterialCode != null && x.MaterialCode.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderByDescending(x => x.Id).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetDetailAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Id).ToListAsync();
+        return Ok(new { master, items });
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsProcessInspectionSpecUpsertDto dto)
+    {
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            var master = new AdoS0QmsProcessInspectionSpec();
+            ApplyProcessInspectionSpec(master, dto);
+            await _rep.AsInsertable(master).ExecuteReturnEntityAsync();
+            await SyncProcessInspectionSpecEntriesAsync(master.Id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(master.Id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsProcessInspectionSpecUpsertDto dto)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            ApplyProcessInspectionSpec(master, dto);
+            await _rep.AsUpdateable(master).ExecuteCommandAsync();
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await SyncProcessInspectionSpecEntriesAsync(id, dto.Items);
+            await db.Ado.CommitTranAsync();
+            return await GetDetailAsync(id);
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var master = await _rep.GetByIdAsync(id);
+        if (master == null) return NotFound();
+        var db = _rep.Context;
+        await db.Ado.BeginTranAsync();
+        try
+        {
+            await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
+            await _rep.DeleteAsync(master);
+            await db.Ado.CommitTranAsync();
+            return Ok(new { message = "删除成功" });
+        }
+        catch
+        {
+            await db.Ado.RollbackTranAsync();
+            throw;
+        }
+    }
+
+    private static void ApplyProcessInspectionSpec(AdoS0QmsProcessInspectionSpec entity, AdoS0QmsProcessInspectionSpecUpsertDto dto)
+    {
+        entity.ApplicableModel = NullIfWhiteSpace(dto.ApplicableModel);
+        entity.FileNumber = dto.FileNumber.Trim();
+        entity.VersionNo = NullIfWhiteSpace(dto.VersionNo);
+        entity.EffectiveDate = NullIfWhiteSpace(dto.EffectiveDate);
+        entity.Attachment = NullIfWhiteSpace(dto.Attachment);
+        entity.MaterialCode = NullIfWhiteSpace(dto.MaterialCode);
+        entity.Attachment2 = NullIfWhiteSpace(dto.Attachment2);
+        entity.Version = dto.Version;
+    }
+
+    private async Task SyncProcessInspectionSpecEntriesAsync(long masterId, IEnumerable<AdoS0QmsProcessInspectionSpecEntryDto> items)
+    {
+        foreach (var item in items)
+        {
+            var entity = new AdoS0QmsProcessInspectionSpecEntry
+            {
+                MasterId = masterId,
+                OperationCode = NullIfWhiteSpace(item.OperationCode),
+                OperationName = NullIfWhiteSpace(item.OperationName),
+                InspectionItem = NullIfWhiteSpace(item.InspectionItem),
+                InspectionMethod = NullIfWhiteSpace(item.InspectionMethod),
+                InspectionSpec = NullIfWhiteSpace(item.InspectionSpec),
+                ImageCategory = NullIfWhiteSpace(item.ImageCategory),
+                InspectionFrequency = NullIfWhiteSpace(item.InspectionFrequency),
+                TechnicalStandard = NullIfWhiteSpace(item.TechnicalStandard),
+                PeelingForce = item.PeelingForce,
+                UpperLimit = NullIfWhiteSpace(item.UpperLimit),
+                LowerLimit = NullIfWhiteSpace(item.LowerLimit)
+            };
+            await _entryRep.AsInsertable(entity).ExecuteCommandAsync();
+        }
+    }
+}

+ 451 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Quality/AdoS0QualityBaseDataControllers.cs

@@ -0,0 +1,451 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Quality;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Quality;
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+using System.Linq.Expressions;
+using static Admin.NET.Plugin.AiDOP.Controllers.S0.Quality.AdoS0QmsControllerHelpers;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Quality;
+
+[ApiController]
+[Route("api/s0/quality/raw-whitelists")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsRawWhitelistsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsRawWhitelist> _rep;
+
+    public AdoS0QmsRawWhitelistsController(SqlSugarRepository<AdoS0QmsRawWhitelist> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.SupplierCode != null && x.SupplierCode.Contains(q.Keyword!)) ||
+                (x.SupplierName != null && x.SupplierName.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.SupplierCode).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id) => OkOrNotFound(await _rep.GetByIdAsync(id));
+
+    [HttpGet("options")]
+    public async Task<IActionResult> GetOptionsAsync()
+    {
+        var list = await _rep.AsQueryable()
+            .OrderBy(x => x.SupplierCode)
+            .Select(x => new AdoS0QualitySimpleOptionDto { Value = x.Id, Label = (x.SupplierCode ?? "") + " / " + (x.SupplierName ?? "") })
+            .ToListAsync();
+        return Ok(list);
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsRawWhitelistUpsertDto dto)
+    {
+        var entity = new AdoS0QmsRawWhitelist
+        {
+            SupplierCode = dto.SupplierCode.Trim(),
+            SupplierName = dto.SupplierName.Trim()
+        };
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsRawWhitelistUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+        entity.SupplierCode = dto.SupplierCode.Trim();
+        entity.SupplierName = dto.SupplierName.Trim();
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id) => await DeleteByIdAsync(_rep, id);
+}
+
+[ApiController]
+[Route("api/s0/quality/sampling-schemes")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsSamplingSchemesController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsSamplingScheme> _rep;
+
+    public AdoS0QmsSamplingSchemesController(SqlSugarRepository<AdoS0QmsSamplingScheme> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.Number != null && x.Number.Contains(q.Keyword!)) ||
+                (x.Name != null && x.Name.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id) => OkOrNotFound(await _rep.GetByIdAsync(id));
+
+    [HttpGet("options")]
+    public async Task<IActionResult> GetOptionsAsync() => Ok(await BuildOptionsAsync(_rep.AsQueryable(), x => x.Id, x => $"{x.Number} / {x.Name}"));
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsSamplingSchemeUpsertDto dto)
+    {
+        var entity = new AdoS0QmsSamplingScheme();
+        ApplySamplingScheme(entity, dto, true);
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsSamplingSchemeUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+        ApplySamplingScheme(entity, dto, false);
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id) => await DeleteByIdAsync(_rep, id);
+
+    private static void ApplySamplingScheme(AdoS0QmsSamplingScheme entity, AdoS0QmsSamplingSchemeUpsertDto dto, bool isCreate)
+    {
+        entity.Number = dto.Number.Trim();
+        entity.Name = dto.Name.Trim();
+        entity.SamplingType = NullIfWhiteSpace(dto.SamplingType);
+        entity.InspectionLevel = NullIfWhiteSpace(dto.InspectionLevel);
+        entity.Strictness = NullIfWhiteSpace(dto.Strictness);
+        entity.AqlValue = NullIfWhiteSpace(dto.AqlValue);
+        entity.InspectionType = NullIfWhiteSpace(dto.InspectionType);
+        entity.InspectOrgId = dto.InspectOrgId;
+        entity.InspectUserId = NullIfWhiteSpace(dto.InspectUserId);
+        entity.Status = NullIfWhiteSpace(dto.Status);
+        entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus);
+        entity.Comment = NullIfWhiteSpace(dto.Comment);
+        if (isCreate) entity.CreateTime = DateTime.Now;
+        entity.ModifyTime = DateTime.Now;
+    }
+}
+
+[ApiController]
+[Route("api/s0/quality/instruments")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsInspectionInstrumentsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsInspectionInstrument> _rep;
+
+    public AdoS0QmsInspectionInstrumentsController(SqlSugarRepository<AdoS0QmsInspectionInstrument> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.Number != null && x.Number.Contains(q.Keyword!)) ||
+                (x.Name != null && x.Name.Contains(q.Keyword!)) ||
+                (x.Model != null && x.Model.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id) => OkOrNotFound(await _rep.GetByIdAsync(id));
+
+    [HttpGet("options")]
+    public async Task<IActionResult> GetOptionsAsync() => Ok(await BuildOptionsAsync(_rep.AsQueryable(), x => x.Id, x => $"{x.Number} / {x.Name}"));
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsInspectionInstrumentUpsertDto dto)
+    {
+        var entity = new AdoS0QmsInspectionInstrument();
+        ApplyInspectionInstrument(entity, dto, true);
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsInspectionInstrumentUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+        ApplyInspectionInstrument(entity, dto, false);
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id) => await DeleteByIdAsync(_rep, id);
+
+    private static void ApplyInspectionInstrument(AdoS0QmsInspectionInstrument entity, AdoS0QmsInspectionInstrumentUpsertDto dto, bool isCreate)
+    {
+        entity.Number = dto.Number.Trim();
+        entity.Name = dto.Name.Trim();
+        entity.Model = NullIfWhiteSpace(dto.Model);
+        entity.Specification = NullIfWhiteSpace(dto.Specification);
+        entity.Manufacturer = NullIfWhiteSpace(dto.Manufacturer);
+        entity.Status = NullIfWhiteSpace(dto.Status);
+        entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus);
+        entity.Comment = NullIfWhiteSpace(dto.Comment);
+        if (isCreate) entity.CreateTime = DateTime.Now;
+        entity.ModifyTime = DateTime.Now;
+    }
+}
+
+[ApiController]
+[Route("api/s0/quality/inspection-methods")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsInspectionMethodsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsInspectionMethod> _rep;
+
+    public AdoS0QmsInspectionMethodsController(SqlSugarRepository<AdoS0QmsInspectionMethod> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.Number != null && x.Number.Contains(q.Keyword!)) ||
+                (x.Name != null && x.Name.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id) => OkOrNotFound(await _rep.GetByIdAsync(id));
+
+    [HttpGet("options")]
+    public async Task<IActionResult> GetOptionsAsync() => Ok(await BuildOptionsAsync(_rep.AsQueryable(), x => x.Id, x => $"{x.Number} / {x.Name}"));
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsInspectionMethodUpsertDto dto)
+    {
+        var entity = new AdoS0QmsInspectionMethod();
+        ApplyInspectionMethod(entity, dto, true);
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsInspectionMethodUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+        ApplyInspectionMethod(entity, dto, false);
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id) => await DeleteByIdAsync(_rep, id);
+
+    private static void ApplyInspectionMethod(AdoS0QmsInspectionMethod entity, AdoS0QmsInspectionMethodUpsertDto dto, bool isCreate)
+    {
+        entity.Number = dto.Number.Trim();
+        entity.Name = dto.Name.Trim();
+        entity.ControlStrategy = NullIfWhiteSpace(dto.ControlStrategy);
+        entity.Status = NullIfWhiteSpace(dto.Status);
+        entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus);
+        entity.Comment = NullIfWhiteSpace(dto.Comment);
+        if (isCreate) entity.CreateTime = DateTime.Now;
+        entity.ModifyTime = DateTime.Now;
+    }
+}
+
+[ApiController]
+[Route("api/s0/quality/inspection-items")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsInspectionItemsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsInspectionItem> _rep;
+
+    public AdoS0QmsInspectionItemsController(SqlSugarRepository<AdoS0QmsInspectionItem> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.Number != null && x.Number.Contains(q.Keyword!)) ||
+                (x.Name != null && x.Name.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id) => OkOrNotFound(await _rep.GetByIdAsync(id));
+
+    [HttpGet("options")]
+    public async Task<IActionResult> GetOptionsAsync() => Ok(await BuildOptionsAsync(_rep.AsQueryable(), x => x.Id, x => $"{x.Number} / {x.Name}"));
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsInspectionItemUpsertDto dto)
+    {
+        var entity = new AdoS0QmsInspectionItem();
+        ApplyInspectionItem(entity, dto, true);
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsInspectionItemUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+        ApplyInspectionItem(entity, dto, false);
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id) => await DeleteByIdAsync(_rep, id);
+
+    private static void ApplyInspectionItem(AdoS0QmsInspectionItem entity, AdoS0QmsInspectionItemUpsertDto dto, bool isCreate)
+    {
+        entity.Number = dto.Number.Trim();
+        entity.Name = dto.Name.Trim();
+        entity.CheckMethodId = dto.CheckMethodId;
+        entity.CheckBasisId = dto.CheckBasisId;
+        entity.CheckInstructId = dto.CheckInstructId;
+        entity.RadioGroupField = NullIfWhiteSpace(dto.RadioGroupField);
+        entity.RadioGroupField1 = NullIfWhiteSpace(dto.RadioGroupField1);
+        entity.MetricType = dto.MetricType;
+        entity.Status = NullIfWhiteSpace(dto.Status);
+        entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus);
+        entity.Comment = NullIfWhiteSpace(dto.Comment);
+        if (isCreate) entity.CreateTime = DateTime.Now;
+        entity.ModifyTime = DateTime.Now;
+    }
+}
+
+[ApiController]
+[Route("api/s0/quality/inspection-frequencies")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsInspectionFrequenciesController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0QmsInspectionFrequency> _rep;
+
+    public AdoS0QmsInspectionFrequenciesController(SqlSugarRepository<AdoS0QmsInspectionFrequency> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+        var query = _rep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x =>
+                (x.Number != null && x.Number.Contains(q.Keyword!)) ||
+                (x.Name != null && x.Name.Contains(q.Keyword!)));
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id) => OkOrNotFound(await _rep.GetByIdAsync(id));
+
+    [HttpGet("options")]
+    public async Task<IActionResult> GetOptionsAsync() => Ok(await BuildOptionsAsync(_rep.AsQueryable(), x => x.Id, x => $"{x.Number} / {x.Name}"));
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsInspectionFrequencyUpsertDto dto)
+    {
+        var entity = new AdoS0QmsInspectionFrequency();
+        ApplyInspectionFrequency(entity, dto, true);
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsInspectionFrequencyUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+        ApplyInspectionFrequency(entity, dto, false);
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id) => await DeleteByIdAsync(_rep, id);
+
+    private static void ApplyInspectionFrequency(AdoS0QmsInspectionFrequency entity, AdoS0QmsInspectionFrequencyUpsertDto dto, bool isCreate)
+    {
+        entity.Number = dto.Number.Trim();
+        entity.Name = dto.Name.Trim();
+        entity.Remark = NullIfWhiteSpace(dto.Remark);
+        entity.Status = NullIfWhiteSpace(dto.Status);
+        entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus);
+        if (isCreate) entity.CreateTime = DateTime.Now;
+        entity.ModifyTime = DateTime.Now;
+    }
+}
+
+internal static partial class AdoS0QmsControllerHelpers
+{
+    internal static async Task<IActionResult> DeleteByIdAsync<TEntity>(SqlSugarRepository<TEntity> rep, long id) where TEntity : class, new()
+    {
+        var item = await rep.GetByIdAsync(id);
+        if (item == null) return new NotFoundResult();
+        await rep.DeleteAsync(item);
+        return new OkObjectResult(new { message = "删除成功" });
+    }
+
+    internal static IActionResult OkOrNotFound<TEntity>(TEntity? entity) where TEntity : class => entity == null ? new NotFoundResult() : new OkObjectResult(entity);
+
+    internal static string? NullIfWhiteSpace(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
+
+    internal static async Task<List<AdoS0QualitySimpleOptionDto>> BuildOptionsAsync<TEntity>(
+        ISugarQueryable<TEntity> query,
+        Expression<Func<TEntity, long>> valueExp,
+        Expression<Func<TEntity, string>> labelExp) where TEntity : class, new()
+    {
+        var rows = await query.ToListAsync();
+        var valueGetter = valueExp.Compile();
+        var labelGetter = labelExp.Compile();
+        return rows.Select(x => new AdoS0QualitySimpleOptionDto
+        {
+            Value = valueGetter(x),
+            Label = labelGetter(x)
+        }).OrderBy(x => x.Label).ToList();
+    }
+}

+ 42 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Quality/AdoS0QualityDictionaryController.cs

@@ -0,0 +1,42 @@
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Quality;
+
+[ApiController]
+[Route("api/s0/quality/dictionary")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0QmsDictionaryController : ControllerBase
+{
+    [HttpGet("nodes")]
+    public IActionResult GetNodes()
+    {
+        var nodes = new[]
+        {
+            new { key = "doc-types", label = "单据类型", route = "", kind = "dict" },
+            new { key = "biz-types", label = "业务类型", route = "", kind = "dict" },
+            new { key = "complaint", label = "客诉", route = "", kind = "dict" },
+            new { key = "inspection-priority", label = "检验优先级", route = "", kind = "dict" },
+            new { key = "product-ncr", label = "产品不合格联络单", route = "", kind = "dict" },
+            new { key = "8d", label = "8D", route = "", kind = "dict" },
+            new { key = "ncr", label = "NCR", route = "", kind = "dict" },
+            new { key = "project", label = "项目管理", route = "", kind = "dict" },
+            new { key = "biochemical", label = "生化", route = "", kind = "dict" },
+            new { key = "capa", label = "CAPA", route = "", kind = "dict" },
+            new { key = "production-record-list", label = "生产记录清单", route = "", kind = "dict" },
+            new { key = "inspection-status", label = "检验状态", route = "", kind = "dict" },
+            new { key = "aql-level", label = "AQL接受水平", route = "", kind = "dict" },
+            new { key = "aql-value", label = "AQL值", route = "", kind = "dict" },
+            new { key = "raw-whitelists", label = "原材料白名单", route = "/aidop/s0/quality/raw-whitelist", kind = "page" },
+            new { key = "sampling-schemes", label = "抽样方案", route = "/aidop/s0/quality/sampling-scheme", kind = "page" },
+            new { key = "instruments", label = "检验仪器", route = "/aidop/s0/quality/instrument", kind = "page" },
+            new { key = "inspection-methods", label = "检验方法", route = "/aidop/s0/quality/inspection-method", kind = "page" },
+            new { key = "inspection-bases", label = "检验依据", route = "/aidop/s0/quality/inspection-basis", kind = "page" },
+            new { key = "inspection-standards", label = "检验标准", route = "/aidop/s0/quality/inspection-standard", kind = "page" },
+            new { key = "inspection-items", label = "检验项目", route = "/aidop/s0/quality/inspection-item", kind = "page" },
+            new { key = "inspection-frequencies", label = "检验频率", route = "/aidop/s0/quality/inspection-frequency", kind = "page" },
+            new { key = "inspection-plans", label = "检验方案", route = "/aidop/s0/quality/inspection-plan", kind = "page" },
+            new { key = "raw-inspection-specs", label = "原材料检验规范", route = "/aidop/s0/quality/raw-inspection-spec", kind = "page" },
+            new { key = "process-inspection-specs", label = "过程检验规范", route = "/aidop/s0/quality/process-inspection-spec", kind = "page" },
+        };
+        return Ok(nodes);
+    }
+}

+ 271 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Quality/AdoS0QualityDtos.cs

@@ -0,0 +1,271 @@
+namespace Admin.NET.Plugin.AiDOP.Dto.S0.Quality;
+
+public class AdoS0QualityPagedQueryDto
+{
+    public string? Keyword { get; set; }
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 20;
+}
+
+public class AdoS0QualitySimpleOptionDto
+{
+    public long Value { get; set; }
+    public string Label { get; set; } = string.Empty;
+}
+
+public class AdoS0QmsRawWhitelistUpsertDto
+{
+    [Required(ErrorMessage = "供应商编码不能为空")]
+    public string SupplierCode { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "供应商名称不能为空")]
+    public string SupplierName { get; set; } = string.Empty;
+}
+
+public class AdoS0QmsSamplingSchemeUpsertDto
+{
+    [Required(ErrorMessage = "编号不能为空")]
+    public string Number { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "名称不能为空")]
+    public string Name { get; set; } = string.Empty;
+
+    public string? SamplingType { get; set; }
+    public string? InspectionLevel { get; set; }
+    public string? Strictness { get; set; }
+    public string? AqlValue { get; set; }
+    public string? InspectionType { get; set; }
+    public long? InspectOrgId { get; set; }
+    public string? InspectUserId { get; set; }
+    public string? Status { get; set; }
+    public string? EnableStatus { get; set; }
+    public string? Comment { get; set; }
+}
+
+public class AdoS0QmsInspectionInstrumentUpsertDto
+{
+    [Required(ErrorMessage = "编号不能为空")]
+    public string Number { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "名称不能为空")]
+    public string Name { get; set; } = string.Empty;
+
+    public string? Model { get; set; }
+    public string? Specification { get; set; }
+    public string? Manufacturer { get; set; }
+    public string? Status { get; set; }
+    public string? EnableStatus { get; set; }
+    public string? Comment { get; set; }
+}
+
+public class AdoS0QmsInspectionMethodUpsertDto
+{
+    [Required(ErrorMessage = "编号不能为空")]
+    public string Number { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "名称不能为空")]
+    public string Name { get; set; } = string.Empty;
+
+    public string? ControlStrategy { get; set; }
+    public string? Status { get; set; }
+    public string? EnableStatus { get; set; }
+    public string? Comment { get; set; }
+}
+
+public class AdoS0QmsInspectionItemUpsertDto
+{
+    [Required(ErrorMessage = "编号不能为空")]
+    public string Number { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "名称不能为空")]
+    public string Name { get; set; } = string.Empty;
+
+    public long? CheckMethodId { get; set; }
+    public long? CheckBasisId { get; set; }
+    public long? CheckInstructId { get; set; }
+    public string? RadioGroupField { get; set; }
+    public string? RadioGroupField1 { get; set; }
+    public long? MetricType { get; set; }
+    public string? Status { get; set; }
+    public string? EnableStatus { get; set; }
+    public string? Comment { get; set; }
+}
+
+public class AdoS0QmsInspectionFrequencyUpsertDto
+{
+    [Required(ErrorMessage = "编号不能为空")]
+    public string Number { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "名称不能为空")]
+    public string Name { get; set; } = string.Empty;
+
+    public string? Remark { get; set; }
+    public string? Status { get; set; }
+    public string? EnableStatus { get; set; }
+}
+
+public class AdoS0QmsInspectionBasisEntryDto
+{
+    public long? Id { get; set; }
+    public long? Seq { get; set; }
+    public string? DocumentNumber { get; set; }
+    public string? DocumentName { get; set; }
+    public string? Attachment { get; set; }
+}
+
+public class AdoS0QmsInspectionBasisUpsertDto
+{
+    [Required(ErrorMessage = "编号不能为空")]
+    public string Number { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "名称不能为空")]
+    public string Name { get; set; } = string.Empty;
+
+    public string? ControlStrategy { get; set; }
+    public long? CreateOrgId { get; set; }
+    public long? UseOrgId { get; set; }
+    public string? Comment { get; set; }
+    public string? Status { get; set; }
+    public string? EnableStatus { get; set; }
+    public List<AdoS0QmsInspectionBasisEntryDto> Items { get; set; } = [];
+}
+
+public class AdoS0QmsInspectionStandardEntryDto
+{
+    public long? Id { get; set; }
+    public long? Seq { get; set; }
+    public string? CheckItems { get; set; }
+    public string? CheckContent { get; set; }
+    public string? NormType { get; set; }
+    public string? SpecValue { get; set; }
+    public decimal? TopValue { get; set; }
+    public decimal? DownValue { get; set; }
+    public long? CheckBasisId { get; set; }
+    public long? CheckMethodId { get; set; }
+    public long? CheckFrequencyId { get; set; }
+    public long? CheckInstructId { get; set; }
+    public string? Unit { get; set; }
+    public long? KeyQuality { get; set; }
+}
+
+public class AdoS0QmsInspectionStandardUpsertDto
+{
+    [Required(ErrorMessage = "编号不能为空")]
+    public string Number { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "名称不能为空")]
+    public string Name { get; set; } = string.Empty;
+
+    public string? Comment { get; set; }
+    public string? ControlStrategy { get; set; }
+    public long? CreateOrgId { get; set; }
+    public long? UseOrgId { get; set; }
+    public string? Status { get; set; }
+    public string? EnableStatus { get; set; }
+    public List<AdoS0QmsInspectionStandardEntryDto> Items { get; set; } = [];
+}
+
+public class AdoS0QmsInspectionPlanEntryDto
+{
+    public long? Id { get; set; }
+    public int? Seq { get; set; }
+    public string? SetupType { get; set; }
+    public string? MaterialCode { get; set; }
+    public string? MaterialName { get; set; }
+    public long? MaterialTypeId { get; set; }
+    public string? SupplierId { get; set; }
+    public long? SamplingSchemeId { get; set; }
+    public long? InspectionStandardId { get; set; }
+    public long? InspectOrgId { get; set; }
+    public long? InspectUserId { get; set; }
+    public long? QRouteId { get; set; }
+    public string? OperationNo { get; set; }
+    public long? OperationId { get; set; }
+    public long? InspectionFrequencyId { get; set; }
+    public string? ProcessSeq { get; set; }
+    public long? InspectionType { get; set; }
+}
+
+public class AdoS0QmsInspectionPlanUpsertDto
+{
+    [Required(ErrorMessage = "编号不能为空")]
+    public string Number { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "名称不能为空")]
+    public string Name { get; set; } = string.Empty;
+
+    public string? BizTypeId { get; set; }
+    public string? Comment { get; set; }
+    public string? ControlStrategy { get; set; }
+    public long? CreateOrgId { get; set; }
+    public long? UseOrgId { get; set; }
+    public string? Status { get; set; }
+    public string? EnableStatus { get; set; }
+    public List<AdoS0QmsInspectionPlanEntryDto> Items { get; set; } = [];
+}
+
+public class AdoS0QmsRawInspectionSpecEntryDto
+{
+    public long? Id { get; set; }
+    public int? Seq { get; set; }
+    public string? InspectionItem { get; set; }
+    public string? InspectionStandard { get; set; }
+    public string? InspectionMethod { get; set; }
+    public string? ImageCategory { get; set; }
+    public string? SamplingScheme { get; set; }
+    public string? Remark { get; set; }
+    public string? Attachment { get; set; }
+    public string? UpperLimit { get; set; }
+    public string? LowerLimit { get; set; }
+}
+
+public class AdoS0QmsRawInspectionSpecUpsertDto
+{
+    [Required(ErrorMessage = "文件编号不能为空")]
+    public string FileNumber { get; set; } = string.Empty;
+
+    public string? VersionNo { get; set; }
+    public string? DrawingNo { get; set; }
+    public string? RawMaterialName { get; set; }
+    public string? MaterialCode { get; set; }
+    public string? EffectiveDate { get; set; }
+    public string? DrawingVersion { get; set; }
+    public string? MaterialGrade { get; set; }
+    public string? CavityOrMold { get; set; }
+    public string? Attachment { get; set; }
+    public string? FileName { get; set; }
+    public string? Title { get; set; }
+    public List<AdoS0QmsRawInspectionSpecEntryDto> Items { get; set; } = [];
+}
+
+public class AdoS0QmsProcessInspectionSpecEntryDto
+{
+    public long? Id { get; set; }
+    public string? OperationCode { get; set; }
+    public string? OperationName { get; set; }
+    public string? InspectionItem { get; set; }
+    public string? InspectionMethod { get; set; }
+    public string? InspectionSpec { get; set; }
+    public string? ImageCategory { get; set; }
+    public string? InspectionFrequency { get; set; }
+    public string? TechnicalStandard { get; set; }
+    public long? PeelingForce { get; set; }
+    public string? UpperLimit { get; set; }
+    public string? LowerLimit { get; set; }
+}
+
+public class AdoS0QmsProcessInspectionSpecUpsertDto
+{
+    public string? ApplicableModel { get; set; }
+
+    [Required(ErrorMessage = "文件编号不能为空")]
+    public string FileNumber { get; set; } = string.Empty;
+
+    public string? VersionNo { get; set; }
+    public string? EffectiveDate { get; set; }
+    public string? Attachment { get; set; }
+    public string? MaterialCode { get; set; }
+    public string? Attachment2 { get; set; }
+    public int? Version { get; set; }
+    public List<AdoS0QmsProcessInspectionSpecEntryDto> Items { get; set; } = [];
+}

+ 49 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoSmartOpsEntities.cs

@@ -0,0 +1,49 @@
+namespace Admin.NET.Plugin.AiDOP.Entity;
+
+/// <summary>
+/// 智慧运营看板订单查询表。
+/// </summary>
+[IgnoreTable]
+[SugarTable("ado_order", "智慧运营看板订单查询表")]
+public class AdoOrder
+{
+    [SugarColumn(ColumnName = "Id", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = false, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "Product", ColumnDescription = "产品", IsNullable = true, Length = 200)]
+    public string? Product { get; set; }
+
+    [SugarColumn(ColumnName = "OrderNo", ColumnDescription = "订单号", IsNullable = true, Length = 100)]
+    public string? OrderNo { get; set; }
+}
+
+/// <summary>
+/// 智慧运营看板计划查询表。
+/// </summary>
+[IgnoreTable]
+[SugarTable("ado_plan", "智慧运营看板计划查询表")]
+public class AdoPlan
+{
+    [SugarColumn(ColumnName = "Id", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = false, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "ProductName", ColumnDescription = "产品名称", IsNullable = true, Length = 200)]
+    public string? ProductName { get; set; }
+}
+
+/// <summary>
+/// 智慧运营看板工单查询表。
+/// </summary>
+[IgnoreTable]
+[SugarTable("ado_work_order", "智慧运营看板工单查询表")]
+public class AdoWorkOrder
+{
+    [SugarColumn(ColumnName = "Id", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = false, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "Product", ColumnDescription = "产品", IsNullable = true, Length = 200)]
+    public string? Product { get; set; }
+
+    [SugarColumn(ColumnName = "WorkCenter", ColumnDescription = "工作中心", IsNullable = true, Length = 100)]
+    public string? WorkCenter { get; set; }
+}

+ 605 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Quality/AdoS0QualityEntities.cs

@@ -0,0 +1,605 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S0.Quality;
+
+// ========== 单表主数据 ==========
+
+[SugarTable("qms_lymjbmd", "原材料白名单")]
+public class AdoS0QmsRawWhitelist
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "gysbm", ColumnDescription = "供应商编码", IsNullable = true, Length = 255)]
+    public string? SupplierCode { get; set; }
+
+    [SugarColumn(ColumnName = "gysmc", ColumnDescription = "供应商名称", IsNullable = true, Length = 255)]
+    public string? SupplierName { get; set; }
+}
+
+[SugarTable("qms_sampscheme", "抽样方案")]
+public class AdoS0QmsSamplingScheme
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "FNUMBER", ColumnDescription = "编号", IsNullable = true, Length = 255)]
+    public string? Number { get; set; }
+
+    [SugarColumn(ColumnName = "FNAME", ColumnDescription = "名称", IsNullable = true, Length = 255)]
+    public string? Name { get; set; }
+
+    [SugarColumn(ColumnName = "FSAMPLINGTYPE", ColumnDescription = "抽样类型", IsNullable = true, Length = 255)]
+    public string? SamplingType { get; set; }
+
+    [SugarColumn(ColumnName = "FINSPECTIONLEVEL", ColumnDescription = "检验水准", IsNullable = true, Length = 255)]
+    public string? InspectionLevel { get; set; }
+
+    [SugarColumn(ColumnName = "FSTRICTNESS", ColumnDescription = "严格程度", IsNullable = true, Length = 255)]
+    public string? Strictness { get; set; }
+
+    [SugarColumn(ColumnName = "FAQLVALUE", ColumnDescription = "AQL值", IsNullable = true, Length = 255)]
+    public string? AqlValue { get; set; }
+
+    [SugarColumn(ColumnName = "FINSPECTIONTYPE", ColumnDescription = "检验类型", IsNullable = true, Length = 255)]
+    public string? InspectionType { get; set; }
+
+    [SugarColumn(ColumnName = "FINSPECTORGID", ColumnDescription = "检验组织", IsNullable = true, ColumnDataType = "bigint")]
+    public long? InspectOrgId { get; set; }
+
+    [SugarColumn(ColumnName = "FINSPECTUSERID", ColumnDescription = "检验员", IsNullable = true, Length = 255)]
+    public string? InspectUserId { get; set; }
+
+    [SugarColumn(ColumnName = "FSTATUS", ColumnDescription = "状态", IsNullable = true, Length = 255)]
+    public string? Status { get; set; }
+
+    [SugarColumn(ColumnName = "FENABLE", ColumnDescription = "启用", IsNullable = true, Length = 255)]
+    public string? EnableStatus { get; set; }
+
+    [SugarColumn(ColumnName = "FCOMMENT", ColumnDescription = "备注", IsNullable = true, Length = 255)]
+    public string? Comment { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATETIME", ColumnDescription = "创建时间", IsNullable = true)]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "FMODIFYTIME", ColumnDescription = "修改时间", IsNullable = true)]
+    public DateTime? ModifyTime { get; set; }
+}
+
+[SugarTable("qms_inspectioninstru", "检验仪器")]
+public class AdoS0QmsInspectionInstrument
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "FNUMBER", ColumnDescription = "编号", IsNullable = true, Length = 255)]
+    public string? Number { get; set; }
+
+    [SugarColumn(ColumnName = "FNAME", ColumnDescription = "名称", IsNullable = true, Length = 255)]
+    public string? Name { get; set; }
+
+    [SugarColumn(ColumnName = "FMODEL", ColumnDescription = "型号", IsNullable = true, Length = 255)]
+    public string? Model { get; set; }
+
+    [SugarColumn(ColumnName = "FSPECIFICATION", ColumnDescription = "规格", IsNullable = true, Length = 255)]
+    public string? Specification { get; set; }
+
+    [SugarColumn(ColumnName = "FMANUFACTURER", ColumnDescription = "生产厂家", IsNullable = true, Length = 255)]
+    public string? Manufacturer { get; set; }
+
+    [SugarColumn(ColumnName = "FSTATUS", ColumnDescription = "状态", IsNullable = true, Length = 255)]
+    public string? Status { get; set; }
+
+    [SugarColumn(ColumnName = "FENABLE", ColumnDescription = "启用", IsNullable = true, Length = 255)]
+    public string? EnableStatus { get; set; }
+
+    [SugarColumn(ColumnName = "FCOMMENT", ColumnDescription = "备注", IsNullable = true, Length = 255)]
+    public string? Comment { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATETIME", ColumnDescription = "创建时间", IsNullable = true)]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "FMODIFYTIME", ColumnDescription = "修改时间", IsNullable = true)]
+    public DateTime? ModifyTime { get; set; }
+}
+
+[SugarTable("qms_inspection_method", "检验方法")]
+public class AdoS0QmsInspectionMethod
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "FNUMBER", ColumnDescription = "编号", IsNullable = true, Length = 255)]
+    public string? Number { get; set; }
+
+    [SugarColumn(ColumnName = "FNAME", ColumnDescription = "名称", IsNullable = true, Length = 255)]
+    public string? Name { get; set; }
+
+    [SugarColumn(ColumnName = "FCTRLSTRATEGY", ColumnDescription = "控制策略", IsNullable = true, Length = 255)]
+    public string? ControlStrategy { get; set; }
+
+    [SugarColumn(ColumnName = "FSTATUS", ColumnDescription = "状态", IsNullable = true, Length = 255)]
+    public string? Status { get; set; }
+
+    [SugarColumn(ColumnName = "FENABLE", ColumnDescription = "启用", IsNullable = true, Length = 255)]
+    public string? EnableStatus { get; set; }
+
+    [SugarColumn(ColumnName = "FCOMMENT", ColumnDescription = "备注", IsNullable = true, Length = 255)]
+    public string? Comment { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATETIME", ColumnDescription = "创建时间", IsNullable = true)]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "FMODIFYTIME", ColumnDescription = "修改时间", IsNullable = true)]
+    public DateTime? ModifyTime { get; set; }
+}
+
+[SugarTable("qms_inspectionitems", "检验项目")]
+public class AdoS0QmsInspectionItem
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "FNUMBER", ColumnDescription = "编号", IsNullable = true, Length = 255)]
+    public string? Number { get; set; }
+
+    [SugarColumn(ColumnName = "FNAME", ColumnDescription = "名称", IsNullable = true, Length = 255)]
+    public string? Name { get; set; }
+
+    [SugarColumn(ColumnName = "FCHECKMETHOD", ColumnDescription = "检验方法", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CheckMethodId { get; set; }
+
+    [SugarColumn(ColumnName = "FCHECKBASIS", ColumnDescription = "检验依据", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CheckBasisId { get; set; }
+
+    [SugarColumn(ColumnName = "FCHECKINSTRUCT", ColumnDescription = "检验指导书", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CheckInstructId { get; set; }
+
+    [SugarColumn(ColumnName = "FRADIOGROUPFIELD", ColumnDescription = "单选组1", IsNullable = true, Length = 255)]
+    public string? RadioGroupField { get; set; }
+
+    [SugarColumn(ColumnName = "FRADIOGROUPFIELD1", ColumnDescription = "单选组2", IsNullable = true, Length = 255)]
+    public string? RadioGroupField1 { get; set; }
+
+    [SugarColumn(ColumnName = "zblx", ColumnDescription = "指标类型", IsNullable = true, ColumnDataType = "bigint")]
+    public long? MetricType { get; set; }
+
+    [SugarColumn(ColumnName = "FSTATUS", ColumnDescription = "状态", IsNullable = true, Length = 255)]
+    public string? Status { get; set; }
+
+    [SugarColumn(ColumnName = "FENABLE", ColumnDescription = "启用", IsNullable = true, Length = 255)]
+    public string? EnableStatus { get; set; }
+
+    [SugarColumn(ColumnName = "FCOMMENT", ColumnDescription = "备注", IsNullable = true, Length = 255)]
+    public string? Comment { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATETIME", ColumnDescription = "创建时间", IsNullable = true)]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "FMODIFYTIME", ColumnDescription = "修改时间", IsNullable = true)]
+    public DateTime? ModifyTime { get; set; }
+}
+
+[SugarTable("qms_inspectionfreq", "检验频率")]
+public class AdoS0QmsInspectionFrequency
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "FNUMBER", ColumnDescription = "编号", IsNullable = true, Length = 255)]
+    public string? Number { get; set; }
+
+    [SugarColumn(ColumnName = "FNAME", ColumnDescription = "名称", IsNullable = true, Length = 255)]
+    public string? Name { get; set; }
+
+    [SugarColumn(ColumnName = "FREMARK", ColumnDescription = "备注", IsNullable = true, Length = 255)]
+    public string? Remark { get; set; }
+
+    [SugarColumn(ColumnName = "FSTATUS", ColumnDescription = "状态", IsNullable = true, Length = 255)]
+    public string? Status { get; set; }
+
+    [SugarColumn(ColumnName = "FENABLE", ColumnDescription = "启用", IsNullable = true, Length = 255)]
+    public string? EnableStatus { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATETIME", ColumnDescription = "创建时间", IsNullable = true)]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "FMODIFYTIME", ColumnDescription = "修改时间", IsNullable = true)]
+    public DateTime? ModifyTime { get; set; }
+}
+
+// ========== 聚合头表 ==========
+
+[SugarTable("qms_inspectioncrit", "检验依据")]
+public class AdoS0QmsInspectionBasis
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "FNUMBER", ColumnDescription = "编号", IsNullable = true, Length = 255)]
+    public string? Number { get; set; }
+
+    [SugarColumn(ColumnName = "FNAME", ColumnDescription = "名称", IsNullable = true, Length = 255)]
+    public string? Name { get; set; }
+
+    [SugarColumn(ColumnName = "FCTRLSTRATEGY", ColumnDescription = "控制策略", IsNullable = true, Length = 255)]
+    public string? ControlStrategy { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATEORGID", ColumnDescription = "创建组织", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CreateOrgId { get; set; }
+
+    [SugarColumn(ColumnName = "FUSEORG", ColumnDescription = "使用组织", IsNullable = true, ColumnDataType = "bigint")]
+    public long? UseOrgId { get; set; }
+
+    [SugarColumn(ColumnName = "FCOMMENT", ColumnDescription = "备注", IsNullable = true, Length = 255)]
+    public string? Comment { get; set; }
+
+    [SugarColumn(ColumnName = "FSTATUS", ColumnDescription = "状态", IsNullable = true, Length = 255)]
+    public string? Status { get; set; }
+
+    [SugarColumn(ColumnName = "FENABLE", ColumnDescription = "启用", IsNullable = true, Length = 255)]
+    public string? EnableStatus { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATETIME", ColumnDescription = "创建时间", IsNullable = true)]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "FMODIFYTIME", ColumnDescription = "修改时间", IsNullable = true)]
+    public DateTime? ModifyTime { get; set; }
+}
+
+[SugarTable("qms_inspectionstd", "检验标准")]
+public class AdoS0QmsInspectionStandard
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "FNUMBER", ColumnDescription = "编号", IsNullable = true, Length = 30)]
+    public string? Number { get; set; }
+
+    [SugarColumn(ColumnName = "FNAME", ColumnDescription = "名称", IsNullable = true, Length = 255)]
+    public string? Name { get; set; }
+
+    [SugarColumn(ColumnName = "FCOMMENT", ColumnDescription = "备注", IsNullable = true, Length = 255)]
+    public string? Comment { get; set; }
+
+    [SugarColumn(ColumnName = "FCTRLSTRATEGY", ColumnDescription = "控制策略", IsNullable = true, Length = 30)]
+    public string? ControlStrategy { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATEORGID", ColumnDescription = "创建组织", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CreateOrgId { get; set; }
+
+    [SugarColumn(ColumnName = "FUSEORG", ColumnDescription = "使用组织", IsNullable = true, ColumnDataType = "bigint")]
+    public long? UseOrgId { get; set; }
+
+    [SugarColumn(ColumnName = "FSTATUS", ColumnDescription = "状态", IsNullable = true, Length = 30)]
+    public string? Status { get; set; }
+
+    [SugarColumn(ColumnName = "FENABLE", ColumnDescription = "启用", IsNullable = true, Length = 30)]
+    public string? EnableStatus { get; set; }
+}
+
+[SugarTable("qms_inspectpro", "检验方案")]
+public class AdoS0QmsInspectionPlan
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "FNUMBER", ColumnDescription = "编号", IsNullable = true, Length = 255)]
+    public string? Number { get; set; }
+
+    [SugarColumn(ColumnName = "FNAME", ColumnDescription = "名称", IsNullable = true, Length = 255)]
+    public string? Name { get; set; }
+
+    [SugarColumn(ColumnName = "FBIZSTYPEID", ColumnDescription = "检验业务类型", IsNullable = true, Length = 255)]
+    public string? BizTypeId { get; set; }
+
+    [SugarColumn(ColumnName = "FCOMMENT", ColumnDescription = "备注", IsNullable = true, Length = 255)]
+    public string? Comment { get; set; }
+
+    [SugarColumn(ColumnName = "FCTRLSTRATEGY", ColumnDescription = "控制策略", IsNullable = true, Length = 255)]
+    public string? ControlStrategy { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATEORGID", ColumnDescription = "创建组织", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CreateOrgId { get; set; }
+
+    [SugarColumn(ColumnName = "FUSEORGID", ColumnDescription = "使用组织", IsNullable = true, ColumnDataType = "bigint")]
+    public long? UseOrgId { get; set; }
+
+    [SugarColumn(ColumnName = "FSTATUS", ColumnDescription = "状态", IsNullable = true, Length = 255)]
+    public string? Status { get; set; }
+
+    [SugarColumn(ColumnName = "FENABLE", ColumnDescription = "启用", IsNullable = true, Length = 255)]
+    public string? EnableStatus { get; set; }
+
+    [SugarColumn(ColumnName = "FCREATETIME", ColumnDescription = "创建时间", IsNullable = true)]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "FMODIFYTIME", ColumnDescription = "修改时间", IsNullable = true)]
+    public DateTime? ModifyTime { get; set; }
+}
+
+[SugarTable("qms_jygf", "原材料检验规范")]
+public class AdoS0QmsRawInspectionSpec
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "wjbh", ColumnDescription = "文件编号", IsNullable = true, Length = 255)]
+    public string? FileNumber { get; set; }
+
+    [SugarColumn(ColumnName = "bb", ColumnDescription = "版本", IsNullable = true, Length = 255)]
+    public string? VersionNo { get; set; }
+
+    [SugarColumn(ColumnName = "th", ColumnDescription = "图号", IsNullable = true, Length = 255)]
+    public string? DrawingNo { get; set; }
+
+    [SugarColumn(ColumnName = "yclmc", ColumnDescription = "原材料名称", IsNullable = true, Length = 255)]
+    public string? RawMaterialName { get; set; }
+
+    [SugarColumn(ColumnName = "wlbm", ColumnDescription = "物料编码", IsNullable = true, Length = 255)]
+    public string? MaterialCode { get; set; }
+
+    [SugarColumn(ColumnName = "sxrq", ColumnDescription = "生效日期", IsNullable = true, Length = 255)]
+    public string? EffectiveDate { get; set; }
+
+    [SugarColumn(ColumnName = "tzbb", ColumnDescription = "图纸版本", IsNullable = true, Length = 255)]
+    public string? DrawingVersion { get; set; }
+
+    [SugarColumn(ColumnName = "czclph", ColumnDescription = "材质/材料牌号", IsNullable = true, Length = 255)]
+    public string? MaterialGrade { get; set; }
+
+    [SugarColumn(ColumnName = "xm", ColumnDescription = "穴/模", IsNullable = true, Length = 255)]
+    public string? CavityOrMold { get; set; }
+
+    [SugarColumn(ColumnName = "fj", ColumnDescription = "附件", IsNullable = true, ColumnDataType = "text")]
+    public string? Attachment { get; set; }
+
+    [SugarColumn(ColumnName = "filename", ColumnDescription = "附件文件名", IsNullable = true, ColumnDataType = "text")]
+    public string? FileName { get; set; }
+
+    [SugarColumn(ColumnName = "title", ColumnDescription = "附件标题", IsNullable = true, Length = 255)]
+    public string? Title { get; set; }
+}
+
+[SugarTable("qms_gcjygf", "过程检验规范")]
+public class AdoS0QmsProcessInspectionSpec
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "syxh", ColumnDescription = "适用型号", IsNullable = true, Length = 255)]
+    public string? ApplicableModel { get; set; }
+
+    [SugarColumn(ColumnName = "wjbh", ColumnDescription = "文件编号", IsNullable = true, Length = 255)]
+    public string? FileNumber { get; set; }
+
+    [SugarColumn(ColumnName = "bb", ColumnDescription = "版本", IsNullable = true, Length = 255)]
+    public string? VersionNo { get; set; }
+
+    [SugarColumn(ColumnName = "sxrj", ColumnDescription = "生效日期", IsNullable = true, Length = 255)]
+    public string? EffectiveDate { get; set; }
+
+    [SugarColumn(ColumnName = "fj", ColumnDescription = "附件", IsNullable = true, ColumnDataType = "text")]
+    public string? Attachment { get; set; }
+
+    [SugarColumn(ColumnName = "wlbm", ColumnDescription = "物料编码", IsNullable = true, ColumnDataType = "text")]
+    public string? MaterialCode { get; set; }
+
+    [SugarColumn(ColumnName = "fj2", ColumnDescription = "附件2", IsNullable = true, ColumnDataType = "text")]
+    public string? Attachment2 { get; set; }
+
+    [SugarColumn(ColumnName = "version", ColumnDescription = "版本号", IsNullable = true, ColumnDataType = "int")]
+    public int? Version { get; set; }
+}
+
+// ========== 聚合子表 ==========
+
+[SugarTable("qms_inspeccrit_entry", "检验依据-技术文档")]
+public class AdoS0QmsInspectionBasisEntry
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "glid", ColumnDescription = "主表ID", IsNullable = true, ColumnDataType = "bigint")]
+    public long? MasterId { get; set; }
+
+    [SugarColumn(ColumnName = "FSEQ", ColumnDescription = "序号", IsNullable = true, ColumnDataType = "bigint")]
+    public long? Seq { get; set; }
+
+    [SugarColumn(ColumnName = "FWORDNUM", ColumnDescription = "文档编号", IsNullable = true, Length = 255)]
+    public string? DocumentNumber { get; set; }
+
+    [SugarColumn(ColumnName = "FWORDNAME", ColumnDescription = "文档名称", IsNullable = true, Length = 255)]
+    public string? DocumentName { get; set; }
+
+    [SugarColumn(ColumnName = "FJ", ColumnDescription = "附件", IsNullable = true, Length = 255)]
+    public string? Attachment { get; set; }
+}
+
+[SugarTable("qms_inspectionstdentry", "检验标准-检验项目")]
+public class AdoS0QmsInspectionStandardEntry
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "FENTRYID", ColumnDescription = "主表ID", IsNullable = true, ColumnDataType = "bigint")]
+    public long? MasterId { get; set; }
+
+    [SugarColumn(ColumnName = "FSEQ", ColumnDescription = "序号", IsNullable = true, ColumnDataType = "bigint")]
+    public long? Seq { get; set; }
+
+    [SugarColumn(ColumnName = "FCHECKITEMS", ColumnDescription = "检验项目", IsNullable = true, Length = 255)]
+    public string? CheckItems { get; set; }
+
+    [SugarColumn(ColumnName = "FCHECKCONTENT", ColumnDescription = "检验内容", IsNullable = true, Length = 255)]
+    public string? CheckContent { get; set; }
+
+    [SugarColumn(ColumnName = "FNORMTYPE", ColumnDescription = "比较符", IsNullable = true, Length = 255)]
+    public string? NormType { get; set; }
+
+    [SugarColumn(ColumnName = "FSPECVALUE", ColumnDescription = "标准", IsNullable = true, Length = 255)]
+    public string? SpecValue { get; set; }
+
+    [SugarColumn(ColumnName = "FTOPVALUE", ColumnDescription = "上限", IsNullable = true, ColumnDataType = "decimal(23,10)")]
+    public decimal? TopValue { get; set; }
+
+    [SugarColumn(ColumnName = "FDOWNVALUE", ColumnDescription = "下限", IsNullable = true, ColumnDataType = "decimal(23,10)")]
+    public decimal? DownValue { get; set; }
+
+    [SugarColumn(ColumnName = "FCHECKBASIS", ColumnDescription = "检验依据", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CheckBasisId { get; set; }
+
+    [SugarColumn(ColumnName = "FCHECKMETHOD", ColumnDescription = "检验方法", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CheckMethodId { get; set; }
+
+    [SugarColumn(ColumnName = "FCHECKFREQ", ColumnDescription = "检验频率", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CheckFrequencyId { get; set; }
+
+    [SugarColumn(ColumnName = "FCHECKINSTRUCT", ColumnDescription = "检验指导书", IsNullable = true, ColumnDataType = "bigint")]
+    public long? CheckInstructId { get; set; }
+
+    [SugarColumn(ColumnName = "FUNITLD", ColumnDescription = "单位", IsNullable = true, Length = 255)]
+    public string? Unit { get; set; }
+
+    [SugarColumn(ColumnName = "FKEYQUALITY", ColumnDescription = "关键质量", IsNullable = true, ColumnDataType = "bigint")]
+    public long? KeyQuality { get; set; }
+}
+
+[SugarTable("qms_inspro_ent", "检验方案-方案设置")]
+public class AdoS0QmsInspectionPlanEntry
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "glid", ColumnDescription = "主表ID", IsNullable = true, ColumnDataType = "bigint")]
+    public long? MasterId { get; set; }
+
+    [SugarColumn(ColumnName = "FSEQ", ColumnDescription = "序号", IsNullable = true, ColumnDataType = "int")]
+    public int? Seq { get; set; }
+
+    [SugarColumn(ColumnName = "FSETUPTYPE", ColumnDescription = "设置类型", IsNullable = true, Length = 255)]
+    public string? SetupType { get; set; }
+
+    [SugarColumn(ColumnName = "FMATERIELID", ColumnDescription = "物料编码", IsNullable = true, Length = 255)]
+    public string? MaterialCode { get; set; }
+
+    [SugarColumn(ColumnName = "wlmc", ColumnDescription = "物料名称", IsNullable = true, Length = 255)]
+    public string? MaterialName { get; set; }
+
+    [SugarColumn(ColumnName = "FMATERIELTYPEID", ColumnDescription = "物料分类", IsNullable = true, ColumnDataType = "bigint")]
+    public long? MaterialTypeId { get; set; }
+
+    [SugarColumn(ColumnName = "FSUPPLIERID", ColumnDescription = "供应商", IsNullable = true, Length = 255)]
+    public string? SupplierId { get; set; }
+
+    [SugarColumn(ColumnName = "FSAMPLEPROID", ColumnDescription = "抽样方案", IsNullable = true, ColumnDataType = "bigint")]
+    public long? SamplingSchemeId { get; set; }
+
+    [SugarColumn(ColumnName = "FINSPECTSTDID", ColumnDescription = "检验标准", IsNullable = true, ColumnDataType = "bigint")]
+    public long? InspectionStandardId { get; set; }
+
+    [SugarColumn(ColumnName = "FINSPECTORGID", ColumnDescription = "检验组织", IsNullable = true, ColumnDataType = "bigint")]
+    public long? InspectOrgId { get; set; }
+
+    [SugarColumn(ColumnName = "FINSPECTUSERID", ColumnDescription = "检验员", IsNullable = true, ColumnDataType = "bigint")]
+    public long? InspectUserId { get; set; }
+
+    [SugarColumn(ColumnName = "FQROUTEID", ColumnDescription = "工艺路线", IsNullable = true, ColumnDataType = "bigint")]
+    public long? QRouteId { get; set; }
+
+    [SugarColumn(ColumnName = "FOPERATIONNO", ColumnDescription = "工序号", IsNullable = true, Length = 255)]
+    public string? OperationNo { get; set; }
+
+    [SugarColumn(ColumnName = "FOPROPERATION", ColumnDescription = "工序", IsNullable = true, ColumnDataType = "bigint")]
+    public long? OperationId { get; set; }
+
+    [SugarColumn(ColumnName = "FWSTRSPROID", ColumnDescription = "检验频率", IsNullable = true, ColumnDataType = "bigint")]
+    public long? InspectionFrequencyId { get; set; }
+
+    [SugarColumn(ColumnName = "FROCESSSEQ", ColumnDescription = "工序顺序", IsNullable = true, Length = 255)]
+    public string? ProcessSeq { get; set; }
+
+    [SugarColumn(ColumnName = "jylx", ColumnDescription = "检验类型", IsNullable = true, ColumnDataType = "bigint")]
+    public long? InspectionType { get; set; }
+}
+
+[SugarTable("qms_jygfzb", "原材料检验规范-检验明细")]
+public class AdoS0QmsRawInspectionSpecEntry
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "glid", ColumnDescription = "主表ID", IsNullable = true, ColumnDataType = "bigint")]
+    public long? MasterId { get; set; }
+
+    [SugarColumn(ColumnName = "xh", ColumnDescription = "序号", IsNullable = true, ColumnDataType = "int")]
+    public int? Seq { get; set; }
+
+    [SugarColumn(ColumnName = "jyxm", ColumnDescription = "检验项目", IsNullable = true, ColumnDataType = "text")]
+    public string? InspectionItem { get; set; }
+
+    [SugarColumn(ColumnName = "jybz", ColumnDescription = "检验标准", IsNullable = true, ColumnDataType = "text")]
+    public string? InspectionStandard { get; set; }
+
+    [SugarColumn(ColumnName = "jygjjyff", ColumnDescription = "检验工具/检验方法", IsNullable = true, ColumnDataType = "text")]
+    public string? InspectionMethod { get; set; }
+
+    [SugarColumn(ColumnName = "txfl", ColumnDescription = "图像分类", IsNullable = true, ColumnDataType = "text")]
+    public string? ImageCategory { get; set; }
+
+    [SugarColumn(ColumnName = "cyfa", ColumnDescription = "抽样方案", IsNullable = true, ColumnDataType = "text")]
+    public string? SamplingScheme { get; set; }
+
+    [SugarColumn(ColumnName = "zs", ColumnDescription = "注释", IsNullable = true, ColumnDataType = "text")]
+    public string? Remark { get; set; }
+
+    [SugarColumn(ColumnName = "fj", ColumnDescription = "附件", IsNullable = true, ColumnDataType = "text")]
+    public string? Attachment { get; set; }
+
+    [SugarColumn(ColumnName = "sx", ColumnDescription = "上限", IsNullable = true, Length = 50)]
+    public string? UpperLimit { get; set; }
+
+    [SugarColumn(ColumnName = "xx", ColumnDescription = "下限", IsNullable = true, Length = 50)]
+    public string? LowerLimit { get; set; }
+}
+
+[SugarTable("qms_gcjygfzb", "过程检验规范-检验明细")]
+public class AdoS0QmsProcessInspectionSpecEntry
+{
+    [SugarColumn(ColumnName = "id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "glid", ColumnDescription = "主表ID", IsNullable = true, ColumnDataType = "bigint")]
+    public long? MasterId { get; set; }
+
+    [SugarColumn(ColumnName = "gxdh", ColumnDescription = "工序代号", IsNullable = true, ColumnDataType = "text")]
+    public string? OperationCode { get; set; }
+
+    [SugarColumn(ColumnName = "gxmc", ColumnDescription = "工序名称", IsNullable = true, ColumnDataType = "text")]
+    public string? OperationName { get; set; }
+
+    [SugarColumn(ColumnName = "jyxm", ColumnDescription = "检验项目", IsNullable = true, ColumnDataType = "text")]
+    public string? InspectionItem { get; set; }
+
+    [SugarColumn(ColumnName = "jyff", ColumnDescription = "检验方法", IsNullable = true, ColumnDataType = "text")]
+    public string? InspectionMethod { get; set; }
+
+    [SugarColumn(ColumnName = "jygg", ColumnDescription = "检验规格", IsNullable = true, ColumnDataType = "text")]
+    public string? InspectionSpec { get; set; }
+
+    [SugarColumn(ColumnName = "txfl", ColumnDescription = "图像分类", IsNullable = true, ColumnDataType = "text")]
+    public string? ImageCategory { get; set; }
+
+    [SugarColumn(ColumnName = "jypc", ColumnDescription = "检验频次", IsNullable = true, ColumnDataType = "text")]
+    public string? InspectionFrequency { get; set; }
+
+    [SugarColumn(ColumnName = "jsbz", ColumnDescription = "技术标准", IsNullable = true, ColumnDataType = "text")]
+    public string? TechnicalStandard { get; set; }
+
+    [SugarColumn(ColumnName = "PeelingForce", ColumnDescription = "PeelingForce", IsNullable = true, ColumnDataType = "bigint")]
+    public long? PeelingForce { get; set; }
+
+    [SugarColumn(ColumnName = "sx", ColumnDescription = "上限", IsNullable = true, Length = 50)]
+    public string? UpperLimit { get; set; }
+
+    [SugarColumn(ColumnName = "xx", ColumnDescription = "下限", IsNullable = true, Length = 50)]
+    public string? LowerLimit { get; set; }
+}

+ 57 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs

@@ -88,6 +88,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             list.Add(m);
         foreach (var m in BuildS0ManufacturingMenus(ct))
             list.Add(m);
+        foreach (var m in BuildS0QualityMenus(ct))
+            list.Add(m);
         foreach (var m in BuildS0WarehouseMenus(ct))
             list.Add(m);
 
@@ -383,6 +385,61 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
         };
     }
 
+    private static IEnumerable<SysMenu> BuildS0QualityMenus(DateTime ct)
+    {
+        const long s0DataModelingMenuId = 1322000000001L;
+        const long subDirId = 1329004500000L;
+
+        yield return new SysMenu
+        {
+            Id = subDirId,
+            Pid = s0DataModelingMenuId,
+            Title = "质量建模",
+            Path = "/aidop/s0/quality",
+            Name = "aidopS0Quality",
+            Component = "Layout",
+            Icon = "ele-DataAnalysis",
+            Type = MenuTypeEnum.Dir,
+            CreateTime = ct,
+            OrderNo = 45,
+            Remark = "S0 质量建模"
+        };
+
+        var leaves = new (long IdOff, string Path, string Name, string Title, string Component, int Order)[]
+        {
+            (1, "/aidop/s0/quality/dictionary", "aidopS0QlyDictionary", "质量字典", "/aidop/s0/quality/QualityDictionaryPage", 10),
+            (2, "/aidop/s0/quality/raw-whitelist", "aidopS0QlyRawWhitelist", "原材料白名单", "/aidop/s0/quality/RawWhitelistList", 20),
+            (3, "/aidop/s0/quality/sampling-scheme", "aidopS0QlySamplingScheme", "抽样方案", "/aidop/s0/quality/SamplingSchemeList", 30),
+            (4, "/aidop/s0/quality/instrument", "aidopS0QlyInstrument", "检验仪器", "/aidop/s0/quality/InspectionInstrumentList", 40),
+            (5, "/aidop/s0/quality/inspection-method", "aidopS0QlyInspectionMethod", "检验方法", "/aidop/s0/quality/InspectionMethodList", 50),
+            (6, "/aidop/s0/quality/inspection-basis", "aidopS0QlyInspectionBasis", "检验依据", "/aidop/s0/quality/InspectionBasisList", 60),
+            (7, "/aidop/s0/quality/inspection-standard", "aidopS0QlyInspectionStandard", "检验标准", "/aidop/s0/quality/InspectionStandardList", 70),
+            (8, "/aidop/s0/quality/inspection-item", "aidopS0QlyInspectionItem", "检验项目", "/aidop/s0/quality/InspectionItemList", 80),
+            (9, "/aidop/s0/quality/inspection-frequency", "aidopS0QlyInspectionFrequency", "检验频率", "/aidop/s0/quality/InspectionFrequencyList", 90),
+            (10, "/aidop/s0/quality/inspection-plan", "aidopS0QlyInspectionPlan", "检验方案", "/aidop/s0/quality/InspectionPlanList", 100),
+            (11, "/aidop/s0/quality/raw-inspection-spec", "aidopS0QlyRawInspectionSpec", "原材料检验规范", "/aidop/s0/quality/RawInspectionSpecList", 110),
+            (12, "/aidop/s0/quality/process-inspection-spec", "aidopS0QlyProcessInspectionSpec", "过程检验规范", "/aidop/s0/quality/ProcessInspectionSpecList", 120),
+        };
+
+        foreach (var (idOff, path, name, title, component, order) in leaves)
+        {
+            yield return new SysMenu
+            {
+                Id = subDirId + idOff,
+                Pid = subDirId,
+                Title = title,
+                Path = path,
+                Name = name,
+                Component = component,
+                Icon = "ele-Document",
+                Type = MenuTypeEnum.Menu,
+                CreateTime = ct,
+                OrderNo = order,
+                Remark = $"S0 {title}"
+            };
+        }
+    }
+
     private static IEnumerable<SysMenu> BuildS0WarehouseMenus(DateTime ct)
     {
         const long s0DataModelingMenuId = 1322000000001L;

+ 0 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Startup.cs

@@ -66,9 +66,6 @@ public class Startup : AppStartup
         using var scope = app.ApplicationServices.CreateScope();
         var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
         db.CodeFirst.InitTables(
-            typeof(AdoOrder),
-            typeof(AdoPlan),
-            typeof(AdoWorkOrder),
             typeof(AdoS0CustMaster),
             typeof(AdoS0ItemMaster),
             typeof(AdoS0ItemSubstituteDetail),