Browse Source

产销建模前后端开发完成&单元测试通过

YY968XX 1 month ago
parent
commit
42d74f5da7
25 changed files with 4199 additions and 117 deletions
  1. 272 0
      Web/src/views/aidop/s0/api/s0SalesApi.ts
  2. 426 0
      Web/src/views/aidop/s0/sales/CustomerList.vue
  3. 534 0
      Web/src/views/aidop/s0/sales/MaterialList.vue
  4. 363 0
      Web/src/views/aidop/s0/sales/OrderPriorityRuleList.vue
  5. 269 42
      doc/plan/S0/Batch2-产销建模迁移方案.md
  6. 577 0
      doc/plan/S0/Batch3-制造建模迁移方案.md
  7. 47 69
      doc/plan/S0/S0迁移.md
  8. 351 0
      doc/plan/S0/spec.md
  9. 117 2
      server/Admin.NET.sln
  10. 22 0
      server/Plugins/Admin.NET.Plugin.AiDOP.Tests/Admin.NET.Plugin.AiDOP.Tests.csproj
  11. 49 0
      server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Sales/AdoS0SalesRulesTests.cs
  12. 30 0
      server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Sales/PagingGuardTests.cs
  13. 132 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0CustomersController.cs
  14. 185 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0MaterialsController.cs
  15. 133 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0OrderPriorityRulesController.cs
  16. 129 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Sales/AdoS0SalesDtos.cs
  17. 69 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0Customer.cs
  18. 137 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0Material.cs
  19. 65 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0OrderPriorityRule.cs
  20. 26 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AdoS0SalesRules.cs
  21. 87 0
      server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0DictDataSeedData.cs
  22. 24 0
      server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0DictTypeSeedData.cs
  23. 64 0
      server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0OrgSeedData.cs
  24. 82 3
      server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs
  25. 9 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Startup.cs

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

@@ -0,0 +1,272 @@
+import service from '/@/utils/request';
+import { SysDictDataApi, SysOrgApi } from '/@/api-services';
+
+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 {
+	label: string;
+	value: string | number;
+}
+
+export interface OrgOption {
+	id: number;
+	pid?: number | null;
+	name?: string | null;
+	code?: string | null;
+	type?: string | null;
+}
+
+export interface S0CustomerRow {
+	id: number;
+	companyRefId: number;
+	factoryRefId: number;
+	code: string;
+	name: string;
+	nameEn?: string | null;
+	customerType?: string | null;
+	contactPerson?: string | null;
+	contactPhone?: string | null;
+	address?: string | null;
+	forbidStatus: string;
+	currency?: string | null;
+	isTaxIncluded: boolean;
+	primarySales?: string | null;
+	backupSales?: string | null;
+	customerLevel: number;
+	remark?: string | null;
+	isEnabled: boolean;
+	createdAt?: string;
+	updatedAt?: string | null;
+}
+
+export interface S0CustomerUpsert {
+	companyRefId: number;
+	factoryRefId: number;
+	code: string;
+	name: string;
+	nameEn?: string;
+	customerType?: string;
+	contactPerson?: string;
+	contactPhone?: string;
+	address?: string;
+	currency?: string;
+	isTaxIncluded: boolean;
+	primarySales?: string;
+	backupSales?: string;
+	customerLevel: number;
+	remark?: string;
+	isEnabled: boolean;
+}
+
+export interface S0MaterialRow {
+	id: number;
+	companyRefId: number;
+	factoryRefId: number;
+	code: string;
+	name: string;
+	nameEn?: string | null;
+	materialType?: string | null;
+	unit?: string | null;
+	spec?: string | null;
+	plCategory?: string | null;
+	drawingNo?: string | null;
+	language?: string | null;
+	bizVersion?: string | null;
+	productCode?: string | null;
+	materialAttribute?: string | null;
+	defaultLocationId?: number | null;
+	defaultRackId?: number | null;
+	stockTypeCode?: string | null;
+	safetyStock?: number | null;
+	shelfLifeDays?: number | null;
+	expireWarningDays?: number | null;
+	purchaseLeadDays?: number | null;
+	minOrderQty?: number | null;
+	maxOrderQty?: number | null;
+	orderMultiple?: number | null;
+	preparationLeadDays?: number | null;
+	isOnDemand: boolean;
+	specialReqType?: string | null;
+	isInspectionRequired: boolean;
+	inspectionDays?: number | null;
+	isKeyMaterial: boolean;
+	isMainMaterial: boolean;
+	isPreprocess: boolean;
+	isAutoBatch: boolean;
+	isLabelRequired: boolean;
+	isBatchFifoReminder: boolean;
+	isBatchFifoStrict: boolean;
+	inventoryTurnoverRate?: number | null;
+	forbidStatus: string;
+	remark?: string | null;
+	isEnabled: boolean;
+	createdAt?: string;
+	updatedAt?: string | null;
+}
+
+export interface S0MaterialUpsert {
+	companyRefId: number;
+	factoryRefId: number;
+	code: string;
+	name: string;
+	nameEn?: string;
+	materialType?: string;
+	unit?: string;
+	spec?: string;
+	plCategory?: string;
+	drawingNo?: string;
+	language?: string;
+	bizVersion?: string;
+	productCode?: string;
+	materialAttribute?: string;
+	defaultLocationId?: number | null;
+	defaultRackId?: number | null;
+	stockTypeCode?: string;
+	safetyStock?: number | null;
+	shelfLifeDays?: number | null;
+	expireWarningDays?: number | null;
+	purchaseLeadDays?: number | null;
+	minOrderQty?: number | null;
+	maxOrderQty?: number | null;
+	orderMultiple?: number | null;
+	preparationLeadDays?: number | null;
+	isOnDemand: boolean;
+	specialReqType?: string;
+	isInspectionRequired: boolean;
+	inspectionDays?: number | null;
+	isKeyMaterial: boolean;
+	isMainMaterial: boolean;
+	isPreprocess: boolean;
+	isAutoBatch: boolean;
+	isLabelRequired: boolean;
+	isBatchFifoReminder: boolean;
+	isBatchFifoStrict: boolean;
+	inventoryTurnoverRate?: number | null;
+	remark?: string;
+	isEnabled: boolean;
+}
+
+export interface S0OrderPriorityRuleRow {
+	id: number;
+	companyRefId: number;
+	factoryRefId: number;
+	code: string;
+	name: string;
+	priorityLevel: number;
+	sortDirection: string;
+	sourceEntity?: string | null;
+	sourceField?: string | null;
+	sourceFieldType?: string | null;
+	sourceLinkField?: string | null;
+	workOrderField?: string | null;
+	workOrderFieldType?: string | null;
+	workOrderLinkField?: string | null;
+	ruleExpr?: string | null;
+	remark?: string | null;
+	isEnabled: boolean;
+	createdAt?: string;
+	updatedAt?: string | null;
+}
+
+export interface S0OrderPriorityRuleUpsert {
+	companyRefId: number;
+	factoryRefId: number;
+	code?: string;
+	name: string;
+	priorityLevel: number;
+	sortDirection: string;
+	sourceEntity?: string;
+	sourceField?: string;
+	sourceFieldType?: string;
+	sourceLinkField?: string;
+	workOrderField?: string;
+	workOrderFieldType?: string;
+	workOrderLinkField?: string;
+	ruleExpr?: string;
+	remark?: string;
+	isEnabled: boolean;
+}
+
+export interface ToggleEnabledBody {
+	isEnabled: boolean;
+}
+
+export const s0CustomersApi = {
+	list: (params: Record<string, unknown>) =>
+		service.get<Paged<S0CustomerRow>>('/api/s0/sales/customers', { params }).then(unwrap),
+	get: (id: number) => service.get<S0CustomerRow>(`/api/s0/sales/customers/${id}`).then(unwrap),
+	create: (body: S0CustomerUpsert) =>
+		service.post<S0CustomerRow>('/api/s0/sales/customers', body).then(unwrap),
+	update: (id: number, body: S0CustomerUpsert) =>
+		service.put<S0CustomerRow>(`/api/s0/sales/customers/${id}`, body).then(unwrap),
+	delete: (id: number) => service.delete(`/api/s0/sales/customers/${id}`).then(unwrap),
+	toggleEnabled: (id: number, body: ToggleEnabledBody) =>
+		service.patch<S0CustomerRow>(`/api/s0/sales/customers/${id}/toggle-enabled`, body).then(unwrap),
+};
+
+export const s0MaterialsApi = {
+	list: (params: Record<string, unknown>) =>
+		service.get<Paged<S0MaterialRow>>('/api/s0/sales/materials', { params }).then(unwrap),
+	get: (id: number) => service.get<S0MaterialRow>(`/api/s0/sales/materials/${id}`).then(unwrap),
+	create: (body: S0MaterialUpsert) =>
+		service.post<S0MaterialRow>('/api/s0/sales/materials', body).then(unwrap),
+	update: (id: number, body: S0MaterialUpsert) =>
+		service.put<S0MaterialRow>(`/api/s0/sales/materials/${id}`, body).then(unwrap),
+	delete: (id: number) => service.delete(`/api/s0/sales/materials/${id}`).then(unwrap),
+	toggleEnabled: (id: number, body: ToggleEnabledBody) =>
+		service.patch<S0MaterialRow>(`/api/s0/sales/materials/${id}/toggle-enabled`, body).then(unwrap),
+};
+
+export const s0OrderPriorityRulesApi = {
+	list: (params: Record<string, unknown>) =>
+		service.get<Paged<S0OrderPriorityRuleRow>>('/api/s0/sales/order-priority-rules', { params }).then(unwrap),
+	get: (id: number) =>
+		service.get<S0OrderPriorityRuleRow>(`/api/s0/sales/order-priority-rules/${id}`).then(unwrap),
+	create: (body: S0OrderPriorityRuleUpsert) =>
+		service.post<S0OrderPriorityRuleRow>('/api/s0/sales/order-priority-rules', body).then(unwrap),
+	update: (id: number, body: S0OrderPriorityRuleUpsert) =>
+		service.put<S0OrderPriorityRuleRow>(`/api/s0/sales/order-priority-rules/${id}`, body).then(unwrap),
+	delete: (id: number) => service.delete(`/api/s0/sales/order-priority-rules/${id}`).then(unwrap),
+	toggleEnabled: (id: number, body: ToggleEnabledBody) =>
+		service.patch<S0OrderPriorityRuleRow>(`/api/s0/sales/order-priority-rules/${id}/toggle-enabled`, body).then(unwrap),
+};
+
+export async function loadDictOptions(code: string): Promise<OptionItem[]> {
+	try {
+		const api = new SysDictDataApi(undefined, undefined, service);
+		const res = await api.apiSysDictDataDataListCodeGet(code);
+		return (res.data.result ?? []).map((d) => ({
+			label: d.label ?? '',
+			value: d.value ?? '',
+		}));
+	} catch (e) {
+		console.warn(`[s0] loadDictOptions(${code}) failed`, e);
+		return [];
+	}
+}
+
+export async function loadOrgList(type?: string): Promise<OrgOption[]> {
+	try {
+		const api = new SysOrgApi(undefined, undefined, service);
+		const res = await api.apiSysOrgListGet(0, undefined, undefined, type);
+		return (res.data.result ?? []).map((item) => ({
+			id: item.id ?? 0,
+			pid: item.pid,
+			name: item.name,
+			code: item.code,
+			type: item.type,
+		}));
+	} catch (e) {
+		console.warn(`[s0] loadOrgList(${type}) failed`, e);
+		return [];
+	}
+}

+ 426 - 0
Web/src/views/aidop/s0/sales/CustomerList.vue

