فهرست منبع

feat(s0): add order priority rule + customer shipping address + material bom version

- 新增 S0 订单优先级业务规则(Entity / DTO / Controller / 前端 List 页面 + 字典维护)
- 字典 3 个新类型 + 10 条新数据(priority_customer_type / order_type / due_status)
- CustMaster 新增 ShippingAddress 字段;前端表格 + 表单字段对齐
- ItemMaster 新增 BomVersion / EnableBomVersion 字段;前端表单对齐
- MaterialList 物料属性输入框改为受控下拉(常规/非标)+ 保存失败用户可见错误
- CustomerList / MaterialList / OrderPriorityRuleList 表格固定 max-height(解决滚动)
- 修复 OrderPriorityRuleList 内联 script 重复 import 标识符;客户类型列冗余 ||
- 版本号 Web 2.4.155 / server 1.0.119
YY968XX 1 هفته پیش
والد
کامیت
e2b6ccb6d8

+ 146 - 1
Web/src/views/aidop/s0/api/s0SalesApi.ts

@@ -1,6 +1,8 @@
 import type { AxiosRequestConfig } from 'axios';
 import service from '/@/utils/request';
-import { SysDictDataApi, SysOrgApi } from '/@/api-services';
+import { SysDictDataApi, SysDictTypeApi, SysOrgApi } from '/@/api-services';
+import type { SysDictData } from '/@/api-services/models';
+import { StatusEnum } from '/@/api-services/models';
 
 function unwrap<T>(res: { data: T }): T {
 	return res.data;
@@ -43,6 +45,7 @@ export interface S0CustMasterRow {
 	taxIn: boolean;
 	custClass?: string | null;
 	address?: string | null;
+	shippingAddress?: string | null;
 	contact?: string | null;
 	position?: string | null;
 	contactInfo?: string | null;
@@ -71,6 +74,7 @@ export interface S0CustMasterUpsert {
 	taxIn: boolean;
 	custClass?: string;
 	address?: string;
+	shippingAddress?: string;
 	contact?: string;
 	position?: string;
 	contactInfo?: string;
@@ -133,6 +137,8 @@ export interface S0ItemMasterRow {
 	remark?: string | null;
 	emtType?: string | null;
 	ownerApplication?: string | null;
+	bomVersion?: string | null;
+	enableBomVersion: boolean;
 	bomDesign?: number | null;
 	routingDes?: number | null;
 	custSupplied: boolean;
@@ -194,6 +200,8 @@ export interface S0ItemMasterUpsert {
 	remark?: string;
 	emtType?: string;
 	ownerApplication?: string;
+	bomVersion?: string;
+	enableBomVersion: boolean;
 	bomDesign?: number | null;
 	routingDes?: number | null;
 	custSupplied: boolean;
@@ -500,6 +508,143 @@ export const s0OrderReviewCyclesApi = {
 		service.patch<S0OrderReviewCycleRow>(`/api/s0/sales/order-review-cycles/${id}/toggle-enabled`, body).then(unwrap),
 };
 
+// ── S0 订单优先级业务规则(ado_s0_sales_order_priority_rule)────────────────
+
+export interface S0PriorityRuleRow {
+	id: number;
+	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;
+	createdAt: string;
+	updatedAt?: string;
+	// 展示标签(后端 IsIgnore 计算列)
+	customerTypeLabel?: string;
+	orderTypeLabel?: string;
+	dueStatusLabel?: string;
+}
+
+export interface S0PriorityRuleUpsert {
+	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;
+	customerType?: string;
+	orderType?: string;
+	dueStatus?: string;
+	remark?: string;
+	isEnabled: boolean;
+}
+
+export const s0PriorityRulesApi = {
+	list: (params: Record<string, unknown>) =>
+		service.get<Paged<S0PriorityRuleRow>>('/api/s0/sales/priority-rules', { params }).then(unwrap),
+	get: (id: number) =>
+		service.get<S0PriorityRuleRow>(`/api/s0/sales/priority-rules/${id}`).then(unwrap),
+	create: (body: S0PriorityRuleUpsert) =>
+		service.post<S0PriorityRuleRow>('/api/s0/sales/priority-rules', body).then(unwrap),
+	update: (id: number, body: S0PriorityRuleUpsert) =>
+		service.put<S0PriorityRuleRow>(`/api/s0/sales/priority-rules/${id}`, body).then(unwrap),
+	delete: (id: number) => service.delete(`/api/s0/sales/priority-rules/${id}`).then(unwrap),
+	toggleEnabled: (id: number, body: { isEnabled: boolean }) =>
+		service.patch<S0PriorityRuleRow>(`/api/s0/sales/priority-rules/${id}/toggle-enabled`, body).then(unwrap),
+};
+
+// ── 系统字典数据 CRUD 封装(供配置维护弹窗各 Tab 使用)────────────────────
+
+export type { SysDictData };
+
+/** 按字典编码读取条目列表(含状态) */
+export async function listDictDataByCode(code: string): Promise<SysDictData[]> {
+	try {
+		const api = new SysDictDataApi(undefined, undefined, service);
+		const res = await api.apiSysDictDataDataListCodeGet(code);
+		return res.data.result ?? [];
+	} catch (e) {
+		console.warn(`[s0] listDictDataByCode(${code}) failed`, e);
+		return [];
+	}
+}
+
+/** 新增字典条目 */
+export async function addDictData(body: {
+	dictTypeId: number;
+	label: string;
+	value: string;
+	orderNo?: number;
+	status?: StatusEnum;
+}): Promise<void> {
+	const api = new SysDictDataApi(undefined, undefined, service);
+	await api.apiSysDictDataAddPost({ ...body, status: body.status ?? StatusEnum.NUMBER_1 });
+}
+
+/** 更新字典条目 */
+export async function updateDictData(body: {
+	id: number;
+	dictTypeId?: number;
+	label: string;
+	value: string;
+	orderNo?: number;
+	status?: StatusEnum;
+}): Promise<void> {
+	const api = new SysDictDataApi(undefined, undefined, service);
+	await api.apiSysDictDataUpdatePost({ ...body });
+}
+
+/** 删除字典条目 */
+export async function deleteDictData(id: number): Promise<void> {
+	const api = new SysDictDataApi(undefined, undefined, service);
+	await api.apiSysDictDataDeletePost({ id });
+}
+
+/** 启停字典条目(StatusEnum.NUMBER_1=启用,NUMBER_2=停用) */
+export async function setDictDataStatus(id: number, status: StatusEnum): Promise<void> {
+	const api = new SysDictDataApi(undefined, undefined, service);
+	await api.apiSysDictDataSetStatusPost({ id, status });
+}
+
+/**
+ * 按字典 code 查询字典类型 Id(供新增条目时传 dictTypeId 使用)。
+ * 结果不缓存;频繁调用场景应由调用方在组件内缓存。
+ */
+export async function getDictTypeIdByCode(code: string): Promise<number | null> {
+	try {
+		const api = new SysDictTypeApi(undefined, undefined, service);
+		const res = await api.apiSysDictTypeDataListGet(code);
+		// apiSysDictTypeDataListGet 返回的是该 code 下的数据列表
+		// 从 dictTypeId 字段取值
+		const items = res.data.result ?? [];
+		if (items.length > 0 && items[0].dictTypeId != null) return items[0].dictTypeId ?? null;
+		return null;
+	} catch (e) {
+		console.warn(`[s0] getDictTypeIdByCode(${code}) failed`, e);
+		return null;
+	}
+}
+
+export { StatusEnum };
+
 export async function loadOrgList(type?: string): Promise<OrgOption[]> {
 	try {
 		const api = new SysOrgApi(undefined, undefined, service);

+ 12 - 3
Web/src/views/aidop/s0/sales/CustomerList.vue

@@ -36,7 +36,7 @@
 			</el-form-item>
 		</el-form>
 
-		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%">
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" max-height="calc(100vh - 260px)">
 			<el-table-column prop="cust" label="客户编码" width="140" show-overflow-tooltip />
 			<el-table-column prop="custFullName" label="客户全称" min-width="200" show-overflow-tooltip />
 			<el-table-column prop="sortName" label="客户简称" min-width="140" show-overflow-tooltip />
@@ -58,7 +58,8 @@
 					</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column prop="address" label="地址" min-width="160" show-overflow-tooltip />
+			<el-table-column prop="address" label="通讯地址" min-width="160" show-overflow-tooltip />
+			<el-table-column prop="shippingAddress" label="收货地址" min-width="160" show-overflow-tooltip />
 			<el-table-column prop="contact" label="联系人" width="100" show-overflow-tooltip />
 			<el-table-column prop="position" label="职务" width="100" show-overflow-tooltip />
 			<el-table-column prop="contactInfo" label="联系方式" width="140" show-overflow-tooltip />
@@ -168,10 +169,15 @@
 						</el-form-item>
 					</el-col>
 					<el-col :span="24">
-						<el-form-item label="地址">
+						<el-form-item label="通讯地址">
 							<el-input v-model="form.address" />
 						</el-form-item>
 					</el-col>
+					<el-col :span="24">
+						<el-form-item label="收货地址">
+							<el-input v-model="form.shippingAddress" />
+						</el-form-item>
+					</el-col>
 					<el-col :span="12">
 						<el-form-item label="联系人">
 							<el-input v-model="form.contact" />
@@ -270,6 +276,7 @@ const form = reactive<CustomerFormModel>({
 	taxIn: true,
 	custClass: '',
 	address: '',
+	shippingAddress: '',
 	contact: '',
 	position: '',
 	contactInfo: '',
@@ -463,6 +470,7 @@ function resetForm() {
 		taxIn: true,
 		custClass: '',
 		address: '',
+		shippingAddress: '',
 		contact: '',
 		position: '',
 		contactInfo: '',
@@ -501,6 +509,7 @@ function openEdit(row: S0CustMasterRow) {
 		taxIn: row.taxIn,
 		custClass: row.custClass ?? '',
 		address: row.address ?? '',
+		shippingAddress: row.shippingAddress ?? '',
 		contact: row.contact ?? '',
 		position: row.position ?? '',
 		contactInfo: row.contactInfo ?? '',

+ 15 - 1
Web/src/views/aidop/s0/sales/MaterialList.vue

@@ -162,7 +162,12 @@
 								</el-select>
 							</el-form-item></el-col>
 							<el-col :span="12"><el-form-item label="计划员"><el-input v-model="form.planner" /></el-form-item></el-col>
-							<el-col :span="12"><el-form-item label="物料属性"><el-input v-model="form.ownerApplication" placeholder="OwnerApplication" /></el-form-item></el-col>
+							<el-col :span="12"><el-form-item label="物料属性">
+								<el-select v-model="form.ownerApplication" clearable filterable style="width: 100%">
+									<el-option label="常规产品" value="常规产品" />
+									<el-option label="非标产品" value="非标产品" />
+								</el-select>
+							</el-form-item></el-col>
 							<el-col :span="12"><el-form-item label="特殊采购类型"><el-input v-model="form.emtType" /></el-form-item></el-col>
 							<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" rows="2" /></el-form-item></el-col>
 						</el-row>
@@ -192,6 +197,8 @@
 							<el-col :span="8"><el-form-item label="保质期(天)"><el-input-number v-model="form.daysBetweenPM" :min="0" style="width: 100%" controls-position="right" /></el-form-item></el-col>
 							<el-col :span="8"><el-form-item label="过期预警(天)"><el-input-number v-model="form.expireAlarmDay" :min="0" style="width: 100%" controls-position="right" /></el-form-item></el-col>
 							<el-col :span="8"><el-form-item label="存货周转率"><el-input-number v-model="form.stockTurnOver" :min="0" :precision="5" style="width: 100%" controls-position="right" /></el-form-item></el-col>
+							<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bomVersion" /></el-form-item></el-col>
+							<el-col :span="8"><el-form-item label="启用BOM版本"><el-switch v-model="form.enableBomVersion" /></el-form-item></el-col>
 							<el-col :span="8"><el-form-item label="BOM设计(天)"><el-input-number v-model="form.bomDesign" :min="0" style="width: 100%" controls-position="right" /></el-form-item></el-col>
 							<el-col :span="8"><el-form-item label="工艺设计(天)"><el-input-number v-model="form.routingDes" :min="0" style="width: 100%" controls-position="right" /></el-form-item></el-col>
 							<el-col :span="8">
@@ -334,6 +341,8 @@ function emptyForm(): S0ItemMasterUpsert {
 		remark: '',
 		emtType: '',
 		ownerApplication: '',
+		bomVersion: '',
+		enableBomVersion: false,
 		bomDesign: null,
 		routingDes: null,
 		custSupplied: false,
@@ -511,6 +520,8 @@ function rowToForm(row: S0ItemMasterRow): void {
 		remark: row.remark ?? '',
 		emtType: row.emtType ?? '',
 		ownerApplication: row.ownerApplication ?? '',
+		bomVersion: row.bomVersion ?? '',
+		enableBomVersion: row.enableBomVersion,
 		bomDesign: row.bomDesign ?? null,
 		routingDes: row.routingDes ?? null,
 		custSupplied: row.custSupplied,
@@ -551,6 +562,9 @@ async function submitForm() {
 		}
 		dialogVisible.value = false;
 		await loadList();
+	} catch (error) {
+		console.error('[aidop/s0/sales/material] save failed', error);
+		ElMessage.error('保存物料失败,请检查填写内容后重试');
 	} finally {
 		saving.value = false;
 	}

+ 523 - 161
Web/src/views/aidop/s0/sales/OrderPriorityRuleList.vue

@@ -1,29 +1,12 @@
 <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.domainCode" clearable style="width: 120px" />
-			</el-form-item>
-			<el-form-item label="名称">
-				<el-input v-model="query.descr" clearable style="width: 140px" />
-			</el-form-item>
-			<el-form-item label="优先级">
-				<el-input-number v-model="query.priority" :min="0" clearable controls-position="right" style="width: 110px" />
-			</el-form-item>
-			<el-form-item label="来源表">
-				<el-input v-model="query.sourceTable" clearable style="width: 130px" />
-			</el-form-item>
-			<el-form-item label="来源字段">
-				<el-input v-model="query.sourceColumn" clearable style="width: 120px" />
-			</el-form-item>
-			<el-form-item label="工单字段">
-				<el-input v-model="query.value" clearable style="width: 120px" />
+				<el-input v-model="query.keyword" placeholder="规则名称/编码" clearable style="width: 180px" />
 			</el-form-item>
 			<el-form-item label="生效">
-				<el-select v-model="query.isActive" clearable placeholder="全部" style="width: 100px">
+				<el-select v-model="query.isEnabled" clearable placeholder="全部" style="width: 100px">
 					<el-option label="是" :value="true" />
 					<el-option label="否" :value="false" />
 				</el-select>
@@ -32,30 +15,35 @@
 				<el-button type="primary" @click="loadList">查询</el-button>
 				<el-button @click="resetQuery">重置</el-button>
 				<el-button type="success" @click="openCreate">新增规则</el-button>
+				<el-button @click="configDialogVisible = true">配置维护</el-button>
 			</el-form-item>
 		</el-form>
 
+		<!-- ── 主列表 ── -->
 		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" max-height="calc(100vh - 260px)">
-			<el-table-column prop="descr" label="名称" min-width="160" show-overflow-tooltip />
-			<el-table-column prop="priority" label="优先级" width="88" align="center" />
-			<el-table-column prop="orderByText" label="排序" width="88" align="center" />
-			<el-table-column prop="sourceTable" label="来源表" width="130" show-overflow-tooltip />
-			<el-table-column prop="sourceColumn" label="来源字段" width="120" show-overflow-tooltip />
-			<el-table-column prop="sourceType" label="来源类型" width="100" show-overflow-tooltip />
-			<el-table-column prop="sourceId" label="来源关联" width="110" show-overflow-tooltip />
-			<el-table-column prop="value" label="工单字段" width="120" show-overflow-tooltip />
-			<el-table-column prop="valueType" label="工单类型" width="100" show-overflow-tooltip />
-			<el-table-column prop="valueId" label="工单关联" width="110" show-overflow-tooltip />
-			<el-table-column label="生效" width="72" align="center">
+			<el-table-column prop="name" label="规则名称" min-width="160" show-overflow-tooltip />
+			<el-table-column label="客户类型" width="120" show-overflow-tooltip>
+				<template #default="{ row }">{{ row.customerTypeLabel || '—' }}</template>
+			</el-table-column>
+			<el-table-column label="订单类型" width="120" show-overflow-tooltip>
+				<template #default="{ row }">{{ row.orderTypeLabel || '—' }}</template>
+			</el-table-column>
+			<el-table-column label="交期条件" width="100" show-overflow-tooltip>
+				<template #default="{ row }">{{ row.dueStatusLabel || '—' }}</template>
+			</el-table-column>
+			<el-table-column prop="priorityLevel" label="优先级" width="80" align="center" />
+			<el-table-column prop="sortDirection" label="排序方向" width="88" align="center" />
+			<el-table-column label="是否生效" width="88" align="center">
 				<template #default="{ row }">
-					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
+					<el-tag :type="row.isEnabled ? 'success' : 'info'" size="small">{{ row.isEnabled ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+			<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
+			<el-table-column label="操作" width="200" fixed="right" align="center">
 				<template #default="{ row }">
 					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
-						{{ row.isActive ? '停用' : '生效' }}
+					<el-button link :type="row.isEnabled ? 'warning' : 'success'" @click="toggleEnabled(row)">
+						{{ row.isEnabled ? '停用' : '生效' }}
 					</el-button>
 					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
 				</template>
@@ -74,42 +62,165 @@
 			/>
 		</div>
 
-		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="760px" destroy-on-close @closed="() => { if (!dialogVisible) resetForm(); }">
-			<el-form ref="formRef" :model="form" :rules="rules" label-width="130px">
+		<!-- ── 新增/编辑规则弹窗 ── -->
+		<el-dialog
+			v-model="ruleDialogVisible"
+			:title="ruleDialogTitle"
+			width="680px"
+			destroy-on-close
+			@closed="resetRuleForm"
+		>
+			<el-form ref="ruleFormRef" :model="ruleForm" :rules="ruleFormRules" label-width="110px">
 				<el-row :gutter="16">
 					<el-col :span="12">
-						<el-form-item label="名称 Descr" prop="descr">
-							<el-input v-model="form.descr" />
+						<el-form-item label="规则编码" prop="code">
+							<el-input v-model="ruleForm.code" placeholder="可自动填充或手动输入" />
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
-						<el-form-item label="优先级">
-							<el-input-number v-model="form.priority" :min="0" style="width: 100%" controls-position="right" />
+						<el-form-item label="规则名称" prop="name">
+							<el-input v-model="ruleForm.name" />
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
-						<el-form-item label="排序 OrderBy">
-							<el-select v-model="form.orderBy" style="width: 100%">
-								<el-option label="正序" :value="true" />
-								<el-option label="倒序" :value="false" />
+						<el-form-item label="客户类型">
+							<el-select v-model="ruleForm.customerType" clearable placeholder="不限" style="width: 100%">
+								<el-option
+									v-for="opt in dictOptions.customerType"
+									:key="opt.value"
+									:label="opt.label"
+									:value="opt.value"
+								/>
 							</el-select>
 						</el-form-item>
 					</el-col>
-					<el-col :span="12"><el-form-item label="生效"><el-switch v-model="form.isActive" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="来源表"><el-input v-model="form.sourceTable" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="来源字段"><el-input v-model="form.sourceColumn" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="来源类型"><el-input v-model="form.sourceType" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="来源关联"><el-input v-model="form.sourceId" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="工单字段 Value"><el-input v-model="form.value" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="工单类型"><el-input v-model="form.valueType" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="工单关联"><el-input v-model="form.valueId" /></el-form-item></el-col>
+					<el-col :span="12">
+						<el-form-item label="订单类型">
+							<el-select v-model="ruleForm.orderType" clearable placeholder="不限" style="width: 100%">
+								<el-option
+									v-for="opt in dictOptions.orderType"
+									:key="opt.value"
+									:label="opt.label"
+									:value="opt.value"
+								/>
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="交期条件">
+							<el-select v-model="ruleForm.dueStatus" clearable placeholder="不限" style="width: 100%">
+								<el-option
+									v-for="opt in dictOptions.dueStatus"
+									:key="opt.value"
+									:label="opt.label"
+									:value="opt.value"
+								/>
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="优先级" prop="priorityLevel">
+							<el-input-number v-model="ruleForm.priorityLevel" :min="1" style="width: 100%" controls-position="right" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="排序方向">
+							<el-select v-model="ruleForm.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-switch v-model="ruleForm.isEnabled" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="24">
+						<el-form-item label="备注">
+							<el-input v-model="ruleForm.remark" type="textarea" :rows="2" />
+						</el-form-item>
+					</el-col>
 				</el-row>
 			</el-form>
 			<template #footer>
-				<el-button @click="dialogVisible = false">取消</el-button>
-				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+				<el-button @click="ruleDialogVisible = false">取消</el-button>
+				<el-button type="primary" :loading="ruleSaving" @click="submitRuleForm">保存</el-button>
 			</template>
 		</el-dialog>
+
+		<!-- ── 配置维护弹窗 ── -->
+		<el-dialog
+			v-model="configDialogVisible"
+			title="订单优先级配置维护"
+			width="860px"
+			destroy-on-close
+			@open="onConfigDialogOpen"
+		>
+			<el-tabs v-model="configActiveTab">
+				<!-- Tab 1: 客户类型 -->
+				<el-tab-pane label="客户类型" name="customerType">
+					<DictDataTab
+						dict-code="s0_order_priority_customer_type"
+						:rows="configTabs.customerType.rows"
+						:loading="configTabs.customerType.loading"
+						@reload="loadConfigTab('customerType')"
+					/>
+				</el-tab-pane>
+
+				<!-- Tab 2: 订单类型 -->
+				<el-tab-pane label="订单类型" name="orderType">
+					<DictDataTab
+						dict-code="s0_order_type"
+						:rows="configTabs.orderType.rows"
+						:loading="configTabs.orderType.loading"
+						@reload="loadConfigTab('orderType')"
+					/>
+				</el-tab-pane>
+
+				<!-- Tab 3: 交期规则 -->
+				<el-tab-pane label="交期规则" name="dueStatus">
+					<DictDataTab
+						dict-code="s0_due_status"
+						:rows="configTabs.dueStatus.rows"
+						:loading="configTabs.dueStatus.loading"
+						:allow-edit="true"
+						:allow-delete="false"
+						@reload="loadConfigTab('dueStatus')"
+					/>
+				</el-tab-pane>
+
+				<!-- Tab 4: 字段映射 -->
+				<el-tab-pane label="字段映射" name="fieldMapping">
+					<div class="field-mapping-panel">
+						<el-alert
+							type="info"
+							:closable="false"
+							title="字段映射来源于现有优先级规则字段配置"
+							description="来源实体、来源字段、工单字段等字段映射信息在各条业务规则中独立维护,请通过「编辑规则」进行调整。"
+						/>
+						<el-table
+							:data="fieldMappingRows"
+							v-loading="fieldMappingLoading"
+							border
+							stripe
+							size="small"
+							style="width: 100%; margin-top: 12px"
+							max-height="320px"
+						>
+							<el-table-column prop="name" label="规则名称" min-width="130" show-overflow-tooltip />
+							<el-table-column prop="sourceEntity" label="来源实体" width="130" show-overflow-tooltip />
+							<el-table-column prop="sourceField" label="来源字段" width="130" show-overflow-tooltip />
+							<el-table-column prop="workOrderField" label="工单字段" width="130" show-overflow-tooltip />
+							<el-table-column prop="workOrderFieldType" label="工单字段类型" width="120" show-overflow-tooltip />
+						</el-table>
+						<div v-if="!fieldMappingLoading && fieldMappingRows.length === 0" class="empty-hint">
+							当前未维护字段映射
+						</div>
+					</div>
+				</el-tab-pane>
+			</el-tabs>
+		</el-dialog>
 	</AidopDemoShell>
 </template>
 
@@ -119,75 +230,47 @@ import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
 import {
-	s0OrderPriorityRulesApi,
-	type S0PriorityCodeRow,
-	type S0PriorityCodeUpsert,
+	s0PriorityRulesApi,
+	type S0PriorityRuleRow,
+	type S0PriorityRuleUpsert,
+	loadDictOptions,
+	listDictDataByCode,
+	addDictData,
+	updateDictData,
+	deleteDictData,
+	setDictDataStatus,
+	getDictTypeIdByCode,
+	type SysDictData,
+	StatusEnum,
+	type OptionItem,
 } from '../api/s0SalesApi';
 
+// ── 旧 API 保留(不删除,不作为主列表数据源)──────────────────────────────
+// s0OrderPriorityRulesApi 仍在 s0SalesApi.ts 中导出,此处不需引用
+
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '订单优先规则');
 
+// ── 查询 ──────────────────────────────────────────────────────────────────
 const query = reactive({
 	keyword: '',
-	domainCode: '',
-	descr: '',
-	priority: undefined as number | undefined,
-	sourceTable: '',
-	sourceColumn: '',
-	value: '',
-	isActive: undefined as boolean | undefined,
+	isEnabled: undefined as boolean | undefined,
 	page: 1,
 	pageSize: 20,
 });
 
 const loading = ref(false);
-const rows = ref<S0PriorityCodeRow[]>([]);
+const rows = ref<S0PriorityRuleRow[]>([]);
 const total = ref(0);
-const dialogVisible = ref(false);
-const dialogTitle = ref('新增规则');
-const editingId = ref<string | null>(null);
-const saving = ref(false);
-const formRef = ref<FormInstance>();
-
-function emptyForm(): S0PriorityCodeUpsert {
-	return {
-		domainCode: '',
-		descr: '',
-		value: '',
-		priority: 1,
-		orderBy: true,
-		sourceTable: '',
-		sourceColumn: '',
-		sourceType: '',
-		sourceId: '',
-		valueType: '',
-		valueId: '',
-		isActive: true,
-		createUser: '',
-		updateUser: '',
-	};
-}
-
-const form = reactive<S0PriorityCodeUpsert>(emptyForm());
-
-const rules: FormRules = {
-	descr: [{ required: true, message: '请填写名称', trigger: 'blur' }],
-};
 
 async function loadList() {
 	loading.value = true;
 	try {
-		const data = await s0OrderPriorityRulesApi.list({
+		const data = await s0PriorityRulesApi.list({
 			page: query.page,
 			pageSize: query.pageSize,
 			keyword: query.keyword || undefined,
-			domainCode: query.domainCode || undefined,
-			descr: query.descr || undefined,
-			priority: query.priority,
-			sourceTable: query.sourceTable || undefined,
-			sourceColumn: query.sourceColumn || undefined,
-			value: query.value || undefined,
-			isActive: query.isActive,
+			isEnabled: query.isEnabled,
 		});
 		rows.value = data.list;
 		total.value = data.total;
@@ -200,105 +283,375 @@ async function loadList() {
 }
 
 function resetQuery() {
-	Object.assign(query, {
-		keyword: '',
-		domainCode: '',
-		descr: '',
-		priority: undefined,
-		sourceTable: '',
-		sourceColumn: '',
-		value: '',
-		isActive: undefined,
-		page: 1,
-	});
+	Object.assign(query, { keyword: '', isEnabled: undefined, page: 1 });
 	void loadList();
 }
 
-function resetForm() {
-	editingId.value = null;
-	Object.assign(form, emptyForm());
-	formRef.value?.clearValidate();
+// ── 字典选项 ──────────────────────────────────────────────────────────────
+const dictOptions = reactive({
+	customerType: [] as OptionItem[],
+	orderType: [] as OptionItem[],
+	dueStatus: [] as OptionItem[],
+});
+
+async function loadDictSelects() {
+	const [ct, ot, ds] = await Promise.all([
+		loadDictOptions('s0_order_priority_customer_type'),
+		loadDictOptions('s0_order_type'),
+		loadDictOptions('s0_due_status'),
+	]);
+	dictOptions.customerType = ct;
+	dictOptions.orderType = ot;
+	dictOptions.dueStatus = ds;
 }
 
-function rowToForm(row: S0PriorityCodeRow) {
-	Object.assign(form, {
-		domainCode: row.domainCode ?? '',
-		descr: row.descr,
-		value: row.value ?? '',
-		priority: row.priority,
-		orderBy: row.orderBy ?? true,
-		sourceTable: row.sourceTable ?? '',
-		sourceColumn: row.sourceColumn ?? '',
-		sourceType: row.sourceType ?? '',
-		sourceId: row.sourceId ?? '',
-		valueType: row.valueType ?? '',
-		valueId: row.valueId ?? '',
-		isActive: row.isActive,
-		createUser: row.createUser ?? '',
-		updateUser: row.updateUser ?? '',
-	});
+// ── 规则表单 ──────────────────────────────────────────────────────────────
+const ruleDialogVisible = ref(false);
+const ruleDialogTitle = ref('新增规则');
+const editingId = ref<number | null>(null);
+const ruleSaving = ref(false);
+const ruleFormRef = ref<FormInstance>();
+
+function emptyRuleForm(): S0PriorityRuleUpsert & { customerType?: string; orderType?: string; dueStatus?: string } {
+	return {
+		companyRefId: 0,
+		factoryRefId: 0,
+		code: '',
+		name: '',
+		priorityLevel: 1,
+		sortDirection: 'asc',
+		customerType: undefined,
+		orderType: undefined,
+		dueStatus: undefined,
+		isEnabled: true,
+		remark: '',
+	};
+}
+
+const ruleForm = reactive(emptyRuleForm());
+
+const ruleFormRules: FormRules = {
+	code: [{ required: true, message: '请填写规则编码', trigger: 'blur' }],
+	name: [{ required: true, message: '请填写规则名称', trigger: 'blur' }],
+	priorityLevel: [{ required: true, message: '请填写优先级', trigger: 'change' }],
+};
+
+function resetRuleForm() {
+	editingId.value = null;
+	Object.assign(ruleForm, emptyRuleForm());
+	ruleFormRef.value?.clearValidate();
 }
 
 function openCreate() {
-	resetForm();
-	dialogTitle.value = '新增规则';
-	dialogVisible.value = true;
+	resetRuleForm();
+	ruleDialogTitle.value = '新增规则';
+	ruleDialogVisible.value = true;
 }
 
-function openEdit(row: S0PriorityCodeRow) {
-	resetForm();
+function parseRuleExpr(row: S0PriorityRuleRow): { customerType?: string; orderType?: string; dueStatus?: string } {
+	// 后端 IsIgnore 标签字段优先;fallback 解析 ruleExpr JSON
+	if (row.customerTypeLabel !== undefined || row.orderTypeLabel !== undefined || row.dueStatusLabel !== undefined) {
+		// 标签字段是展示用的,实际 value 需从 ruleExpr 解析
+	}
+	if (!row.ruleExpr) return {};
+	try {
+		const parsed = JSON.parse(row.ruleExpr) as Record<string, unknown>;
+		return {
+			customerType: typeof parsed.customerType === 'string' ? parsed.customerType : undefined,
+			orderType: typeof parsed.orderType === 'string' ? parsed.orderType : undefined,
+			dueStatus: typeof parsed.dueStatus === 'string' ? parsed.dueStatus : undefined,
+		};
+	} catch {
+		return {};
+	}
+}
+
+function openEdit(row: S0PriorityRuleRow) {
+	resetRuleForm();
 	editingId.value = row.id;
-	dialogTitle.value = `编辑规则 ${row.descr}`;
-	rowToForm(row);
-	dialogVisible.value = true;
+	ruleDialogTitle.value = `编辑规则 ${row.name}`;
+	const expr = parseRuleExpr(row);
+	Object.assign(ruleForm, {
+		companyRefId: row.companyRefId,
+		factoryRefId: row.factoryRefId,
+		code: row.code,
+		name: row.name,
+		priorityLevel: row.priorityLevel,
+		sortDirection: row.sortDirection,
+		customerType: expr.customerType,
+		orderType: expr.orderType,
+		dueStatus: expr.dueStatus,
+		sourceEntity: row.sourceEntity,
+		sourceField: row.sourceField,
+		sourceFieldType: row.sourceFieldType,
+		sourceLinkField: row.sourceLinkField,
+		workOrderField: row.workOrderField,
+		workOrderFieldType: row.workOrderFieldType,
+		workOrderLinkField: row.workOrderLinkField,
+		isEnabled: row.isEnabled,
+		remark: row.remark ?? '',
+	});
+	ruleDialogVisible.value = true;
 }
 
-async function submitForm() {
-	await formRef.value?.validate();
-	saving.value = true;
+async function submitRuleForm() {
+	await ruleFormRef.value?.validate();
+	ruleSaving.value = true;
 	try {
-		const payload: S0PriorityCodeUpsert = { ...form };
-		if (editingId.value) {
-			await s0OrderPriorityRulesApi.update(editingId.value, payload);
+		const payload: S0PriorityRuleUpsert = {
+			companyRefId: ruleForm.companyRefId,
+			factoryRefId: ruleForm.factoryRefId,
+			code: ruleForm.code,
+			name: ruleForm.name,
+			priorityLevel: ruleForm.priorityLevel,
+			sortDirection: ruleForm.sortDirection,
+			customerType: (ruleForm as any).customerType || undefined,
+			orderType: (ruleForm as any).orderType || undefined,
+			dueStatus: (ruleForm as any).dueStatus || undefined,
+			sourceEntity: ruleForm.sourceEntity,
+			sourceField: ruleForm.sourceField,
+			sourceFieldType: ruleForm.sourceFieldType,
+			sourceLinkField: ruleForm.sourceLinkField,
+			workOrderField: ruleForm.workOrderField,
+			workOrderFieldType: ruleForm.workOrderFieldType,
+			workOrderLinkField: ruleForm.workOrderLinkField,
+			isEnabled: ruleForm.isEnabled,
+			remark: ruleForm.remark,
+		};
+		if (editingId.value !== null) {
+			await s0PriorityRulesApi.update(editingId.value, payload);
 			ElMessage.success('已保存');
 		} else {
-			await s0OrderPriorityRulesApi.create(payload);
+			await s0PriorityRulesApi.create(payload);
 			ElMessage.success('已创建');
 		}
-		dialogVisible.value = false;
+		ruleDialogVisible.value = false;
 		await loadList();
+	} catch {
+		ElMessage.error('保存订单优先规则失败,请检查填写内容后重试');
 	} finally {
-		saving.value = false;
+		ruleSaving.value = false;
 	}
 }
 
-function onDelete(row: S0PriorityCodeRow) {
-	ElMessageBox.confirm(`确定删除规则「${row.descr}」?`, '确认', { type: 'warning' })
+function onDelete(row: S0PriorityRuleRow) {
+	ElMessageBox.confirm(`确定删除规则「${row.name}」?`, '确认', { type: 'warning' })
 		.then(async () => {
-			await s0OrderPriorityRulesApi.delete(row.id);
+			await s0PriorityRulesApi.delete(row.id);
 			ElMessage.success('已删除');
 			await loadList();
 		})
 		.catch(() => {});
 }
 
-function toggleActive(row: S0PriorityCodeRow) {
-	const next = !row.isActive;
+function toggleEnabled(row: S0PriorityRuleRow) {
+	const next = !row.isEnabled;
 	const actionText = next ? '生效' : '停用';
-	ElMessageBox.confirm(`确定${actionText}规则「${row.descr}」?`, '确认', { type: 'warning' })
+	ElMessageBox.confirm(`确定${actionText}规则「${row.name}」?`, '确认', { type: 'warning' })
 		.then(async () => {
-			await s0OrderPriorityRulesApi.toggleEnabled(row.id, { isActive: next });
+			await s0PriorityRulesApi.toggleEnabled(row.id, { isEnabled: next });
 			ElMessage.success(`${actionText}成功`);
 			await loadList();
 		})
 		.catch(() => {});
 }
 
+// ── 配置维护弹窗 ──────────────────────────────────────────────────────────
+const configDialogVisible = ref(false);
+const configActiveTab = ref('customerType');
+
+type ConfigTabKey = 'customerType' | 'orderType' | 'dueStatus';
+const CONFIG_TAB_CODES: Record<ConfigTabKey, string> = {
+	customerType: 's0_order_priority_customer_type',
+	orderType: 's0_order_type',
+	dueStatus: 's0_due_status',
+};
+
+const configTabs = reactive<Record<ConfigTabKey, { rows: SysDictData[]; loading: boolean }>>({
+	customerType: { rows: [], loading: false },
+	orderType: { rows: [], loading: false },
+	dueStatus: { rows: [], loading: false },
+});
+
+async function loadConfigTab(tab: ConfigTabKey) {
+	configTabs[tab].loading = true;
+	try {
+		configTabs[tab].rows = await listDictDataByCode(CONFIG_TAB_CODES[tab]);
+	} finally {
+		configTabs[tab].loading = false;
+	}
+}
+
+async function onConfigDialogOpen() {
+	await Promise.all([
+		loadConfigTab('customerType'),
+		loadConfigTab('orderType'),
+		loadConfigTab('dueStatus'),
+		loadFieldMapping(),
+	]);
+}
+
+watch(configActiveTab, (tab) => {
+	if (tab === 'fieldMapping') void loadFieldMapping();
+});
+
+// 字段映射 Tab
+const fieldMappingRows = ref<S0PriorityRuleRow[]>([]);
+const fieldMappingLoading = ref(false);
+
+async function loadFieldMapping() {
+	fieldMappingLoading.value = true;
+	try {
+		const data = await s0PriorityRulesApi.list({ page: 1, pageSize: 200 });
+		fieldMappingRows.value = data.list.filter(
+			(r) => r.sourceEntity || r.sourceField || r.workOrderField,
+		);
+	} catch {
+		fieldMappingRows.value = [];
+	} finally {
+		fieldMappingLoading.value = false;
+	}
+}
+
 onMounted(async () => {
-	await loadList();
+	await Promise.all([loadList(), loadDictSelects()]);
 });
 </script>
 
+<!-- ── DictDataTab 子组件(inline defineComponent 风格,避免新建文件) ── -->
+<script lang="ts">
+import { defineComponent, toRefs } from 'vue';
+
+const DictDataTab = defineComponent({
+	name: 'DictDataTab',
+	props: {
+		dictCode: { type: String, required: true },
+		rows: { type: Array as () => SysDictData[], default: () => [] },
+		loading: { type: Boolean, default: false },
+		allowEdit: { type: Boolean, default: true },
+		allowDelete: { type: Boolean, default: true },
+	},
+	emits: ['reload'],
+	setup(props, { emit }) {
+		const editDialogVisible = ref(false);
+		const editForm = ref<{ id: number | null; label: string; value: string; orderNo: number }>({
+			id: null, label: '', value: '', orderNo: 100,
+		});
+		const editSaving = ref(false);
+		const dictTypeIdCache = ref<number | null>(null);
+
+		async function ensureDictTypeId(): Promise<number | null> {
+			if (dictTypeIdCache.value !== null) return dictTypeIdCache.value;
+			dictTypeIdCache.value = await getDictTypeIdByCode(props.dictCode);
+			return dictTypeIdCache.value;
+		}
+
+		function openAdd() {
+			editForm.value = { id: null, label: '', value: '', orderNo: (props.rows.length + 1) * 10 };
+			editDialogVisible.value = true;
+		}
+
+		function openEditRow(row: SysDictData) {
+			editForm.value = { id: row.id ?? null, label: row.label, value: row.value, orderNo: row.orderNo ?? 100 };
+			editDialogVisible.value = true;
+		}
+
+		async function saveEdit() {
+			if (!editForm.value.label || !editForm.value.value) {
+				ElMessage.warning('名称和编码均为必填');
+				return;
+			}
+			editSaving.value = true;
+			try {
+				if (editForm.value.id === null) {
+					const typeId = await ensureDictTypeId();
+					if (!typeId) { ElMessage.error('无法获取字典类型,请刷新后重试'); return; }
+					await addDictData({ dictTypeId: typeId, label: editForm.value.label, value: editForm.value.value, orderNo: editForm.value.orderNo });
+					ElMessage.success('已新增');
+				} else {
+					await updateDictData({ id: editForm.value.id, label: editForm.value.label, value: editForm.value.value, orderNo: editForm.value.orderNo });
+					ElMessage.success('已保存');
+				}
+				editDialogVisible.value = false;
+				emit('reload');
+			} catch {
+				ElMessage.error('保存失败,请稍后重试');
+			} finally {
+				editSaving.value = false;
+			}
+		}
+
+		async function toggleStatus(row: SysDictData) {
+			const isEnabled = row.status === StatusEnum.NUMBER_1;
+			const next = isEnabled ? StatusEnum.NUMBER_2 : StatusEnum.NUMBER_1;
+			const label = isEnabled ? '停用' : '启用';
+			await ElMessageBox.confirm(`确定${label}「${row.label}」?`, '确认', { type: 'warning' });
+			await setDictDataStatus(row.id!, next);
+			ElMessage.success(`已${label}`);
+			emit('reload');
+		}
+
+		async function doDelete(row: SysDictData) {
+			await ElMessageBox.confirm(`确定删除「${row.label}」?删除后不可恢复。`, '确认', { type: 'warning' });
+			await deleteDictData(row.id!);
+			ElMessage.success('已删除');
+			emit('reload');
+		}
+
+		function statusLabel(row: SysDictData) {
+			return row.status === StatusEnum.NUMBER_1 ? '启用' : '停用';
+		}
+		function statusType(row: SysDictData): 'success' | 'info' {
+			return row.status === StatusEnum.NUMBER_1 ? 'success' : 'info';
+		}
+
+		return {
+			editDialogVisible, editForm, editSaving,
+			openAdd, openEditRow, saveEdit, toggleStatus, doDelete,
+			statusLabel, statusType,
+		};
+	},
+	template: `
+		<div>
+			<div style="margin-bottom:10px" v-if="allowEdit">
+				<el-button size="small" type="primary" @click="openAdd">新增</el-button>
+			</div>
+			<el-table :data="rows" v-loading="loading" border stripe size="small" max-height="320px">
+				<el-table-column prop="label" label="名称" min-width="120" />
+				<el-table-column prop="value" label="编码" min-width="120" />
+				<el-table-column label="状态" width="80" align="center">
+					<template #default="{ row }">
+						<el-tag :type="statusType(row)" size="small">{{ statusLabel(row) }}</el-tag>
+					</template>
+				</el-table-column>
+				<el-table-column label="操作" width="160" align="center">
+					<template #default="{ row }">
+						<el-button v-if="allowEdit" link type="primary" @click="openEditRow(row)">编辑</el-button>
+						<el-button link :type="row.status === 1 ? 'warning' : 'success'" @click="toggleStatus(row)">
+							{{ row.status === 1 ? '停用' : '启用' }}
+						</el-button>
+						<el-button v-if="allowDelete" link type="danger" @click="doDelete(row)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<el-dialog v-model="editDialogVisible" :title="editForm.id ? '编辑条目' : '新增条目'" width="420px" append-to-body destroy-on-close>
+				<el-form label-width="80px">
+					<el-form-item label="名称"><el-input v-model="editForm.label" /></el-form-item>
+					<el-form-item label="编码"><el-input v-model="editForm.value" /></el-form-item>
+					<el-form-item label="排序"><el-input-number v-model="editForm.orderNo" :min="1" controls-position="right" style="width:100%" /></el-form-item>
+				</el-form>
+				<template #footer>
+					<el-button @click="editDialogVisible = false">取消</el-button>
+					<el-button type="primary" :loading="editSaving" @click="saveEdit">保存</el-button>
+				</template>
+			</el-dialog>
+		</div>
+	`,
+});
+
+export { DictDataTab };
+</script>
+
 <style scoped lang="scss">
 @import '/@/views/aidop/styles/aidop-demo.scss';
 
@@ -311,4 +664,13 @@ onMounted(async () => {
 	display: flex;
 	justify-content: flex-end;
 }
+
+.field-mapping-panel {
+	.empty-hint {
+		text-align: center;
+		color: #909399;
+		padding: 24px 0;
+		font-size: 13px;
+	}
+}
 </style>

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.118</AssemblyVersion>
-    <FileVersion>1.0.118</FileVersion>
-    <Version>1.0.118</Version>
+    <AssemblyVersion>1.0.119</AssemblyVersion>
+    <FileVersion>1.0.119</FileVersion>
+    <Version>1.0.119</Version>
   </PropertyGroup>
 
   <ItemGroup>

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

@@ -82,6 +82,7 @@ public class AdoS0CustomersController : ControllerBase
             TaxIn = dto.TaxIn,
             CustClass = dto.CustClass,
             Address = dto.Address,
+            ShippingAddress = dto.ShippingAddress,
             Contact = dto.Contact,
             Position = dto.Position,
             ContactInfo = dto.ContactInfo,
@@ -122,6 +123,7 @@ public class AdoS0CustomersController : ControllerBase
         entity.TaxIn = dto.TaxIn;
         entity.CustClass = dto.CustClass;
         entity.Address = dto.Address;
+        entity.ShippingAddress = dto.ShippingAddress;
         entity.Contact = dto.Contact;
         entity.Position = dto.Position;
         entity.ContactInfo = dto.ContactInfo;

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

@@ -199,6 +199,8 @@ public class AdoS0MaterialsController : ControllerBase
         Remark = dto.Remark,
         EMTType = dto.EMTType,
         OwnerApplication = dto.OwnerApplication,
+        BomVersion = dto.BomVersion,
+        EnableBomVersion = dto.EnableBomVersion,
         BOMDesign = dto.BOMDesign,
         RoutingDes = dto.RoutingDes,
         CustSupplied = dto.CustSupplied,
@@ -260,6 +262,8 @@ public class AdoS0MaterialsController : ControllerBase
         entity.Remark = dto.Remark;
         entity.EMTType = dto.EMTType;
         entity.OwnerApplication = dto.OwnerApplication;
+        entity.BomVersion = dto.BomVersion;
+        entity.EnableBomVersion = dto.EnableBomVersion;
         entity.BOMDesign = dto.BOMDesign;
         entity.RoutingDes = dto.RoutingDes;
         entity.CustSupplied = dto.CustSupplied;

+ 164 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0PriorityRulesController.cs

@@ -0,0 +1,164 @@
+using System.Text.Json;
+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 订单优先级业务规则(ado_s0_sales_order_priority_rule)
+/// </summary>
+[ApiController]
+[Route("api/s0/sales/priority-rules")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0PriorityRulesController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0PriorityRule> _rep;
+
+    public AdoS0PriorityRulesController(SqlSugarRepository<AdoS0PriorityRule> rep)
+    {
+        _rep = rep;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0PriorityRuleQueryDto 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(q.IsEnabled.HasValue, x => x.IsEnabled == q.IsEnabled!.Value)
+            .WhereIF(
+                !string.IsNullOrWhiteSpace(q.Keyword),
+                x => x.Code.Contains(q.Keyword!) || x.Name.Contains(q.Keyword!));
+
+        var total = await query.CountAsync();
+        var list = await query
+            .OrderBy(x => x.PriorityLevel)
+            .Skip((q.Page - 1) * q.PageSize)
+            .Take(q.PageSize)
+            .ToListAsync();
+
+        foreach (var item in list)
+            FillRuleExprLabels(item);
+
+        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);
+        if (item == null) return NotFound();
+        FillRuleExprLabels(item);
+        return Ok(item);
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0PriorityRuleUpsertDto dto)
+    {
+        var now = DateTime.Now;
+        var entity = MapToEntity(dto);
+        entity.CreatedAt = now;
+
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        FillRuleExprLabels(entity);
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0PriorityRuleUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+
+        ApplyUpsert(entity, dto);
+        entity.UpdatedAt = DateTime.Now;
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        FillRuleExprLabels(entity);
+        return Ok(entity);
+    }
+
+    [HttpPatch("{id:long}/toggle-enabled")]
+    public async Task<IActionResult> ToggleEnabledAsync(long id, [FromBody] AdoS0PriorityRuleToggleDto 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 = "删除成功" });
+    }
+
+    // ── helpers ──────────────────────────────────────────────────────────────
+
+    private static AdoS0PriorityRule MapToEntity(AdoS0PriorityRuleUpsertDto dto)
+    {
+        var entity = new AdoS0PriorityRule();
+        ApplyUpsert(entity, dto);
+        return entity;
+    }
+
+    private static void ApplyUpsert(AdoS0PriorityRule entity, AdoS0PriorityRuleUpsertDto dto)
+    {
+        entity.CompanyRefId     = dto.CompanyRefId;
+        entity.FactoryRefId     = dto.FactoryRefId;
+        entity.Code             = dto.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.IsEnabled        = dto.IsEnabled;
+        entity.Remark           = dto.Remark;
+        entity.RuleExpr = BuildRuleExpr(dto.CustomerType, dto.OrderType, dto.DueStatus);
+    }
+
+    private static string? BuildRuleExpr(string? customerType, string? orderType, string? dueStatus)
+    {
+        if (string.IsNullOrWhiteSpace(customerType) &&
+            string.IsNullOrWhiteSpace(orderType) &&
+            string.IsNullOrWhiteSpace(dueStatus))
+            return null;
+
+        return JsonSerializer.Serialize(new
+        {
+            customerType,
+            orderType,
+            dueStatus,
+        });
+    }
+
+    private static void FillRuleExprLabels(AdoS0PriorityRule item)
+    {
+        if (string.IsNullOrWhiteSpace(item.RuleExpr)) return;
+        try
+        {
+            var doc = JsonSerializer.Deserialize<JsonElement>(item.RuleExpr);
+            if (doc.TryGetProperty("customerType", out var ct)) item.CustomerTypeLabel = ct.GetString();
+            if (doc.TryGetProperty("orderType",    out var ot)) item.OrderTypeLabel    = ot.GetString();
+            if (doc.TryGetProperty("dueStatus",    out var ds)) item.DueStatusLabel    = ds.GetString();
+        }
+        catch { /* 忽略格式错误的历史数据 */ }
+    }
+}

+ 41 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Sales/AdoS0PriorityRuleDtos.cs

@@ -0,0 +1,41 @@
+namespace Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
+
+public class AdoS0PriorityRuleQueryDto
+{
+    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 AdoS0PriorityRuleUpsertDto
+{
+    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 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; }
+    /// <summary>客户类型(写入 RuleExpr JSON)</summary>
+    public string? CustomerType { get; set; }
+    /// <summary>订单类型(写入 RuleExpr JSON)</summary>
+    public string? OrderType { get; set; }
+    /// <summary>到期状态(写入 RuleExpr JSON)</summary>
+    public string? DueStatus { get; set; }
+    public string? Remark { get; set; }
+    public bool IsEnabled { get; set; } = true;
+}
+
+public class AdoS0PriorityRuleToggleDto
+{
+    public bool IsEnabled { get; set; }
+}

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

@@ -107,6 +107,7 @@ public class AdoS0CustMasterUpsertDto
     public bool TaxIn { get; set; } = true;
     public string? CustClass { get; set; }
     public string? Address { get; set; }
+    public string? ShippingAddress { get; set; }
     public string? Contact { get; set; }
     public string? Position { get; set; }
     public string? ContactInfo { get; set; }
@@ -208,6 +209,8 @@ public class AdoS0ItemMasterUpsertDto
     public string? Remark { get; set; }
     public string? EMTType { get; set; }
     public string? OwnerApplication { get; set; }
+    public string? BomVersion { get; set; }
+    public bool EnableBomVersion { get; set; }
     public int? BOMDesign { get; set; }
     public int? RoutingDes { get; set; }
     public bool CustSupplied { get; set; }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0CustMaster.cs

@@ -52,9 +52,12 @@ public class AdoS0CustMaster : ITenantIdFilter
     [SugarColumn(ColumnName = "cust_class", ColumnDescription = "客户级别", Length = 50, IsNullable = true)]
     public string? CustClass { get; set; }
 
-    [SugarColumn(ColumnName = "address", ColumnDescription = "地址", Length = 500, IsNullable = true)]
+    [SugarColumn(ColumnName = "address", ColumnDescription = "通讯地址", Length = 500, IsNullable = true)]
     public string? Address { get; set; }
 
+    [SugarColumn(ColumnName = "shipping_address", ColumnDescription = "收货地址", Length = 500, IsNullable = true)]
+    public string? ShippingAddress { get; set; }
+
     [SugarColumn(ColumnName = "contact", ColumnDescription = "联系人", Length = 200, IsNullable = true)]
     public string? Contact { get; set; }
 

+ 6 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ItemMaster.cs

@@ -151,6 +151,12 @@ public class AdoS0ItemMaster : ITenantIdFilter
     [SugarColumn(ColumnName = "OwnerApplication", ColumnDescription = "物料属性", Length = 100, IsNullable = true)]
     public string? OwnerApplication { get; set; }
 
+    [SugarColumn(ColumnName = "bom_version", ColumnDescription = "BOM版本", Length = 50, IsNullable = true)]
+    public string? BomVersion { get; set; }
+
+    [SugarColumn(ColumnName = "enable_bom_version", ColumnDescription = "启用BOM版本", ColumnDataType = "boolean")]
+    public bool EnableBomVersion { get; set; }
+
     [SugarColumn(ColumnName = "bom_design", ColumnDescription = "BOM设计周期(天)", IsNullable = true)]
     public int? BOMDesign { get; set; }
 

+ 77 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0PriorityRule.cs

@@ -0,0 +1,77 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+/// <summary>
+/// S0 订单优先级业务规则(ado_s0_sales_order_priority_rule)
+/// </summary>
+[SugarTable("ado_s0_sales_order_priority_rule", "S0 订单优先级业务规则")]
+public class AdoS0PriorityRule
+{
+    [SugarColumn(ColumnName = "Id", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true)]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "CompanyRefId", ColumnDescription = "公司 ID")]
+    public long CompanyRefId { get; set; }
+
+    [SugarColumn(ColumnName = "FactoryRefId", ColumnDescription = "工厂 ID")]
+    public long FactoryRefId { get; set; }
+
+    [SugarColumn(ColumnName = "Code", ColumnDescription = "规则编码", Length = 100)]
+    public string Code { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "Name", ColumnDescription = "规则名称", Length = 200)]
+    public string Name { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "PriorityLevel", ColumnDescription = "优先级数值(越小越高)")]
+    public int PriorityLevel { get; set; }
+
+    [SugarColumn(ColumnName = "SortDirection", ColumnDescription = "排序方向(asc/desc)", Length = 10)]
+    public string SortDirection { get; set; } = "asc";
+
+    [SugarColumn(ColumnName = "SourceEntity", ColumnDescription = "来源实体", Length = 200, IsNullable = true)]
+    public string? SourceEntity { get; set; }
+
+    [SugarColumn(ColumnName = "SourceField", ColumnDescription = "来源字段", Length = 200, IsNullable = true)]
+    public string? SourceField { get; set; }
+
+    [SugarColumn(ColumnName = "SourceFieldType", ColumnDescription = "来源字段类型", Length = 100, IsNullable = true)]
+    public string? SourceFieldType { get; set; }
+
+    [SugarColumn(ColumnName = "SourceLinkField", ColumnDescription = "来源关联字段", Length = 200, IsNullable = true)]
+    public string? SourceLinkField { get; set; }
+
+    [SugarColumn(ColumnName = "WorkOrderField", ColumnDescription = "工单字段", Length = 200, IsNullable = true)]
+    public string? WorkOrderField { get; set; }
+
+    [SugarColumn(ColumnName = "WorkOrderFieldType", ColumnDescription = "工单字段类型", Length = 100, IsNullable = true)]
+    public string? WorkOrderFieldType { get; set; }
+
+    [SugarColumn(ColumnName = "WorkOrderLinkField", ColumnDescription = "工单关联字段", Length = 200, IsNullable = true)]
+    public string? WorkOrderLinkField { get; set; }
+
+    [SugarColumn(ColumnName = "RuleExpr", ColumnDescription = "规则表达式 JSON", Length = 1000, IsNullable = true)]
+    public string? RuleExpr { get; set; }
+
+    [SugarColumn(ColumnName = "Remark", ColumnDescription = "备注", Length = 500, IsNullable = true)]
+    public string? Remark { get; set; }
+
+    [SugarColumn(ColumnName = "IsEnabled", ColumnDescription = "是否启用", ColumnDataType = "tinyint(1)")]
+    public bool IsEnabled { get; set; } = true;
+
+    [SugarColumn(ColumnName = "CreatedAt", ColumnDescription = "创建时间")]
+    public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "UpdatedAt", ColumnDescription = "更新时间", IsNullable = true)]
+    public DateTime? UpdatedAt { get; set; }
+
+    /// <summary>客户类型显示标签(不落库,由业务层填充)</summary>
+    [SugarColumn(IsIgnore = true)]
+    public string? CustomerTypeLabel { get; set; }
+
+    /// <summary>订单类型显示标签(不落库,由业务层填充)</summary>
+    [SugarColumn(IsIgnore = true)]
+    public string? OrderTypeLabel { get; set; }
+
+    /// <summary>到期状态显示标签(不落库,由业务层填充)</summary>
+    [SugarColumn(IsIgnore = true)]
+    public string? DueStatusLabel { get; set; }
+}

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

@@ -22,6 +22,10 @@ public class S0DictDataSeedData : ISqlSugarEntitySeedData<SysDictData>
         var docStatusId         = types[8].Id;
         var crTermsId           = types[9].Id;
         var taxClassId          = types[10].Id;
+        // types[11] = s0_supply_category(无数据条目)
+        var priorityCustomerTypeId = types[12].Id;
+        var orderTypeId            = types[13].Id;
+        var dueStatusId            = types[14].Id;
 
         long seq = 1329900100001L;
 
@@ -92,6 +96,22 @@ public class S0DictDataSeedData : ISqlSugarEntitySeedData<SysDictData>
             D(seq++, taxClassId, "增值税专票 6%", "vat_6", 102, ct),
             D(seq++, taxClassId, "增值税普票", "vat_normal", 103, ct),
             D(seq++, taxClassId, "免税", "exempt", 104, ct),
+
+            // ── s0_order_priority_customer_type ──
+            D(seq++, priorityCustomerTypeId, "战略客户", "strategic", 100, ct),
+            D(seq++, priorityCustomerTypeId, "常规客户", "regular",   101, ct),
+
+            // ── s0_order_type ──
+            D(seq++, orderTypeId, "MTS",       "mts",      100, ct),
+            D(seq++, orderTypeId, "MTO",       "mto",      101, ct),
+            D(seq++, orderTypeId, "ETO",       "eto",      102, ct),
+            D(seq++, orderTypeId, "计划",      "plan",     103, ct),
+            D(seq++, orderTypeId, "非量产工单", "non_mass", 104, ct),
+
+            // ── s0_due_status ──
+            D(seq++, dueStatusId, "逾期", "overdue", 100, ct),
+            D(seq++, dueStatusId, "正常", "normal",  101, ct),
+            D(seq++, dueStatusId, "不限", "*",       102, ct),
         };
     }
 

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

@@ -23,6 +23,9 @@ public class S0DictTypeSeedData : ISqlSugarEntitySeedData<SysDictType>
             new SysDictType { Id = 1329900001010L, Name = "S0-支付方式",     Code = "s0_cr_terms",           SysFlag = YesNoEnum.N, OrderNo = 509, Remark = "S0 供应商/客户支付方式(CrTerms)",          Status = StatusEnum.Enable, CreateTime = ct },
             new SysDictType { Id = 1329900001011L, Name = "S0-税类型",       Code = "s0_tax_class",          SysFlag = YesNoEnum.N, OrderNo = 510, Remark = "S0 税类型(TaxClass)",                      Status = StatusEnum.Enable, CreateTime = ct },
             new SysDictType { Id = 1329900001012L, Name = "S0-供应类别",   Code = "s0_supply_category",    SysFlag = YesNoEnum.N, OrderNo = 511, Remark = "S0 货源清单供应类别(SupplierType)",           Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001013L, Name = "S0-优先级客户类型", Code = "s0_order_priority_customer_type", SysFlag = YesNoEnum.N, OrderNo = 512, Remark = "S0 订单优先级规则 - 客户类型过滤条件",  Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001014L, Name = "S0-订单类型",     Code = "s0_order_type",         SysFlag = YesNoEnum.N, OrderNo = 513, Remark = "S0 订单优先级规则 - 订单类型过滤条件",            Status = StatusEnum.Enable, CreateTime = ct },
+            new SysDictType { Id = 1329900001015L, Name = "S0-到期状态",     Code = "s0_due_status",         SysFlag = YesNoEnum.N, OrderNo = 514, Remark = "S0 订单优先级规则 - 到期状态过滤条件",            Status = StatusEnum.Enable, CreateTime = ct },
         };
     }
 }