@@ -0,0 +1,426 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="S0 / Sales / 客户管理">
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="关键字">
+				<el-input v-model="query.keyword" placeholder="编码/名称" clearable style="width: 180px" />
+			</el-form-item>
+			<el-form-item label="公司">
+				<el-select v-model="query.companyRefId" clearable filterable placeholder="全部" style="width: 180px">
+					<el-option v-for="item in companyOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="工厂">
+				<el-select v-model="query.factoryRefId" clearable filterable placeholder="全部" style="width: 180px">
+					<el-option v-for="item in filteredFactoryOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="启用">
+				<el-select v-model="query.isEnabled" clearable placeholder="全部" style="width: 120px">
+					<el-option label="启用" :value="true" />
+					<el-option label="禁用" :value="false" />
+				</el-select>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="loadList">查询</el-button>
+				<el-button @click="resetQuery">重置</el-button>
+				<el-button type="success" @click="openCreate">新增客户</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%">
+			<el-table-column prop="code" label="编码" width="140" show-overflow-tooltip />
+			<el-table-column prop="name" label="名称" min-width="180" show-overflow-tooltip />
+			<el-table-column prop="nameEn" label="英文名" min-width="160" show-overflow-tooltip />
+			<el-table-column prop="customerType" label="客户类型" width="120" show-overflow-tooltip />
+			<el-table-column prop="contactPerson" label="联系人" width="120" show-overflow-tooltip />
+			<el-table-column prop="contactPhone" label="电话" width="140" show-overflow-tooltip />
+			<el-table-column label="启用" width="90" align="center">
+				<template #default="{ row }">
+					<el-tag :type="row.isEnabled ? 'success' : 'info'" size="small">
+						{{ row.isEnabled ? '是' : '否' }}
+					</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column label="操作" width="240" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isEnabled ? 'warning' : 'success'" @click="toggleEnabled(row)">
+						{{ row.isEnabled ? '禁用' : '启用' }}
+					</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="680px" destroy-on-close @closed="resetForm">
+			<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+				<el-row :gutter="16">
+					<el-col :span="12">
+						<el-form-item label="公司" prop="companyRefId">
+							<el-select v-model="form.companyRefId" filterable style="width: 100%">
+								<el-option v-for="item in companyOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="工厂" prop="factoryRefId">
+							<el-select v-model="form.factoryRefId" filterable style="width: 100%">
+								<el-option v-for="item in formFactoryOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="编码" prop="code">
+							<el-input v-model="form.code" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="名称" prop="name">
+							<el-input v-model="form.name" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="英文名">
+							<el-input v-model="form.nameEn" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="客户类型">
+							<el-select v-model="form.customerType" clearable filterable style="width: 100%">
+								<el-option v-for="item in customerTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="联系人">
+							<el-input v-model="form.contactPerson" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="联系电话">
+							<el-input v-model="form.contactPhone" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="24">
+						<el-form-item label="地址">
+							<el-input v-model="form.address" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="币种">
+							<el-select v-model="form.currency" clearable filterable style="width: 100%">
+								<el-option v-for="item in currencyOptions" :key="item.value" :label="item.label" :value="item.value" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="客户等级" prop="customerLevel">
+							<el-input-number v-model="form.customerLevel" :min="1" :max="9" style="width: 100%" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="主销售员">
+							<el-input v-model="form.primarySales" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="备用销售员">
+							<el-input v-model="form.backupSales" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="是否含税">
+							<el-switch v-model="form.isTaxIncluded" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="启用">
+							<el-switch v-model="form.isEnabled" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="24">
+						<el-form-item label="备注">
+							<el-input v-model="form.remark" type="textarea" rows="3" />
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<el-button @click="dialogVisible = false">取消</el-button>
+				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+			</template>
+		</el-dialog>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS0SalesCustomer">
+import { computed, onMounted, reactive, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
+import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import {
+	loadDictOptions,
+	loadOrgList,
+	s0CustomersApi,
+	type OrgOption,
+	type OptionItem,
+	type S0CustomerRow,
+	type S0CustomerUpsert,
+} from '../api/s0SalesApi';
+
+const route = useRoute();
+const pageTitle = computed(() => (route.meta?.title as string) || '客户管理');
+
+const query = reactive({
+	keyword: '',
+	companyRefId: undefined as number | undefined,
+	factoryRefId: undefined as number | undefined,
+	isEnabled: undefined as boolean | undefined,
+	page: 1,
+	pageSize: 20,
+});
+
+const loading = ref(false);
+const rows = ref<S0CustomerRow[]>([]);
+const total = ref(0);
+
+const dialogVisible = ref(false);
+const dialogTitle = ref('新增客户');
+const editingId = ref<number | null>(null);
+const saving = ref(false);
+const formRef = ref<FormInstance>();
+
+const companyOptions = ref<OrgOption[]>([]);
+const factoryOptions = ref<OrgOption[]>([]);
+const customerTypeOptions = ref<OptionItem[]>([]);
+const currencyOptions = ref<OptionItem[]>([]);
+
+const form = reactive<S0CustomerUpsert>({
+	companyRefId: 0,
+	factoryRefId: 0,
+	code: '',
+	name: '',
+	nameEn: '',
+	customerType: '',
+	contactPerson: '',
+	contactPhone: '',
+	address: '',
+	currency: '',
+	isTaxIncluded: true,
+	primarySales: '',
+	backupSales: '',
+	customerLevel: 1,
+	remark: '',
+	isEnabled: true,
+});
+
+const rules: FormRules = {
+	companyRefId: [{ required: true, message: '请选择公司', trigger: 'change' }],
+	factoryRefId: [{ required: true, message: '请选择工厂', trigger: 'change' }],
+	code: [{ required: true, message: '请填写编码', trigger: 'blur' }],
+	name: [{ required: true, message: '请填写名称', trigger: 'blur' }],
+	customerLevel: [{ required: true, message: '请填写客户等级', trigger: 'change' }],
+};
+
+const filteredFactoryOptions = computed(() => {
+	if (!query.companyRefId) return factoryOptions.value;
+	return factoryOptions.value.filter((item) => item.pid === query.companyRefId);
+});
+
+const formFactoryOptions = computed(() => {
+	if (!form.companyRefId) return factoryOptions.value;
+	return factoryOptions.value.filter((item) => item.pid === form.companyRefId);
+});
+
+watch(
+	() => form.companyRefId,
+	() => {
+		if (!formFactoryOptions.value.some((item) => item.id === form.factoryRefId)) {
+			form.factoryRefId = 0;
+		}
+	}
+);
+
+watch(
+	() => query.companyRefId,
+	() => {
+		if (!filteredFactoryOptions.value.some((item) => item.id === query.factoryRefId)) {
+			query.factoryRefId = undefined;
+		}
+	}
+);
+
+async function loadOptions() {
+	const [companies, factories, customerTypes, currencies] = await Promise.all([
+		loadOrgList('201'),
+		loadOrgList('501'),
+		loadDictOptions('s0_customer_type'),
+		loadDictOptions('s0_currency'),
+	]);
+	companyOptions.value = companies;
+	factoryOptions.value = factories;
+	customerTypeOptions.value = customerTypes;
+	currencyOptions.value = currencies;
+
+	if (!companies.length || !factories.length || !customerTypes.length || !currencies.length) {
+		ElMessage.warning('部分下拉数据加载失败,请检查平台字典或组织种子');
+	}
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await s0CustomersApi.list({
+			page: query.page,
+			pageSize: query.pageSize,
+			keyword: query.keyword || undefined,
+			companyRefId: query.companyRefId || undefined,
+			factoryRefId: query.factoryRefId || undefined,
+			isEnabled: query.isEnabled,
+		});
+		rows.value = data.list;
+		total.value = data.total;
+	} catch {
+		rows.value = [];
+		total.value = 0;
+	}
+	finally {
+		loading.value = false;
+	}
+}
+
+function resetQuery() {
+	query.keyword = '';
+	query.companyRefId = undefined;
+	query.factoryRefId = undefined;
+	query.isEnabled = undefined;
+	query.page = 1;
+	void loadList();
+}
+
+function resetForm() {
+	editingId.value = null;
+	Object.assign(form, {
+		companyRefId: 0,
+		factoryRefId: 0,
+		code: '',
+		name: '',
+		nameEn: '',
+		customerType: '',
+		contactPerson: '',
+		contactPhone: '',
+		address: '',
+		currency: '',
+		isTaxIncluded: true,
+		primarySales: '',
+		backupSales: '',
+		customerLevel: 1,
+		remark: '',
+		isEnabled: true,
+	});
+	formRef.value?.clearValidate();
+}
+
+function openCreate() {
+	resetForm();
+	dialogTitle.value = '新增客户';
+	dialogVisible.value = true;
+}
+
+function openEdit(row: S0CustomerRow) {
+	resetForm();
+	editingId.value = row.id;
+	dialogTitle.value = `编辑客户 ${row.code}`;
+	Object.assign(form, {
+		companyRefId: row.companyRefId,
+		factoryRefId: row.factoryRefId,
+		code: row.code,
+		name: row.name,
+		nameEn: row.nameEn || '',
+		customerType: row.customerType || '',
+		contactPerson: row.contactPerson || '',
+		contactPhone: row.contactPhone || '',
+		address: row.address || '',
+		currency: row.currency || '',
+		isTaxIncluded: row.isTaxIncluded,
+		primarySales: row.primarySales || '',
+		backupSales: row.backupSales || '',
+		customerLevel: row.customerLevel,
+		remark: row.remark || '',
+		isEnabled: row.isEnabled,
+	});
+	dialogVisible.value = true;
+}
+
+async function submitForm() {
+	await formRef.value?.validate();
+	saving.value = true;
+	try {
+		const payload: S0CustomerUpsert = { ...form };
+		if (editingId.value) {
+			await s0CustomersApi.update(editingId.value, payload);
+			ElMessage.success('已保存');
+		} else {
+			await s0CustomersApi.create(payload);
+			ElMessage.success('已创建');
+		}
+		dialogVisible.value = false;
+		await loadList();
+	} finally {
+		saving.value = false;
+	}
+}
+
+function onDelete(row: S0CustomerRow) {
+	ElMessageBox.confirm(`确定删除客户「${row.name}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await s0CustomersApi.delete(row.id);
+			ElMessage.success('已删除');
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+function toggleEnabled(row: S0CustomerRow) {
+	const nextEnabled = !row.isEnabled;
+	const actionText = nextEnabled ? '启用' : '禁用';
+	ElMessageBox.confirm(`确定${actionText}客户「${row.name}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await s0CustomersApi.toggleEnabled(row.id, { isEnabled: nextEnabled });
+			ElMessage.success(`${actionText}成功`);
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+onMounted(async () => {
+	await loadOptions();
+	await 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>

+ 534 - 0
Web/src/views/aidop/s0/sales/MaterialList.vue

@@ -0,0 +1,534 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="S0 / Sales / 物料管理">
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="编码">
+				<el-input v-model="query.code" clearable style="width: 150px" />
+			</el-form-item>
+			<el-form-item label="名称">
+				<el-input v-model="query.name" clearable style="width: 160px" />
+			</el-form-item>
+			<el-form-item label="规格">
+				<el-input v-model="query.spec" clearable style="width: 140px" />
+			</el-form-item>
+			<el-form-item label="图纸编号">
+				<el-input v-model="query.drawingNo" clearable style="width: 150px" />
+			</el-form-item>
+			<el-form-item label="计划类别">
+				<el-select v-model="query.plCategory" clearable filterable style="width: 140px">
+					<el-option v-for="item in plCategoryOptions" :key="item.value" :label="item.label" :value="item.value" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="物料类型">
+				<el-select v-model="query.materialType" clearable filterable style="width: 140px">
+					<el-option v-for="item in materialTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="语言">
+				<el-input v-model="query.language" clearable style="width: 120px" />
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="loadList">查询</el-button>
+				<el-button @click="resetQuery">重置</el-button>
+				<el-button type="success" @click="openCreate">新增物料</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%">
+			<el-table-column prop="code" label="编码" width="140" show-overflow-tooltip />
+			<el-table-column prop="name" label="名称" min-width="180" show-overflow-tooltip />
+			<el-table-column prop="spec" label="规格" min-width="150" show-overflow-tooltip />
+			<el-table-column prop="unit" label="单位" width="90" show-overflow-tooltip />
+			<el-table-column prop="materialType" label="物料类型" width="120" show-overflow-tooltip />
+			<el-table-column prop="plCategory" label="计划类别" width="120" show-overflow-tooltip />
+			<el-table-column label="启用" width="90" align="center">
+				<template #default="{ row }">
+					<el-tag :type="row.isEnabled ? 'success' : 'info'" size="small">
+						{{ row.isEnabled ? '是' : '否' }}
+					</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column label="操作" width="240" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isEnabled ? 'warning' : 'success'" @click="toggleEnabled(row)">
+						{{ row.isEnabled ? '禁用' : '启用' }}
+					</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="900px" destroy-on-close @closed="resetForm">
+			<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+				<el-tabs v-model="activeTab">
+					<el-tab-pane label="归属" name="owner">
+						<el-row :gutter="16">
+							<el-col :span="12">
+								<el-form-item label="公司" prop="companyRefId">
+									<el-select v-model="form.companyRefId" filterable style="width: 100%">
+										<el-option v-for="item in companyOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+							<el-col :span="12">
+								<el-form-item label="工厂" prop="factoryRefId">
+									<el-select v-model="form.factoryRefId" filterable style="width: 100%">
+										<el-option v-for="item in formFactoryOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+						</el-row>
+					</el-tab-pane>
+
+					<el-tab-pane label="基本信息" name="basic">
+						<el-row :gutter="16">
+							<el-col :span="12"><el-form-item label="编码" prop="code"><el-input v-model="form.code" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="名称" prop="name"><el-input v-model="form.name" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="英文名"><el-input v-model="form.nameEn" /></el-form-item></el-col>
+							<el-col :span="12">
+								<el-form-item label="物料类型">
+									<el-select v-model="form.materialType" clearable filterable style="width: 100%">
+										<el-option v-for="item in materialTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+							<el-col :span="12"><el-form-item label="单位"><el-input v-model="form.unit" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="规格"><el-input v-model="form.spec" /></el-form-item></el-col>
+							<el-col :span="12">
+								<el-form-item label="计划类别">
+									<el-select v-model="form.plCategory" clearable filterable style="width: 100%">
+										<el-option v-for="item in plCategoryOptions" :key="item.value" :label="item.label" :value="item.value" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+							<el-col :span="12"><el-form-item label="图纸编号"><el-input v-model="form.drawingNo" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="语言"><el-input v-model="form.language" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="业务版本"><el-input v-model="form.bizVersion" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="产品编码"><el-input v-model="form.productCode" /></el-form-item></el-col>
+							<el-col :span="12">
+								<el-form-item label="物料属性">
+									<el-select v-model="form.materialAttribute" clearable filterable style="width: 100%">
+										<el-option v-for="item in materialAttributeOptions" :key="item.value" :label="item.label" :value="item.value" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+						</el-row>
+					</el-tab-pane>
+
+					<el-tab-pane label="库存参数" name="stock">
+						<el-row :gutter="16">
+							<el-col :span="12">
+								<el-form-item label="库存类型">
+									<el-select v-model="form.stockTypeCode" clearable filterable style="width: 100%">
+										<el-option v-for="item in stockTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+							<el-col :span="12"><el-form-item label="安全库存"><el-input-number v-model="form.safetyStock" :min="0" :precision="5" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="保质期(天)"><el-input-number v-model="form.shelfLifeDays" :min="0" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="预警天数"><el-input-number v-model="form.expireWarningDays" :min="0" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="默认库位ID"><el-input-number v-model="form.defaultLocationId" :min="0" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="默认货架ID"><el-input-number v-model="form.defaultRackId" :min="0" style="width: 100%" /></el-form-item></el-col>
+						</el-row>
+					</el-tab-pane>
+
+					<el-tab-pane label="采购参数" name="purchase">
+						<el-row :gutter="16">
+							<el-col :span="12"><el-form-item label="采购提前期"><el-input-number v-model="form.purchaseLeadDays" :min="0" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="最小订货量"><el-input-number v-model="form.minOrderQty" :min="0" :precision="5" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="最大订货量"><el-input-number v-model="form.maxOrderQty" :min="0" :precision="5" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="订货倍数"><el-input-number v-model="form.orderMultiple" :min="0" :precision="5" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="备料提前期"><el-input-number v-model="form.preparationLeadDays" :min="0" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="按需采购"><el-switch v-model="form.isOnDemand" /></el-form-item></el-col>
+							<el-col :span="12">
+								<el-form-item label="特殊需求">
+									<el-select v-model="form.specialReqType" clearable filterable style="width: 100%">
+										<el-option v-for="item in specialReqTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+									</el-select>
+								</el-form-item>
+							</el-col>
+						</el-row>
+					</el-tab-pane>
+
+					<el-tab-pane label="质量/管控" name="quality">
+						<el-row :gutter="16">
+							<el-col :span="12"><el-form-item label="需检验"><el-switch v-model="form.isInspectionRequired" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="检验天数"><el-input-number v-model="form.inspectionDays" :min="0" style="width: 100%" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="关键物料"><el-switch v-model="form.isKeyMaterial" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="主要物料"><el-switch v-model="form.isMainMaterial" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="需预处理"><el-switch v-model="form.isPreprocess" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="自动批次"><el-switch v-model="form.isAutoBatch" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="需打标签"><el-switch v-model="form.isLabelRequired" /></el-form-item></el-col>
+						</el-row>
+					</el-tab-pane>
+
+					<el-tab-pane label="批次管理" name="batch">
+						<el-row :gutter="16">
+							<el-col :span="12"><el-form-item label="FIFO提醒"><el-switch v-model="form.isBatchFifoReminder" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="FIFO严格"><el-switch v-model="form.isBatchFifoStrict" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="库存周转率"><el-input-number v-model="form.inventoryTurnoverRate" :min="0" :precision="5" style="width: 100%" /></el-form-item></el-col>
+						</el-row>
+					</el-tab-pane>
+
+					<el-tab-pane label="状态" name="status">
+						<el-row :gutter="16">
+							<el-col :span="12"><el-form-item label="启用"><el-switch v-model="form.isEnabled" /></el-form-item></el-col>
+							<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" rows="3" /></el-form-item></el-col>
+						</el-row>
+					</el-tab-pane>
+				</el-tabs>
+			</el-form>
+			<template #footer>
+				<el-button @click="dialogVisible = false">取消</el-button>
+				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+			</template>
+		</el-dialog>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS0SalesMaterial">
+import { computed, onMounted, reactive, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
+import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import {
+	loadDictOptions,
+	loadOrgList,
+	s0MaterialsApi,
+	type OptionItem,
+	type OrgOption,
+	type S0MaterialRow,
+	type S0MaterialUpsert,
+} from '../api/s0SalesApi';
+
+const route = useRoute();
+const pageTitle = computed(() => (route.meta?.title as string) || '物料管理');
+
+const activeTab = ref('owner');
+const query = reactive({
+	code: '',
+	name: '',
+	spec: '',
+	drawingNo: '',
+	plCategory: '',
+	materialType: '',
+	language: '',
+	page: 1,
+	pageSize: 20,
+});
+
+const loading = ref(false);
+const rows = ref<S0MaterialRow[]>([]);
+const total = ref(0);
+const dialogVisible = ref(false);
+const dialogTitle = ref('新增物料');
+const editingId = ref<number | null>(null);
+const saving = ref(false);
+const formRef = ref<FormInstance>();
+
+const companyOptions = ref<OrgOption[]>([]);
+const factoryOptions = ref<OrgOption[]>([]);
+const materialTypeOptions = ref<OptionItem[]>([]);
+const plCategoryOptions = ref<OptionItem[]>([]);
+const stockTypeOptions = ref<OptionItem[]>([]);
+const specialReqTypeOptions = ref<OptionItem[]>([]);
+const materialAttributeOptions = ref<OptionItem[]>([]);
+
+const form = reactive<S0MaterialUpsert>({
+	companyRefId: 0,
+	factoryRefId: 0,
+	code: '',
+	name: '',
+	nameEn: '',
+	materialType: '',
+	unit: '',
+	spec: '',
+	plCategory: '',
+	drawingNo: '',
+	language: '',
+	bizVersion: '',
+	productCode: '',
+	materialAttribute: '',
+	defaultLocationId: null,
+	defaultRackId: null,
+	stockTypeCode: '',
+	safetyStock: null,
+	shelfLifeDays: null,
+	expireWarningDays: null,
+	purchaseLeadDays: null,
+	minOrderQty: null,
+	maxOrderQty: null,
+	orderMultiple: null,
+	preparationLeadDays: null,
+	isOnDemand: false,
+	specialReqType: '',
+	isInspectionRequired: false,
+	inspectionDays: null,
+	isKeyMaterial: false,
+	isMainMaterial: false,
+	isPreprocess: false,
+	isAutoBatch: false,
+	isLabelRequired: false,
+	isBatchFifoReminder: false,
+	isBatchFifoStrict: false,
+	inventoryTurnoverRate: null,
+	remark: '',
+	isEnabled: true,
+});
+
+const rules: FormRules = {
+	companyRefId: [{ required: true, message: '请选择公司', trigger: 'change' }],
+	factoryRefId: [{ required: true, message: '请选择工厂', trigger: 'change' }],
+	code: [{ required: true, message: '请填写编码', trigger: 'blur' }],
+	name: [{ required: true, message: '请填写名称', trigger: 'blur' }],
+};
+
+const formFactoryOptions = computed(() => {
+	if (!form.companyRefId) return factoryOptions.value;
+	return factoryOptions.value.filter((item) => item.pid === form.companyRefId);
+});
+
+watch(
+	() => form.companyRefId,
+	() => {
+		if (!formFactoryOptions.value.some((item) => item.id === form.factoryRefId)) {
+			form.factoryRefId = 0;
+		}
+	}
+);
+
+async function loadOptions() {
+	const [companies, factories, materialTypes, plCategories, stockTypes, specialReqTypes, materialAttributes] = await Promise.all([
+		loadOrgList('201'),
+		loadOrgList('501'),
+		loadDictOptions('s0_material_type'),
+		loadDictOptions('s0_pl_category'),
+		loadDictOptions('s0_stock_type'),
+		loadDictOptions('s0_special_req_type'),
+		loadDictOptions('s0_material_attribute'),
+	]);
+
+	companyOptions.value = companies;
+	factoryOptions.value = factories;
+	materialTypeOptions.value = materialTypes;
+	plCategoryOptions.value = plCategories;
+	stockTypeOptions.value = stockTypes;
+	specialReqTypeOptions.value = specialReqTypes;
+	materialAttributeOptions.value = materialAttributes;
+
+	if (!companies.length || !factories.length || !materialTypes.length) {
+		ElMessage.warning('部分下拉数据加载失败,请检查平台种子数据');
+	}
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await s0MaterialsApi.list({
+			page: query.page,
+			pageSize: query.pageSize,
+			code: query.code || undefined,
+			name: query.name || undefined,
+			spec: query.spec || undefined,
+			drawingNo: query.drawingNo || undefined,
+			plCategory: query.plCategory || undefined,
+			materialType: query.materialType || undefined,
+			language: query.language || undefined,
+		});
+		rows.value = data.list;
+		total.value = data.total;
+	} catch {
+		rows.value = [];
+		total.value = 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function resetQuery() {
+	Object.assign(query, {
+		code: '',
+		name: '',
+		spec: '',
+		drawingNo: '',
+		plCategory: '',
+		materialType: '',
+		language: '',
+		page: 1,
+	});
+	void loadList();
+}
+
+function resetForm() {
+	editingId.value = null;
+	activeTab.value = 'owner';
+	Object.assign(form, {
+		companyRefId: 0,
+		factoryRefId: 0,
+		code: '',
+		name: '',
+		nameEn: '',
+		materialType: '',
+		unit: '',
+		spec: '',
+		plCategory: '',
+		drawingNo: '',
+		language: '',
+		bizVersion: '',
+		productCode: '',
+		materialAttribute: '',
+		defaultLocationId: null,
+		defaultRackId: null,
+		stockTypeCode: '',
+		safetyStock: null,
+		shelfLifeDays: null,
+		expireWarningDays: null,
+		purchaseLeadDays: null,
+		minOrderQty: null,
+		maxOrderQty: null,
+		orderMultiple: null,
+		preparationLeadDays: null,
+		isOnDemand: false,
+		specialReqType: '',
+		isInspectionRequired: false,
+		inspectionDays: null,
+		isKeyMaterial: false,
+		isMainMaterial: false,
+		isPreprocess: false,
+		isAutoBatch: false,
+		isLabelRequired: false,
+		isBatchFifoReminder: false,
+		isBatchFifoStrict: false,
+		inventoryTurnoverRate: null,
+		remark: '',
+		isEnabled: true,
+	});
+	formRef.value?.clearValidate();
+}
+
+function openCreate() {
+	resetForm();
+	dialogTitle.value = '新增物料';
+	dialogVisible.value = true;
+}
+
+function openEdit(row: S0MaterialRow) {
+	resetForm();
+	editingId.value = row.id;
+	dialogTitle.value = `编辑物料 ${row.code}`;
+	Object.assign(form, {
+		companyRefId: row.companyRefId,
+		factoryRefId: row.factoryRefId,
+		code: row.code,
+		name: row.name,
+		nameEn: row.nameEn || '',
+		materialType: row.materialType || '',
+		unit: row.unit || '',
+		spec: row.spec || '',
+		plCategory: row.plCategory || '',
+		drawingNo: row.drawingNo || '',
+		language: row.language || '',
+		bizVersion: row.bizVersion || '',
+		productCode: row.productCode || '',
+		materialAttribute: row.materialAttribute || '',
+		defaultLocationId: row.defaultLocationId ?? null,
+		defaultRackId: row.defaultRackId ?? null,
+		stockTypeCode: row.stockTypeCode || '',
+		safetyStock: row.safetyStock ?? null,
+		shelfLifeDays: row.shelfLifeDays ?? null,
+		expireWarningDays: row.expireWarningDays ?? null,
+		purchaseLeadDays: row.purchaseLeadDays ?? null,
+		minOrderQty: row.minOrderQty ?? null,
+		maxOrderQty: row.maxOrderQty ?? null,
+		orderMultiple: row.orderMultiple ?? null,
+		preparationLeadDays: row.preparationLeadDays ?? null,
+		isOnDemand: row.isOnDemand,
+		specialReqType: row.specialReqType || '',
+		isInspectionRequired: row.isInspectionRequired,
+		inspectionDays: row.inspectionDays ?? null,
+		isKeyMaterial: row.isKeyMaterial,
+		isMainMaterial: row.isMainMaterial,
+		isPreprocess: row.isPreprocess,
+		isAutoBatch: row.isAutoBatch,
+		isLabelRequired: row.isLabelRequired,
+		isBatchFifoReminder: row.isBatchFifoReminder,
+		isBatchFifoStrict: row.isBatchFifoStrict,
+		inventoryTurnoverRate: row.inventoryTurnoverRate ?? null,
+		remark: row.remark || '',
+		isEnabled: row.isEnabled,
+	});
+	dialogVisible.value = true;
+}
+
+async function submitForm() {
+	await formRef.value?.validate();
+	saving.value = true;
+	try {
+		const payload: S0MaterialUpsert = { ...form };
+		if (editingId.value) {
+			await s0MaterialsApi.update(editingId.value, payload);
+			ElMessage.success('已保存');
+		} else {
+			await s0MaterialsApi.create(payload);
+			ElMessage.success('已创建');
+		}
+		dialogVisible.value = false;
+		await loadList();
+	} finally {
+		saving.value = false;
+	}
+}
+
+function onDelete(row: S0MaterialRow) {
+	ElMessageBox.confirm(`确定删除物料「${row.name}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await s0MaterialsApi.delete(row.id);
+			ElMessage.success('已删除');
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+function toggleEnabled(row: S0MaterialRow) {
+	const nextEnabled = !row.isEnabled;
+	const actionText = nextEnabled ? '启用' : '禁用';
+	ElMessageBox.confirm(`确定${actionText}物料「${row.name}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await s0MaterialsApi.toggleEnabled(row.id, { isEnabled: nextEnabled });
+			ElMessage.success(`${actionText}成功`);
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+onMounted(async () => {
+	await loadOptions();
+	await 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>

+ 363 - 0
Web/src/views/aidop/s0/sales/OrderPriorityRuleList.vue

@@ -0,0 +1,363 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="S0 / Sales / 订单优先规则">
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="关键字">
+				<el-input v-model="query.keyword" placeholder="编码/名称" clearable style="width: 180px" />
+			</el-form-item>
+			<el-form-item label="来源实体">
+				<el-input v-model="query.sourceEntity" clearable style="width: 180px" />
+			</el-form-item>
+			<el-form-item label="启用">
+				<el-select v-model="query.isEnabled" clearable placeholder="全部" style="width: 120px">
+					<el-option label="启用" :value="true" />
+					<el-option label="禁用" :value="false" />
+				</el-select>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="loadList">查询</el-button>
+				<el-button @click="resetQuery">重置</el-button>
+				<el-button type="success" @click="openCreate">新增规则</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%">
+			<el-table-column prop="code" label="编码" width="170" show-overflow-tooltip />
+			<el-table-column prop="name" label="名称" min-width="180" show-overflow-tooltip />
+			<el-table-column prop="priorityLevel" label="优先级" width="100" align="center" />
+			<el-table-column prop="sortDirection" label="排序方向" width="100" align="center" />
+			<el-table-column prop="sourceEntity" label="来源实体" width="140" show-overflow-tooltip />
+			<el-table-column prop="sourceField" label="来源字段" width="140" show-overflow-tooltip />
+			<el-table-column label="启用" width="90" align="center">
+				<template #default="{ row }">
+					<el-tag :type="row.isEnabled ? 'success' : 'info'" size="small">
+						{{ row.isEnabled ? '是' : '否' }}
+					</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column label="操作" width="240" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isEnabled ? 'warning' : 'success'" @click="toggleEnabled(row)">
+						{{ row.isEnabled ? '禁用' : '启用' }}
+					</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-row :gutter="16">
+					<el-col :span="12">
+						<el-form-item label="公司" prop="companyRefId">
+							<el-select v-model="form.companyRefId" filterable style="width: 100%">
+								<el-option v-for="item in companyOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="工厂" prop="factoryRefId">
+							<el-select v-model="form.factoryRefId" filterable style="width: 100%">
+								<el-option v-for="item in formFactoryOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="编码">
+							<el-input v-model="form.code" placeholder="留空自动生成" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="名称" prop="name">
+							<el-input v-model="form.name" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="优先级">
+							<el-input-number v-model="form.priorityLevel" :min="1" style="width: 100%" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="排序方向">
+							<el-select v-model="form.sortDirection" style="width: 100%">
+								<el-option label="ASC" value="ASC" />
+								<el-option label="DESC" value="DESC" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12"><el-form-item label="来源实体"><el-input v-model="form.sourceEntity" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="来源字段"><el-input v-model="form.sourceField" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="来源字段类型"><el-input v-model="form.sourceFieldType" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="来源关联字段"><el-input v-model="form.sourceLinkField" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="工单字段"><el-input v-model="form.workOrderField" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="工单字段类型"><el-input v-model="form.workOrderFieldType" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="工单关联字段"><el-input v-model="form.workOrderLinkField" /></el-form-item></el-col>
+					<el-col :span="24">
+						<el-form-item label="规则表达式">
+							<el-input v-model="form.ruleExpr" type="textarea" :rows="4" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="启用">
+							<el-switch v-model="form.isEnabled" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="24">
+						<el-form-item label="备注">
+							<el-input v-model="form.remark" type="textarea" rows="3" />
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<el-button @click="dialogVisible = false">取消</el-button>
+				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+			</template>
+		</el-dialog>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS0SalesOrderPriorityRule">
+import { computed, onMounted, reactive, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
+import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import {
+	loadOrgList,
+	s0OrderPriorityRulesApi,
+	type OrgOption,
+	type S0OrderPriorityRuleRow,
+	type S0OrderPriorityRuleUpsert,
+} from '../api/s0SalesApi';
+
+const route = useRoute();
+const pageTitle = computed(() => (route.meta?.title as string) || '订单优先规则');
+
+const query = reactive({
+	keyword: '',
+	sourceEntity: '',
+	isEnabled: undefined as boolean | undefined,
+	page: 1,
+	pageSize: 20,
+});
+
+const loading = ref(false);
+const rows = ref<S0OrderPriorityRuleRow[]>([]);
+const total = ref(0);
+const dialogVisible = ref(false);
+const dialogTitle = ref('新增规则');
+const editingId = ref<number | null>(null);
+const saving = ref(false);
+const formRef = ref<FormInstance>();
+
+const companyOptions = ref<OrgOption[]>([]);
+const factoryOptions = ref<OrgOption[]>([]);
+
+const formFactoryOptions = computed(() => {
+	if (!form.companyRefId) return factoryOptions.value;
+	return factoryOptions.value.filter((item) => item.pid === form.companyRefId);
+});
+
+watch(
+	() => form.companyRefId,
+	() => {
+		if (!formFactoryOptions.value.some((item) => item.id === form.factoryRefId)) {
+			form.factoryRefId = 0;
+		}
+	}
+);
+
+const form = reactive<S0OrderPriorityRuleUpsert>({
+	companyRefId: 0,
+	factoryRefId: 0,
+	code: '',
+	name: '',
+	priorityLevel: 1,
+	sortDirection: 'ASC',
+	sourceEntity: '',
+	sourceField: '',
+	sourceFieldType: '',
+	sourceLinkField: '',
+	workOrderField: '',
+	workOrderFieldType: '',
+	workOrderLinkField: '',
+	ruleExpr: '',
+	remark: '',
+	isEnabled: true,
+});
+
+const rules: FormRules = {
+	companyRefId: [{ required: true, message: '请选择公司', trigger: 'change' }],
+	factoryRefId: [{ required: true, message: '请选择工厂', trigger: 'change' }],
+	name: [{ required: true, message: '请填写名称', trigger: 'blur' }],
+};
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await s0OrderPriorityRulesApi.list({
+			page: query.page,
+			pageSize: query.pageSize,
+			keyword: query.keyword || undefined,
+			sourceEntity: query.sourceEntity || undefined,
+			isEnabled: query.isEnabled,
+		});
+		rows.value = data.list;
+		total.value = data.total;
+	} catch {
+		rows.value = [];
+		total.value = 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function resetQuery() {
+	query.keyword = '';
+	query.sourceEntity = '';
+	query.isEnabled = undefined;
+	query.page = 1;
+	void loadList();
+}
+
+function resetForm() {
+	editingId.value = null;
+	Object.assign(form, {
+		companyRefId: 0,
+		factoryRefId: 0,
+		code: '',
+		name: '',
+		priorityLevel: 1,
+		sortDirection: 'ASC',
+		sourceEntity: '',
+		sourceField: '',
+		sourceFieldType: '',
+		sourceLinkField: '',
+		workOrderField: '',
+		workOrderFieldType: '',
+		workOrderLinkField: '',
+		ruleExpr: '',
+		remark: '',
+		isEnabled: true,
+	});
+	formRef.value?.clearValidate();
+}
+
+function openCreate() {
+	resetForm();
+	dialogTitle.value = '新增规则';
+	dialogVisible.value = true;
+}
+
+function openEdit(row: S0OrderPriorityRuleRow) {
+	resetForm();
+	editingId.value = row.id;
+	dialogTitle.value = `编辑规则 ${row.code}`;
+	Object.assign(form, {
+		companyRefId: row.companyRefId,
+		factoryRefId: row.factoryRefId,
+		code: row.code,
+		name: row.name,
+		priorityLevel: row.priorityLevel,
+		sortDirection: row.sortDirection,
+		sourceEntity: row.sourceEntity || '',
+		sourceField: row.sourceField || '',
+		sourceFieldType: row.sourceFieldType || '',
+		sourceLinkField: row.sourceLinkField || '',
+		workOrderField: row.workOrderField || '',
+		workOrderFieldType: row.workOrderFieldType || '',
+		workOrderLinkField: row.workOrderLinkField || '',
+		ruleExpr: row.ruleExpr || '',
+		remark: row.remark || '',
+		isEnabled: row.isEnabled,
+	});
+	dialogVisible.value = true;
+}
+
+async function submitForm() {
+	await formRef.value?.validate();
+	saving.value = true;
+	try {
+		const payload: S0OrderPriorityRuleUpsert = { ...form };
+		if (!payload.code) delete payload.code;
+		if (editingId.value) {
+			await s0OrderPriorityRulesApi.update(editingId.value, payload);
+			ElMessage.success('已保存');
+		} else {
+			await s0OrderPriorityRulesApi.create(payload);
+			ElMessage.success('已创建');
+		}
+		dialogVisible.value = false;
+		await loadList();
+	} finally {
+		saving.value = false;
+	}
+}
+
+function onDelete(row: S0OrderPriorityRuleRow) {
+	ElMessageBox.confirm(`确定删除规则「${row.name}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await s0OrderPriorityRulesApi.delete(row.id);
+			ElMessage.success('已删除');
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+function toggleEnabled(row: S0OrderPriorityRuleRow) {
+	const nextEnabled = !row.isEnabled;
+	const actionText = nextEnabled ? '启用' : '禁用';
+	ElMessageBox.confirm(`确定${actionText}规则「${row.name}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			await s0OrderPriorityRulesApi.toggleEnabled(row.id, { isEnabled: nextEnabled });
+			ElMessage.success(`${actionText}成功`);
+			await loadList();
+		})
+		.catch(() => {});
+}
+
+async function loadOptions() {
+	try {
+		const [companies, factories] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
+		companyOptions.value = companies;
+		factoryOptions.value = factories;
+		if (!companies.length || !factories.length) {
+			ElMessage.warning('公司或工厂组织数据加载失败,请检查组织种子');
+		}
+	} catch (e) {
+		console.warn('[OrderPriorityRule] loadOptions failed', e);
+		companyOptions.value = [];
+		factoryOptions.value = [];
+	}
+}
+
+onMounted(async () => {
+	await Promise.all([loadList(), loadOptions()]);
+});
+</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>

+ 269 - 42
doc/plan/S0/Batch2-产销建模迁移方案.md

@@ -4,6 +4,22 @@
 
 ---
 
+## 零、项目级开发约束(执行本 Batch 前先对照)
+
+| 约束项 | 本 Batch 的执行要求 |
+|------|------------------|
+| 改前确认 | 本轮新增实体、DTO、Controller、前端页面、菜单前,先列改动清单并确认 |
+| 跨模块影响 | 若会影响平台字典、组织机构、共享组件、菜单种子或公共 API,先单独说明影响面 |
+| 最小改动 | 仅落地 Sales 相关内容,不顺手改 Manufacturing / Warehouse / Quality 结构 |
+| 平台复用 | 公司/工厂、业务字典复用 `SysOrg` / `SysDictType` / `SysDictData`,不重复建设 Platform 表 |
+| 运行期数据源 | 下拉项统一从数据库读取,不从工程内常量或种子文件回读 |
+| 文件修改策略 | 以新增为主;修改 `Startup.cs`、插件 `SysMenuSeedData.cs` 时仅做追加 |
+| 前端风格 | 统一使用 `<script setup lang="ts">`、Admin.NET `service` / `api-services` 风格 |
+| 版本号策略 | 只有在实际执行 Git 提交时才做双端 patch 升号;当前开发阶段不单独改版本 |
+| 验收顺序 | 先后端编译和表创建,再前端页面和菜单,最后联调下拉与 CRUD |
+
+---
+
 ## 〇、与原计划的修正
 
 对比 `S0迁移.md` Batch 2 部分,本方案做了以下修正:
@@ -82,6 +98,15 @@ server/Plugins/Admin.NET.Plugin.AiDOP/
     └─ AdoS0OrderPriorityRulesController.cs
 ```
 
+Phase 1 已落地的前置文件:
+
+```
+server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/
+  S0DictTypeSeedData.cs        ← 初始化 8 组 s0_* 业务字典类型
+  S0DictDataSeedData.cs        ← 初始化对应字典值
+  S0OrgSeedData.cs             ← 初始化示范公司 / 工厂组织节点
+```
+
 ### 2.2 后端修改(追加,不替换)
 
 ```
@@ -114,6 +139,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 
 /// <summary>客户主数据(S0 Sales / sales_customer)</summary>
 [SugarTable("ado_s0_sales_customer", "S0 客户主数据")]
+[SugarIndex("uk_ado_s0_sales_customer_code", nameof(Code), OrderByType.Asc, true)]
 public class AdoS0Customer
 {
     [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
@@ -134,7 +160,7 @@ public class AdoS0Customer
     [SugarColumn(ColumnDescription = "英文名称", Length = 200, IsNullable = true)]
     public string? NameEn { get; set; }
 
-    [SugarColumn(ColumnDescription = "客户类型", Length = 100, IsNullable = true)]
+    [SugarColumn(ColumnDescription = "客户类型", Length = 50, IsNullable = true)]
     public string? CustomerType { get; set; }
 
     [SugarColumn(ColumnDescription = "联系人", Length = 100, IsNullable = true)]
@@ -146,10 +172,10 @@ public class AdoS0Customer
     [SugarColumn(ColumnDescription = "地址", Length = 500, IsNullable = true)]
     public string? Address { get; set; }
 
-    [SugarColumn(ColumnDescription = "禁用状态", Length = 20)]
+    [SugarColumn(ColumnDescription = "禁用状态", Length = 50)]
     public string ForbidStatus { get; set; } = "normal";
 
-    [SugarColumn(ColumnDescription = "币种", Length = 50, IsNullable = true)]
+    [SugarColumn(ColumnDescription = "币种", Length = 20, IsNullable = true)]
     public string? Currency { get; set; }
 
     [SugarColumn(ColumnDescription = "是否含税", ColumnDataType = "boolean")]
@@ -164,7 +190,7 @@ public class AdoS0Customer
     [SugarColumn(ColumnDescription = "客户等级")]
     public int CustomerLevel { get; set; } = 1;
 
-    [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
+    [SugarColumn(ColumnDescription = "备注", Length = 1000, IsNullable = true)]
     public string? Remark { get; set; }
 
     [SugarColumn(ColumnDescription = "启用", ColumnDataType = "boolean")]
@@ -188,17 +214,17 @@ public class AdoS0Customer
 | 4 | Code | string | 100 | — | code | Customer |
 | 5 | Name | string | 200 | — | name | Customer |
 | 6 | NameEn | string? | 200 | — | name_en | Customer |
-| 7 | CustomerType | string? | 100 | — | customer_type | Customer |
+| 7 | CustomerType | string? | 50 | — | customer_type | Customer |
 | 8 | ContactPerson | string? | 100 | — | contact_person | Customer |
 | 9 | ContactPhone | string? | 50 | — | contact_phone | Customer |
 | 10 | Address | string? | 500 | — | address | Customer |
-| 11 | ForbidStatus | string | 20 | "normal" | forbid_status | Customer |
-| 12 | Currency | string? | 50 | — | currency | Customer |
+| 11 | ForbidStatus | string | 50 | "normal" | forbid_status | Customer |
+| 12 | Currency | string? | 20 | — | currency | Customer |
 | 13 | IsTaxIncluded | bool | — | true | is_tax_included | Customer |
 | 14 | PrimarySales | string? | 100 | — | primary_sales | Customer |
 | 15 | BackupSales | string? | 100 | — | backup_sales | Customer |
 | 16 | CustomerLevel | int | — | 1 | customer_level | Customer |
-| 17 | Remark | string? | 500 | — | remark | Customer |
+| 17 | Remark | string? | 1000 | — | remark | Customer |
 | 18 | IsEnabled | bool | — | true | is_enabled | BaseEntity |
 | 19 | CreatedAt | DateTime | — | Now | created_at | BaseEntity |
 | 20 | UpdatedAt | DateTime? | — | — | updated_at | BaseEntity |
@@ -517,6 +543,8 @@ namespace Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
 // ══════════════════════════════════════
 public class AdoS0CustomerQueryDto
 {
+    public long? CompanyRefId { get; set; }
+    public long? FactoryRefId { get; set; }
     public string? Keyword { get; set; }       // 模糊搜索 Code / Name
     public bool? IsEnabled { get; set; }
     public int Page { get; set; } = 1;
@@ -549,6 +577,8 @@ public class AdoS0CustomerUpsertDto
 // ══════════════════════════════════════
 public class AdoS0MaterialQueryDto
 {
+    public long? CompanyRefId { get; set; }
+    public long? FactoryRefId { get; set; }
     public string? Keyword { get; set; }       // 模糊搜索 Code / Name
     public string? Code { get; set; }
     public string? Name { get; set; }
@@ -617,6 +647,8 @@ public class AdoS0MaterialUpsertDto
 // ══════════════════════════════════════
 public class AdoS0OrderPriorityRuleQueryDto
 {
+    public long? CompanyRefId { get; set; }
+    public long? FactoryRefId { get; set; }
     public string? Keyword { get; set; }       // 模糊搜索 Code / Name
     public string? SourceEntity { get; set; }
     public bool? IsEnabled { get; set; }
@@ -628,7 +660,7 @@ public class AdoS0OrderPriorityRuleUpsertDto
 {
     public long CompanyRefId { get; set; }
     public long FactoryRefId { get; set; }
-    public string Code { get; set; } = string.Empty;
+    public string? Code { get; set; }                    // 可选,后端为空时自动生成
     public string Name { get; set; } = string.Empty;
     public int PriorityLevel { get; set; }
     public string SortDirection { get; set; } = "ASC";
@@ -649,6 +681,23 @@ public class AdoS0OrderPriorityRuleUpsertDto
 
 ## 五、控制器路由
 
+> **通用规则**:
+> 1. 每个 Controller 的 `GetPagedAsync` 方法入口第一行必须调用分页防御:
+>    ```csharp
+>    (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+>    ```
+> 2. 列表查询必须加 `.OrderByDescending(x => x.CreatedAt)` 保证分页结果稳定。
+> 3. Create/Update 方法中强制联动 ForbidStatus 与 IsEnabled:
+>    ```csharp
+>    entity.ForbidStatus = dto.IsEnabled ? "normal" : "forbidden";
+>    ```
+>    前端不展示 ForbidStatus 独立下拉,仅保留 IsEnabled 开关。
+> 4. 所有列表查询须包含公司/工厂过滤:
+>    ```csharp
+>    .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
+>    .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
+>    ```
+
 ### 5.1 AdoS0CustomersController
 
 路由 `api/s0/sales/customers`,风格对标 `OrderController.cs`:
@@ -659,6 +708,7 @@ GET    /api/s0/sales/customers/{id:long}
 POST   /api/s0/sales/customers
 PUT    /api/s0/sales/customers/{id:long}
 DELETE /api/s0/sales/customers/{id:long}
+PATCH  /api/s0/sales/customers/{id:long}/toggle-enabled  ← 仅切换 isEnabled
 ```
 
 查询过滤逻辑:
@@ -679,6 +729,7 @@ GET    /api/s0/sales/materials/{id:long}
 POST   /api/s0/sales/materials
 PUT    /api/s0/sales/materials/{id:long}
 DELETE /api/s0/sales/materials/{id:long}
+PATCH  /api/s0/sales/materials/{id:long}/toggle-enabled  ← 仅切换 isEnabled
 ```
 
 查询过滤逻辑:
@@ -706,6 +757,7 @@ GET    /api/s0/sales/order-priority-rules/{id:long}
 POST   /api/s0/sales/order-priority-rules
 PUT    /api/s0/sales/order-priority-rules/{id:long}
 DELETE /api/s0/sales/order-priority-rules/{id:long}
+PATCH  /api/s0/sales/order-priority-rules/{id:long}/toggle-enabled  ← 仅切换 isEnabled
 ```
 
 查询过滤逻辑:
@@ -717,6 +769,13 @@ DELETE /api/s0/sales/order-priority-rules/{id:long}
 .WhereIF(q.IsEnabled.HasValue, x => x.IsEnabled == q.IsEnabled!.Value)
 ```
 
+**CreateAsync 特殊逻辑**:Code 为可选字段,后端兜底自动生成:
+
+```csharp
+if (string.IsNullOrWhiteSpace(dto.Code))
+    dto.Code = $"RULE-{DateTime.Now:yyyyMMddHHmmss}-{Random.Shared.Next(1000, 9999)}";
+```
+
 ---
 
 ## 六、Startup.cs 修改
@@ -743,29 +802,63 @@ typeof(AdoS0Customer), typeof(AdoS0Material), typeof(AdoS0OrderPriorityRule)
 
 - `import service from '/@/utils/request'`
 - 3 组 API 对象:`s0CustomersApi`、`s0MaterialsApi`、`s0OrderPriorityRulesApi`
-- 每组提供 `list`、`get`、`create`、`update`、`delete` 方法
+- 每组提供 `list`、`get`、`create`、`update`、`delete`、`toggleEnabled` 方法
 - TS 接口:`S0CustomerRow` / `S0CustomerUpsert`、`S0MaterialRow` / `S0MaterialUpsert`、`S0OrderPriorityRuleRow` / `S0OrderPriorityRuleUpsert`
-- 公共分页接口 `Paged<T> { total, page, pageSize, list }`
+- 公共分页接口 `Paged<T> { total, page, pageSize, list }`(注:S0 原 API 用 `items`,新契约改为 `list`,与现有 `legacyAidop.ts` 对齐)
+
+**平台下拉数据 helper(统一拆包 AdminResult)**:
+
+```typescript
+import { SysDictDataApi, SysOrgApi } from '/@/api-services';
+
+export async function loadDictOptions(code: string) {
+  try {
+    const api = new SysDictDataApi();
+    const res = await api.apiSysDictDataDataListCodeGet(code);
+    return (res.data.result ?? []).map(d => ({ label: d.label!, value: d.value! }));
+  } catch (e) {
+    console.warn(`[s0] loadDictOptions(${code}) failed`, e);
+    return [];
+  }
+}
+
+export async function loadOrgList(type?: string) {
+  try {
+    const api = new SysOrgApi();
+    const res = await api.apiSysOrgListGet(0, undefined, undefined, type);
+    return res.data.result ?? [];
+  } catch (e) {
+    console.warn(`[s0] loadOrgList(${type}) failed`, e);
+    return [];
+  }
+}
+```
+
+> 页面中统一调用 `loadDictOptions('s0_material_type')` / `loadOrgList('201')`,不直接实例化 `SysDictDataApi` / `SysOrgApi` class。
 
 ---
 
 ## 八、前端页面设计
 
+> **样式导入规则**:S0 页面在 `s0/sales/` 子目录下,不使用相对路径引用 `aidop-demo.scss`,统一用 alias:
+> ```scss
+> @import '/@/views/aidop/styles/aidop-demo.scss';
+> ```
+
 ### 8.1 CustomerList.vue(name="aidopS0SalesCustomer")
 
 - **搜索栏**:编码/名称关键字(el-input)
-- **表格列**:code、name、nameEn、customerType、contactPerson、contactPhone、forbidStatus(tag)、isEnabled(tag)
-- **操作**:编辑、删除
+- **表格列**:code、name、nameEn、customerType、contactPerson、contactPhone、isEnabled(tag)
+- **操作**:编辑、删除、启用/禁用(toggle-enabled)
 - **弹窗表单**:
-  - companyRefId(下拉 → `SysOrgApi` 按 Type=201)、factoryRefId(下拉 → `SysOrgApi` 按 Type=501 + Pid 联动)
+  - companyRefId(下拉 → `loadOrgList('201')`)、factoryRefId(下拉 → `loadOrgList('501')` + Pid 联动)
   - code(必填)、name(必填)、nameEn
-  - customerType(下拉 → `SysDictDataApi` code=`s0_customer_type`)
+  - customerType(下拉 → `loadDictOptions('s0_customer_type')`)
   - contactPerson、contactPhone、address
-  - forbidStatus(下拉 → `SysDictDataApi` code=`s0_forbid_status`)
-  - currency(下拉 → `SysDictDataApi` code=`s0_currency`)
+  - currency(下拉 → `loadDictOptions('s0_currency')`)
   - isTaxIncluded(switch)
   - primarySales、backupSales
-  - customerLevel(el-input-number, min=1)
+  - customerLevel(el-input-number, min=1, max=9)
   - isEnabled(switch)、remark(textarea)
 - **组件引用**:`import AidopDemoShell from '../../components/AidopDemoShell.vue'`
 
@@ -774,16 +867,16 @@ typeof(AdoS0Customer), typeof(AdoS0Material), typeof(AdoS0OrderPriorityRule)
 - **搜索栏**:编码、名称、规格、图纸编号、计划类别(select)、物料类型(select)、语言(select)
 - **表格列**(精简展示):code、name、spec、unit、materialType、plCategory、isEnabled(tag)
 - **弹窗表单**(使用 `el-tabs` 分组):
-  - **Tab 0 归属**:companyRefId(下拉 → `SysOrgApi` Type=201)、factoryRefId(下拉 → `SysOrgApi` Type=501 联动)
-  - **Tab 1 基本信息**:code(必填)、name(必填)、nameEn、materialType(下拉 → `s0_material_type`)、unit、spec、plCategory(下拉 → `s0_pl_category`)、drawingNo、language、bizVersion、productCode、materialAttribute(下拉 → `s0_material_attribute`)
-  - **Tab 2 库存参数**:stockTypeCode(下拉 → `s0_stock_type`)、safetyStock、shelfLifeDays、expireWarningDays、defaultLocationId(输入框,Batch 4 后升级)、defaultRackId(输入框,Batch 4 后升级)
-  - **Tab 3 采购参数**:purchaseLeadDays、minOrderQty、maxOrderQty、orderMultiple、preparationLeadDays、isOnDemand(switch)、specialReqType(下拉 → `s0_special_req_type`)
+  - **Tab 0 归属**:companyRefId(下拉 → `loadOrgList('201')`)、factoryRefId(下拉 → `loadOrgList('501')` 联动)
+  - **Tab 1 基本信息**:code(必填)、name(必填)、nameEn(预留,非必填)、materialType(下拉 → `loadDictOptions('s0_material_type')`)、unit、spec、plCategory(下拉 → `loadDictOptions('s0_pl_category')`)、drawingNo、language、bizVersion、productCode、materialAttribute(下拉 → `loadDictOptions('s0_material_attribute')`)
+  - **Tab 2 库存参数**:stockTypeCode(下拉 → `loadDictOptions('s0_stock_type')`)、safetyStock、shelfLifeDays、expireWarningDays、defaultLocationId(输入框,Batch 4 后升级)、defaultRackId(输入框,Batch 4 后升级)
+  - **Tab 3 采购参数**:purchaseLeadDays、minOrderQty、maxOrderQty、orderMultiple、preparationLeadDays、isOnDemand(switch)、specialReqType(下拉 → `loadDictOptions('s0_special_req_type')`)
   - **Tab 4 质量/管控**:isInspectionRequired(switch)、inspectionDays、isKeyMaterial(switch)、isMainMaterial(switch)、isPreprocess(switch)、isAutoBatch(switch)、isLabelRequired(switch)
   - **Tab 5 批次管理**:isBatchFifoReminder(switch)、isBatchFifoStrict(switch)、inventoryTurnoverRate
-  - **Tab 6 状态**:forbidStatus(下拉 → `s0_forbid_status`)、isEnabled(switch)、remark(textarea)
+  - **Tab 6 状态**:isEnabled(switch)、remark(textarea)
 
-> 下拉选项统一从平台字典 API 获取(`SysDictDataApi.apiSysDictDataDataListCodeGet`),
-> 不再硬编码。S0 原 `MaterialManagement.vue` 中的 fallback 选项仅作为种子数据参考
+> 下拉选项统一通过 `loadDictOptions` / `loadOrgList` helper 获取(§7),内部已处理 AdminResult 拆包和失败兜底。
+> ForbidStatus 不在前端展示,由后端 Create/Update 自动联动(§5 通用规则)
 
 ### 8.3 OrderPriorityRuleList.vue(name="aidopS0SalesOrderPriorityRule")
 
@@ -857,25 +950,131 @@ private static IEnumerable<SysMenu> BuildS0SalesMenus(DateTime ct)
 }
 ```
 
+> **挂载验证要点**:
+> 1. `BuildS0SalesMenus` 必须在 `HasData()` 内被调用(`foreach (var m in BuildS0SalesMenus(ct)) list.Add(m);`),否则 `AidopMenuLinkSync.EnsureLinked` 无法将其关联到租户/角色,侧栏不会出现。
+> 2. 落地后启动验证:查 `sys_menu` 表确认 `Id = 1329002000001` 存在。
+> 3. Id 冲突验算:`ModuleDefinitions` 自动生成的 Id 范围为 `1321000001000`~`1322000000060`(16 模块约 60 叶子),新 Id `1329002000000`~`1329002000003` 远在此范围外,无冲突。
+
 ---
 
-## 十、执行步骤
+## 十、执行步骤(20 步,逐步标记)
 
-```
-前置   确认 Batch 1 种子已就绪                      →  S0DictSeedData.cs 已创建,平台字典和组织节点已入库
-Step 1   阅读本文档 + S0 源文件                    →  理解字段定义
-Step 2   新建 Entity/S0/Sales/ 下 3 个实体文件      →  [SugarTable] + [SugarColumn] 格式
-Step 3   新建 Dto/S0/Sales/AdoS0SalesDtos.cs       →  3 组 QueryDto + UpsertDto
-Step 4   新建 Controllers/S0/Sales/ 下 3 个控制器   →  [AllowAnonymous][NonUnify] 插件风格
-Step 5   修改 Startup.cs                           →  InitTables 追加 3 实体 + using
-Step 6   后端编译验证                               →  dotnet build 无报错
-Step 7   新建 Web/src/views/aidop/s0/api/s0SalesApi.ts
-Step 8   新建 Web/src/views/aidop/s0/sales/ 下 3 个 Vue 页面
-           (下拉调用 SysOrgApi + SysDictDataApi,不硬编码选项)
-Step 9   修改插件 SysMenuSeedData.cs               →  追加 BuildS0SalesMenus()(S0DirId=1321000001000L)
-Step 10  前端编译验证                               →  pnpm dev 无 TS 错误
-Step 11  集成验收                                   →  按第十一节标准逐项检查
-```
+> 每步完成后检查无误,标记 `[x]`;发现问题立即修复后再标记。
+
+### 阶段 A:后端实体层
+
+- [x] **Step 1** — 新建 `Entity/S0/Sales/AdoS0Customer.cs`
+  - 按 §3.1 定义,含唯一索引 `[SugarIndex]`、ForbidStatus Length=50、Remark Length=1000、Currency Length=20
+  - 检查:文件存在,字段数 = 20,namespace 正确
+
+- [x] **Step 2** — 新建 `Entity/S0/Sales/AdoS0Material.cs`
+  - 按 §3.2 定义,43 字段,NameEn 保留
+  - 检查:文件存在,字段数 = 43,decimal 精度 `(18,5)`
+
+- [x] **Step 3** — 新建 `Entity/S0/Sales/AdoS0OrderPriorityRule.cs`
+  - 按 §3.3 定义,19 字段,RuleExpr Length=1000
+  - 检查:文件存在,字段数 = 19
+
+### 阶段 B:后端 DTO 层
+
+- [x] **Step 4** — 新建 `Dto/S0/Sales/AdoS0SalesDtos.cs`
+  - 按 §4 定义,3 组 QueryDto(含 CompanyRefId/FactoryRefId)+ 3 组 UpsertDto + 1 组 `AdoS0ToggleEnabledDto`
+  - OrderPriorityRuleUpsertDto.Code 为 `string?`(可选)
+  - 检查:7 个 class,QueryDto 均含分页 + 公司/工厂过滤字段
+
+### 阶段 C:后端控制器层
+
+- [x] **Step 5** — 新建 `Controllers/S0/Sales/AdoS0CustomersController.cs`
+  - 6 个端点(5 CRUD + 1 toggle-enabled)
+  - 入口 PagingGuard + OrderByDescending + CompanyRefId/FactoryRefId WhereIF
+  - Create/Update 强制联动 ForbidStatus
+  - 检查:路由 `api/s0/sales/customers`,6 个 Action
+
+- [x] **Step 6** — 新建 `Controllers/S0/Sales/AdoS0MaterialsController.cs`
+  - 6 个端点,多条件过滤(Code/Name/Spec/DrawingNo/PlCategory/MaterialType/Language)
+  - 检查:路由 `api/s0/sales/materials`,6 个 Action
+
+- [x] **Step 7** — 新建 `Controllers/S0/Sales/AdoS0OrderPriorityRulesController.cs`
+  - 6 个端点,CreateAsync 中 Code 为空时自动生成
+  - 检查:路由 `api/s0/sales/order-priority-rules`,6 个 Action
+
+### 阶段 D:后端注册 + 编译
+
+- [x] **Step 8** — 修改 `Startup.cs`
+  - 顶部追加 `using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;`
+  - InitTables 追加 `typeof(AdoS0Customer), typeof(AdoS0Material), typeof(AdoS0OrderPriorityRule)`
+  - 检查:diff 仅新增 2 行(using + 类型),不改已有内容
+
+- [x] **Step 9** — 后端编译验证
+  - 执行 `dotnet build` 无 error
+  - 检查:Build succeeded,0 error
+
+### 阶段 E:前端 API 层
+
+- [x] **Step 10** — 新建 `Web/src/views/aidop/s0/api/s0SalesApi.ts`(CRUD 部分)
+  - 3 组 API 对象:s0CustomersApi / s0MaterialsApi / s0OrderPriorityRulesApi
+  - 每组 6 方法(list/get/create/update/delete/toggleEnabled)
+  - TS 接口:Row + Upsert + Paged<T>(list 字段名,非 items)
+  - 检查:文件存在,无 TS 类型错误
+
+- [x] **Step 11** — 在 `s0SalesApi.ts` 中补充平台 helper
+  - `loadDictOptions(code)` — 实例化 SysDictDataApi,拆 AdminResult,try-catch 兜底
+  - `loadOrgList(type?)` — 实例化 SysOrgApi(id=0),拆 AdminResult,try-catch 兜底
+  - 检查:两个 helper 导出正确
+
+### 阶段 F:前端页面层
+
+- [x] **Step 12** — 新建 `Web/src/views/aidop/s0/sales/CustomerList.vue`
+  - name="aidopS0SalesCustomer",AidopDemoShell 包裹
+  - 搜索栏 + 表格 + 弹窗表单(含公司/工厂/字典下拉 via helper)
+  - customerLevel max=9,无 ForbidStatus 下拉,有 toggle-enabled 操作按钮
+  - SCSS alias `@import '/@/views/aidop/styles/aidop-demo.scss'`
+  - 检查:文件存在,`<script setup lang="ts">`
+
+- [x] **Step 13** — 新建 `Web/src/views/aidop/s0/sales/MaterialList.vue`
+  - name="aidopS0SalesMaterial",el-tabs 分 7 组表单(Tab 0~6)
+  - nameEn 预留非必填,Tab 6 无 ForbidStatus 下拉
+  - 检查:文件存在,el-tabs 含 7 个 tab-pane
+
+- [x] **Step 14** — 新建 `Web/src/views/aidop/s0/sales/OrderPriorityRuleList.vue`
+  - name="aidopS0SalesOrderPriorityRule"
+  - Code 非必填(前端不设 required 规则)
+  - ruleExpr 用 textarea rows=4
+  - 检查:文件存在
+
+### 阶段 G:菜单注册
+
+- [x] **Step 15** — 修改 `SeedData/SysMenuSeedData.cs`
+  - 在 HasData() 的 `BuildAidopSmartOpsSeedMenus` 之后追加 `foreach (var m in BuildS0SalesMenus(ct)) list.Add(m);`
+  - 新增私有方法 `BuildS0SalesMenus`(4 个 SysMenu:1 Dir + 3 Menu)
+  - 检查:diff 仅追加,Id=1329002000000~1329002000003 无冲突
+
+### 阶段 H:前端编译
+
+- [x] **Step 16** — 前端编译验证
+  - 执行 `pnpm dev`(或 `vue-tsc --noEmit`)无 TS 类型错误
+  - 检查:无 error,页面路由可访问
+
+### 阶段 I:集成验收
+
+- [x] **Step 17** — 验证数据库表创建
+  - 启动后检查 3 张表存在:`ado_s0_sales_customer`、`ado_s0_sales_material`、`ado_s0_sales_order_priority_rule`
+  - 检查:表结构与实体字段一致
+
+- [x] **Step 18** — 验证 Swagger 接口
+  - 共 18 个端点可见(每实体 6 个:5 CRUD + 1 toggle-enabled)
+  - 检查:GET/POST/PUT/DELETE/PATCH 均可调通
+
+- [x] **Step 19** — 验证菜单侧栏
+  - 前端侧栏 "Ai-DOP > S0 运营建模 > 产销建模" 可见,含 3 个子菜单
+  - 查 `sys_menu` 表 Id=1329002000001 存在
+  - 检查:数据库已确认 `SysMenu` 存在 1329002000000~1329002000003 四条记录;页面跳转待浏览器侧最终点验
+
+- [x] **Step 20** — 验证页面 CRUD + 下拉
+  - 客户管理:增删改查 + toggle-enabled + 下拉(公司/工厂/客户类型/币种)正常
+  - 物料管理:分 Tab 表单保存,全部字段入库,字典下拉正常
+  - 订单优先规则:Code 留空可自动生成,ruleExpr 长文本可保存
+  - 检查:每页至少创建 1 条 → 编辑 → 查询 → 禁用 → 删除 完整流程
 
 ---
 
@@ -883,7 +1082,7 @@ Step 11  集成验收                                   →  按第十一节标
 
 - [ ] 后端 `dotnet build` 无报错
 - [ ] 开发环境启动后自动创建 3 张表:`ado_s0_sales_customer`、`ado_s0_sales_material`、`ado_s0_sales_order_priority_rule`
-- [ ] Swagger 中可见 `/api/s0/sales/*` 共 15 个接口(每实体 5 个 CRUD
+- [ ] Swagger 中可见 `/api/s0/sales/*` 共 18 个接口(每实体 5 个 CRUD + 1 个 toggle-enabled
 - [ ] 前端 `pnpm dev` 无 TypeScript 类型错误
 - [ ] 侧栏菜单 "Ai-DOP > S0 运营建模 > 产销建模" 可见,含 3 个子菜单
 - [ ] 客户管理页:增删改查正常,表单含全部 17 个业务字段
@@ -912,9 +1111,37 @@ Batch 1 不再新建独立实体/页面,改为向 `SysOrg` + `SysDictType`/`Sy
 | Currency(币种) | 同上 | `s0_currency` |
 | ForbidStatus(禁用状态) | 同上 | `s0_forbid_status` |
 
+运行约束:
+- `SeedData/*.cs` 负责初始化默认值,运行期前端和接口统一以数据库中的 `sys_org`、`sys_dict_type`、`sys_dict_data` 为权威数据源。
+- 如果业务人员在“字典管理”或“机构管理”页面修改了数据,页面应读取修改后的数据库结果,而不是回读工程中的种子文件。
+
 ### 后续关联
 
 | 后续 Batch | 关联点 | 动作 |
 |-----------|--------|------|
 | Batch 4(仓储) | Material.DefaultLocationId / DefaultRackId | 前端升级为仓储 Location/Rack 下拉 |
 | Batch 3(制造) | 制造实体引用 sales_material(物料 FK) | Material 表需先于制造迁移完成 |
+
+---
+
+## 十三、GAP Review 修正记录
+
+> 逐项审查后的修正,已同步回写到上文对应章节。
+
+| GAP | 等级 | 问题 | 决策 | 已回写 |
+|-----|------|------|------|--------|
+| GAP-1 | 严重 | Customer 是否缺 Nickname/TaxType/ShipTo | 查 S0 DB 确认:**这三列不存在**,DTO 幽灵字段。但发现 ForbidStatus 长度 20→50、Remark 500→1000、Currency 50→20 需对齐 DB;Code 需加唯一索引 | 已修正 §3.1 实体 + 字段对照表 |
+| GAP-2 | 中等 | 缺 toggle-enabled 端点 | 每个 Controller 增加 `PATCH /{id}/toggle-enabled`;接口总数 15→18 | 已修正 §5.1~5.3 路由 + §11 验收标准 |
+| GAP-3 | 中等 | 分页无 PagingGuard 保护 | 每个 Controller 的 GetPagedAsync 入口加 `PagingGuard.Normalize` | 已修正 §5 通用规则 |
+| GAP-4 | 中等 | 列表查询无排序致分页不稳定 | 统一加 `.OrderByDescending(x => x.CreatedAt)` | 已修正 §5 通用规则(与 GAP-3 合并) |
+| GAP-5 | 严重 | SysDictDataApi/SysOrgApi 调用方式写错 | 在 s0SalesApi.ts 封装 `loadDictOptions` / `loadOrgList` helper,内部正确实例化 class 并拆 AdminResult | 已修正 §7 |
+| GAP-6 | 中等 | AdminResult vs 裸 JSON 响应混用 | 由 GAP-5 的 helper 方案统一解决,页面代码不直接接触两种格式 | 已随 GAP-5 一并解决 |
+| GAP-7 | 低 | S0 分页用 items、新 API 用 list | 在 §7 注明契约变更,代码不改 | 已修正 §7 |
+| GAP-8 | 中等 | ForbidStatus 与 IsEnabled 语义耦合 | 后端 Create/Update 中强制联动:`entity.ForbidStatus = dto.IsEnabled ? "normal" : "forbidden"`;前端不展示 ForbidStatus 下拉 | 已修正 §5 通用规则 |
+| GAP-9 | 低 | OrderPriorityRule.Code 无自动生成 | Code 改为可选,后端 CreateAsync 中当 Code 为空时自动生成 `RULE-{timestamp}-{rand}` | 已修正 §4 DTO + §5.3 |
+| GAP-10 | 中等 | QueryDto 缺 CompanyRefId/FactoryRefId 过滤 | 三个 QueryDto 各加 `long? CompanyRefId` / `long? FactoryRefId`,Controller 加对应 WhereIF | 已修正 §4 DTO + §5 通用规则 |
+| GAP-11 | 低 | Customer Level 缺上限校验 | 前端 `el-input-number :max="9"` | 已修正 §8.1 |
+| GAP-12 | 严重 | MenuSeed 挂载遗漏风险 + Id 冲突 | 在 §9 末尾加验证要点(HasData 调用检查 + 启动后查 sys_menu + Id 范围验算),已确认无冲突 | 已修正 §9 |
+| GAP-13 | 低 | 字典 API 失败时下拉为空 | `loadDictOptions` / `loadOrgList` 内加 try-catch,失败返回空数组 + console.warn;页面 onMounted 加载失败时 ElMessage.warning 提示 | 已修正 §7 helper |
+| GAP-14 | 中等 | SCSS 引用路径错误(s0/sales/ 目录层级深) | 统一使用 alias `@import '/@/views/aidop/styles/aidop-demo.scss'` | 已修正 §8 通用规则 |
+| GAP-15 | 低 | Material.NameEn 在 S0 前端未使用 | 实体/DTO 保留(DB 有此列),前端 Tab 1 表单中作为非必填预留输入框 | 已修正 §8.2 |

+ 577 - 0
doc/plan/S0/Batch3-制造建模迁移方案.md

@@ -0,0 +1,577 @@
+# Batch 3:制造建模(Manufacturing)迁移方案
+
+> 本文档是 [S0迁移.md](S0迁移.md) Batch 3 的**细化版**,结构对齐 [spec.md](spec.md)(§1–§18),并补充与 [Batch2-产销建模迁移方案.md](Batch2-产销建模迁移方案.md) 同级的执行约束与技术附录,可直接交由 Cursor / AI 编程助手逐步执行。
+
+---
+
+## 零、项目级开发约束(执行本 Batch 前先对照)
+
+| 约束项 | 本 Batch 的执行要求 |
+|--------|---------------------|
+| 改前确认 | 新增实体、DTO、Controller、前端页、菜单前,先列改动清单(目标、文件、行为变化、风险、不做项) |
+| 跨模块影响 | 涉及 `Startup.cs`、`SysMenuSeedData.cs`、共享样式、平台字典追加时,单独写影响面 |
+| 最小改动 | 仅落地 Manufacturing 域,不顺手改 Warehouse/Quality 等后续 Batch 目录结构 |
+| 平台与 Batch2 | 公司/工厂仍用 `SysOrg`;物料主数据用 Batch2 的 `/api/s0/sales/materials`(或既有 `s0SalesApi`) |
+| 运行期数据源 | 下拉项从数据库/API 读取,不写死常量替代主数据 |
+| 文件修改策略 | 以新增为主;`Startup.cs`、`SysMenuSeedData.cs` **仅追加** |
+| 前端风格 | `<script setup lang="ts">` + Admin.NET `service`,禁用 S0 `http.js` / `useCrudPage.js` |
+| 版本号策略 | 仅在产生 Git 提交时双端同步升 patch |
+| 验收顺序 | 后端编译与表创建 → Swagger → 前端菜单与页面 → 聚合页主子表联调 |
+
+---
+
+## 〇、与 S0迁移.md 的修正
+
+| # | S0迁移.md 原计划 | 实际情况(以 S0 源码为准) | 修正 |
+|---|-----------------|---------------------------|------|
+| 1 | 新建实体约 **12** 个 | `S0.Domain/Manufacturing/ManufacturingEntities.cs` 共 **20** 张表(含 BOM 子表、工艺工序子表、线体岗位技能子表、前处理要素参数子表等) | 实体 **20** 个,全部 CodeFirst |
+| 2 | 页面约 **18** 个 | `src/router/index.js` 中制造业务路由 **17** 条;`ManufacturingCrudPage` / `ManufacturingAggregatePage` 为壳组件,不单独占菜单 | 菜单叶子 **17** 个 |
+| 3 | 未列 `Bom` / `BomItem` 等 | BOM、工艺路线、线体岗位、前处理要素在 S0 为 **主子聚合** 保存 | 4 组 **聚合 API**,子表无独立 REST(与 S0 一致) |
+| 4 | 未列 `SopDocument` | 存在 `mfg_sop_document`,对应 `SopMaintenanceList.vue` | 增加 `AdoS0SopDocument` 与 SOP 维护页 |
+| 5 | `PreprocessElementParam` 误解 | 前端 `PreprocessElementParamList.vue` 实际读写 **`element-params`** 且 `elementCategory=preprocess`;`mfg_preprocess_element_param` 由 **前处理要素聚合** 维护 | 文档 §6 / §12 区分「要素参数主表过滤页」与「聚合子表」 |
+| 6 | 路由为 `api/manufacturing/*` | AiDOP 与 Batch2 一致,统一为 `api/s0/manufacturing/*` | 全部加 `s0` 前缀 |
+| 7 | S0 子表禁用 `PUT .../toggle-enabled` | 与 Batch2 对齐:主表仍提供 `PATCH /{id}/toggle-enabled`(S0 部分为 `PUT`,迁移时统一为 **PATCH**) | 见 §7、§十 |
+
+**权威源路径(只读,禁止改 S0 工程):**
+
+- 后端实体:`/home/yy968/work/s0/s0-operating-modeling/backend/src/S0.Domain/Manufacturing/ManufacturingEntities.cs`
+- 聚合服务:`.../S0.Application/Manufacturing/Aggregates/ManufacturingAggregateServices.cs`
+- 控制器:`.../S0.Api/Controllers/Manufacturing/ManufacturingControllers.cs`
+- 前端 API:`.../src/api/modules/manufacturing.js`
+- 前端路由:`.../src/router/index.js`(`/manufacturing/*`)
+
+---
+
+## 1. 文档信息
+
+- **文档名称**:Batch 3 制造建模(Manufacturing)迁移方案
+- **文档版本**:v0.1
+- **创建日期**:2026-04-08
+- **最后更新日期**:2026-04-08
+- **当前状态**:草稿
+- **负责人**:(填写)
+- **参与角色**:产品 / 开发 / 测试 / 业务方
+- **关联链接**:[S0迁移.md](S0迁移.md)、[Batch2-产销建模迁移方案.md](Batch2-产销建模迁移方案.md)、[spec.md](spec.md)
+
+---
+
+## 2. 项目背景
+
+### 2.1 背景说明
+
+S0 运营建模独立项目中,制造主数据(标准 BOM、工艺路线、产线、工作中心、SOP 等)运行在 EF Core + 分层架构下。Batch 3 将这些能力迁入 AiDOPWarehouse 插件(SqlSugar + 单层 Entity/Dto/Controller),并与 Batch 2 物料主数据、Batch 1 组织与字典衔接。
+
+### 2.2 当前痛点
+
+- 制造数据与产销主数据分属旧 S0 工程,与 AiDOP 统一菜单、权限、部署不一致。
+- S0 迁移总纲中 Batch 3 实体数、页面数与源码不一致,易导致漏表、漏页。
+- 部分功能为 **主子表聚合**,不能简单按「一实体一 CRUD」理解。
+
+### 2.3 机会或价值
+
+- 统一技术栈与路由规范(`/api/s0/manufacturing/*`),降低联调成本。
+- BOM/工艺路线与物料 `ado_s0_sales_material` 在同一目标库,便于后续计划与工单 Batch 扩展。
+
+---
+
+## 3. 项目目标
+
+### 3.1 总体目标
+
+在 AiDOP 插件中落地 S0 制造域 **20 张表**、**17 个可运营页面**,行为与 S0 对齐,接口风格与 Batch 2 一致。
+
+### 3.2 具体目标
+
+- 完成 20 个 SqlSugar 实体 + `Startup.cs` `InitTables` 注册。
+- 完成 **16** 个 REST 资源(含 4 个聚合写模型;**生产/前处理要素参数** 两页共用 `element-params`),分页响应字段为 `list`(非 S0 的 `items`)。
+- 完成 `Web/src/views/aidop/s0/manufacturing/` 下 17 个页面与 `s0ManufacturingApi.ts`。
+- 完成 `BuildS0ManufacturingMenus`,`SubDirId = 1329003000000L`。
+
+### 3.3 成功指标(可选)
+
+- 制造子目录下 17 个菜单均可打开并完成至少一条「新增 → 编辑 → 查询 → 禁用 → 删除」流程(聚合页含子行保存)。
+- `dotnet build` 与前端类型检查无 error。
+
+### 3.4 本期不追求的目标
+
+- 不迁移 `ManufacturingTodoPage`、纯展示类聚合大屏(无独立业务写入)。
+- 不接 S0 原文件服务:`SopDocument.FileId` 本期用 **数值占位**(同 Batch 2 仓储 ID 策略)。
+- 不解决跨域历史数据迁移(仅结构与功能对齐,数据导入另案)。
+
+---
+
+## 4. 项目范围
+
+### 4.1 本期范围内
+
+- 20 个实体及表(见 §8.1)。
+- 17 个菜单页面(见 §10.1)。
+- 4 组聚合接口:BOM、Routing、LinePost、PreprocessElement(见 §6、§9)。
+- 其余 12 个资源为标准分页 CRUD + `PATCH /{id}/toggle-enabled`(与 4 个聚合合计 **16** 个 REST 资源)。
+
+### 4.2 本期范围外
+
+- Warehouse 主数据下拉:`location_id`、`workshop_id`、`department_id` 等 **仅 long 占位**,Batch 4 再换下拉。
+- Supply 供应商:`routing_operation.supplier_id` **仅 long 占位**,Batch 6 再换下拉。
+- 导航属性(如 `ProductionLine.Location`)不迁移,仅保留外键列。
+
+### 4.3 边界说明
+
+- **物料**:主数据归属 Batch 2;制造页通过 API 拉取物料列表供选择。
+- **工序**:使用本 Batch `standard-operations` 资源(对标 S0 `StandardOperation`)。
+- **字典**:制造若需新业务字典,在 `S0DictTypeSeedData` / `S0DictDataSeedData` **追加**(与 Batch 1 约定一致)。
+
+---
+
+## 5. 用户与使用场景
+
+### 5.1 目标用户
+
+- 工艺工程师、生产准备工程师、IE、系统管理员。
+
+### 5.2 用户特征
+
+- 具备制造主数据维护权限;熟悉 BOM、工艺路线、产线等概念。
+
+### 5.3 核心使用场景(与 17 个路由页对应)
+
+| 场景 | 触发条件 | 用户目标 | 期望结果 |
+|------|----------|----------|----------|
+| 维护标准 BOM | 新产品导入 | 建立父项物料与子项用量 | 保存后子表与头表一并落库 |
+| 维护工艺路线 | 定义加工路径 | 为物料绑定工序序列 | 保存后 `routing_operation` 与头表一致 |
+| 维护产线与线边 | 产线资源规划 | 维护产线、工位、线边物料 | 各表 CRUD 正常 |
+| 维护 SOP | 文档受控 | 维护 SOP 类型与文档元数据 | 表字段完整;文件 ID 可占位 |
+
+(其余页面场景同类:主数据维护 + 列表检索。)
+
+---
+
+## 6. 功能需求
+
+> 每个功能点:描述、前置条件、权限、验收要点。
+
+### 6.1 功能列表总览
+
+| 编号 | 功能名称 | 优先级 | 简述 |
+|------|----------|--------|------|
+| FR-001 | 标准 BOM | P0 | BOM 头 + BOM 行聚合增删改查 |
+| FR-002 | 标准工序 | P0 | `StandardOperation` CRUD |
+| FR-003 | 产线 | P0 | `ProductionLine` CRUD |
+| FR-004 | 工艺路线 | P0 | 路线头 + 工序行聚合 |
+| FR-005 | 物料替代 | P0 | `MaterialSubstitution` CRUD |
+| FR-006 | 工单控制参数 | P1 | `WorkOrderControl` CRUD |
+| FR-007 | 人员技能 | P1 | `PersonSkill` CRUD |
+| FR-008 | 人员技能分配 | P1 | `PersonSkillAssignment` CRUD |
+| FR-009 | 线体岗位 | P0 | 岗位头 + 岗位技能行聚合 |
+| FR-010 | 工作中心 | P0 | `WorkCenter` CRUD |
+| FR-011 | 线边物料 | P0 | `LineMaterial` CRUD |
+| FR-012 | 生产/前处理要素参数 | P0 | 同一 `ElementParam` 表与 `element-params` API;`ProductionElementParamList` 与 `PreprocessElementParamList` 两页按 `elementCategory` 区分(推荐后端查询参数过滤,见 §12) |
+| FR-013 | 物料工艺要素 | P0 | `MaterialProcessElement` CRUD |
+| FR-014 | 前处理要素 | P0 | 要素头 + 参数行聚合 |
+| FR-015 | SOP 文件类型 | P1 | `SopFileType` CRUD |
+| FR-016 | SOP 维护 | P1 | `SopDocument` CRUD |
+
+### 6.2 聚合功能详细说明(FR-001 / FR-004 / FR-009 / FR-014)
+
+对齐 S0 `ManufacturingAggregateServices.cs` 行为摘要:
+
+| 聚合 | 主表 | 子表 | Create | Update |
+|------|------|------|--------|--------|
+| BOM | `mfg_bom` | `mfg_bom_item` | 插入头后逐行插子项 | 更新头;删除旧子行再插入请求中子行(全量替换) |
+| Routing | `mfg_routing` | `mfg_routing_operation` | 同上 | 同上 |
+| LinePost | `mfg_line_post` | `mfg_line_post_skill` | 同上 | 同上 |
+| PreprocessElement | `mfg_preprocess_element` | `mfg_preprocess_element_param` | 同上 | 同上 |
+
+**业务规则:**
+
+- S0 `BomService` 要求 `BomItems` 非空;迁移时保持一致或在文档/接口注释中说明校验错误信息。
+- 子表行通常带 `sort_no` 或顺序索引;更新时以请求体为准覆盖。
+
+**权限**:开发阶段 `[AllowAnonymous]`;上线改为 `[Authorize]`(见 §11)。
+
+---
+
+## 7. 非功能需求
+
+### 7.1 性能
+
+- 列表分页默认 `PageSize` 20;`PagingGuard` 限制最大页大小(与 Batch 2 一致)。
+- **推荐**:`ElementParam` 列表接口支持 `elementCategory` 查询参数,避免前端拉全量再过滤(见 §12)。
+
+### 7.2 可用性
+
+- 列表默认 `OrderByDescending(x => x.CreatedAt)` 保证分页稳定。
+
+### 7.3 安全
+
+- 当前 `[AllowAnonymous]` + `[NonUnify]` 与 Batch 2 插件风格一致;上线前收紧。
+
+### 7.4 兼容性
+
+- 浏览器与现有 AiDOP Web 一致。
+
+### 7.5 可维护性
+
+- Controller 按资源拆分多文件,避免单文件过大。
+- 路由、表名、Vue `name` 与本文档 §9、§10 一致。
+
+### 7.6 合规
+
+- 无额外合规要求(按公司规范走代码审查)。
+
+---
+
+## 8. 数据需求
+
+### 8.1 核心数据对象(20)
+
+| S0 表名 | 实体类(S0) | 目标表名 | 目标实体类名 |
+|---------|--------------|----------|--------------|
+| `mfg_bom` | Bom | `ado_s0_mfg_bom` | `AdoS0MfgBom` |
+| `mfg_bom_item` | BomItem | `ado_s0_mfg_bom_item` | `AdoS0MfgBomItem` |
+| `mfg_standard_operation` | StandardOperation | `ado_s0_mfg_standard_operation` | `AdoS0MfgStandardOperation` |
+| `mfg_production_line` | ProductionLine | `ado_s0_mfg_production_line` | `AdoS0MfgProductionLine` |
+| `mfg_routing` | Routing | `ado_s0_mfg_routing` | `AdoS0MfgRouting` |
+| `mfg_routing_operation` | RoutingOperation | `ado_s0_mfg_routing_operation` | `AdoS0MfgRoutingOperation` |
+| `mfg_person_skill_assignment` | PersonSkillAssignment | `ado_s0_mfg_person_skill_assignment` | `AdoS0MfgPersonSkillAssignment` |
+| `mfg_material_substitution` | MaterialSubstitution | `ado_s0_mfg_material_substitution` | `AdoS0MfgMaterialSubstitution` |
+| `mfg_work_order_control` | WorkOrderControl | `ado_s0_mfg_work_order_control` | `AdoS0MfgWorkOrderControl` |
+| `mfg_person_skill` | PersonSkill | `ado_s0_mfg_person_skill` | `AdoS0MfgPersonSkill` |
+| `mfg_line_post` | LinePost | `ado_s0_mfg_line_post` | `AdoS0MfgLinePost` |
+| `mfg_line_post_skill` | LinePostSkill | `ado_s0_mfg_line_post_skill` | `AdoS0MfgLinePostSkill` |
+| `mfg_work_center` | WorkCenter | `ado_s0_mfg_work_center` | `AdoS0MfgWorkCenter` |
+| `mfg_line_material` | LineMaterial | `ado_s0_mfg_line_material` | `AdoS0MfgLineMaterial` |
+| `mfg_element_param` | ElementParam | `ado_s0_mfg_element_param` | `AdoS0MfgElementParam` |
+| `mfg_material_process_element` | MaterialProcessElement | `ado_s0_mfg_material_process_element` | `AdoS0MfgMaterialProcessElement` |
+| `mfg_preprocess_element` | PreprocessElement | `ado_s0_mfg_preprocess_element` | `AdoS0MfgPreprocessElement` |
+| `mfg_preprocess_element_param` | PreprocessElementParam | `ado_s0_mfg_preprocess_element_param` | `AdoS0MfgPreprocessElementParam` |
+| `mfg_sop_file_type` | SopFileType | `ado_s0_mfg_sop_file_type` | `AdoS0MfgSopFileType` |
+| `mfg_sop_document` | SopDocument | `ado_s0_mfg_sop_document` | `AdoS0MfgSopDocument` |
+
+### 8.2 字段与基类规则
+
+- S0 `BaseEntity` 字段映射:`Id, CompanyId, FactoryId, IsEnabled, CreatedAt, UpdatedAt` 内联到实体;**不迁移** `IsDeleted`、`VersionNo`、`CreatedBy`、`UpdatedBy`(与 [S0迁移.md](S0迁移.md) §3.2 一致)。
+- 业务列从 `ManufacturingEntities.cs` 逐列 `[SugarColumn]`;**不迁移** EF 导航属性。
+- `DateOnly?`(如 `LinePostSkill.EffectiveDate`)在 SqlSugar 中映射为兼容类型(如 `DateTime` 或字符串,按项目既有惯例与 Batch 2 对齐)。
+
+### 8.3 数据流转
+
+- 页面 → `service` → `/api/s0/manufacturing/*` → SqlSugar → `ado_s0_mfg_*`。
+- 物料 ID、工序 ID 等引用 Batch 2 与制造本域主表。
+
+### 8.4 外键与跨 Batch
+
+| 字段 | 引用 | Batch 3 策略 |
+|------|------|--------------|
+| `material_id` 等 | `ado_s0_sales_material` | `el-select` 远程/分页拉 `/api/s0/sales/materials` |
+| `operation_id` | `ado_s0_mfg_standard_operation` | 拉 standard-operations 列表 |
+| `location_id` / `workshop_id` | 仓储 | `el-input-number` 占位,Batch 4 改下拉 |
+| `department_id`(WorkCenter) | 仓储 | 同上 |
+| `supplier_id`(RoutingOperation) | 供应 | 占位,Batch 6 改下拉 |
+| `production_line_id` 等 | 制造内部表 | 下拉拉制造 API |
+
+---
+
+## 9. 接口与系统集成
+
+### 9.1 依赖
+
+| 系统/模块 | 用途 | 对接方式 |
+|-----------|------|----------|
+| Batch 2 Sales | 物料主数据 | REST |
+| Batch 1 平台 | 组织、字典(如需) | 既有 `SysOrgApi` / `SysDictDataApi` |
+| 后续 Warehouse / Supply | 库位、部门、供应商 | 本期仅 ID 占位 |
+
+### 9.2 REST 资源一览(S0 → AiDOP)
+
+| S0 路由 | AiDOP 目标路由 | 聚合 |
+|---------|----------------|------|
+| `GET/POST/PUT/DELETE` + toggle | `/api/s0/manufacturing/boms` | 是(body 含 `bomItems`) |
+| … | `/api/s0/manufacturing/routings` | 是(`routingOperations`) |
+| … | `/api/s0/manufacturing/line-posts` | 是(`linePostSkills`) |
+| … | `/api/s0/manufacturing/preprocess-elements` | 是(`params`) |
+| … | `/api/s0/manufacturing/standard-operations` | 否 |
+| … | `/api/s0/manufacturing/production-lines` | 否 |
+| … | `/api/s0/manufacturing/material-substitutions` | 否 |
+| … | `/api/s0/manufacturing/work-order-controls` | 否 |
+| … | `/api/s0/manufacturing/person-skills` | 否 |
+| … | `/api/s0/manufacturing/person-skill-assignments` | 否 |
+| … | `/api/s0/manufacturing/work-centers` | 否 |
+| … | `/api/s0/manufacturing/line-materials` | 否 |
+| … | `/api/s0/manufacturing/element-params` | 否(建议支持 `elementCategory` 查询) |
+| … | `/api/s0/manufacturing/material-process-elements` | 否(分页查询字段对齐 S0 `MaterialProcessElementQuery`) |
+| … | `/api/s0/manufacturing/sop-file-types` | 否 |
+| … | `/api/s0/manufacturing/sop-documents` | 否(分页查询字段对齐 S0 `SopDocumentQuery`) |
+
+### 9.3 通用控制器约定(与 Batch 2 对齐)
+
+- `GET /`:分页,`{ total, page, pageSize, list }`;入口 `PagingGuard.Normalize`。
+- `GET /{id}`:详情;聚合资源 **展开子表集合**。
+- `POST /` / `PUT /{id}`` / `DELETE /{id}`:标准 REST;聚合在单事务内写主表与子表。
+- `PATCH /{id}/toggle-enabled`:body `{ isEnabled: boolean }`;与 S0 `PUT` 路径不同但语义一致。
+
+### 9.4 第三方依赖
+
+- 无。
+
+---
+
+## 10. UI / 交互要求
+
+### 10.1 页面/模块清单(17)
+
+| # | S0 路由 | 目标路径(建议) | Vue 文件名 | `name` / 路由 name |
+|---|---------|------------------|------------|---------------------|
+| 1 | `/manufacturing/standard-bom` | `/aidop/s0/manufacturing/standard-bom` | `StandardBomManagement.vue` | `aidopS0MfgStandardBom` |
+| 2 | `/manufacturing/standard-process` | `.../standard-operation` | `StandardProcessList.vue` | `aidopS0MfgStandardOperation` |
+| 3 | `/manufacturing/production-lines` | `.../production-line` | `ProductionLineList.vue` | `aidopS0MfgProductionLine` |
+| 4 | `/manufacturing/routings` | `.../routing` | `RoutingList.vue` | `aidopS0MfgRouting` |
+| 5 | `/manufacturing/material-substitution` | `.../material-substitution` | `MaterialSubstitutionList.vue` | `aidopS0MfgMaterialSubstitution` |
+| 6 | `/manufacturing/workorder-control` | `.../work-order-control` | `WorkOrderControlParams.vue` | `aidopS0MfgWorkOrderControl` |
+| 7 | `/manufacturing/personnel-skill` | `.../person-skill` | `PersonnelSkillList.vue` | `aidopS0MfgPersonSkill` |
+| 8 | `/manufacturing/person-skill-assignment` | `.../person-skill-assignment` | `PersonSkillAssignmentList.vue` | `aidopS0MfgPersonSkillAssignment` |
+| 9 | `/manufacturing/line-post` | `.../line-post` | `LinePostList.vue` | `aidopS0MfgLinePost` |
+| 10 | `/manufacturing/work-centers` | `.../work-center` | `WorkCenterList.vue` | `aidopS0MfgWorkCenter` |
+| 11 | `/manufacturing/line-material` | `.../line-material` | `LineMaterialList.vue` | `aidopS0MfgLineMaterial` |
+| 12 | `/manufacturing/element-params` | `.../element-param-production` | `ProductionElementParamList.vue` | `aidopS0MfgElementParamProduction` |
+| 13 | `/manufacturing/material-process-element` | `.../material-process-element` | `MaterialProcessElementList.vue` | `aidopS0MfgMaterialProcessElement` |
+| 14 | `/manufacturing/preprocess-element` | `.../preprocess-element` | `PreprocessElementList.vue` | `aidopS0MfgPreprocessElement` |
+| 15 | `/manufacturing/preprocess-element-param` | `.../preprocess-element-param` | `PreprocessElementParamList.vue` | `aidopS0MfgElementParamPreprocess` |
+| 16 | `/manufacturing/sop-file-type` | `.../sop-file-type` | `SopFileTypeList.vue` | `aidopS0MfgSopFileType` |
+| 17 | `/manufacturing/sop-maintenance` | `.../sop-document` | `SopMaintenanceList.vue` | `aidopS0MfgSopDocument` |
+
+> 菜单种子:**17 个叶子菜单 + 1 个子目录**;`SubDirId + 1` … `SubDirId + 17`。口径以 S0 `router/index.js` 为准。
+
+### 10.2 交互规则
+
+- 聚合页:主表单 + 子表可编辑网格;保存走单次 `POST`/`PUT`。
+- 列表页:查询区 + 表格 + 分页 + 弹窗或抽屉表单(对齐 Batch 2 `CustomerList` 复杂度即可)。
+- `AidopDemoShell` 相对路径:`../../components/AidopDemoShell.vue`(与 Batch 2 `sales` 子目录一致)。
+- 样式:`@import '/@/views/aidop/styles/aidop-demo.scss'`。
+
+### 10.3 文案
+
+- 成功/失败/空状态/权限提示与 Batch 2 同等级即可。
+
+### 10.4 设计稿
+
+- 无单独设计稿时,以 Element Plus 默认布局 + 现有 Aidop 演示页风格为准。
+
+---
+
+## 11. 权限与角色设计
+
+| 角色 | 可见范围 | 可操作范围 | 限制说明 |
+|------|----------|------------|----------|
+| 开发调试 | 全部 S0 制造菜单 | CRUD | `[AllowAnonymous]` |
+| 生产用户 | 按租户菜单 | CRUD | 上线后 `Authorize` + 菜单权限 |
+
+### 11.1 特殊规则
+
+- `toggle-enabled` 与 `IsEnabled` 语义与 Batch 2 一致。
+
+---
+
+## 12. 异常与边界情况
+
+- **聚合保存校验失败**:返回 400 + 明确消息(如 BOM 子行缺 `materialId`)。
+- **子表全量替换**:更新后子行 ID 变化属预期;前端应以服务端返回为准刷新。
+- **ElementParam 两页**:
+  - **方案 A**:前端分页拉取后按 `elementCategory` 过滤(与 S0 一致,实现快)。
+  - **方案 B(推荐)**:`GET /api/s0/manufacturing/element-params` 增加 `elementCategory` 参数,数据库侧过滤,减少流量。
+- **网络异常**:列表加载失败 `ElMessage` 提示;与 Batch 2 helper 一致。
+
+---
+
+## 13. 风险、依赖与约束
+
+| 风险 | 影响 | 概率 | 应对 |
+|------|------|------|------|
+| 聚合事务与 SqlSugar 多表顺序 | 中 | 中 | 先插主再插子;更新先删子再插;必要时 `ITransaction` |
+| 实体数量多、工期 | 高 | 中 | 按 §十阶段交付,每阶段可验收 |
+| 外键占位导致用户困惑 | 低 | 高 | 表单项 `placeholder` 说明 Batch 4/6 将改为下拉 |
+
+**约束**:遵守 [S0迁移.md](S0迁移.md) 零节项目级约束;S0 源码只读。
+
+---
+
+## 14. 验收标准
+
+| 编号 | 验收项 | 验收标准 |
+|------|--------|----------|
+| AC-001 | 后端编译 | `dotnet build` 无 error |
+| AC-002 | 表结构 | 启动后存在 20 张 `ado_s0_mfg_*` 表且与实体一致 |
+| AC-003 | Swagger | `/api/s0/manufacturing/*` 下 **16 个资源**均有完整 CRUD + toggle(聚合资源含展开 GET;要素参数仅 1 组路由) |
+| AC-004 | 菜单 | 侧栏「制造建模」子目录下 **17** 个菜单可打开 |
+| AC-005 | 聚合页 | BOM、Routing、LinePost、PreprocessElement 各完成「含子行保存」一次 |
+| AC-006 | 占位说明 | 库位/部门/供应商占位字段在 UI 有说明或可接受文档备注 |
+
+**接口数量说明**:16 资源 × 6 动作 = **96** 个 Action(若某资源列表 GET 带扩展查询参数,仍计为同一 Action)。
+
+---
+
+## 15. 开发计划与里程碑
+
+| 阶段 | 内容 | 输出物 |
+|------|------|--------|
+| A | 20 实体 | `Entity/S0/Manufacturing/*.cs` |
+| B | DTO | `Dto/S0/Manufacturing/*.cs`(Query/Upsert/聚合嵌套 DTO) |
+| C | Controller | `Controllers/S0/Manufacturing/*.cs` |
+| D | 注册 | `Startup.cs` InitTables + using |
+| E | 前端 API | `s0ManufacturingApi.ts` |
+| F | 17 页面 | `manufacturing/*.vue` |
+| G | 菜单 | `BuildS0ManufacturingMenus` |
+| H | 编译 | 前后端无 error |
+| I | 集成验收 | §14 勾选 |
+
+---
+
+## 16. 上线与运维要求
+
+- 与 Batch 2 同一插件发布节奏;回滚时禁用菜单或回滚版本。
+- 监控:接口 5xx、慢查询(制造列表若未加索引需后续优化)。
+
+---
+
+## 17. 待确认问题
+
+- SOP 文件上传是否在本期接平台附件服务。
+- `elementCategory` 是否采用后端过滤(推荐)或沿用 S0 前端过滤。
+
+---
+
+## 18. 变更记录
+
+| 版本 | 日期 | 变更内容 | 变更人 |
+|------|------|----------|--------|
+| v0.1 | 2026-04-08 | 初稿:spec 结构 + 20 表 + 17 页 + 聚合说明 | — |
+
+---
+
+## 附录 A:目标文件清单(后端)
+
+```
+server/Plugins/Admin.NET.Plugin.AiDOP/
+├── Entity/S0/Manufacturing/
+│   ├── AdoS0MfgBom.cs
+│   ├── AdoS0MfgBomItem.cs
+│   ├── AdoS0MfgStandardOperation.cs
+│   ├── AdoS0MfgProductionLine.cs
+│   ├── AdoS0MfgRouting.cs
+│   ├── AdoS0MfgRoutingOperation.cs
+│   ├── AdoS0MfgPersonSkillAssignment.cs
+│   ├── AdoS0MfgMaterialSubstitution.cs
+│   ├── AdoS0MfgWorkOrderControl.cs
+│   ├── AdoS0MfgPersonSkill.cs
+│   ├── AdoS0MfgLinePost.cs
+│   ├── AdoS0MfgLinePostSkill.cs
+│   ├── AdoS0MfgWorkCenter.cs
+│   ├── AdoS0MfgLineMaterial.cs
+│   ├── AdoS0MfgElementParam.cs
+│   ├── AdoS0MfgMaterialProcessElement.cs
+│   ├── AdoS0MfgPreprocessElement.cs
+│   ├── AdoS0MfgPreprocessElementParam.cs
+│   ├── AdoS0MfgSopFileType.cs
+│   └── AdoS0MfgSopDocument.cs
+├── Dto/S0/Manufacturing/
+│   └── AdoS0ManufacturingDtos.cs(可拆分为 Masters / Aggregates 多文件)
+└── Controllers/S0/Manufacturing/
+    └── (每资源一个 Controller,或按团队习惯分组)
+```
+
+---
+
+## 附录 B:目标文件清单(前端)
+
+```
+Web/src/views/aidop/s0/
+├── api/s0ManufacturingApi.ts
+└── manufacturing/
+    ├── StandardBomManagement.vue
+    ├── StandardProcessList.vue
+    ├── ProductionLineList.vue
+    ├── RoutingList.vue
+    ├── MaterialSubstitutionList.vue
+    ├── WorkOrderControlParams.vue
+    ├── PersonnelSkillList.vue
+    ├── PersonSkillAssignmentList.vue
+    ├── LinePostList.vue
+    ├── WorkCenterList.vue
+    ├── LineMaterialList.vue
+    ├── ProductionElementParamList.vue
+    ├── MaterialProcessElementList.vue
+    ├── PreprocessElementList.vue
+    ├── PreprocessElementParamList.vue
+    ├── SopFileTypeList.vue
+    └── SopMaintenanceList.vue
+```
+
+---
+
+## 附录 C:菜单 SeedData 约定
+
+- 方法名:`BuildS0ManufacturingMenus`
+- `S0DirId = 1321000001000L`
+- `SubDirId = 1329003000000L`(制造建模目录)
+- 子菜单:`SubDirId + 1` … `SubDirId + 17`(**17** 个菜单项,与 §10.1 一致)
+- 在 `HasData()` 末尾追加:`foreach (var m in BuildS0ManufacturingMenus(ct)) list.Add(m);`
+
+---
+
+## 附录 D:执行步骤(分阶段勾选)
+
+### 阶段 A:后端实体层
+
+- [ ] Step 1 — 按 `ManufacturingEntities.cs` 建立 20 个实体类,表名 `ado_s0_mfg_*`,字段内联 BaseEntity 规则
+- [ ] Step 2 — 为需唯一性的头表(如 `Code`)按 Batch 2 惯例增加 `[SugarIndex]`(若 S0 有唯一约束则对齐)
+
+### 阶段 B:后端 DTO 层
+
+- [ ] Step 3 — 主数据 Query/Upsert DTO
+- [ ] Step 4 — 聚合 DTO:`BomUpsertDto`(含 `List<BomItemUpsertDto>`)等,对齐 S0 请求形状并适配 `list` 分页响应
+
+### 阶段 C:后端控制器层
+
+- [ ] Step 5 — **12** 个标准 CRUD Controller + `PATCH toggle-enabled`(含共用的 `element-params`)
+- [ ] Step 6 — **4** 个聚合 Controller(BOM/Routing/LinePost/PreprocessElement)
+
+### 阶段 D:注册与编译
+
+- [ ] Step 7 — `Startup.cs` 追加 20 个 `typeof`
+- [ ] Step 8 — `dotnet build`
+
+### 阶段 E–G:前端与菜单
+
+- [ ] Step 9 — `s0ManufacturingApi.ts`(unwrap、`Paged<T>` 用 `list`)
+- [ ] Step 10 — 17 个页面
+- [ ] Step 11 — `BuildS0ManufacturingMenus`
+
+### 阶段 H–I:验收
+
+- [ ] Step 12 — 前端类型检查 / dev 启动
+- [ ] Step 13 — §14 验收表逐项确认
+
+---
+
+## 附录 E:与 Batch 2 的依赖关系
+
+```mermaid
+flowchart LR
+  subgraph batch2 [Batch2_Sales]
+    Mat[ado_s0_sales_material]
+  end
+  subgraph batch3 [Batch3_Manufacturing]
+    Bom[ado_s0_mfg_bom]
+    BomItem[ado_s0_mfg_bom_item]
+    Rt[ado_s0_mfg_routing]
+    RtOp[ado_s0_mfg_routing_operation]
+    Bom --> BomItem
+    Rt --> RtOp
+  end
+  Mat --> Bom
+  Mat --> Rt
+```
+
+---
+
+**全文结束**

+ 47 - 69
doc/plan/S0/S0迁移.md

@@ -4,6 +4,28 @@
 
 ---
 
+## 零、项目级开发约束
+
+| 约束项 | 要求 | 落地方式 |
+|------|------|---------|
+| 改前确认 | 动手前先列本轮改动清单,待确认后再改 | 清单至少包含目标、涉及文件、行为变化、风险/影响面、不做项 |
+| 跨模块影响 | 若会影响共享组件、公共接口、菜单种子、配置、数据库等,必须先预警再执行 | 单独写“跨模块影响”小节,等待决策 |
+| 改动范围 | 小问题做最小改动,非必要不大重构 | 不默认调整目录结构、全局配置、Docker/Nginx、种子大重构 |
+| 目标架构 | 遵守 `Admin.NET + SqlSugar + 动态菜单` | 严禁引入 EF Core 作为目标实现 |
+| 主数据策略 | 组织、字典等基础主数据优先复用平台能力 | 优先落到 `SysOrg`、`SysDictType`、`SysDictData` |
+| 运行期数据源 | `SeedData/*.cs` 只负责初始化,运行期以数据库为准 | 前端与接口统一读取 `sys_org`、`sys_dict_type`、`sys_dict_data` |
+| 文件修改策略 | 优先新增文件;若需改现有文件,只做追加或局部最小修改 | `Startup.cs`、`SysMenuSeedData.cs`、`aidopMenuDisplay.ts` 禁止整段替换 |
+| 前端实现风格 | 对齐现有 Web 端技术栈 | 使用 `<script setup lang="ts">` + Admin.NET `service` 实例,禁用 S0 的 `http.js` / `useCrudPage.js` |
+| 版本号策略 | 仅在产生 Git 提交时执行双端同步升 patch | `Web/package.json` 与 `server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj` 同提交递增 |
+| 验收门槛 | 每个 Batch 必须独立通过验收后再进入下一批 | 至少覆盖编译、启动、菜单、接口、页面或数据验证 |
+
+约束来源:
+- `AGENTS.md`
+- `.cursor/rules/collaboration-scope.mdc`
+- `.cursor/rules/version-bump-on-commit.mdc`
+
+---
+
 ## 一、总体原则
 
 1. **分批迁移**:共 7 批,按依赖关系排序,每批独立可运行。
@@ -11,6 +33,8 @@
 3. **架构折叠**:S0 的 Domain / Application / Infrastructure 四层 → 插件单层(Entity + Dto + Controller)。
 4. **前端风格对齐**:`<script setup lang="ts">` + Admin.NET `service` 实例,禁用 S0 的 `http.js` 和 `useCrudPage.js`。
 5. **不动现有文件**:只新增文件;若需修改 `Startup.cs`、`SysMenuSeedData.cs`、`aidopMenuDisplay.ts`,追加内容,不替换已有内容。
+6. **基础主数据优先复用平台能力**:组织机构、字典等基础数据优先落到 `SysOrg`、`SysDictType`、`SysDictData`,不重复建设 S0 独立平台表。
+7. **初始化由 SeedData 声明,运行期以数据库为准**:工程中的 `SeedData/*.cs` 只负责初始化,前端页面与业务接口统一读取数据库中的 `sys_org`、`sys_dict_type`、`sys_dict_data`。
 
 ---
 
@@ -429,7 +453,7 @@ db.CodeFirst.InitTables(
 |-------|------|-----------|--------|--------|--------------|
 | 1 | Platform 基础平台(**复用平台**) | `Domain/Platform` | 0(复用 SysOrg + SysDictType/Data) | 0(复用已有页面) | 无需新建 |
 | 2 | Sales 产销建模 | `Domain/Sales`、`api/modules/sales.js`、`views/CustomerManagement.vue` 等 | 3 | 3 | `1329002000000L` |
-| 3 | Manufacturing 制造建模 | `Domain/Manufacturing`、`api/modules/manufacturing.js`、`views/manufacturing/` | 12 | 18 | `1329003000000L` |
+| 3 | Manufacturing 制造建模 | `Domain/Manufacturing`、`api/modules/manufacturing.js`、`views/manufacturing/` | 20 | 17 | `1329003000000L` |
 | 4 | Warehouse 仓储建模 | `Domain/Warehouse`、`api/modules/warehouse.js`、`views/warehouse/` | 12 | 12 | `1329004000000L` |
 | 5 | Quality 质量建模 | `Domain/Quality`、`api/modules/quality.js`、`views/quality/` | 16 | 16 | `1329005000000L` |
 | 6 | Supply 供应建模 | `Domain/Supply`、`api/modules/supply.js`、`views/supply/` | 4 | 3 | `1329006000000L` |
@@ -491,13 +515,20 @@ S0 的 Company → Factory 父子关系映射到 `SysOrg` 树:
 
 #### 1.4 后端改动
 
-**新建文件(1 个):**
+**新建文件(3 个,已采用该方案):**
 ```
-server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0DictSeedData.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0DictTypeSeedData.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0DictDataSeedData.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0OrgSeedData.cs
 ```
 
 **不新建实体、不新建控制器、不修改 Startup.cs**(`SysDictType`/`SysDictData`/`SysOrg` 已由框架 CodeFirst 管理)。
 
+说明:
+- `S0DictTypeSeedData.cs`:声明 `s0_material_type`、`s0_pl_category` 等 8 组业务字典类型。
+- `S0DictDataSeedData.cs`:声明对应字典值,启动后写入 `sys_dict_data`。
+- `S0OrgSeedData.cs`:声明示范公司与工厂节点,启动后写入 `sys_org`。
+
 #### 1.5 前端改动
 
 **无新建页面**。使用已有页面:
@@ -520,9 +551,10 @@ SysDictDataApi.apiSysDictDataDataListCodeGet('s0_material_type')
 
 #### 1.7 Batch 1 验收标准
 
-- [ ] `S0DictSeedData.cs` 编译通过
+- [ ] `S0DictTypeSeedData.cs`、`S0DictDataSeedData.cs`、`S0OrgSeedData.cs` 编译通过
 - [ ] 启动后 `sys_dict_type` 中可见 `s0_*` 前缀的字典类型
 - [ ] 启动后 `sys_dict_data` 中可见对应字典值
+- [ ] 启动后 `sys_org` 中可见 `S0-C001`、`S0-F001`、`S0-F002` 组织节点
 - [ ] 前端"字典管理"页面可查看和编辑 S0 业务字典
 - [ ] 前端"机构管理"页面可创建公司/工厂组织节点
 
@@ -596,79 +628,25 @@ Web/src/views/aidop/s0/
 
 ### Batch 3:Manufacturing 制造建模
 
+> **详细方案见** [Batch3-制造建模迁移方案.md](Batch3-制造建模迁移方案.md)
+
+**修正说明(与旧版本节对比):** `S0.Domain/Manufacturing/ManufacturingEntities.cs` 共 **20** 张表(含 BOM 行、工艺工序行、线体岗位技能、前处理要素参数子表、SOP 文档等),不是 12 个实体;`src/router/index.js` 制造业务路由 **17** 条,对应 **17** 个菜单叶子;其中 **BOM / Routing / LinePost / PreprocessElement** 为 **主子聚合** 接口(与 S0 `ManufacturingAggregateServices` 一致)。REST 资源数为 **16**(`element-params` 被「生产要素参数」「前处理要素参数」两页共用)。AiDOP 路由前缀为 `/api/s0/manufacturing/*`。
+
 **源文件参考路径(只读):**
+
 ```
 /home/yy968/work/s0/s0-operating-modeling/backend/src/
-  S0.Api/Controllers/Manufacturing/ManufacturingControllers.cs  ← 多个 Controller 合并在一文件
-  S0.Application/Manufacturing/
+  S0.Domain/Manufacturing/ManufacturingEntities.cs
+  S0.Api/Controllers/Manufacturing/ManufacturingControllers.cs
+  S0.Application/Manufacturing/Aggregates/ManufacturingAggregateServices.cs
+  S0.Application/Manufacturing/Masters/
 
 /home/yy968/work/s0/s0-operating-modeling/src/
   api/modules/manufacturing.js
-  views/manufacturing/
-    RoutingList.vue
-    ProductionLineList.vue
-    StandardProcessList.vue
-    StandardBomManagement.vue
-    WorkOrderControlParams.vue
-    MaterialSubstitutionList.vue
-    PersonnelSkillList.vue
-    PersonSkillAssignmentList.vue
-    LineMaterialList.vue
-    LinePostList.vue
-    WorkCenterList.vue
-    MaterialProcessElementList.vue
-    ProductionElementParamList.vue
-    PreprocessElementList.vue
-    PreprocessElementParamList.vue
-    SopFileTypeList.vue
-    SopMaintenanceList.vue
-    ManufacturingCrudPage.vue   ← 含多个子路由入口
-    ManufacturingAggregatePage.vue
-    ManufacturingTodoPage.vue
+  views/manufacturing/*.vue(17 个路由页 + ManufacturingCrudPage / ManufacturingAggregatePage 壳组件)
 ```
 
-**后端 — 新建实体(约 12 个):**
-
-```
-Entity/S0/Manufacturing/
-  AdoS0StandardOperation.cs    → ado_s0_mfg_standard_operation
-  AdoS0ProductionLine.cs       → ado_s0_mfg_production_line
-  AdoS0Routing.cs              → ado_s0_mfg_routing
-  AdoS0RoutingOperation.cs     → ado_s0_mfg_routing_operation
-  AdoS0MaterialSubstitution.cs → ado_s0_mfg_material_substitution
-  AdoS0WorkOrderControl.cs     → ado_s0_mfg_work_order_control
-  AdoS0PersonSkill.cs          → ado_s0_mfg_person_skill
-  AdoS0WorkCenter.cs           → ado_s0_mfg_work_center
-  AdoS0LineMaterial.cs         → ado_s0_mfg_line_material
-  AdoS0LinePost.cs             → ado_s0_mfg_line_post
-  AdoS0SopFileType.cs          → ado_s0_mfg_sop_file_type
-  AdoS0MaterialProcessElement.cs → ado_s0_mfg_material_process_element
-```
-
-> **实体字段**:从 `ManufacturingControllers.cs` 和 `AppDbContext.cs` 的 `ConfigureManufacturing` 段读取全部字段定义,逐一转为 `[SugarColumn]` 格式。
-
-**前端 — 新建文件:**
-
-```
-Web/src/views/aidop/s0/
-  api/s0ManufacturingApi.ts
-  manufacturing/
-    RoutingList.vue              ← name="aidopS0MfgRouting"
-    ProductionLineList.vue       ← name="aidopS0MfgProductionLine"
-    StandardProcessList.vue      ← name="aidopS0MfgStandardProcess"
-    WorkOrderControlParams.vue   ← name="aidopS0MfgWorkOrderControl"
-    MaterialSubstitutionList.vue ← name="aidopS0MfgMaterialSubstitution"
-    PersonnelSkillList.vue       ← name="aidopS0MfgPersonSkill"
-    WorkCenterList.vue           ← name="aidopS0MfgWorkCenter"
-    LineMaterialList.vue         ← name="aidopS0MfgLineMaterial"
-    LinePostList.vue             ← name="aidopS0MfgLinePost"
-    SopFileTypeList.vue          ← name="aidopS0MfgSopFileType"
-    MaterialProcessElementList.vue ← name="aidopS0MfgMaterialProcessElement"
-```
-
-> `ManufacturingAggregatePage.vue` / `ManufacturingTodoPage.vue` 是 S0 的聚合展示页,暂不迁移(标注为后续补充)。
-
-**菜单 SeedData 追加(方法名 `BuildS0ManufacturingMenus`,SubDirId = 1329003000000L)**
+**菜单 SeedData 追加**(方法名 `BuildS0ManufacturingMenus`,`SubDirId = 1329003000000L`,子菜单 `SubDirId+1` … `+17`)
 
 ---
 

+ 351 - 0
doc/plan/S0/spec.md

@@ -0,0 +1,351 @@
+# 项目名称
+[填写项目名称]
+
+## 1. 文档信息
+- 文档版本:
+- 创建日期:
+- 最后更新日期:
+- 当前状态:草稿 / 评审中 / 已确认 / 开发中 / 已完成
+- 负责人:
+- 参与角色:产品 / 开发 / 测试 / 设计 / 业务方 / 运维
+- 关联链接:原始需求、原型图、设计稿、接口文档、任务链接、会议纪要
+
+---
+
+## 2. 项目背景
+### 2.1 背景说明
+[说明为什么要做这个项目,现有流程/系统存在什么问题]
+
+### 2.2 当前痛点
+- 痛点 1:
+- 痛点 2:
+- 痛点 3:
+
+### 2.3 机会或价值
+[说明该项目对业务、用户、效率、成本、合规、风险控制等方面的价值]
+
+---
+
+## 3. 项目目标
+### 3.1 总体目标
+[一句话概括本项目要解决什么问题]
+
+### 3.2 具体目标
+- 目标 1:
+- 目标 2:
+- 目标 3:
+
+### 3.3 成功指标(可选)
+- 指标 1:
+- 指标 2:
+- 指标 3:
+
+### 3.4 本期不追求的目标
+- 不追求 1:
+- 不追求 2:
+
+---
+
+## 4. 项目范围
+### 4.1 本期范围内
+- 功能/模块 1:
+- 功能/模块 2:
+- 功能/模块 3:
+
+### 4.2 本期范围外
+- 不包含 1:
+- 不包含 2:
+- 不包含 3:
+
+### 4.3 边界说明
+[说明与其他系统、其他团队、其他功能之间的边界,避免责任不清]
+
+---
+
+## 5. 用户与使用场景
+### 5.1 目标用户
+- 用户角色 1:
+- 用户角色 2:
+- 用户角色 3:
+
+### 5.2 用户特征
+[填写用户的基础特征、权限级别、业务熟悉程度、使用频率等]
+
+### 5.3 核心使用场景
+#### 场景 1
+- 触发条件:
+- 用户目标:
+- 操作步骤:
+- 期望结果:
+
+#### 场景 2
+- 触发条件:
+- 用户目标:
+- 操作步骤:
+- 期望结果:
+
+#### 场景 3
+- 触发条件:
+- 用户目标:
+- 操作步骤:
+- 期望结果:
+
+---
+
+## 6. 功能需求
+> 建议每个功能点都写清楚:功能描述、前置条件、业务规则、异常情况、权限要求、验收方式。
+
+### 6.1 功能列表总览
+| 编号 | 功能名称 | 优先级 | 简述 |
+|---|---|---|---|
+| FR-001 | [功能名称] | P0/P1/P2 | [一句话描述] |
+| FR-002 | [功能名称] | P0/P1/P2 | [一句话描述] |
+| FR-003 | [功能名称] | P0/P1/P2 | [一句话描述] |
+
+### 6.2 详细功能需求
+
+#### FR-001 [功能名称]
+- 功能描述:
+- 目标用户:
+- 前置条件:
+- 触发方式:
+- 正常流程:
+  1.
+  2.
+  3.
+- 业务规则:
+  - 规则 1:
+  - 规则 2:
+- 权限要求:
+- 输入项:
+- 输出结果:
+- 异常处理:
+  - 异常 1:
+  - 异常 2:
+- 日志/审计要求(如有):
+- 备注:
+
+#### FR-002 [功能名称]
+- 功能描述:
+- 目标用户:
+- 前置条件:
+- 触发方式:
+- 正常流程:
+  1.
+  2.
+  3.
+- 业务规则:
+  - 规则 1:
+  - 规则 2:
+- 权限要求:
+- 输入项:
+- 输出结果:
+- 异常处理:
+- 日志/审计要求(如有):
+- 备注:
+
+---
+
+## 7. 非功能需求
+### 7.1 性能要求
+- 页面响应时间:
+- 接口响应时间:
+- 并发要求:
+- 数据处理规模:
+
+### 7.2 可用性要求
+- 可用性目标:
+- 容错要求:
+- 失败后的兜底方案:
+
+### 7.3 安全要求
+- 登录认证方式:
+- 权限控制要求:
+- 敏感数据处理要求:
+- 操作审计要求:
+- 数据传输/存储安全要求:
+
+### 7.4 兼容性要求
+- 浏览器:
+- 操作系统:
+- 移动端/PC端:
+- 分辨率要求:
+
+### 7.5 可维护性要求
+- 代码规范要求:
+- 日志规范要求:
+- 配置管理要求:
+- 监控告警要求:
+
+### 7.6 合规要求(如有)
+- 法律法规:
+- 行业规范:
+- 公司内部规范:
+
+---
+
+## 8. 数据需求
+### 8.1 核心数据对象
+| 数据对象 | 来源 | 用途 | 是否新增 | 备注 |
+|---|---|---|---|---|
+| [对象名] | [来源] | [用途] | 是/否 | [备注] |
+
+### 8.2 字段要求(如关键字段较少可写在这里)
+| 字段名 | 类型 | 必填 | 说明 | 校验规则 |
+|---|---|---|---|---|
+| [字段] | [类型] | 是/否 | [说明] | [规则] |
+
+### 8.3 数据流转
+[描述数据从哪里来,经过哪些处理,流向哪里]
+
+### 8.4 数据保留与删除策略(如有)
+- 保留时长:
+- 删除规则:
+- 归档规则:
+
+---
+
+## 9. 接口与系统集成
+### 9.1 对外/对内依赖系统
+| 系统名称 | 用途 | 对接方式 | 负责人 | 风险 |
+|---|---|---|---|---|
+| [系统名] | [用途] | API/消息队列/数据库等 | [负责人] | [风险] |
+
+### 9.2 接口需求
+#### API-001 [接口名称]
+- 接口说明:
+- 请求方式:
+- 请求路径:
+- 请求参数:
+- 返回结果:
+- 错误码:
+- 调用方:
+- 权限要求:
+- 限流要求(如有):
+
+### 9.3 第三方依赖
+- 第三方服务 1:
+- 第三方服务 2:
+
+---
+
+## 10. UI / 交互要求(如适用)
+### 10.1 页面/模块清单
+- 页面 1:
+- 页面 2:
+- 页面 3:
+
+### 10.2 交互规则
+- 交互规则 1:
+- 交互规则 2:
+- 交互规则 3:
+
+### 10.3 提示文案与错误文案要求
+- 成功提示:
+- 失败提示:
+- 空状态提示:
+- 权限不足提示:
+
+### 10.4 设计稿/原型链接
+[填写链接]
+
+---
+
+## 11. 权限与角色设计
+| 角色 | 可见范围 | 可操作范围 | 限制说明 |
+|---|---|---|---|
+| [角色名] | [范围] | [操作] | [说明] |
+
+### 11.1 特殊权限规则
+- 规则 1:
+- 规则 2:
+
+---
+
+## 12. 异常与边界情况
+- 边界情况 1:
+- 边界情况 2:
+- 边界情况 3:
+- 非法输入处理:
+- 网络异常处理:
+- 重复提交处理:
+- 空数据处理:
+- 权限不足处理:
+
+---
+
+## 13. 风险、依赖与约束
+### 13.1 外部依赖
+- 依赖 1:
+- 依赖 2:
+
+### 13.2 项目风险
+| 风险 | 影响 | 概率 | 应对方案 |
+|---|---|---|---|
+| [风险项] | 高/中/低 | 高/中/低 | [应对] |
+
+### 13.3 约束条件
+- 时间约束:
+- 技术约束:
+- 资源约束:
+- 合规约束:
+
+---
+
+## 14. 验收标准
+> 这里必须写成“能测、能判定”的表达。
+
+### 14.1 功能验收
+| 编号 | 验收项 | 验收标准 |
+|---|---|---|
+| AC-001 | [验收项] | [明确通过条件] |
+| AC-002 | [验收项] | [明确通过条件] |
+| AC-003 | [验收项] | [明确通过条件] |
+
+### 14.2 非功能验收
+- 性能验收标准:
+- 安全验收标准:
+- 权限验收标准:
+- 兼容性验收标准:
+
+### 14.3 不通过条件
+- 情况 1:
+- 情况 2:
+- 情况 3:
+
+---
+
+## 15. 开发计划与里程碑
+| 阶段 | 内容 | 开始日期 | 结束日期 | 负责人 | 输出物 |
+|---|---|---|---|---|---|
+| 需求确认 | [内容] | [日期] | [日期] | [负责人] | [输出物] |
+| 设计评审 | [内容] | [日期] | [日期] | [负责人] | [输出物] |
+| 开发实现 | [内容] | [日期] | [日期] | [负责人] | [输出物] |
+| 测试验收 | [内容] | [日期] | [日期] | [负责人] | [输出物] |
+| 上线发布 | [内容] | [日期] | [日期] | [负责人] | [输出物] |
+
+---
+
+## 16. 上线与运维要求
+- 上线环境:
+- 发布方式:
+- 回滚方案:
+- 监控项:
+- 告警项:
+- 运维负责人:
+
+---
+
+## 17. 待确认问题
+- 问题 1:
+- 问题 2:
+- 问题 3:
+
+---
+
+## 18. 变更记录
+| 版本 | 日期 | 变更内容 | 变更人 |
+|---|---|---|---|
+| v0.1 | [日期] | 初稿创建 | [姓名] |
+| v0.2 | [日期] | 补充功能范围与验收标准 | [姓名] |
+| v1.0 | [日期] | 评审确认版 | [姓名] |

+ 117 - 2
server/Admin.NET.sln

@@ -1,7 +1,7 @@
-
+
 Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 18
-VisualStudioVersion = 18.5.11605.296 insiders
+VisualStudioVersion = 18.5.11605.296
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Application", "Admin.NET.Application\Admin.NET.Application.csproj", "{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}"
 EndProject
@@ -34,59 +34,173 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.WorkWeixin
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.AiDOP", "Plugins\Admin.NET.Plugin.AiDOP\Admin.NET.Plugin.AiDOP.csproj", "{A1D00000-0000-4000-8000-000000000001}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.AiDOP.Tests", "Plugins\Admin.NET.Plugin.AiDOP.Tests\Admin.NET.Plugin.AiDOP.Tests.csproj", "{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
 		Release|Any CPU = Release|Any CPU
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|x64.Build.0 = Debug|Any CPU
+		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|x86.Build.0 = Debug|Any CPU
 		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|x64.ActiveCfg = Release|Any CPU
+		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|x64.Build.0 = Release|Any CPU
+		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|x86.ActiveCfg = Release|Any CPU
+		{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|x86.Build.0 = Release|Any CPU
 		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|x64.Build.0 = Debug|Any CPU
+		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|x86.Build.0 = Debug|Any CPU
 		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|x64.ActiveCfg = Release|Any CPU
+		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|x64.Build.0 = Release|Any CPU
+		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|x86.ActiveCfg = Release|Any CPU
+		{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|x86.Build.0 = Release|Any CPU
 		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|x64.Build.0 = Debug|Any CPU
+		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|x86.Build.0 = Debug|Any CPU
 		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|x64.ActiveCfg = Release|Any CPU
+		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|x64.Build.0 = Release|Any CPU
+		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|x86.ActiveCfg = Release|Any CPU
+		{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|x86.Build.0 = Release|Any CPU
 		{11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|x64.Build.0 = Debug|Any CPU
+		{11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|x86.Build.0 = Debug|Any CPU
 		{11EA630B-4600-4236-A117-CE6C6CD67586}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{11EA630B-4600-4236-A117-CE6C6CD67586}.Release|Any CPU.Build.0 = Release|Any CPU
+		{11EA630B-4600-4236-A117-CE6C6CD67586}.Release|x64.ActiveCfg = Release|Any CPU
+		{11EA630B-4600-4236-A117-CE6C6CD67586}.Release|x64.Build.0 = Release|Any CPU
+		{11EA630B-4600-4236-A117-CE6C6CD67586}.Release|x86.ActiveCfg = Release|Any CPU
+		{11EA630B-4600-4236-A117-CE6C6CD67586}.Release|x86.Build.0 = Release|Any CPU
 		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Debug|x64.Build.0 = Debug|Any CPU
+		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Debug|x86.Build.0 = Debug|Any CPU
 		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|Any CPU.Build.0 = Release|Any CPU
+		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|x64.ActiveCfg = Release|Any CPU
+		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|x64.Build.0 = Release|Any CPU
+		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|x86.ActiveCfg = Release|Any CPU
+		{57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|x86.Build.0 = Release|Any CPU
 		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Debug|x64.Build.0 = Debug|Any CPU
+		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Debug|x86.Build.0 = Debug|Any CPU
 		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Release|x64.ActiveCfg = Release|Any CPU
+		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Release|x64.Build.0 = Release|Any CPU
+		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Release|x86.ActiveCfg = Release|Any CPU
+		{C4A288D5-0FAA-4F43-9072-B97635D7871D}.Release|x86.Build.0 = Release|Any CPU
 		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|x64.Build.0 = Debug|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|x86.Build.0 = Debug|Any CPU
 		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|x64.ActiveCfg = Release|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|x64.Build.0 = Release|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|x86.ActiveCfg = Release|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|x86.Build.0 = Release|Any CPU
 		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Debug|x64.Build.0 = Debug|Any CPU
+		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Debug|x86.Build.0 = Debug|Any CPU
 		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Release|Any CPU.Build.0 = Release|Any CPU
+		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Release|x64.ActiveCfg = Release|Any CPU
+		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Release|x64.Build.0 = Release|Any CPU
+		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Release|x86.ActiveCfg = Release|Any CPU
+		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Release|x86.Build.0 = Release|Any CPU
 		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Debug|x64.Build.0 = Debug|Any CPU
+		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Debug|x86.Build.0 = Debug|Any CPU
 		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|Any CPU.Build.0 = Release|Any CPU
+		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|x64.ActiveCfg = Release|Any CPU
+		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|x64.Build.0 = Release|Any CPU
+		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|x86.ActiveCfg = Release|Any CPU
+		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|x86.Build.0 = Release|Any CPU
 		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|x64.Build.0 = Debug|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|x86.Build.0 = Debug|Any CPU
 		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|Any CPU.Build.0 = Release|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|x64.ActiveCfg = Release|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|x64.Build.0 = Release|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|x86.ActiveCfg = Release|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|x86.Build.0 = Release|Any CPU
 		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Debug|x64.Build.0 = Debug|Any CPU
+		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Debug|x86.Build.0 = Debug|Any CPU
 		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Release|x64.ActiveCfg = Release|Any CPU
+		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Release|x64.Build.0 = Release|Any CPU
+		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Release|x86.ActiveCfg = Release|Any CPU
+		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Release|x86.Build.0 = Release|Any CPU
 		{A1D00000-0000-4000-8000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{A1D00000-0000-4000-8000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A1D00000-0000-4000-8000-000000000001}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{A1D00000-0000-4000-8000-000000000001}.Debug|x64.Build.0 = Debug|Any CPU
+		{A1D00000-0000-4000-8000-000000000001}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{A1D00000-0000-4000-8000-000000000001}.Debug|x86.Build.0 = Debug|Any CPU
 		{A1D00000-0000-4000-8000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{A1D00000-0000-4000-8000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A1D00000-0000-4000-8000-000000000001}.Release|x64.ActiveCfg = Release|Any CPU
+		{A1D00000-0000-4000-8000-000000000001}.Release|x64.Build.0 = Release|Any CPU
+		{A1D00000-0000-4000-8000-000000000001}.Release|x86.ActiveCfg = Release|Any CPU
+		{A1D00000-0000-4000-8000-000000000001}.Release|x86.Build.0 = Release|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Debug|x64.Build.0 = Debug|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Debug|x86.Build.0 = Debug|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Release|x64.ActiveCfg = Release|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Release|x64.Build.0 = Release|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Release|x86.ActiveCfg = Release|Any CPU
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -99,6 +213,7 @@ Global
 		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
 		{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
 		{A1D00000-0000-4000-8000-000000000001} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
+		{7E83D655-6A60-47CE-8323-6C2A11E9ABC5} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {5CD801D7-984A-4F5C-8FA2-211B7A5EA9F3}

+ 22 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/Admin.NET.Plugin.AiDOP.Tests.csproj

@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>disable</Nullable>
+    <IsPackable>false</IsPackable>
+    <NoWarn>1701;1702;1591;8632</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
+    <PackageReference Include="xunit" Version="2.9.2" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Admin.NET.Plugin.AiDOP\Admin.NET.Plugin.AiDOP.csproj" />
+  </ItemGroup>
+</Project>

+ 49 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Sales/AdoS0SalesRulesTests.cs

@@ -0,0 +1,49 @@
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+using Xunit;
+
+namespace Admin.NET.Plugin.AiDOP.Tests.S0.Sales;
+
+/// <summary>
+/// S0 Batch2 产销:客户/物料 ForbidStatus 与订单优先规则编码规则(单元测试)。
+/// </summary>
+public class AdoS0SalesRulesTests
+{
+    [Theory]
+    [InlineData(true, "normal")]
+    [InlineData(false, "forbidden")]
+    public void ForbidStatusFromIsEnabled_MatchesBatch2Contract(bool isEnabled, string expected)
+    {
+        Assert.Equal(expected, AdoS0SalesRules.ForbidStatusFromIsEnabled(isEnabled));
+    }
+
+    [Fact]
+    public void ResolveOrderPriorityRuleCodeForCreate_WhenCodeProvided_ReturnsAsIs()
+    {
+        const string code = "MY-RULE-01";
+        var now = new DateTime(2026, 4, 8, 12, 30, 45);
+        Assert.Equal(code, AdoS0SalesRules.ResolveOrderPriorityRuleCodeForCreate(code, now, 1234));
+    }
+
+    [Fact]
+    public void ResolveOrderPriorityRuleCodeForCreate_WhenEmpty_GeneratesRulePrefixWithTimestampAndSuffix()
+    {
+        var now = new DateTime(2026, 4, 8, 12, 30, 45);
+        var result = AdoS0SalesRules.ResolveOrderPriorityRuleCodeForCreate(null, now, 5678);
+        Assert.Equal("RULE-20260408123045-5678", result);
+    }
+
+    [Theory]
+    [InlineData(null)]
+    [InlineData("")]
+    [InlineData("   ")]
+    public void ResolveOrderPriorityRuleCodeForUpdate_WhenBlank_KeepsExisting(string dtoCode)
+    {
+        Assert.Equal("OLD", AdoS0SalesRules.ResolveOrderPriorityRuleCodeForUpdate(dtoCode, "OLD"));
+    }
+
+    [Fact]
+    public void ResolveOrderPriorityRuleCodeForUpdate_WhenProvided_Replaces()
+    {
+        Assert.Equal("NEW", AdoS0SalesRules.ResolveOrderPriorityRuleCodeForUpdate("NEW", "OLD"));
+    }
+}

+ 30 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Sales/PagingGuardTests.cs

@@ -0,0 +1,30 @@
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+using Xunit;
+
+namespace Admin.NET.Plugin.AiDOP.Tests.S0.Sales;
+
+/// <summary>
+/// S0 Batch2 列表分页与 PagingGuard 行为一致(单元测试)。
+/// </summary>
+public class PagingGuardTests
+{
+    [Theory]
+    [InlineData(0, 0, 1, 10)]
+    [InlineData(-1, -5, 1, 10)]
+    [InlineData(2, 10, 2, 10)]
+    [InlineData(1, 500, 1, 200)]
+    public void Normalize_ClampsPageAndPageSize(int page, int pageSize, int expectedPage, int expectedPageSize)
+    {
+        var (p, ps) = PagingGuard.Normalize(page, pageSize);
+        Assert.Equal(expectedPage, p);
+        Assert.Equal(expectedPageSize, ps);
+    }
+
+    [Fact]
+    public void Normalize_CustomMaxPageSize_IsRespected()
+    {
+        var (p, ps) = PagingGuard.Normalize(1, 100, maxPageSize: 50);
+        Assert.Equal(1, p);
+        Assert.Equal(50, ps);
+    }
+}

+ 132 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0CustomersController.cs

@@ -0,0 +1,132 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
+
+/// <summary>
+/// S0 客户主数据
+/// </summary>
+[ApiController]
+[Route("api/s0/sales/customers")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0CustomersController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0Customer> _rep;
+
+    public AdoS0CustomersController(SqlSugarRepository<AdoS0Customer> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0CustomerQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+
+        var query = _rep.AsQueryable()
+            .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId.Value)
+            .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => x.Code.Contains(q.Keyword!) || x.Name.Contains(q.Keyword!))
+            .WhereIF(q.IsEnabled.HasValue, x => x.IsEnabled == q.IsEnabled.Value);
+
+        var total = await query.CountAsync();
+        var list = await query
+            .OrderByDescending(x => x.CreatedAt)
+            .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)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        return item == null ? NotFound() : Ok(item);
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0CustomerUpsertDto dto)
+    {
+        var entity = new AdoS0Customer
+        {
+            CompanyRefId = dto.CompanyRefId,
+            FactoryRefId = dto.FactoryRefId,
+            Code = dto.Code,
+            Name = dto.Name,
+            NameEn = dto.NameEn,
+            CustomerType = dto.CustomerType,
+            ContactPerson = dto.ContactPerson,
+            ContactPhone = dto.ContactPhone,
+            Address = dto.Address,
+            Currency = dto.Currency,
+            IsTaxIncluded = dto.IsTaxIncluded,
+            PrimarySales = dto.PrimarySales,
+            BackupSales = dto.BackupSales,
+            CustomerLevel = dto.CustomerLevel,
+            Remark = dto.Remark,
+            IsEnabled = dto.IsEnabled,
+            CreatedAt = DateTime.Now
+        };
+        entity.ForbidStatus = AdoS0SalesRules.ForbidStatusFromIsEnabled(dto.IsEnabled);
+
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0CustomerUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+
+        entity.CompanyRefId = dto.CompanyRefId;
+        entity.FactoryRefId = dto.FactoryRefId;
+        entity.Code = dto.Code;
+        entity.Name = dto.Name;
+        entity.NameEn = dto.NameEn;
+        entity.CustomerType = dto.CustomerType;
+        entity.ContactPerson = dto.ContactPerson;
+        entity.ContactPhone = dto.ContactPhone;
+        entity.Address = dto.Address;
+        entity.Currency = dto.Currency;
+        entity.IsTaxIncluded = dto.IsTaxIncluded;
+        entity.PrimarySales = dto.PrimarySales;
+        entity.BackupSales = dto.BackupSales;
+        entity.CustomerLevel = dto.CustomerLevel;
+        entity.Remark = dto.Remark;
+        entity.IsEnabled = dto.IsEnabled;
+        entity.ForbidStatus = AdoS0SalesRules.ForbidStatusFromIsEnabled(dto.IsEnabled);
+        entity.UpdatedAt = DateTime.Now;
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpPatch("{id:long}/toggle-enabled")]
+    public async Task<IActionResult> ToggleEnabledAsync(long id, [FromBody] AdoS0ToggleEnabledDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+
+        entity.IsEnabled = dto.IsEnabled;
+        entity.ForbidStatus = AdoS0SalesRules.ForbidStatusFromIsEnabled(dto.IsEnabled);
+        entity.UpdatedAt = DateTime.Now;
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        if (item == null) return NotFound();
+
+        await _rep.DeleteAsync(item);
+        return Ok(new { message = "删除成功" });
+    }
+}

+ 185 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0MaterialsController.cs

@@ -0,0 +1,185 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
+
+/// <summary>
+/// S0 物料主数据
+/// </summary>
+[ApiController]
+[Route("api/s0/sales/materials")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0MaterialsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0Material> _rep;
+
+    public AdoS0MaterialsController(SqlSugarRepository<AdoS0Material> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0MaterialQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+
+        var query = _rep.AsQueryable()
+            .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId.Value)
+            .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => x.Code.Contains(q.Keyword!) || x.Name.Contains(q.Keyword!))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Code), x => x.Code.Contains(q.Code!))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Name), x => x.Name.Contains(q.Name!))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Spec), x => x.Spec!.Contains(q.Spec!))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.DrawingNo), x => x.DrawingNo!.Contains(q.DrawingNo!))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.PlCategory), x => x.PlCategory == q.PlCategory)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.MaterialType), x => x.MaterialType == q.MaterialType)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Language), x => x.Language == q.Language)
+            .WhereIF(q.IsEnabled.HasValue, x => x.IsEnabled == q.IsEnabled.Value);
+
+        var total = await query.CountAsync();
+        var list = await query
+            .OrderByDescending(x => x.CreatedAt)
+            .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)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        return item == null ? NotFound() : Ok(item);
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0MaterialUpsertDto dto)
+    {
+        var entity = new AdoS0Material
+        {
+            CompanyRefId = dto.CompanyRefId,
+            FactoryRefId = dto.FactoryRefId,
+            Code = dto.Code,
+            Name = dto.Name,
+            NameEn = dto.NameEn,
+            MaterialType = dto.MaterialType,
+            Unit = dto.Unit,
+            Spec = dto.Spec,
+            PlCategory = dto.PlCategory,
+            DrawingNo = dto.DrawingNo,
+            Language = dto.Language,
+            BizVersion = dto.BizVersion,
+            ProductCode = dto.ProductCode,
+            MaterialAttribute = dto.MaterialAttribute,
+            DefaultLocationId = dto.DefaultLocationId,
+            DefaultRackId = dto.DefaultRackId,
+            StockTypeCode = dto.StockTypeCode,
+            SafetyStock = dto.SafetyStock,
+            ShelfLifeDays = dto.ShelfLifeDays,
+            ExpireWarningDays = dto.ExpireWarningDays,
+            PurchaseLeadDays = dto.PurchaseLeadDays,
+            MinOrderQty = dto.MinOrderQty,
+            MaxOrderQty = dto.MaxOrderQty,
+            OrderMultiple = dto.OrderMultiple,
+            PreparationLeadDays = dto.PreparationLeadDays,
+            IsOnDemand = dto.IsOnDemand,
+            SpecialReqType = dto.SpecialReqType,
+            IsInspectionRequired = dto.IsInspectionRequired,
+            InspectionDays = dto.InspectionDays,
+            IsKeyMaterial = dto.IsKeyMaterial,
+            IsMainMaterial = dto.IsMainMaterial,
+            IsPreprocess = dto.IsPreprocess,
+            IsAutoBatch = dto.IsAutoBatch,
+            IsLabelRequired = dto.IsLabelRequired,
+            IsBatchFifoReminder = dto.IsBatchFifoReminder,
+            IsBatchFifoStrict = dto.IsBatchFifoStrict,
+            InventoryTurnoverRate = dto.InventoryTurnoverRate,
+            Remark = dto.Remark,
+            IsEnabled = dto.IsEnabled,
+            CreatedAt = DateTime.Now
+        };
+        entity.ForbidStatus = AdoS0SalesRules.ForbidStatusFromIsEnabled(dto.IsEnabled);
+
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0MaterialUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+
+        entity.CompanyRefId = dto.CompanyRefId;
+        entity.FactoryRefId = dto.FactoryRefId;
+        entity.Code = dto.Code;
+        entity.Name = dto.Name;
+        entity.NameEn = dto.NameEn;
+        entity.MaterialType = dto.MaterialType;
+        entity.Unit = dto.Unit;
+        entity.Spec = dto.Spec;
+        entity.PlCategory = dto.PlCategory;
+        entity.DrawingNo = dto.DrawingNo;
+        entity.Language = dto.Language;
+        entity.BizVersion = dto.BizVersion;
+        entity.ProductCode = dto.ProductCode;
+        entity.MaterialAttribute = dto.MaterialAttribute;
+        entity.DefaultLocationId = dto.DefaultLocationId;
+        entity.DefaultRackId = dto.DefaultRackId;
+        entity.StockTypeCode = dto.StockTypeCode;
+        entity.SafetyStock = dto.SafetyStock;
+        entity.ShelfLifeDays = dto.ShelfLifeDays;
+        entity.ExpireWarningDays = dto.ExpireWarningDays;
+        entity.PurchaseLeadDays = dto.PurchaseLeadDays;
+        entity.MinOrderQty = dto.MinOrderQty;
+        entity.MaxOrderQty = dto.MaxOrderQty;
+        entity.OrderMultiple = dto.OrderMultiple;
+        entity.PreparationLeadDays = dto.PreparationLeadDays;
+        entity.IsOnDemand = dto.IsOnDemand;
+        entity.SpecialReqType = dto.SpecialReqType;
+        entity.IsInspectionRequired = dto.IsInspectionRequired;
+        entity.InspectionDays = dto.InspectionDays;
+        entity.IsKeyMaterial = dto.IsKeyMaterial;
+        entity.IsMainMaterial = dto.IsMainMaterial;
+        entity.IsPreprocess = dto.IsPreprocess;
+        entity.IsAutoBatch = dto.IsAutoBatch;
+        entity.IsLabelRequired = dto.IsLabelRequired;
+        entity.IsBatchFifoReminder = dto.IsBatchFifoReminder;
+        entity.IsBatchFifoStrict = dto.IsBatchFifoStrict;
+        entity.InventoryTurnoverRate = dto.InventoryTurnoverRate;
+        entity.Remark = dto.Remark;
+        entity.IsEnabled = dto.IsEnabled;
+        entity.ForbidStatus = AdoS0SalesRules.ForbidStatusFromIsEnabled(dto.IsEnabled);
+        entity.UpdatedAt = DateTime.Now;
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpPatch("{id:long}/toggle-enabled")]
+    public async Task<IActionResult> ToggleEnabledAsync(long id, [FromBody] AdoS0ToggleEnabledDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+
+        entity.IsEnabled = dto.IsEnabled;
+        entity.ForbidStatus = AdoS0SalesRules.ForbidStatusFromIsEnabled(dto.IsEnabled);
+        entity.UpdatedAt = DateTime.Now;
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        if (item == null) return NotFound();
+
+        await _rep.DeleteAsync(item);
+        return Ok(new { message = "删除成功" });
+    }
+}

+ 133 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0OrderPriorityRulesController.cs

@@ -0,0 +1,133 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
+
+/// <summary>
+/// S0 订单优先规则
+/// </summary>
+[ApiController]
+[Route("api/s0/sales/order-priority-rules")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0OrderPriorityRulesController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0OrderPriorityRule> _rep;
+
+    public AdoS0OrderPriorityRulesController(SqlSugarRepository<AdoS0OrderPriorityRule> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0OrderPriorityRuleQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+
+        var query = _rep.AsQueryable()
+            .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId.Value)
+            .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => x.Code.Contains(q.Keyword!) || x.Name.Contains(q.Keyword!))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.SourceEntity), x => x.SourceEntity == q.SourceEntity)
+            .WhereIF(q.IsEnabled.HasValue, x => x.IsEnabled == q.IsEnabled.Value);
+
+        var total = await query.CountAsync();
+        var list = await query
+            .OrderByDescending(x => x.CreatedAt)
+            .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)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        return item == null ? NotFound() : Ok(item);
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0OrderPriorityRuleUpsertDto dto)
+    {
+        var entity = new AdoS0OrderPriorityRule
+        {
+            CompanyRefId = dto.CompanyRefId,
+            FactoryRefId = dto.FactoryRefId,
+            Code = AdoS0SalesRules.ResolveOrderPriorityRuleCodeForCreate(
+                dto.Code,
+                DateTime.Now,
+                Random.Shared.Next(1000, 9999)),
+            Name = dto.Name,
+            PriorityLevel = dto.PriorityLevel,
+            SortDirection = dto.SortDirection,
+            SourceEntity = dto.SourceEntity,
+            SourceField = dto.SourceField,
+            SourceFieldType = dto.SourceFieldType,
+            SourceLinkField = dto.SourceLinkField,
+            WorkOrderField = dto.WorkOrderField,
+            WorkOrderFieldType = dto.WorkOrderFieldType,
+            WorkOrderLinkField = dto.WorkOrderLinkField,
+            RuleExpr = dto.RuleExpr,
+            Remark = dto.Remark,
+            IsEnabled = dto.IsEnabled,
+            CreatedAt = DateTime.Now
+        };
+
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0OrderPriorityRuleUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+
+        entity.CompanyRefId = dto.CompanyRefId;
+        entity.FactoryRefId = dto.FactoryRefId;
+        entity.Code = AdoS0SalesRules.ResolveOrderPriorityRuleCodeForUpdate(dto.Code, entity.Code);
+        entity.Name = dto.Name;
+        entity.PriorityLevel = dto.PriorityLevel;
+        entity.SortDirection = dto.SortDirection;
+        entity.SourceEntity = dto.SourceEntity;
+        entity.SourceField = dto.SourceField;
+        entity.SourceFieldType = dto.SourceFieldType;
+        entity.SourceLinkField = dto.SourceLinkField;
+        entity.WorkOrderField = dto.WorkOrderField;
+        entity.WorkOrderFieldType = dto.WorkOrderFieldType;
+        entity.WorkOrderLinkField = dto.WorkOrderLinkField;
+        entity.RuleExpr = dto.RuleExpr;
+        entity.Remark = dto.Remark;
+        entity.IsEnabled = dto.IsEnabled;
+        entity.UpdatedAt = DateTime.Now;
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpPatch("{id:long}/toggle-enabled")]
+    public async Task<IActionResult> ToggleEnabledAsync(long id, [FromBody] AdoS0ToggleEnabledDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+
+        entity.IsEnabled = dto.IsEnabled;
+        entity.UpdatedAt = DateTime.Now;
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        if (item == null) return NotFound();
+
+        await _rep.DeleteAsync(item);
+        return Ok(new { message = "删除成功" });
+    }
+}

+ 129 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Sales/AdoS0SalesDtos.cs

@@ -0,0 +1,129 @@
+namespace Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
+
+public class AdoS0ToggleEnabledDto
+{
+    public bool IsEnabled { get; set; }
+}
+
+public class AdoS0CustomerQueryDto
+{
+    public long? CompanyRefId { get; set; }
+    public long? FactoryRefId { get; set; }
+    public string? Keyword { get; set; }
+    public bool? IsEnabled { get; set; }
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 20;
+}
+
+public class AdoS0CustomerUpsertDto
+{
+    public long CompanyRefId { get; set; }
+    public long FactoryRefId { get; set; }
+    public string Code { get; set; } = string.Empty;
+    public string Name { get; set; } = string.Empty;
+    public string? NameEn { get; set; }
+    public string? CustomerType { get; set; }
+    public string? ContactPerson { get; set; }
+    public string? ContactPhone { get; set; }
+    public string? Address { get; set; }
+    public string ForbidStatus { get; set; } = "normal";
+    public string? Currency { get; set; }
+    public bool IsTaxIncluded { get; set; } = true;
+    public string? PrimarySales { get; set; }
+    public string? BackupSales { get; set; }
+    public int CustomerLevel { get; set; } = 1;
+    public string? Remark { get; set; }
+    public bool IsEnabled { get; set; } = true;
+}
+
+public class AdoS0MaterialQueryDto
+{
+    public long? CompanyRefId { get; set; }
+    public long? FactoryRefId { get; set; }
+    public string? Keyword { get; set; }
+    public string? Code { get; set; }
+    public string? Name { get; set; }
+    public string? Spec { get; set; }
+    public string? DrawingNo { get; set; }
+    public string? PlCategory { get; set; }
+    public string? MaterialType { get; set; }
+    public string? Language { get; set; }
+    public bool? IsEnabled { get; set; }
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 20;
+}
+
+public class AdoS0MaterialUpsertDto
+{
+    public long CompanyRefId { get; set; }
+    public long FactoryRefId { get; set; }
+    public string Code { get; set; } = string.Empty;
+    public string Name { get; set; } = string.Empty;
+    public string? NameEn { get; set; }
+    public string? MaterialType { get; set; }
+    public string? Unit { get; set; }
+    public string? Spec { get; set; }
+    public string? PlCategory { get; set; }
+    public string? DrawingNo { get; set; }
+    public string? Language { get; set; }
+    public string? BizVersion { get; set; }
+    public string? ProductCode { get; set; }
+    public string? MaterialAttribute { get; set; }
+    public long? DefaultLocationId { get; set; }
+    public long? DefaultRackId { get; set; }
+    public string? StockTypeCode { get; set; }
+    public decimal? SafetyStock { get; set; }
+    public int? ShelfLifeDays { get; set; }
+    public int? ExpireWarningDays { get; set; }
+    public int? PurchaseLeadDays { get; set; }
+    public decimal? MinOrderQty { get; set; }
+    public decimal? MaxOrderQty { get; set; }
+    public decimal? OrderMultiple { get; set; }
+    public int? PreparationLeadDays { get; set; }
+    public bool IsOnDemand { get; set; }
+    public string? SpecialReqType { get; set; }
+    public bool IsInspectionRequired { get; set; }
+    public int? InspectionDays { get; set; }
+    public bool IsKeyMaterial { get; set; }
+    public bool IsMainMaterial { get; set; }
+    public bool IsPreprocess { get; set; }
+    public bool IsAutoBatch { get; set; }
+    public bool IsLabelRequired { get; set; }
+    public bool IsBatchFifoReminder { get; set; }
+    public bool IsBatchFifoStrict { get; set; }
+    public decimal? InventoryTurnoverRate { get; set; }
+    public string ForbidStatus { get; set; } = "normal";
+    public bool IsEnabled { get; set; } = true;
+    public string? Remark { get; set; }
+}
+
+public class AdoS0OrderPriorityRuleQueryDto
+{
+    public long? CompanyRefId { get; set; }
+    public long? FactoryRefId { get; set; }
+    public string? Keyword { get; set; }
+    public string? SourceEntity { get; set; }
+    public bool? IsEnabled { get; set; }
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 20;
+}
+
+public class AdoS0OrderPriorityRuleUpsertDto
+{
+    public long CompanyRefId { get; set; }
+    public long FactoryRefId { get; set; }
+    public string? Code { get; set; }
+    public string Name { get; set; } = string.Empty;
+    public int PriorityLevel { get; set; }
+    public string SortDirection { get; set; } = "ASC";
+    public string? SourceEntity { get; set; }
+    public string? SourceField { get; set; }
+    public string? SourceFieldType { get; set; }
+    public string? SourceLinkField { get; set; }
+    public string? WorkOrderField { get; set; }
+    public string? WorkOrderFieldType { get; set; }
+    public string? WorkOrderLinkField { get; set; }
+    public string? RuleExpr { get; set; }
+    public string? Remark { get; set; }
+    public bool IsEnabled { get; set; } = true;
+}

+ 69 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0Customer.cs

@@ -0,0 +1,69 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+/// <summary>
+/// 客户主数据(S0 Sales / sales_customer)
+/// </summary>
+[SugarTable("ado_s0_sales_customer", "S0 客户主数据")]
+[SugarIndex("uk_ado_s0_sales_customer_code", nameof(Code), OrderByType.Asc, true)]
+public class AdoS0Customer
+{
+    [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnDescription = "关联公司 ID", ColumnDataType = "bigint")]
+    public long CompanyRefId { get; set; }
+
+    [SugarColumn(ColumnDescription = "关联工厂 ID", ColumnDataType = "bigint")]
+    public long FactoryRefId { get; set; }
+
+    [SugarColumn(ColumnDescription = "客户编码", Length = 100)]
+    public string Code { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnDescription = "客户名称", Length = 200)]
+    public string Name { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnDescription = "英文名称", Length = 200, IsNullable = true)]
+    public string? NameEn { get; set; }
+
+    [SugarColumn(ColumnDescription = "客户类型", Length = 50, IsNullable = true)]
+    public string? CustomerType { get; set; }
+
+    [SugarColumn(ColumnDescription = "联系人", Length = 100, IsNullable = true)]
+    public string? ContactPerson { get; set; }
+
+    [SugarColumn(ColumnDescription = "联系电话", Length = 50, IsNullable = true)]
+    public string? ContactPhone { get; set; }
+
+    [SugarColumn(ColumnDescription = "地址", Length = 500, IsNullable = true)]
+    public string? Address { get; set; }
+
+    [SugarColumn(ColumnDescription = "禁用状态", Length = 50)]
+    public string ForbidStatus { get; set; } = "normal";
+
+    [SugarColumn(ColumnDescription = "币种", Length = 20, IsNullable = true)]
+    public string? Currency { get; set; }
+
+    [SugarColumn(ColumnDescription = "是否含税", ColumnDataType = "boolean")]
+    public bool IsTaxIncluded { get; set; } = true;
+
+    [SugarColumn(ColumnDescription = "主销售员", Length = 100, IsNullable = true)]
+    public string? PrimarySales { get; set; }
+
+    [SugarColumn(ColumnDescription = "备用销售员", Length = 100, IsNullable = true)]
+    public string? BackupSales { get; set; }
+
+    [SugarColumn(ColumnDescription = "客户等级")]
+    public int CustomerLevel { get; set; } = 1;
+
+    [SugarColumn(ColumnDescription = "备注", Length = 1000, IsNullable = true)]
+    public string? Remark { get; set; }
+
+    [SugarColumn(ColumnDescription = "启用", ColumnDataType = "boolean")]
+    public bool IsEnabled { get; set; } = true;
+
+    [SugarColumn(ColumnDescription = "创建时间")]
+    public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)]
+    public DateTime? UpdatedAt { get; set; }
+}

+ 137 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0Material.cs

@@ -0,0 +1,137 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+/// <summary>
+/// 物料主数据(S0 Sales / sales_material)
+/// </summary>
+[SugarTable("ado_s0_sales_material", "S0 物料主数据")]
+public class AdoS0Material
+{
+    [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnDescription = "关联公司 ID", ColumnDataType = "bigint")]
+    public long CompanyRefId { get; set; }
+
+    [SugarColumn(ColumnDescription = "关联工厂 ID", ColumnDataType = "bigint")]
+    public long FactoryRefId { get; set; }
+
+    [SugarColumn(ColumnDescription = "物料编码", Length = 100)]
+    public string Code { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnDescription = "物料名称", Length = 200)]
+    public string Name { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnDescription = "英文名称", Length = 200, IsNullable = true)]
+    public string? NameEn { get; set; }
+
+    [SugarColumn(ColumnDescription = "物料类型", Length = 100, IsNullable = true)]
+    public string? MaterialType { get; set; }
+
+    [SugarColumn(ColumnDescription = "单位", Length = 50, IsNullable = true)]
+    public string? Unit { get; set; }
+
+    [SugarColumn(ColumnDescription = "规格型号", Length = 200, IsNullable = true)]
+    public string? Spec { get; set; }
+
+    [SugarColumn(ColumnDescription = "计划类别", Length = 100, IsNullable = true)]
+    public string? PlCategory { get; set; }
+
+    [SugarColumn(ColumnDescription = "图纸编号", Length = 100, IsNullable = true)]
+    public string? DrawingNo { get; set; }
+
+    [SugarColumn(ColumnDescription = "语言版本", Length = 50, IsNullable = true)]
+    public string? Language { get; set; }
+
+    [SugarColumn(ColumnDescription = "业务版本", Length = 50, IsNullable = true)]
+    public string? BizVersion { get; set; }
+
+    [SugarColumn(ColumnDescription = "产品编码", Length = 100, IsNullable = true)]
+    public string? ProductCode { get; set; }
+
+    [SugarColumn(ColumnDescription = "物料属性", Length = 50, IsNullable = true)]
+    public string? MaterialAttribute { get; set; }
+
+    [SugarColumn(ColumnDescription = "默认库位 ID", ColumnDataType = "bigint", IsNullable = true)]
+    public long? DefaultLocationId { get; set; }
+
+    [SugarColumn(ColumnDescription = "默认货架 ID", ColumnDataType = "bigint", IsNullable = true)]
+    public long? DefaultRackId { get; set; }
+
+    [SugarColumn(ColumnDescription = "库存类型编码", Length = 50, IsNullable = true)]
+    public string? StockTypeCode { get; set; }
+
+    [SugarColumn(ColumnDescription = "安全库存", ColumnDataType = "decimal(18,5)", IsNullable = true)]
+    public decimal? SafetyStock { get; set; }
+
+    [SugarColumn(ColumnDescription = "保质期(天)", IsNullable = true)]
+    public int? ShelfLifeDays { get; set; }
+
+    [SugarColumn(ColumnDescription = "到期预警天数", IsNullable = true)]
+    public int? ExpireWarningDays { get; set; }
+
+    [SugarColumn(ColumnDescription = "采购提前期(天)", IsNullable = true)]
+    public int? PurchaseLeadDays { get; set; }
+
+    [SugarColumn(ColumnDescription = "最小订货量", ColumnDataType = "decimal(18,5)", IsNullable = true)]
+    public decimal? MinOrderQty { get; set; }
+
+    [SugarColumn(ColumnDescription = "最大订货量", ColumnDataType = "decimal(18,5)", IsNullable = true)]
+    public decimal? MaxOrderQty { get; set; }
+
+    [SugarColumn(ColumnDescription = "订货倍数", ColumnDataType = "decimal(18,5)", IsNullable = true)]
+    public decimal? OrderMultiple { get; set; }
+
+    [SugarColumn(ColumnDescription = "备料提前期(天)", IsNullable = true)]
+    public int? PreparationLeadDays { get; set; }
+
+    [SugarColumn(ColumnDescription = "按需采购", ColumnDataType = "boolean")]
+    public bool IsOnDemand { get; set; }
+
+    [SugarColumn(ColumnDescription = "特殊需求类型", Length = 50, IsNullable = true)]
+    public string? SpecialReqType { get; set; }
+
+    [SugarColumn(ColumnDescription = "需检验", ColumnDataType = "boolean")]
+    public bool IsInspectionRequired { get; set; }
+
+    [SugarColumn(ColumnDescription = "检验天数", IsNullable = true)]
+    public int? InspectionDays { get; set; }
+
+    [SugarColumn(ColumnDescription = "关键物料", ColumnDataType = "boolean")]
+    public bool IsKeyMaterial { get; set; }
+
+    [SugarColumn(ColumnDescription = "主要物料", ColumnDataType = "boolean")]
+    public bool IsMainMaterial { get; set; }
+
+    [SugarColumn(ColumnDescription = "需预处理", ColumnDataType = "boolean")]
+    public bool IsPreprocess { get; set; }
+
+    [SugarColumn(ColumnDescription = "自动批次", ColumnDataType = "boolean")]
+    public bool IsAutoBatch { get; set; }
+
+    [SugarColumn(ColumnDescription = "需打标签", ColumnDataType = "boolean")]
+    public bool IsLabelRequired { get; set; }
+
+    [SugarColumn(ColumnDescription = "批次先进先出提醒", ColumnDataType = "boolean")]
+    public bool IsBatchFifoReminder { get; set; }
+
+    [SugarColumn(ColumnDescription = "批次先进先出严格", ColumnDataType = "boolean")]
+    public bool IsBatchFifoStrict { get; set; }
+
+    [SugarColumn(ColumnDescription = "库存周转率", ColumnDataType = "decimal(18,5)", IsNullable = true)]
+    public decimal? InventoryTurnoverRate { get; set; }
+
+    [SugarColumn(ColumnDescription = "禁用状态", Length = 20)]
+    public string ForbidStatus { get; set; } = "normal";
+
+    [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
+    public string? Remark { get; set; }
+
+    [SugarColumn(ColumnDescription = "启用", ColumnDataType = "boolean")]
+    public bool IsEnabled { get; set; } = true;
+
+    [SugarColumn(ColumnDescription = "创建时间")]
+    public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)]
+    public DateTime? UpdatedAt { get; set; }
+}

+ 65 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0OrderPriorityRule.cs

@@ -0,0 +1,65 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+/// <summary>
+/// 订单优先规则(S0 Sales / sales_order_priority_rule)
+/// </summary>
+[SugarTable("ado_s0_sales_order_priority_rule", "S0 订单优先规则")]
+public class AdoS0OrderPriorityRule
+{
+    [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnDescription = "关联公司 ID", ColumnDataType = "bigint")]
+    public long CompanyRefId { get; set; }
+
+    [SugarColumn(ColumnDescription = "关联工厂 ID", ColumnDataType = "bigint")]
+    public long FactoryRefId { get; set; }
+
+    [SugarColumn(ColumnDescription = "规则编码", Length = 100)]
+    public string Code { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnDescription = "规则名称", Length = 200)]
+    public string Name { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnDescription = "优先级别")]
+    public int PriorityLevel { get; set; }
+
+    [SugarColumn(ColumnDescription = "排序方向", Length = 10)]
+    public string SortDirection { get; set; } = "ASC";
+
+    [SugarColumn(ColumnDescription = "来源实体", Length = 200, IsNullable = true)]
+    public string? SourceEntity { get; set; }
+
+    [SugarColumn(ColumnDescription = "来源字段", Length = 200, IsNullable = true)]
+    public string? SourceField { get; set; }
+
+    [SugarColumn(ColumnDescription = "来源字段类型", Length = 100, IsNullable = true)]
+    public string? SourceFieldType { get; set; }
+
+    [SugarColumn(ColumnDescription = "来源关联字段", Length = 200, IsNullable = true)]
+    public string? SourceLinkField { get; set; }
+
+    [SugarColumn(ColumnDescription = "工单字段", Length = 200, IsNullable = true)]
+    public string? WorkOrderField { get; set; }
+
+    [SugarColumn(ColumnDescription = "工单字段类型", Length = 100, IsNullable = true)]
+    public string? WorkOrderFieldType { get; set; }
+
+    [SugarColumn(ColumnDescription = "工单关联字段", Length = 200, IsNullable = true)]
+    public string? WorkOrderLinkField { get; set; }
+
+    [SugarColumn(ColumnDescription = "规则表达式", Length = 1000, IsNullable = true)]
+    public string? RuleExpr { get; set; }
+
+    [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
+    public string? Remark { get; set; }
+
+    [SugarColumn(ColumnDescription = "启用", ColumnDataType = "boolean")]
+    public bool IsEnabled { get; set; } = true;
+
+    [SugarColumn(ColumnDescription = "创建时间")]
+    public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)]
+    public DateTime? UpdatedAt { get; set; }
+}

+ 26 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AdoS0SalesRules.cs

@@ -0,0 +1,26 @@
+namespace Admin.NET.Plugin.AiDOP.Infrastructure;
+
+/// <summary>
+/// S0 产销(Batch2)可单测的业务规则:与控制器中的赋值保持一致。
+/// </summary>
+public static class AdoS0SalesRules
+{
+    public static string ForbidStatusFromIsEnabled(bool isEnabled) =>
+        isEnabled ? "normal" : "forbidden";
+
+    /// <summary>
+    /// 创建规则:未填编码时按时间戳与后缀生成,与控制器原逻辑一致。
+    /// </summary>
+    public static string ResolveOrderPriorityRuleCodeForCreate(string? dtoCode, DateTime now, int randomSuffix)
+    {
+        if (!string.IsNullOrWhiteSpace(dtoCode))
+            return dtoCode;
+        return $"RULE-{now:yyyyMMddHHmmss}-{randomSuffix}";
+    }
+
+    /// <summary>
+    /// 更新规则:空编码表示不修改原编码。
+    /// </summary>
+    public static string ResolveOrderPriorityRuleCodeForUpdate(string? dtoCode, string existingCode) =>
+        string.IsNullOrWhiteSpace(dtoCode) ? existingCode : dtoCode;
+}

+ 87 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0DictDataSeedData.cs

@@ -0,0 +1,87 @@
+namespace Admin.NET.Plugin.AiDOP;
+
+/// <summary>
+/// S0 业务字典值种子(灌入 SysDictData,与 S0DictTypeSeedData 配套)
+/// </summary>
+[IncreSeed]
+public class S0DictDataSeedData : ISqlSugarEntitySeedData<SysDictData>
+{
+    public IEnumerable<SysDictData> HasData()
+    {
+        var ct = DateTime.Parse("2022-02-10 00:00:00");
+        var types = new S0DictTypeSeedData().HasData().ToList();
+
+        var materialTypeId      = types[0].Id;
+        var plCategoryId        = types[1].Id;
+        var stockTypeId         = types[2].Id;
+        var specialReqTypeId    = types[3].Id;
+        var materialAttributeId = types[4].Id;
+        var customerTypeId      = types[5].Id;
+        var currencyId          = types[6].Id;
+        var forbidStatusId      = types[7].Id;
+
+        long seq = 1329900100001L;
+
+        return new[]
+        {
+            // ── s0_material_type ──
+            D(seq++, materialTypeId, "原材料",   "raw",       100, ct),
+            D(seq++, materialTypeId, "半成品",   "semi",      101, ct),
+            D(seq++, materialTypeId, "成品",     "finished",  102, ct),
+            D(seq++, materialTypeId, "辅料",     "aux",       103, ct),
+            D(seq++, materialTypeId, "工装夹具", "fixture",   104, ct),
+            D(seq++, materialTypeId, "包装件",   "packaging", 105, ct),
+
+            // ── s0_pl_category ──
+            D(seq++, plCategoryId, "采购件", "purchase",    100, ct),
+            D(seq++, plCategoryId, "自制件", "manufacture", 101, ct),
+            D(seq++, plCategoryId, "委外件", "outsource",   102, ct),
+
+            // ── s0_stock_type ──
+            D(seq++, stockTypeId, "正常库存", "normal",     100, ct),
+            D(seq++, stockTypeId, "待检库存", "inspection", 101, ct),
+            D(seq++, stockTypeId, "寄售库存", "consigned",  102, ct),
+            D(seq++, stockTypeId, "安全库存", "safety",     103, ct),
+
+            // ── s0_special_req_type ──
+            D(seq++, specialReqTypeId, "常规", "common", 100, ct),
+            D(seq++, specialReqTypeId, "加急", "urgent", 101, ct),
+            D(seq++, specialReqTypeId, "出口", "export", 102, ct),
+            D(seq++, specialReqTypeId, "定制", "custom", 103, ct),
+
+            // ── s0_material_attribute ──
+            D(seq++, materialAttributeId, "标准件", "standard",    100, ct),
+            D(seq++, materialAttributeId, "采购件", "purchase",    101, ct),
+            D(seq++, materialAttributeId, "自制件", "manufacture", 102, ct),
+            D(seq++, materialAttributeId, "委外件", "outsource",   103, ct),
+
+            // ── s0_customer_type ──
+            D(seq++, customerTypeId, "内销", "domestic", 100, ct),
+            D(seq++, customerTypeId, "外销", "overseas", 101, ct),
+            D(seq++, customerTypeId, "经销", "dealer",   102, ct),
+
+            // ── s0_currency ──
+            D(seq++, currencyId, "CNY", "CNY", 100, ct),
+            D(seq++, currencyId, "USD", "USD", 101, ct),
+            D(seq++, currencyId, "EUR", "EUR", 102, ct),
+            D(seq++, currencyId, "JPY", "JPY", 103, ct),
+            D(seq++, currencyId, "HKD", "HKD", 104, ct),
+
+            // ── s0_forbid_status ──
+            D(seq++, forbidStatusId, "正常", "normal",    100, ct),
+            D(seq++, forbidStatusId, "禁用", "forbidden", 101, ct),
+        };
+    }
+
+    private static SysDictData D(long id, long typeId, string label, string value, int order, DateTime ct) =>
+        new()
+        {
+            Id = id,
+            DictTypeId = typeId,
+            Label = label,
+            Value = value,
+            OrderNo = order,
+            Status = StatusEnum.Enable,
+            CreateTime = ct,
+        };
+}

+ 24 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0DictTypeSeedData.cs

@@ -0,0 +1,24 @@
+namespace Admin.NET.Plugin.AiDOP;
+
+/// <summary>
+/// S0 业务字典类型种子(灌入 SysDictType,编码加 s0_ 前缀与系统字典隔离)
+/// </summary>
+[IncreSeed]
+public class S0DictTypeSeedData : ISqlSugarEntitySeedData<SysDictType>
+{
+    public IEnumerable<SysDictType> HasData()
+    {
+        var ct = DateTime.Parse("2022-02-10 00:00:00");
+        return new[]
+        {
+            new SysDictType { Id = 1329900001001L, Name = "S0-物料类型",     Code = "s0_material_type",      SysFlag = YesNoEnum.N, OrderNo = 500, Remark = "S0 物料类型(raw/semi/finished 等)",    Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001002L, Name = "S0-计划类别",     Code = "s0_pl_category",        SysFlag = YesNoEnum.N, OrderNo = 501, Remark = "S0 计划类别(采购/自制/委外)",           Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001003L, Name = "S0-库存类型",     Code = "s0_stock_type",         SysFlag = YesNoEnum.N, OrderNo = 502, Remark = "S0 库存类型",                            Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001004L, Name = "S0-特殊需求类型", Code = "s0_special_req_type",   SysFlag = YesNoEnum.N, OrderNo = 503, Remark = "S0 特殊需求类型",                        Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001005L, Name = "S0-物料属性",     Code = "s0_material_attribute", SysFlag = YesNoEnum.N, OrderNo = 504, Remark = "S0 物料属性",                            Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001006L, Name = "S0-客户类型",     Code = "s0_customer_type",      SysFlag = YesNoEnum.N, OrderNo = 505, Remark = "S0 客户类型",                            Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001007L, Name = "S0-币种",         Code = "s0_currency",           SysFlag = YesNoEnum.N, OrderNo = 506, Remark = "S0 币种",                                Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001008L, Name = "S0-禁用状态",     Code = "s0_forbid_status",      SysFlag = YesNoEnum.N, OrderNo = 507, Remark = "S0 禁用状态",                            Status = StatusEnum.Enable, CreateTime = ct },
+        };
+    }
+}

+ 64 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0OrgSeedData.cs

@@ -0,0 +1,64 @@
+namespace Admin.NET.Plugin.AiDOP;
+
+/// <summary>
+/// S0 示范组织节点种子(灌入 SysOrg,公司 Type=201 / 工厂 Type=501)
+/// </summary>
+[IncreSeed]
+public class S0OrgSeedData : ISqlSugarEntitySeedData<SysOrg>
+{
+    private const long S0CompanyId  = 1329900200001L;
+    private const long S0Factory1Id = 1329900200002L;
+    private const long S0Factory2Id = 1329900200003L;
+
+    public IEnumerable<SysOrg> HasData()
+    {
+        var ct = DateTime.Parse("2022-02-10 00:00:00");
+        var tenantId = SqlSugarConst.DefaultTenantId;
+
+        return new[]
+        {
+            new SysOrg
+            {
+                Id = S0CompanyId,
+                Pid = tenantId,
+                Name = "S0示范公司",
+                Code = "S0-C001",
+                Type = "201",
+                Level = 2,
+                OrderNo = 200,
+                Status = StatusEnum.Enable,
+                Remark = "S0 运营建模示范公司",
+                TenantId = tenantId,
+                CreateTime = ct,
+            },
+            new SysOrg
+            {
+                Id = S0Factory1Id,
+                Pid = S0CompanyId,
+                Name = "一工厂",
+                Code = "S0-F001",
+                Type = "501",
+                Level = 3,
+                OrderNo = 201,
+                Status = StatusEnum.Enable,
+                Remark = "S0 示范工厂 1",
+                TenantId = tenantId,
+                CreateTime = ct,
+            },
+            new SysOrg
+            {
+                Id = S0Factory2Id,
+                Pid = S0CompanyId,
+                Name = "二工厂",
+                Code = "S0-F002",
+                Type = "501",
+                Level = 3,
+                OrderNo = 202,
+                Status = StatusEnum.Enable,
+                Remark = "S0 示范工厂 2",
+                TenantId = tenantId,
+                CreateTime = ct,
+            },
+        };
+    }
+}

+ 82 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs

@@ -60,7 +60,9 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             {
                 menuSeq++;
                 idx++;
-                var component = ResolveComponent(modCode, idx);
+                var isDir = DirOverrides.Contains((modCode, idx));
+                var component = isDir ? "Layout" : ResolveComponent(modCode, idx);
+                var menuType = isDir ? MenuTypeEnum.Dir : MenuTypeEnum.Menu;
                 list.Add(new SysMenu
                 {
                     Id = 1322000000000L + menuSeq,
@@ -69,10 +71,10 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
                     Path = $"/aidop/{codeLower}/{idx:000}",
                     Name = $"aidop{mod.Code}{idx:000}",
                     Component = component,
-                    Type = MenuTypeEnum.Menu,
+                    Type = menuType,
                     CreateTime = ct,
                     OrderNo = subOrder++,
-                    Icon = "ele-Document",
+                    Icon = isDir ? "ele-Folder" : "ele-Document",
                     Remark = BuildRemark(mod.Code, leaf)
                 });
             }
@@ -80,6 +82,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
 
         foreach (var m in BuildAidopSmartOpsSeedMenus(ct))
             list.Add(m);
+        foreach (var m in BuildS0SalesMenus(ct))
+            list.Add(m);
 
         return list;
     }
@@ -162,6 +166,14 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
         { ("S1", 4), "/aidop/demo/cockpit" },
     };
 
+    /// <summary>
+    /// 需要从 Menu 提升为 Dir(目录)的叶子节点;用于承载子级菜单。
+    /// </summary>
+    private static readonly HashSet<(string Mod, int Leaf)> DirOverrides = new()
+    {
+        { ("S0", 1) }, // 数据建模 → 目录,产销建模挂在其下
+    };
+
     private static string BuildRemark(string code, (string Title, string Desc, string Complexity, string Days, string Note) leaf)
     {
         var notePart = string.IsNullOrWhiteSpace(leaf.Note) ? "" : $" | {leaf.Note}";
@@ -169,6 +181,73 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
         return s.Length <= 256 ? s : s[..256];
     }
 
+    private static IEnumerable<SysMenu> BuildS0SalesMenus(DateTime ct)
+    {
+        // 与 ModuleDefinitions 中 S0 首项「数据建模」的生成 Id 一致(全局 menuSeq=1 → 1322000000001)
+        const long s0DataModelingMenuId = 1322000000001L;
+        const long subDirId = 1329002000000L;
+
+        yield return new SysMenu
+        {
+            Id = subDirId,
+            Pid = s0DataModelingMenuId,
+            Title = "产销建模",
+            Path = "/aidop/s0/sales",
+            Name = "aidopS0Sales",
+            Component = "Layout",
+            Icon = "ele-ShoppingCart",
+            Type = MenuTypeEnum.Dir,
+            CreateTime = ct,
+            OrderNo = 20,
+            Remark = "S0 产销建模"
+        };
+
+        yield return new SysMenu
+        {
+            Id = subDirId + 1,
+            Pid = subDirId,
+            Title = "客户管理",
+            Path = "/aidop/s0/sales/customer",
+            Name = "aidopS0SalesCustomer",
+            Component = "/aidop/s0/sales/CustomerList",
+            Icon = "ele-User",
+            Type = MenuTypeEnum.Menu,
+            CreateTime = ct,
+            OrderNo = 1,
+            Remark = "S0 客户管理"
+        };
+
+        yield return new SysMenu
+        {
+            Id = subDirId + 2,
+            Pid = subDirId,
+            Title = "物料管理",
+            Path = "/aidop/s0/sales/material",
+            Name = "aidopS0SalesMaterial",
+            Component = "/aidop/s0/sales/MaterialList",
+            Icon = "ele-Box",
+            Type = MenuTypeEnum.Menu,
+            CreateTime = ct,
+            OrderNo = 2,
+            Remark = "S0 物料管理"
+        };
+
+        yield return new SysMenu
+        {
+            Id = subDirId + 3,
+            Pid = subDirId,
+            Title = "订单优先规则",
+            Path = "/aidop/s0/sales/order-priority-rule",
+            Name = "aidopS0SalesOrderPriorityRule",
+            Component = "/aidop/s0/sales/OrderPriorityRuleList",
+            Icon = "ele-Sort",
+            Type = MenuTypeEnum.Menu,
+            CreateTime = ct,
+            OrderNo = 3,
+            Remark = "S0 订单优先规则"
+        };
+    }
+
     private static readonly (string Code, string L1, (string Title, string Desc, string Complexity, string Days, string Note)[] Leaves)[] ModuleDefinitions =
     {
         ("S0", "S0 运营建模", new[]

+ 9 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Startup.cs

@@ -1,5 +1,6 @@
 using System.Diagnostics;
 using Admin.NET.Plugin.AiDOP.Entity;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
@@ -38,6 +39,13 @@ 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));
+        db.CodeFirst.InitTables(
+            typeof(AdoOrder),
+            typeof(AdoPlan),
+            typeof(AdoWorkOrder),
+            typeof(AdoS0Customer),
+            typeof(AdoS0Material),
+            typeof(AdoS0OrderPriorityRule)
+        );
     }
 }