Browse Source

feat(order): 新增销售订单评审前后端,重构 AiDOP 订单模块

- 新增后端 Order 服务(SeOrderService、Dto、Entity),使用 IDynamicApiController+ITransient 模式,明细子表采用三路合并更新策略
- 新增前端订单评审列表、销售订单表单、变更单表单、客户/物料选择组件及 API 接口文件
- 删除旧 OrderController/PlanController/WorkOrderController 及对应 Dto/Entity,统一迁移到 Order/ 目录
- 解决与 upstream 的合并冲突:Database.json 保留本地 dopdemo 连接串,Startup.cs 保留 S0 实体 CodeFirst 注册
Pengxy 1 month ago
parent
commit
ea24ff6ef6
31 changed files with 1834 additions and 1010 deletions
  1. 1 1
      Web/package.json
  2. 133 0
      Web/src/views/aidop/api/seOrderReview.ts
  3. 186 170
      Web/src/views/aidop/business/orderList.vue
  4. 177 0
      Web/src/views/aidop/business/salesOrderChangeForm.vue
  5. 268 0
      Web/src/views/aidop/business/salesOrderForm.vue
  6. 86 0
      Web/src/views/aidop/business/selectCustomer.vue
  7. 87 0
      Web/src/views/aidop/business/selectItem.vue
  8. 3 3
      ai-dop-platform/README.md
  9. 3 3
      ai-dop-platform/docs/GETTING_STARTED.md
  10. 116 28
      ai-dop-platform/tools/aidop_init.ps1
  11. 2 1
      server/Admin.NET.Application/Admin.NET.Application.csproj
  12. 3 1
      server/Admin.NET.Application/Configuration/Database.json
  13. 1 1
      server/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj
  14. 1 1
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  15. 4 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Admin.NET.Plugin.AiDOP.csproj
  16. 0 147
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/OrderController.cs
  17. 0 146
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/PlanController.cs
  18. 0 193
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/WorkOrderController.cs
  19. 0 44
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/OrderDtos.cs
  20. 0 44
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/PlanDtos.cs
  21. 0 53
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/WorkOrderDtos.cs
  22. 0 53
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoOrder.cs
  23. 0 53
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoPlan.cs
  24. 0 68
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoWorkOrder.cs
  25. 3 0
      server/Plugins/Admin.NET.Plugin.AiDOP/GlobalUsings.cs
  26. 100 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/Dto/SeOrderDto.cs
  27. 77 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/Entity/SeOrder.cs
  28. 81 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/Entity/SeOrderChange.cs
  29. 67 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/Entity/SeOrderEntry.cs
  30. 430 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/SeOrderService.cs
  31. 5 0
      server/global.json

+ 1 - 1
Web/package.json

@@ -79,7 +79,7 @@
 		"vue-i18n": "^11.3.0",
 		"vue-json-pretty": "^2.6.0",
 		"vue-plugin-hiprint": "^0.0.60",
-		"vue-router": "^5.0.3",
+		"vue-router": "^4.5.1",
 		"vue-signature-pad": "^3.0.2",
 		"vue3-tree-org": "^4.2.2",
 		"xlsx-js-style": "^1.2.0"

+ 133 - 0
Web/src/views/aidop/api/seOrderReview.ts

@@ -0,0 +1,133 @@
+import service from '/@/utils/request';
+
+export interface Paged<T> {
+	total: number;
+	page: number;
+	pageSize: number;
+	list: T[];
+}
+
+export interface SeOrderReviewRow {
+	id: number;
+	billNo?: string;
+	orderType?: number;
+	customNo?: string;
+	sortName?: string;
+	rDate?: string | null;
+	progress?: number | null;
+	state?: string | null;
+	changeContent?: string | null;
+	changeNum?: number | null;
+	flowState?: string | null;
+}
+
+export interface SeOrderEntry {
+	id?: number | null;
+	entrySeq?: number | null;
+	itemNumber?: string | null;
+	itemName?: string | null;
+	specification?: string | null;
+	qty?: number | null;
+	planDate?: string | null;
+	sysCapacityDate?: string | null;
+	date?: string | null;
+	remark?: string | null;
+	unit?: string | null;
+	deliverCount?: number | null;
+	progress?: number | null;
+}
+
+export interface SeOrderDetail {
+	id: number;
+	billNo?: string;
+	orderType?: number;
+	customId?: number | null;
+	customNo?: string;
+	customName?: string;
+	date?: string | null;
+	customLevel?: number | null;
+	urgent?: number | null;
+	billFrom?: string | null;
+	country?: string | null;
+	rDate?: string | null;
+	entries: SeOrderEntry[];
+}
+
+export interface SeOrderUpsert {
+	id?: number | null;
+	billNo: string;
+	orderType: number;
+	customId?: number | null;
+	customNo: string;
+	customName: string;
+	date?: string | null;
+	customLevel?: number | null;
+	urgent: number;
+	billFrom?: string | null;
+	country: string;
+	rDate?: string | null;
+	entries: SeOrderEntry[];
+}
+
+export interface SeOrderChangeSave {
+	seOrderId: number;
+	billNo: string;
+	orderType: number;
+	customId?: number | null;
+	customNo: string;
+	customName: string;
+	date?: string | null;
+	customLevel?: number | null;
+	urgent: number;
+	billFrom?: string | null;
+	country: string;
+	rDate?: string | null;
+	changeReason: string;
+	changeType: string;
+	changeContent: string;
+	entries: SeOrderEntry[];
+}
+
+export interface SimpleKv {
+	label: string;
+	value: string;
+	id?: number | null;
+	extra?: string | null;
+}
+
+export function fetchSeOrderReviewList(params: Record<string, unknown>) {
+	return service.get<Paged<SeOrderReviewRow>>('/api/Order/seorder/list', { params }).then((r) => r.data);
+}
+
+export function fetchSeOrderDetail(id: number) {
+	return service.get<SeOrderDetail>(`/api/Order/seorder/${id}`).then((r) => r.data);
+}
+
+export function saveSeOrder(body: SeOrderUpsert) {
+	return service.post('/api/Order/seorder/save', body).then((r) => r.data);
+}
+
+export function reviewSeOrder(id: number) {
+	return service.post(`/api/Order/seorder/${id}/review`).then((r) => r.data);
+}
+
+export function confirmSeOrderDelivery(id: number) {
+	return service.post(`/api/Order/seorder/${id}/confirm-delivery`).then((r) => r.data);
+}
+
+export function unlockSeOrder(id: number) {
+	return service.post(`/api/Order/seorder/${id}/unlock`).then((r) => r.data);
+}
+
+export function createSeOrderChange(id: number, body: SeOrderChangeSave) {
+	return service.post(`/api/Order/seorder/${id}/change`, body).then((r) => r.data);
+}
+
+export function fetchCustomers(params: Record<string, unknown>) {
+	return service.get<Paged<SimpleKv>>('/api/Order/seorder/customers', { params }).then((r) => r.data);
+}
+
+export function fetchItems(params: Record<string, unknown>) {
+	return service.get<Paged<SimpleKv>>('/api/Order/seorder/items', { params }).then((r) => r.data);
+}
+

+ 186 - 170
Web/src/views/aidop/business/orderList.vue

@@ -1,47 +1,72 @@
 <template>
-	<AidopDemoShell :title="pageTitle" subtitle="与 Demo /api/Order 契约一致,对接插件 AdoOrder">
+	<AidopDemoShell :title="pageTitle" subtitle="订单评审列表(crm_seorder)">
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
 			<el-form-item label="订单号">
-				<el-input v-model="query.orderNo" placeholder="模糊查询" clearable style="width: 160px" />
+				<el-input v-model="query.billNo" placeholder="模糊查询" clearable style="width: 160px" />
 			</el-form-item>
-			<el-form-item label="客户">
-				<el-input v-model="query.customerName" placeholder="客户名称" clearable style="width: 140px" />
+			<el-form-item label="客户编码">
+				<el-input v-model="query.customNo" placeholder="模糊查询" clearable style="width: 160px" />
 			</el-form-item>
 			<el-form-item label="状态">
-				<el-select v-model="query.status" placeholder="全部" clearable style="width: 120px">
-					<el-option v-for="s in orderStatuses" :key="s" :label="s" :value="s" />
+				<el-select v-model="query.state" placeholder="全部" clearable style="width: 120px">
+					<el-option v-for="s in stateOptions" :key="s" :label="s" :value="s" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="类别">
+				<el-select v-model="query.orderType" placeholder="全部" clearable style="width: 120px">
+					<el-option label="销售" :value="1" />
+					<el-option label="计划" :value="2" />
 				</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="orderNo" label="订单号" width="160" show-overflow-tooltip />
-			<el-table-column prop="customerName" label="客户" min-width="120" show-overflow-tooltip />
-			<el-table-column prop="product" label="产品" min-width="120" show-overflow-tooltip />
-			<el-table-column prop="quantity" label="数量" width="80" align="center" />
-			<el-table-column label="单价" width="100" align="right">
-				<template #default="{ row }">{{ row.unitPrice }}</template>
+		<div class="toolbar">
+			<el-button type="success" @click="openCreate">添加</el-button>
+			<el-button type="primary" :disabled="!selectedId" @click="onReview">订单评审</el-button>
+			<el-button type="warning" :disabled="!selectedId" @click="onConfirmDelivery">交期确认</el-button>
+			<el-button :disabled="!selectedId" @click="onUnlock">资源解锁</el-button>
+
+			<el-popover placement="bottom" width="220" trigger="click">
+				<template #reference>
+					<el-button text>列设置</el-button>
+				</template>
+				<el-checkbox v-for="c in columnToggles" :key="c.key" v-model="c.visible" class="col-toggle">
+					{{ c.label }}
+				</el-checkbox>
+			</el-popover>
+		</div>
+
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" @selection-change="onSelectionChange">
+			<el-table-column type="selection" width="44" fixed="left" />
+			<el-table-column v-if="col.billNo" prop="billNo" label="订单号" width="180" fixed="left" show-overflow-tooltip />
+			<el-table-column v-if="col.orderType" label="类别" width="90" align="center">
+				<template #default="{ row }">{{ orderTypeText(row.orderType) }}</template>
 			</el-table-column>
-			<el-table-column label="金额" width="100" align="right">
-				<template #default="{ row }">{{ row.amount }}</template>
+			<el-table-column v-if="col.customNo" prop="customNo" label="客户编码" width="140" show-overflow-tooltip />
+			<el-table-column v-if="col.rDate" label="下单日期" width="160" align="center">
+				<template #default="{ row }">{{ fmtDateTime(row.rDate) }}</template>
 			</el-table-column>
-			<el-table-column prop="status" label="状态" width="100" align="center">
+			<el-table-column v-if="col.state" prop="state" label="状态" width="90" fixed="left" align="center">
 				<template #default="{ row }">
-					<el-tag :type="orderStatusTag(row.status)" size="small">{{ row.status }}</el-tag>
+					<el-tag size="small" :type="stateTag(row.state)">{{ row.state || '—' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="交货日期" width="120" align="center">
-				<template #default="{ row }">{{ fmtDay(row.deliveryDate) }}</template>
+			<el-table-column v-if="col.changeContent" prop="changeContent" label="变更内容" min-width="160" show-overflow-tooltip />
+			<el-table-column v-if="col.flowState" prop="flowState" label="审批进度" width="110" align="center">
+				<template #default="{ row }">
+					<el-tag size="small" :type="row.flowState === '审批中' ? 'warning' : 'success'">{{ row.flowState || '—' }}</el-tag>
+				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="200" fixed="right" align="center">
+
+			<el-table-column label="行操作" width="220" fixed="right" align="center">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="primary" v-if="canEdit(row)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="primary" @click="openView(row)">查看</el-button>
+					<el-button link type="warning" v-if="canChange(row)" @click="openChange(row)">变更</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -58,210 +83,189 @@
 			/>
 		</div>
 
-		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px" destroy-on-close @closed="resetForm">
-			<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
-				<el-form-item label="客户名称" prop="customerName">
-					<el-input v-model="form.customerName" />
-				</el-form-item>
-				<el-form-item label="产品" prop="product">
-					<el-input v-model="form.product" />
-				</el-form-item>
-				<el-form-item label="数量" prop="quantity">
-					<el-input-number v-model="form.quantity" :min="1" />
-				</el-form-item>
-				<el-form-item label="单价" prop="unitPrice">
-					<el-input-number v-model="form.unitPrice" :min="0" :precision="2" :step="0.01" />
-				</el-form-item>
-				<el-form-item label="交货日期">
-					<el-date-picker v-model="form.deliveryDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
-				</el-form-item>
-				<el-form-item v-if="editingId" label="状态" prop="status">
-					<el-select v-model="form.status" style="width: 100%">
-						<el-option v-for="s in orderStatuses" :key="s" :label="s" :value="s" />
-					</el-select>
-				</el-form-item>
-				<el-form-item label="备注">
-					<el-input v-model="form.remark" type="textarea" rows="2" />
-				</el-form-item>
-			</el-form>
-			<template #footer>
-				<el-button @click="dialogVisible = false">取消</el-button>
-				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
-			</template>
+		<el-dialog v-model="formDialogVisible" :title="formDialogTitle" width="980px" destroy-on-close>
+			<SalesOrderForm :mode="formMode" :order-id="editingId" @cancel="formDialogVisible = false" @saved="onSaved" />
+		</el-dialog>
+
+		<el-dialog v-model="changeDialogVisible" title="销售订单变更" width="980px" destroy-on-close>
+			<SalesOrderChangeForm :order-id="editingId" @cancel="changeDialogVisible = false" @submitted="onChanged" />
 		</el-dialog>
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopBusinessOrderList">
-import { computed, reactive, ref, onMounted } from 'vue';
+import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
-import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
+import { ElMessage } from 'element-plus';
 import { formatDate } from '/@/utils/formatTime';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
+import SalesOrderForm from './salesOrderForm.vue';
+import SalesOrderChangeForm from './salesOrderChangeForm.vue';
 import {
-	createOrder,
-	deleteOrder,
-	fetchOrders,
-	updateOrder,
-	type OrderRow,
-} from '../api/legacyAidop';
+	confirmSeOrderDelivery,
+	fetchSeOrderReviewList,
+	reviewSeOrder,
+	type SeOrderReviewRow,
+	unlockSeOrder,
+} from '../api/seOrderReview';
 
 const route = useRoute();
-const pageTitle = computed(() => (route.meta?.title as string) || '订单管理');
+const pageTitle = computed(() => (route.meta?.title as string) || '订单评审');
 
-const orderStatuses = ['待处理', '生产中', '已完成', '已取消'] as const;
+const stateOptions = ['新建', '评审', '确认'] as const;
 
 const query = reactive({
-	orderNo: '',
-	customerName: '',
-	status: '' as string,
+	billNo: '',
+	customNo: '',
+	state: '' as string,
+	orderType: undefined as number | undefined,
 	page: 1,
 	pageSize: 10,
 });
 
 const loading = ref(false);
-const rows = ref<OrderRow[]>([]);
+const rows = ref<SeOrderReviewRow[]>([]);
 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 form = reactive({
-	customerName: '',
-	product: '',
-	quantity: 1,
-	unitPrice: 0,
-	deliveryDate: '' as string | undefined,
-	remark: '',
-	status: '待处理',
+const selectedId = ref<number | null>(null);
+
+const col = reactive({
+	billNo: true,
+	orderType: true,
+	customNo: true,
+	rDate: true,
+	state: true,
+	changeContent: true,
+	flowState: true,
 });
 
-const rules = computed<FormRules>(() => ({
-	customerName: [{ required: true, message: '必填', trigger: 'blur' }],
-	product: [{ required: true, message: '必填', trigger: 'blur' }],
-	quantity: [{ required: true, message: '必填', trigger: 'change' }],
-	unitPrice: [{ required: true, message: '必填', trigger: 'change' }],
-	...(editingId.value
-		? { status: [{ required: true, message: '必填', trigger: 'change' }] }
-		: {}),
-}));
-
-function fmtDay(v: string | null | undefined) {
+const columnToggles = computed(() => [
+	{ key: 'billNo', label: '订单号', visible: col.billNo },
+	{ key: 'orderType', label: '类别', visible: col.orderType },
+	{ key: 'customNo', label: '客户编码', visible: col.customNo },
+	{ key: 'rDate', label: '下单日期', visible: col.rDate },
+	{ key: 'state', label: '状态', visible: col.state },
+	{ key: 'changeContent', label: '变更内容', visible: col.changeContent },
+	{ key: 'flowState', label: '审批进度', visible: col.flowState },
+]);
+
+function fmtDateTime(v?: string | null) {
 	if (!v) return '—';
 	try {
-		return formatDate(new Date(v), 'YYYY-mm-dd');
+		return formatDate(new Date(v), 'YYYY-mm-dd HH:MM');
 	} catch {
 		return v;
 	}
 }
 
-function orderStatusTag(s: string) {
-	const m: Record<string, string> = {
-		待处理: 'info',
-		生产中: 'warning',
-		已完成: 'success',
-		已取消: 'danger',
-	};
-	return m[s] || 'info';
+function orderTypeText(v?: number) {
+	return v === 1 ? '销售' : v === 2 ? '计划' : '—';
+}
+
+function stateTag(s?: string | null) {
+	if (s === '确认') return 'success';
+	if (s === '评审') return 'warning';
+	return 'info';
+}
+
+function canEdit(r: SeOrderReviewRow) {
+	return r.state === '新建' || r.state === '评审';
+}
+
+function canChange(r: SeOrderReviewRow) {
+	return r.state === '确认' && r.flowState !== '审批中';
+}
+
+function resetQuery() {
+	query.billNo = '';
+	query.customNo = '';
+	query.state = '';
+	query.orderType = undefined;
+	query.page = 1;
+	loadList();
+}
+
+function onSelectionChange(sel: SeOrderReviewRow[]) {
+	selectedId.value = sel?.[0]?.id ?? null;
 }
 
 async function loadList() {
 	loading.value = true;
 	try {
-		const data = await fetchOrders({
+		const data = await fetchSeOrderReviewList({
 			page: query.page,
 			pageSize: query.pageSize,
-			orderNo: query.orderNo || undefined,
-			customerName: query.customerName || undefined,
-			status: query.status || undefined,
+			billNo: query.billNo || undefined,
+			customNo: query.customNo || undefined,
+			state: query.state || undefined,
+			orderType: query.orderType ?? undefined,
 		});
 		rows.value = data.list;
 		total.value = data.total;
-	} catch {
-		rows.value = [];
-		total.value = 0;
 	} finally {
 		loading.value = false;
 	}
 }
 
-function resetQuery() {
-	query.orderNo = '';
-	query.customerName = '';
-	query.status = '';
-	query.page = 1;
-	loadList();
+async function onReview() {
+	if (!selectedId.value) return;
+	const res = await reviewSeOrder(selectedId.value);
+	ElMessage.success(`${res?.message || '已评审'}${res?.estimatedDeliveryDate ? `,预估交期:${fmtDateTime(res.estimatedDeliveryDate)}` : ''}`);
+	await loadList();
 }
 
-function resetForm() {
-	editingId.value = null;
-	form.customerName = '';
-	form.product = '';
-	form.quantity = 1;
-	form.unitPrice = 0;
-	form.deliveryDate = undefined;
-	form.remark = '';
-	form.status = '待处理';
-	formRef.value?.clearValidate();
+async function onConfirmDelivery() {
+	if (!selectedId.value) return;
+	await confirmSeOrderDelivery(selectedId.value);
+	ElMessage.success('已确认交期');
+	await loadList();
 }
 
+async function onUnlock() {
+	if (!selectedId.value) return;
+	await unlockSeOrder(selectedId.value);
+	ElMessage.success('解锁成功');
+}
+
+const formDialogVisible = ref(false);
+const changeDialogVisible = ref(false);
+const editingId = ref<number | null>(null);
+const formMode = ref<'create' | 'edit' | 'view'>('create');
+const formDialogTitle = computed(() =>
+	formMode.value === 'create' ? '添加销售订单' : formMode.value === 'edit' ? '编辑销售订单' : '查看销售订单'
+);
+
 function openCreate() {
-	resetForm();
-	dialogTitle.value = '新增订单';
-	dialogVisible.value = true;
+	editingId.value = null;
+	formMode.value = 'create';
+	formDialogVisible.value = true;
 }
 
-function openEdit(row: OrderRow) {
+function openEdit(row: SeOrderReviewRow) {
 	editingId.value = row.id;
-	dialogTitle.value = `编辑订单 ${row.orderNo}`;
-	form.customerName = row.customerName;
-	form.product = row.product;
-	form.quantity = row.quantity;
-	form.unitPrice = row.unitPrice;
-	form.deliveryDate = row.deliveryDate ? fmtDay(row.deliveryDate) : undefined;
-	form.remark = row.remark || '';
-	form.status = row.status;
-	dialogVisible.value = true;
+	formMode.value = 'edit';
+	formDialogVisible.value = true;
 }
 
-async function submitForm() {
-	await formRef.value?.validate().catch(() => Promise.reject());
-	saving.value = true;
-	try {
-		const body: Record<string, unknown> = {
-			customerName: form.customerName,
-			product: form.product,
-			quantity: form.quantity,
-			unitPrice: form.unitPrice,
-			remark: form.remark || undefined,
-		};
-		if (form.deliveryDate) body.deliveryDate = form.deliveryDate;
-		if (editingId.value) {
-			body.status = form.status;
-			await updateOrder(editingId.value, body);
-			ElMessage.success('已保存');
-		} else {
-			await createOrder(body);
-			ElMessage.success('已创建');
-		}
-		dialogVisible.value = false;
-		await loadList();
-	} finally {
-		saving.value = false;
-	}
+function openView(row: SeOrderReviewRow) {
+	editingId.value = row.id;
+	formMode.value = 'view';
+	formDialogVisible.value = true;
+}
+
+function openChange(row: SeOrderReviewRow) {
+	editingId.value = row.id;
+	changeDialogVisible.value = true;
+}
+
+async function onSaved() {
+	formDialogVisible.value = false;
+	await loadList();
 }
 
-function onDelete(row: OrderRow) {
-	ElMessageBox.confirm(`确定删除订单「${row.orderNo}」?`, '确认', { type: 'warning' })
-		.then(async () => {
-			await deleteOrder(row.id);
-			ElMessage.success('已删除');
-			await loadList();
-		})
-		.catch(() => {});
+async function onChanged() {
+	changeDialogVisible.value = false;
+	await loadList();
 }
 
 onMounted(() => loadList());
@@ -279,4 +283,16 @@ onMounted(() => loadList());
 	display: flex;
 	justify-content: flex-end;
 }
+
+.toolbar {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+	margin-bottom: 12px;
+}
+
+.col-toggle {
+	display: block;
+	margin: 4px 0;
+}
 </style>

+ 177 - 0
Web/src/views/aidop/business/salesOrderChangeForm.vue

@@ -0,0 +1,177 @@
+<template>
+	<div v-loading="loading">
+		<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
+			<el-row :gutter="16">
+				<el-col :span="12">
+					<el-form-item label="订单编号">
+						<el-input v-model="form.billNo" disabled />
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="订单类别">
+						<el-select v-model="form.orderType" disabled style="width: 100%">
+							<el-option label="销售" :value="1" />
+							<el-option label="计划" :value="2" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="客户名称">
+						<el-input v-model="form.customName" disabled />
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="签订日期">
+						<el-date-picker v-model="form.date" disabled type="date" value-format="YYYY-MM-DD" style="width: 100%" />
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="客户下单日期">
+						<el-date-picker v-model="form.rDate" disabled type="date" value-format="YYYY-MM-DD" style="width: 100%" />
+					</el-form-item>
+				</el-col>
+
+				<el-col :span="12">
+					<el-form-item label="变更原因" prop="changeReason">
+						<el-select v-model="form.changeReason" 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="变更要求" prop="changeType">
+						<el-select v-model="form.changeType" style="width: 100%">
+							<el-option label="变更订单" value="变更订单" />
+							<el-option label="取消订单" value="取消订单" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+				<el-col :span="24">
+					<el-form-item label="变更内容" prop="changeContent">
+						<el-input v-model="form.changeContent" type="textarea" rows="3" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+
+			<el-divider content-position="left">明细(只读)</el-divider>
+			<el-table :data="form.entries" border stripe style="width: 100%">
+				<el-table-column prop="itemNumber" label="产品代码" width="160" show-overflow-tooltip />
+				<el-table-column prop="itemName" label="产品名称" min-width="160" show-overflow-tooltip />
+				<el-table-column prop="specification" label="规格型号" min-width="160" show-overflow-tooltip />
+				<el-table-column prop="unit" label="计量单位" width="100" />
+				<el-table-column prop="qty" label="订单数量" width="110" align="center" />
+				<el-table-column prop="deliverCount" label="已发货数量" width="110" align="center" />
+				<el-table-column prop="progressText" label="订单进度" width="110" />
+			</el-table>
+		</el-form>
+
+		<div class="footer">
+			<el-button @click="$emit('cancel')">关闭</el-button>
+			<el-button :loading="saving" @click="onSave">保存</el-button>
+			<el-button type="primary" :loading="submitting" @click="onSubmit">发送</el-button>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import { createSeOrderChange, fetchSeOrderDetail, type SeOrderChangeSave } from '../api/seOrderReview';
+
+const props = defineProps<{ orderId: number | null }>();
+const emit = defineEmits<{ (e: 'cancel'): void; (e: 'submitted'): void }>();
+
+const loading = ref(false);
+const saving = ref(false);
+const submitting = ref(false);
+const formRef = ref<FormInstance>();
+
+const form = reactive<SeOrderChangeSave>({
+	seOrderId: 0,
+	billNo: '',
+	orderType: 2,
+	customNo: '',
+	customName: '',
+	date: null,
+	customLevel: null,
+	urgent: 0,
+	billFrom: null,
+	country: '中国',
+	rDate: null,
+	changeReason: '',
+	changeType: '',
+	changeContent: '',
+	entries: [],
+});
+
+const rules = computed<FormRules>(() => ({
+	changeReason: [{ required: true, message: '必填', trigger: 'change' }],
+	changeType: [{ required: true, message: '必填', trigger: 'change' }],
+	changeContent: [{ required: true, message: '必填', trigger: 'blur' }],
+}));
+
+async function loadDetail() {
+	if (!props.orderId) return;
+	loading.value = true;
+	try {
+		const d = await fetchSeOrderDetail(props.orderId);
+		form.seOrderId = d.id;
+		form.billNo = d.billNo || '';
+		form.orderType = d.orderType ?? 2;
+		form.customId = d.customId ?? null;
+		form.customNo = d.customNo || '';
+		form.customName = d.customName || '';
+		form.date = d.date || null;
+		form.customLevel = d.customLevel ?? null;
+		form.urgent = d.urgent ?? 0;
+		form.billFrom = d.billFrom ?? null;
+		form.country = d.country || '中国';
+		form.rDate = d.rDate || null;
+		form.entries = (d.entries || []).map((x) => ({
+			...x,
+			progressText: x.progress == null ? '' : String(x.progress),
+		}));
+	} finally {
+		loading.value = false;
+	}
+}
+
+async function doSave(submit: boolean) {
+	await formRef.value?.validate().catch(() => Promise.reject());
+	if (!props.orderId) return;
+	await createSeOrderChange(props.orderId, form);
+	ElMessage.success(submit ? '已发送变更申请' : '已保存');
+	emit('submitted');
+}
+
+async function onSave() {
+	saving.value = true;
+	try {
+		await doSave(false);
+	} finally {
+		saving.value = false;
+	}
+}
+
+async function onSubmit() {
+	submitting.value = true;
+	try {
+		await doSave(true);
+	} finally {
+		submitting.value = false;
+	}
+}
+
+onMounted(loadDetail);
+</script>
+
+<style scoped lang="scss">
+.footer {
+	margin-top: 14px;
+	display: flex;
+	justify-content: flex-end;
+	gap: 10px;
+}
+</style>
+

+ 268 - 0
Web/src/views/aidop/business/salesOrderForm.vue

@@ -0,0 +1,268 @@
+<template>
+	<div>
+		<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" v-loading="loading">
+			<el-row :gutter="16">
+				<el-col :span="12">
+					<el-form-item label="订单编号" prop="billNo">
+						<el-input v-model="form.billNo" :disabled="isView" />
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="订单类别" prop="orderType">
+						<el-select v-model="form.orderType" :disabled="isView" style="width: 100%">
+							<el-option label="销售" :value="1" />
+							<el-option label="计划" :value="2" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="客户名称" prop="customName">
+						<el-input v-model="form.customName" :disabled="true" placeholder="请选择客户" />
+						<el-button class="ml8" :disabled="isView" @click="customerDlg = true">选择</el-button>
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="签订日期" prop="date">
+						<el-date-picker v-model="form.date" :disabled="isView" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="客户级别" prop="customLevel">
+						<el-input-number v-model="form.customLevel" :disabled="isView" :min="0" style="width: 100%" />
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="是否加急" prop="urgent">
+						<el-radio-group v-model="form.urgent" :disabled="isView">
+							<el-radio :value="1">是</el-radio>
+							<el-radio :value="0">否</el-radio>
+						</el-radio-group>
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="客户订单号" prop="billFrom">
+						<el-input v-model="form.billFrom" :disabled="isView" />
+					</el-form-item>
+				</el-col>
+				<el-col :span="12">
+					<el-form-item label="国家" prop="country">
+						<el-select v-model="form.country" :disabled="isView" 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="客户下单日期" prop="rDate">
+						<el-date-picker v-model="form.rDate" :disabled="isView" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+
+			<el-divider content-position="left">明细</el-divider>
+
+			<div class="sub-toolbar">
+				<el-button type="success" :disabled="isView" @click="addRow">添加</el-button>
+			</div>
+
+			<el-table :data="form.entries" border stripe style="width: 100%">
+				<el-table-column label="行号" width="70" align="center">
+					<template #default="{ $index }">{{ $index + 1 }}</template>
+				</el-table-column>
+				<el-table-column label="产品代码" width="160">
+					<template #default="{ row }">
+						<el-input v-model="row.itemNumber" :disabled="true" placeholder="选择物料" />
+						<el-button class="ml8" :disabled="isView" @click="openItemDlg(row)">选择</el-button>
+					</template>
+				</el-table-column>
+				<el-table-column label="产品名称" min-width="160">
+					<template #default="{ row }">
+						<el-input v-model="row.itemName" :disabled="true" />
+					</template>
+				</el-table-column>
+				<el-table-column label="规格型号" min-width="160">
+					<template #default="{ row }">
+						<el-input v-model="row.specification" :disabled="true" />
+					</template>
+				</el-table-column>
+				<el-table-column label="订单数量" width="120" align="center">
+					<template #default="{ row }">
+						<el-input-number v-model="row.qty" :disabled="isView" :min="0" style="width: 100%" />
+					</template>
+				</el-table-column>
+				<el-table-column label="客户要求交期" width="140" align="center">
+					<template #default="{ row }">
+						<el-date-picker v-model="row.planDate" :disabled="isView" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
+					</template>
+				</el-table-column>
+				<el-table-column label="系统建议交期(产能)" width="160" align="center">
+					<template #default="{ row }">
+						<el-date-picker v-model="row.sysCapacityDate" :disabled="isView" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
+					</template>
+				</el-table-column>
+				<el-table-column label="最终交货日期" width="140" align="center">
+					<template #default="{ row }">
+						<el-date-picker v-model="row.date" :disabled="isView" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
+					</template>
+				</el-table-column>
+				<el-table-column label="备注" min-width="160">
+					<template #default="{ row }">
+						<el-input v-model="row.remark" :disabled="isView" />
+					</template>
+				</el-table-column>
+				<el-table-column label="操作" width="90" fixed="right" align="center">
+					<template #default="{ $index }">
+						<el-button link type="danger" :disabled="isView" @click="removeRow($index)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+		</el-form>
+
+		<div class="footer">
+			<el-button @click="$emit('cancel')">取消</el-button>
+			<el-button type="primary" :disabled="isView" :loading="saving" @click="onSave">保存</el-button>
+		</div>
+
+		<el-dialog v-model="customerDlg" title="选择客户" width="820px" append-to-body destroy-on-close>
+			<SelectCustomer @picked="onPickCustomer" />
+		</el-dialog>
+
+		<el-dialog v-model="itemDlgVisible" title="选择物料" width="920px" append-to-body destroy-on-close>
+			<SelectItem @picked="onPickItem" />
+		</el-dialog>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import SelectCustomer from './selectCustomer.vue';
+import SelectItem from './selectItem.vue';
+import { fetchSeOrderDetail, saveSeOrder, type SeOrderEntry, type SeOrderUpsert } from '../api/seOrderReview';
+
+const props = defineProps<{
+	mode: 'create' | 'edit' | 'view';
+	orderId: number | null;
+}>();
+
+const emit = defineEmits<{
+	(e: 'cancel'): void;
+	(e: 'saved'): void;
+}>();
+
+const isView = computed(() => props.mode === 'view');
+const loading = ref(false);
+const saving = ref(false);
+const formRef = ref<FormInstance>();
+
+const form = reactive<SeOrderUpsert>({
+	id: null,
+	billNo: '',
+	orderType: 2,
+	customId: null,
+	customNo: '',
+	customName: '',
+	date: null,
+	customLevel: null,
+	urgent: 0,
+	billFrom: null,
+	country: '中国',
+	rDate: new Date().toISOString().slice(0, 10),
+	entries: [],
+});
+
+const rules = computed<FormRules>(() => ({
+	billNo: [{ required: true, message: '必填', trigger: 'blur' }],
+	orderType: [{ required: true, message: '必填', trigger: 'change' }],
+	customName: [{ required: true, message: '必填', trigger: 'change' }],
+}));
+
+async function loadDetail() {
+	if (!props.orderId) return;
+	loading.value = true;
+	try {
+		const d = await fetchSeOrderDetail(props.orderId);
+		form.id = d.id;
+		form.billNo = d.billNo || '';
+		form.orderType = d.orderType ?? 2;
+		form.customId = d.customId ?? null;
+		form.customNo = d.customNo || '';
+		form.customName = d.customName || '';
+		form.date = d.date || null;
+		form.customLevel = d.customLevel ?? null;
+		form.urgent = d.urgent ?? 0;
+		form.billFrom = d.billFrom ?? null;
+		form.country = d.country || '中国';
+		form.rDate = d.rDate || form.rDate;
+		form.entries = (d.entries || []).map((x, i) => ({ ...x, entrySeq: x.entrySeq ?? i + 1 }));
+	} finally {
+		loading.value = false;
+	}
+}
+
+function addRow() {
+	const next: SeOrderEntry = { entrySeq: (form.entries?.length || 0) + 1, qty: 0 };
+	form.entries.push(next);
+}
+
+function removeRow(idx: number) {
+	form.entries.splice(idx, 1);
+}
+
+const customerDlg = ref(false);
+function onPickCustomer(v: { value: string; label: string; id?: number | null }) {
+	form.customId = v.id ?? null;
+	form.customNo = v.value;
+	form.customName = v.label;
+	customerDlg.value = false;
+}
+
+const itemDlgVisible = ref(false);
+let itemTargetRow: SeOrderEntry | null = null;
+function openItemDlg(row: SeOrderEntry) {
+	itemTargetRow = row;
+	itemDlgVisible.value = true;
+}
+function onPickItem(v: { value: string; label: string; extra?: string | null }) {
+	if (!itemTargetRow) return;
+	itemTargetRow.itemNumber = v.value;
+	itemTargetRow.itemName = v.label;
+	if (v.extra) {
+		const [descr1] = v.extra.split('|');
+		itemTargetRow.specification = descr1 || itemTargetRow.specification;
+	}
+	itemDlgVisible.value = false;
+}
+
+async function onSave() {
+	await formRef.value?.validate().catch(() => Promise.reject());
+	saving.value = true;
+	try {
+		await saveSeOrder(form);
+		ElMessage.success('保存成功');
+		emit('saved');
+	} finally {
+		saving.value = false;
+	}
+}
+
+onMounted(() => {
+	if (props.mode !== 'create') loadDetail();
+});
+</script>
+
+<style scoped lang="scss">
+.footer {
+	margin-top: 14px;
+	display: flex;
+	justify-content: flex-end;
+	gap: 10px;
+}
+.ml8 {
+	margin-left: 8px;
+}
+.sub-toolbar {
+	margin: 8px 0;
+}
+</style>
+

+ 86 - 0
Web/src/views/aidop/business/selectCustomer.vue

@@ -0,0 +1,86 @@
+<template>
+	<div>
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="关键词">
+				<el-input v-model="query.keyword" placeholder="客户编号/名称" clearable style="width: 240px" />
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="loadList">查询</el-button>
+				<el-button @click="reset">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" height="420" @row-dblclick="onDblClick">
+			<el-table-column prop="value" label="客户编号" width="160" />
+			<el-table-column prop="label" label="客户名称" min-width="220" show-overflow-tooltip />
+		</el-table>
+
+		<div class="pager">
+			<el-pagination
+				v-model:current-page="query.page"
+				v-model:page-size="query.pageSize"
+				:total="total"
+				:page-sizes="[10, 20, 50]"
+				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="loadList"
+			/>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="aidopBusinessSelectCustomer">
+import { onMounted, reactive, ref } from 'vue';
+import { fetchCustomers, type SimpleKv } from '../api/seOrderReview';
+
+const emit = defineEmits<{ (e: 'picked', v: SimpleKv): void }>();
+
+const query = reactive({
+	keyword: '',
+	page: 1,
+	pageSize: 10,
+});
+
+const loading = ref(false);
+const rows = ref<SimpleKv[]>([]);
+const total = ref(0);
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchCustomers({
+			keyword: query.keyword || undefined,
+			page: query.page,
+			pageSize: query.pageSize,
+		});
+		rows.value = data.list || [];
+		total.value = data.total || 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function reset() {
+	query.keyword = '';
+	query.page = 1;
+	loadList();
+}
+
+function onDblClick(row: SimpleKv) {
+	emit('picked', row);
+}
+
+onMounted(loadList);
+</script>
+
+<style scoped lang="scss">
+.mb12 {
+	margin-bottom: 12px;
+}
+.pager {
+	margin-top: 10px;
+	display: flex;
+	justify-content: flex-end;
+}
+</style>
+

+ 87 - 0
Web/src/views/aidop/business/selectItem.vue

@@ -0,0 +1,87 @@
+<template>
+	<div>
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="关键词">
+				<el-input v-model="query.keyword" placeholder="物料编号/名称" clearable style="width: 260px" />
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="loadList">查询</el-button>
+				<el-button @click="reset">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" height="420" @row-dblclick="onDblClick">
+			<el-table-column prop="value" label="物料编号" width="160" />
+			<el-table-column prop="label" label="物料名称" min-width="180" show-overflow-tooltip />
+			<el-table-column prop="extra" label="规格/单位" min-width="180" show-overflow-tooltip />
+		</el-table>
+
+		<div class="pager">
+			<el-pagination
+				v-model:current-page="query.page"
+				v-model:page-size="query.pageSize"
+				:total="total"
+				:page-sizes="[10, 20, 50]"
+				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="loadList"
+			/>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="aidopBusinessSelectItem">
+import { onMounted, reactive, ref } from 'vue';
+import { fetchItems, type SimpleKv } from '../api/seOrderReview';
+
+const emit = defineEmits<{ (e: 'picked', v: SimpleKv): void }>();
+
+const query = reactive({
+	keyword: '',
+	page: 1,
+	pageSize: 10,
+});
+
+const loading = ref(false);
+const rows = ref<SimpleKv[]>([]);
+const total = ref(0);
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchItems({
+			keyword: query.keyword || undefined,
+			page: query.page,
+			pageSize: query.pageSize,
+		});
+		rows.value = data.list || [];
+		total.value = data.total || 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function reset() {
+	query.keyword = '';
+	query.page = 1;
+	loadList();
+}
+
+function onDblClick(row: SimpleKv) {
+	emit('picked', row);
+}
+
+onMounted(loadList);
+</script>
+
+<style scoped lang="scss">
+.mb12 {
+	margin-bottom: 12px;
+}
+.pager {
+	margin-top: 10px;
+	display: flex;
+	justify-content: flex-end;
+}
+</style>
+

+ 3 - 3
ai-dop-platform/README.md

@@ -12,13 +12,13 @@
 
 ## 环境要求
 
-- **.NET 8 SDK**(与 `TargetFrameworks` 一致;若使用 net10 需对应 SDK)。
-- **Node.js**(**`Web/`**)。
+- **.NET 10 SDK**(`server/global.json` 指定 `10.0.x`;入口 `Admin.NET.Web.Entry` 目标 `net10.0`)。
+- **Node.js ≥ 18** + **pnpm**(`Web/package.json` 中 `packageManager: pnpm@10.32.1`)。
 - **MySQL**(连接串见 `server/Admin.NET.Application/Configuration/Database.json` 等)。
 
 ## 快速开始
 
-1. 启动后端:在 **`server/Admin.NET.Web.Entry/`** 下 `dotnet run --framework net8.0`(端口见 `launchSettings.json`,常见为 **5005**)。
+1. 启动后端:在 **`server/Admin.NET.Web.Entry/`** 下 `dotnet run`(端口见 `launchSettings.json`,常见为 **5005**)。
 2. **Development** 下插件会对 `ado_*` 表做 **CodeFirst**(见 [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md))。
 3. 启动管理端:在 **`Web/`** 下 `pnpm install`(或 `npm install`)、`pnpm dev`;`VITE_API_URL` 指向后端。
 

+ 3 - 3
ai-dop-platform/docs/GETTING_STARTED.md

@@ -7,7 +7,7 @@
 - 工作区为 **`ai-dop-platform/`** 时:`../server/Admin.NET.sln`
 - 工作区为 **主库根目录**(含 `server/`、`Web/`、`ai-dop-platform/`)时:`server/Admin.NET.sln`
 
-其中 **启动项目** 为 `Admin.NET.Web.Entry`。
+其中 **启动项目** 为 `Admin.NET.Web.Entry`(目标框架 `net10.0`)
 
 ## 2. 数据库
 
@@ -59,7 +59,7 @@ Vite 会拉起开发服务器;端口在 `Web/.env` 的 `VITE_PORT`(默认 **
 
 ## 5. 构建验证
 
-在已安装 **.NET 8+** SDK 的机器上(默认当前目录为 **`ai-dop-platform/`**;若在主库根目录,将 `../server` 改为 `server`):
+在已安装 **.NET 10 SDK**(`server/global.json` 要求 `10.0.x`)的机器上(默认当前目录为 **`ai-dop-platform/`**;若在主库根目录,将 `../server` 改为 `server`):
 
 ```bash
 dotnet restore ../server/Admin.NET.sln
@@ -68,7 +68,7 @@ dotnet build ../server/Admin.NET.sln -c Release
 
 若首次编译出现 `NETSDK1004`(找不到 `project.assets.json`),请先执行上一行的 `dotnet restore` 再构建。
 
-(当前 CI/本机若仅有 .NET 6 SDK 会报错,需升级 SDK。)
+(当前 CI/本机若仅有 .NET 8 或更低版本 SDK 会报错,需升级至 .NET 10 SDK。)
 
 ## 6. 团队 Cursor 规则
 

+ 116 - 28
ai-dop-platform/tools/aidop_init.ps1

@@ -5,7 +5,19 @@ param(
 $ErrorActionPreference = "Stop"
 
 function Write-Step($msg) {
-    Write-Host "[AIDOP INIT] $msg"
+    Write-Host "[AIDOP INIT] $msg" -ForegroundColor Cyan
+}
+
+function Write-Ok($msg) {
+    Write-Host "[  OK  ] $msg" -ForegroundColor Green
+}
+
+function Write-Warn($msg) {
+    Write-Host "[ WARN ] $msg" -ForegroundColor Yellow
+}
+
+function Write-Fail($msg) {
+    Write-Host "[ FAIL ] $msg" -ForegroundColor Red
 }
 
 function Test-Command($name) {
@@ -15,61 +27,137 @@ function Test-Command($name) {
 function Invoke-Checked($command, [string[]]$arguments) {
     & $command @arguments
     if ($LASTEXITCODE -ne 0) {
-        throw "$command failed with exit code $LASTEXITCODE"
+        throw "$command $($arguments -join ' ') failed with exit code $LASTEXITCODE"
     }
 }
 
-$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+# ---------- paths ----------
+$scriptDir    = Split-Path -Parent $MyInvocation.MyCommand.Path
 $platformRoot = Split-Path -Parent $scriptDir
-$repoRoot = Split-Path -Parent $platformRoot
+$repoRoot     = Split-Path -Parent $platformRoot
 
-$webDir = Join-Path $repoRoot "Web"
-$entryDir = Join-Path $repoRoot "server/Admin.NET.Web.Entry"
-$coreCsproj = Join-Path $repoRoot "server/Admin.NET.Core/Admin.NET.Core.csproj"
+$webDir    = Join-Path $repoRoot "Web"
+$serverDir = Join-Path $repoRoot "server"
+$entryDir  = Join-Path $serverDir "Admin.NET.Web.Entry"
+$slnFile   = Join-Path $serverDir "Admin.NET.sln"
 
 Write-Step "Repo root: $repoRoot"
-Write-Step "Current directory: $(Get-Location)"
+Write-Host ""
+
+# ---------- 1. directory sanity ----------
+Write-Step "1/6  Checking directory structure..."
 
-if (-not (Test-Path $webDir)) { throw "Web directory not found: $webDir (expected next to ai-dop-platform/)" }
-if (-not (Test-Path $entryDir)) { throw "Web.Entry directory not found: $entryDir" }
-if (-not (Test-Path $coreCsproj)) { throw "Core csproj not found: $coreCsproj" }
+$dirChecks = @(
+    @{ Path = $webDir;    Label = "Web (frontend)" },
+    @{ Path = $serverDir; Label = "server (backend)" },
+    @{ Path = $entryDir;  Label = "Admin.NET.Web.Entry (startup project)" },
+    @{ Path = $slnFile;   Label = "Admin.NET.sln" }
+)
+
+foreach ($chk in $dirChecks) {
+    if (-not (Test-Path $chk.Path)) {
+        throw "$($chk.Label) not found: $($chk.Path)"
+    }
+    Write-Ok $chk.Label
+}
+
+# ---------- 2. dotnet ----------
+Write-Step "2/6  Checking .NET SDK..."
 
 if (-not (Test-Command "dotnet")) {
-    throw "dotnet not found. Please install .NET SDK 8.x (and 10.x if restore reports NETSDK1045)."
+    Write-Fail "dotnet CLI not found."
+    throw "Please install .NET 10 SDK from https://dotnet.microsoft.com/download"
+}
+
+$dotnetVersion = (dotnet --version).Trim()
+Write-Ok "dotnet $dotnetVersion"
+
+if (-not $dotnetVersion.StartsWith("10.")) {
+    Write-Warn "global.json requires SDK 10.0.x but found $dotnetVersion — restore/build may fail."
 }
 
-$dotnetVersion = (dotnet --version)
-Write-Step "dotnet version: $dotnetVersion"
+# ---------- 3. node / pnpm ----------
+Write-Step "3/6  Checking Node.js & pnpm..."
 
 if (-not (Test-Command "node")) {
-    throw "node not found. Please install Node.js >=18 first."
+    throw "node not found. Please install Node.js >= 18 from https://nodejs.org"
+}
+
+$nodeVersion = (node --version).Trim()
+Write-Ok "node $nodeVersion"
+
+$hasPnpm = Test-Command "pnpm"
+if ($hasPnpm) {
+    $pnpmVersion = (pnpm --version).Trim()
+    Write-Ok "pnpm $pnpmVersion"
+} else {
+    Write-Warn "pnpm not found; will fall back to npm. Recommend: npm install -g pnpm"
 }
 
-$nodeVersion = (node --version)
-Write-Step "node version: $nodeVersion"
+# ---------- 4. frontend deps ----------
+Write-Step "4/6  Installing frontend dependencies..."
 
-Write-Step "Install frontend dependencies + hooks"
 Push-Location $webDir
 try {
-    if (Test-Command "pnpm") {
+    if ($hasPnpm) {
         Invoke-Checked "pnpm" @("install")
     } else {
-        Write-Step "pnpm not found, fallback to npm install --legacy-peer-deps"
         Invoke-Checked "npm" @("install", "--legacy-peer-deps")
     }
-    Invoke-Checked "npm" @("run", "prepare")
+
+    if ($hasPnpm) {
+        Invoke-Checked "pnpm" @("run", "prepare")
+    } else {
+        Invoke-Checked "npm" @("run", "prepare")
+    }
+    Write-Ok "Frontend dependencies installed & git hooks prepared"
 } finally {
     Pop-Location
 }
 
-Write-Step "Build backend core (net8.0)"
-Invoke-Checked "dotnet" @("build", $coreCsproj, "--framework", "net8.0")
+# ---------- 5. backend restore & build ----------
+Write-Step "5/6  Restoring & building backend solution..."
 
+Invoke-Checked "dotnet" @("restore", $slnFile)
+Write-Ok "dotnet restore succeeded"
+
+Invoke-Checked "dotnet" @("build", $slnFile, "-c", "Debug", "--no-restore")
+Write-Ok "dotnet build succeeded"
+
+# ---------- 6. summary ----------
+Write-Host ""
+Write-Step "6/6  Environment summary"
+Write-Host "  -----------------------------------------------"
+Write-Host "    .NET SDK     : $dotnetVersion"
+Write-Host "    Node.js      : $nodeVersion"
+if ($hasPnpm) {
+    Write-Host "    pnpm         : $pnpmVersion"
+}
+Write-Host "    Backend entry : $entryDir"
+Write-Host "    Backend URL   : http://localhost:5005"
+Write-Host "    Frontend dir  : $webDir"
+Write-Host "    Frontend URL  : http://localhost:8888"
+Write-Host "  -----------------------------------------------"
+Write-Host ""
+
+# ---------- optional: start services ----------
 if ($StartServices) {
-    Write-Step "Starting backend and frontend services"
-    Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$entryDir'; dotnet run --framework net8.0"
-    Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$webDir'; if (Get-Command pnpm -ErrorAction SilentlyContinue) { pnpm dev } else { npm run dev }"
-    Write-Step "Services started. Backend: http://localhost:5005  Frontend: http://localhost:8888"
+    Write-Step "Starting backend (dotnet run)..."
+    Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$entryDir'; dotnet run"
+
+    Write-Step "Starting frontend (pnpm dev)..."
+    if ($hasPnpm) {
+        Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$webDir'; pnpm dev"
+    } else {
+        Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$webDir'; npm run dev"
+    }
+
+    Write-Host ""
+    Write-Ok "Services launched in separate windows."
+    Write-Host "    Backend  -> http://localhost:5005"
+    Write-Host "    Frontend -> http://localhost:8888"
+    Write-Host ""
+    Write-Host "  Tip: start backend first; frontend dev-server proxies API to :5005."
 }
 
-Write-Step "Initialization complete."
+Write-Ok "Initialization complete."

+ 2 - 1
server/Admin.NET.Application/Admin.NET.Application.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
+    <TargetFrameworks>net10.0</TargetFrameworks>
     <NoWarn>1701;1702;1591;8632</NoWarn>
     <DocumentationFile></DocumentationFile>
     <ImplicitUsings>enable</ImplicitUsings>
@@ -9,6 +9,7 @@
     <Nullable>disable</Nullable>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
+    <AnalysisLevel>5.0</AnalysisLevel>
   </PropertyGroup>
 
   <ItemGroup>

+ 3 - 1
server/Admin.NET.Application/Configuration/Database.json

@@ -10,7 +10,9 @@
         //"ConfigId": "1300000000001", // 默认库标识-禁止修改
         "DbType": "MySql",
         "DbNickName": "系统库",
-        "ConnectionString": "Server=123.60.180.165;Port=3306;Database=aidopdev;Uid=aidopremote;Pwd=1234567890aiDOP#;SslMode=None;Charset=utf8mb4;AllowLoadLocalInfile=true;AllowUserVariables=true;",
+        "ConnectionString": "Server=123.60.180.165;Port=3306;Database=dopdemo;Uid=root;Pwd=5heng=uN;SslMode=None;Charset=utf8mb4;AllowLoadLocalInfile=true;AllowUserVariables=true;",
+        //"ConnectionString": "Server=106.14.73.46;Port=3306;Database=aidopcore;Uid=aidopremote;Pwd=AidOp#Remote2026$Secure;SslMode=None;Charset=utf8mb4;AllowLoadLocalInfile=true;AllowUserVariables=true;",
+        //"ConnectionString": "Server=123.60.180.165;Port=3306;Database=aidopdev;Uid=aidopremote;Pwd=1234567890aiDOP#;SslMode=None;Charset=utf8mb4;AllowLoadLocalInfile=true;AllowUserVariables=true;",
         // 本地 SQLite 示例(切回时改 DbType 为 Sqlite 并恢复下行连接串)
         //"DbType": "Sqlite",
         //"ConnectionString": "DataSource=./Admin.NET.db",

+ 1 - 1
server/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
+    <TargetFrameworks>net10.0</TargetFrameworks>
     <NoWarn>1701;1702;1591</NoWarn>
     <DocumentationFile></DocumentationFile>
     <GenerateDocumentationFile>True</GenerateDocumentationFile>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
-    <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
+    <TargetFrameworks>net10.0</TargetFrameworks>
     <ImplicitUsings>enable</ImplicitUsings>
     <SatelliteResourceLanguages>zh-Hans</SatelliteResourceLanguages>
     <PublishReadyToRunComposite>true</PublishReadyToRunComposite>

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

@@ -14,4 +14,8 @@
     <ProjectReference Include="..\..\Admin.NET.Core\Admin.NET.Core.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Folder Include="Sales\Dto\" />
+  </ItemGroup>
+
 </Project>

+ 0 - 147
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/OrderController.cs

@@ -1,147 +0,0 @@
-using Admin.NET.Plugin.AiDOP.Dto;
-using Admin.NET.Plugin.AiDOP.Entity;
-using Admin.NET.Plugin.AiDOP.Infrastructure;
-
-namespace Admin.NET.Plugin.AiDOP.Controllers;
-
-/// <summary>
-/// 与 Gitee Ai-DOP Demo 的 <c>api/Order</c> 路由保持一致,便于前端与脚本迁移。
-/// </summary>
-[ApiController]
-[Route("api/[controller]")]
-[AllowAnonymous]
-[NonUnify]
-public class OrderController : ControllerBase
-{
-    private readonly SqlSugarRepository<AdoOrder> _orderRep;
-
-    public OrderController(SqlSugarRepository<AdoOrder> orderRep)
-    {
-        _orderRep = orderRep;
-    }
-
-    [HttpGet]
-    public async Task<IActionResult> GetOrders([FromQuery] OrderQueryDto query)
-    {
-        (query.Page, query.PageSize) = PagingGuard.Normalize(query.Page, query.PageSize);
-        var q = _orderRep.AsQueryable();
-
-        if (!string.IsNullOrWhiteSpace(query.OrderNo))
-            q = q.Where(x => x.OrderNo.Contains(query.OrderNo));
-        if (!string.IsNullOrWhiteSpace(query.CustomerName))
-            q = q.Where(x => x.CustomerName.Contains(query.CustomerName));
-        if (!string.IsNullOrWhiteSpace(query.Status))
-            q = q.Where(x => x.Status == query.Status);
-
-        var total = await q.CountAsync();
-        var list = await q
-            .OrderByDescending(x => x.CreatedTime)
-            .Skip((query.Page - 1) * query.PageSize)
-            .Take(query.PageSize)
-            .ToListAsync();
-
-        return Ok(new
-        {
-            total,
-            page = query.Page,
-            pageSize = query.PageSize,
-            list = list.Select(x => new OrderDetailDto
-            {
-                Id = x.Id,
-                OrderNo = x.OrderNo,
-                CustomerName = x.CustomerName,
-                Product = x.Product,
-                Quantity = x.Quantity,
-                UnitPrice = x.UnitPrice,
-                Amount = x.Amount,
-                Status = x.Status,
-                DeliveryDate = x.DeliveryDate,
-                Remark = x.Remark,
-                CreatedTime = x.CreatedTime,
-                UpdatedTime = x.UpdatedTime,
-                CreatedBy = x.CreatedBy
-            }).ToList()
-        });
-    }
-
-    [HttpGet("{id:long}")]
-    public async Task<IActionResult> GetOrder(long id)
-    {
-        var item = await _orderRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        return Ok(new OrderDetailDto
-        {
-            Id = item.Id,
-            OrderNo = item.OrderNo,
-            CustomerName = item.CustomerName,
-            Product = item.Product,
-            Quantity = item.Quantity,
-            UnitPrice = item.UnitPrice,
-            Amount = item.Amount,
-            Status = item.Status,
-            DeliveryDate = item.DeliveryDate,
-            Remark = item.Remark,
-            CreatedTime = item.CreatedTime,
-            UpdatedTime = item.UpdatedTime,
-            CreatedBy = item.CreatedBy
-        });
-    }
-
-    [HttpPost]
-    public async Task<IActionResult> CreateOrder([FromBody] OrderCreateDto dto)
-    {
-        var item = new AdoOrder
-        {
-            OrderNo = $"SO{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}",
-            CustomerName = dto.CustomerName,
-            Product = dto.Product,
-            Quantity = dto.Quantity,
-            UnitPrice = dto.UnitPrice,
-            Amount = dto.Quantity * dto.UnitPrice,
-            DeliveryDate = dto.DeliveryDate,
-            Remark = dto.Remark,
-            Status = "待处理",
-            CreatedTime = DateTime.Now
-        };
-
-        await _orderRep.AsInsertable(item).ExecuteReturnEntityAsync();
-
-        return Ok(new { id = item.Id, message = "创建成功" });
-    }
-
-    [HttpPut("{id:long}")]
-    public async Task<IActionResult> UpdateOrder(long id, [FromBody] OrderUpdateDto dto)
-    {
-        var item = await _orderRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        item.CustomerName = dto.CustomerName;
-        item.Product = dto.Product;
-        item.Quantity = dto.Quantity;
-        item.UnitPrice = dto.UnitPrice;
-        item.Amount = dto.Quantity * dto.UnitPrice;
-        item.DeliveryDate = dto.DeliveryDate;
-        item.Remark = dto.Remark;
-        item.Status = dto.Status;
-        item.UpdatedTime = DateTime.Now;
-
-        await _orderRep.AsUpdateable(item).ExecuteCommandAsync();
-
-        return Ok(new { message = "更新成功" });
-    }
-
-    [HttpDelete("{id:long}")]
-    public async Task<IActionResult> DeleteOrder(long id)
-    {
-        var item = await _orderRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        await _orderRep.DeleteAsync(item);
-
-        return Ok(new { message = "删除成功" });
-    }
-}

+ 0 - 146
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/PlanController.cs

@@ -1,146 +0,0 @@
-using Admin.NET.Plugin.AiDOP.Dto;
-using Admin.NET.Plugin.AiDOP.Entity;
-using Admin.NET.Plugin.AiDOP.Infrastructure;
-
-namespace Admin.NET.Plugin.AiDOP.Controllers;
-
-[ApiController]
-[Route("api/[controller]")]
-[AllowAnonymous]
-[NonUnify]
-public class PlanController : ControllerBase
-{
-    private readonly SqlSugarRepository<AdoPlan> _planRep;
-
-    public PlanController(SqlSugarRepository<AdoPlan> planRep)
-    {
-        _planRep = planRep;
-    }
-
-    [HttpGet]
-    public async Task<IActionResult> GetPlans([FromQuery] PlanQueryDto query)
-    {
-        (query.Page, query.PageSize) = PagingGuard.Normalize(query.Page, query.PageSize);
-        var q = _planRep.AsQueryable();
-
-        if (!string.IsNullOrWhiteSpace(query.PlanNo))
-            q = q.Where(x => x.PlanNo.Contains(query.PlanNo));
-        if (!string.IsNullOrWhiteSpace(query.PlanType))
-            q = q.Where(x => x.PlanType == query.PlanType);
-        if (!string.IsNullOrWhiteSpace(query.Status))
-            q = q.Where(x => x.Status == query.Status);
-
-        var total = await q.CountAsync();
-        var list = await q
-            .OrderByDescending(x => x.CreatedTime)
-            .Skip((query.Page - 1) * query.PageSize)
-            .Take(query.PageSize)
-            .ToListAsync();
-
-        return Ok(new
-        {
-            total,
-            page = query.Page,
-            pageSize = query.PageSize,
-            list = list.Select(x => new PlanDetailDto
-            {
-                Id = x.Id,
-                PlanNo = x.PlanNo,
-                PlanType = x.PlanType,
-                ProductName = x.ProductName,
-                Quantity = x.Quantity,
-                CompletedQuantity = x.CompletedQuantity,
-                StartDate = x.StartDate,
-                EndDate = x.EndDate,
-                Status = x.Status,
-                Priority = x.Priority,
-                Remark = x.Remark,
-                CreatedTime = x.CreatedTime,
-                UpdatedTime = x.UpdatedTime,
-                CreatedBy = x.CreatedBy
-            }).ToList()
-        });
-    }
-
-    [HttpGet("{id:long}")]
-    public async Task<IActionResult> GetPlan(long id)
-    {
-        var item = await _planRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        return Ok(new PlanDetailDto
-        {
-            Id = item.Id,
-            PlanNo = item.PlanNo,
-            PlanType = item.PlanType,
-            ProductName = item.ProductName,
-            Quantity = item.Quantity,
-            CompletedQuantity = item.CompletedQuantity,
-            StartDate = item.StartDate,
-            EndDate = item.EndDate,
-            Status = item.Status,
-            Priority = item.Priority,
-            Remark = item.Remark,
-            CreatedTime = item.CreatedTime,
-            UpdatedTime = item.UpdatedTime,
-            CreatedBy = item.CreatedBy
-        });
-    }
-
-    [HttpPost]
-    public async Task<IActionResult> CreatePlan([FromBody] PlanCreateDto dto)
-    {
-        var item = new AdoPlan
-        {
-            PlanNo = $"PL{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}",
-            PlanType = dto.PlanType,
-            ProductName = dto.ProductName,
-            Quantity = dto.Quantity,
-            StartDate = dto.StartDate,
-            EndDate = dto.EndDate,
-            Priority = dto.Priority,
-            Remark = dto.Remark,
-            Status = "未开始",
-            CreatedTime = DateTime.Now
-        };
-
-        await _planRep.AsInsertable(item).ExecuteReturnEntityAsync();
-
-        return Ok(new { id = item.Id, message = "创建成功" });
-    }
-
-    [HttpPut("{id:long}")]
-    public async Task<IActionResult> UpdatePlan(long id, [FromBody] PlanUpdateDto dto)
-    {
-        var item = await _planRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        item.PlanType = dto.PlanType;
-        item.ProductName = dto.ProductName;
-        item.Quantity = dto.Quantity;
-        item.StartDate = dto.StartDate;
-        item.EndDate = dto.EndDate;
-        item.Priority = dto.Priority;
-        item.Remark = dto.Remark;
-        item.Status = dto.Status;
-        item.UpdatedTime = DateTime.Now;
-
-        await _planRep.AsUpdateable(item).ExecuteCommandAsync();
-
-        return Ok(new { message = "更新成功" });
-    }
-
-    [HttpDelete("{id:long}")]
-    public async Task<IActionResult> DeletePlan(long id)
-    {
-        var item = await _planRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        await _planRep.DeleteAsync(item);
-
-        return Ok(new { message = "删除成功" });
-    }
-}

+ 0 - 193
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/WorkOrderController.cs

@@ -1,193 +0,0 @@
-using Admin.NET.Plugin.AiDOP.Dto;
-using Admin.NET.Plugin.AiDOP.Entity;
-using Admin.NET.Plugin.AiDOP.Infrastructure;
-
-namespace Admin.NET.Plugin.AiDOP.Controllers;
-
-[ApiController]
-[Route("api/[controller]")]
-[AllowAnonymous]
-[NonUnify]
-public class WorkOrderController : ControllerBase
-{
-    private readonly SqlSugarRepository<AdoWorkOrder> _workOrderRep;
-    private readonly SqlSugarRepository<AdoPlan> _planRep;
-
-    public WorkOrderController(
-        SqlSugarRepository<AdoWorkOrder> workOrderRep,
-        SqlSugarRepository<AdoPlan> planRep)
-    {
-        _workOrderRep = workOrderRep;
-        _planRep = planRep;
-    }
-
-    [HttpGet]
-    public async Task<IActionResult> GetWorkOrders([FromQuery] WorkOrderQueryDto query)
-    {
-        (query.Page, query.PageSize) = PagingGuard.Normalize(query.Page, query.PageSize);
-        var q = _workOrderRep.AsQueryable();
-
-        if (!string.IsNullOrWhiteSpace(query.WorkOrderNo))
-            q = q.Where(x => x.WorkOrderNo.Contains(query.WorkOrderNo));
-        if (!string.IsNullOrWhiteSpace(query.PlanNo))
-            q = q.Where(x => x.PlanNo != null && x.PlanNo.Contains(query.PlanNo));
-        if (!string.IsNullOrWhiteSpace(query.Status))
-            q = q.Where(x => x.Status == query.Status);
-        if (!string.IsNullOrWhiteSpace(query.WorkCenter))
-            q = q.Where(x => x.WorkCenter == query.WorkCenter);
-
-        var total = await q.CountAsync();
-        var list = await q
-            .OrderByDescending(x => x.CreatedTime)
-            .Skip((query.Page - 1) * query.PageSize)
-            .Take(query.PageSize)
-            .ToListAsync();
-
-        return Ok(new
-        {
-            total,
-            page = query.Page,
-            pageSize = query.PageSize,
-            list = list.Select(x => new WorkOrderDetailDto
-            {
-                Id = x.Id,
-                WorkOrderNo = x.WorkOrderNo,
-                PlanNo = x.PlanNo,
-                Product = x.Product,
-                Quantity = x.Quantity,
-                CompletedQuantity = x.CompletedQuantity,
-                WorkCenter = x.WorkCenter,
-                Team = x.Team,
-                Owner = x.Owner,
-                StartDate = x.StartDate,
-                PlanEndDate = x.PlanEndDate,
-                ActualEndDate = x.ActualEndDate,
-                Status = x.Status,
-                Priority = x.Priority,
-                Remark = x.Remark,
-                CreatedTime = x.CreatedTime,
-                UpdatedTime = x.UpdatedTime,
-                CreatedBy = x.CreatedBy
-            }).ToList()
-        });
-    }
-
-    [HttpGet("{id:long}")]
-    public async Task<IActionResult> GetWorkOrder(long id)
-    {
-        var item = await _workOrderRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        return Ok(new WorkOrderDetailDto
-        {
-            Id = item.Id,
-            WorkOrderNo = item.WorkOrderNo,
-            PlanNo = item.PlanNo,
-            Product = item.Product,
-            Quantity = item.Quantity,
-            CompletedQuantity = item.CompletedQuantity,
-            WorkCenter = item.WorkCenter,
-            Team = item.Team,
-            Owner = item.Owner,
-            StartDate = item.StartDate,
-            PlanEndDate = item.PlanEndDate,
-            ActualEndDate = item.ActualEndDate,
-            Status = item.Status,
-            Priority = item.Priority,
-            Remark = item.Remark,
-            CreatedTime = item.CreatedTime,
-            UpdatedTime = item.UpdatedTime,
-            CreatedBy = item.CreatedBy
-        });
-    }
-
-    [HttpPost]
-    public async Task<IActionResult> CreateWorkOrder([FromBody] WorkOrderCreateDto dto)
-    {
-        string? planNo = null;
-        if (dto.PlanId is > 0)
-        {
-            var plan = await _planRep.GetByIdAsync(dto.PlanId.Value);
-            planNo = plan?.PlanNo;
-        }
-
-        var item = new AdoWorkOrder
-        {
-            WorkOrderNo = $"WO{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}",
-            PlanId = dto.PlanId,
-            PlanNo = planNo,
-            Product = dto.Product,
-            Quantity = dto.Quantity,
-            WorkCenter = dto.WorkCenter,
-            Team = dto.Team,
-            Owner = dto.Owner,
-            StartDate = dto.StartDate,
-            PlanEndDate = dto.PlanEndDate,
-            Priority = dto.Priority,
-            Remark = dto.Remark,
-            Status = "待生产",
-            CreatedTime = DateTime.Now
-        };
-
-        await _workOrderRep.AsInsertable(item).ExecuteReturnEntityAsync();
-
-        return Ok(new { id = item.Id, message = "创建成功" });
-    }
-
-    [HttpPut("{id:long}")]
-    public async Task<IActionResult> UpdateWorkOrder(long id, [FromBody] WorkOrderUpdateDto dto)
-    {
-        var item = await _workOrderRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        item.Product = dto.Product;
-        item.Quantity = dto.Quantity;
-        item.CompletedQuantity = dto.CompletedQuantity;
-        item.WorkCenter = dto.WorkCenter;
-        item.Team = dto.Team;
-        item.Owner = dto.Owner;
-        item.StartDate = dto.StartDate;
-        item.PlanEndDate = dto.PlanEndDate;
-        item.Priority = dto.Priority;
-        item.Remark = dto.Remark;
-        item.Status = dto.Status;
-        item.UpdatedTime = DateTime.Now;
-
-        if (dto.Status == "已完工" && item.ActualEndDate == null)
-            item.ActualEndDate = DateTime.Now;
-
-        await _workOrderRep.AsUpdateable(item).ExecuteCommandAsync();
-
-        return Ok(new { message = "更新成功" });
-    }
-
-    [HttpDelete("{id:long}")]
-    public async Task<IActionResult> DeleteWorkOrder(long id)
-    {
-        var item = await _workOrderRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        await _workOrderRep.DeleteAsync(item);
-
-        return Ok(new { message = "删除成功" });
-    }
-
-    [HttpPost("{id:long}/complete")]
-    public async Task<IActionResult> CompleteWorkOrder(long id, [FromBody] object? _)
-    {
-        var item = await _workOrderRep.GetByIdAsync(id);
-        if (item == null)
-            return NotFound();
-
-        item.Status = "已完工";
-        item.ActualEndDate = DateTime.Now;
-        item.UpdatedTime = DateTime.Now;
-
-        await _workOrderRep.AsUpdateable(item).ExecuteCommandAsync();
-
-        return Ok(new { message = "完工成功" });
-    }
-}

+ 0 - 44
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/OrderDtos.cs

@@ -1,44 +0,0 @@
-namespace Admin.NET.Plugin.AiDOP.Dto;
-
-public class OrderCreateDto
-{
-    public string CustomerName { get; set; } = string.Empty;
-    public string Product { get; set; } = string.Empty;
-    public int Quantity { get; set; }
-    public decimal UnitPrice { get; set; }
-    public DateTime? DeliveryDate { get; set; }
-    public string? Remark { get; set; }
-}
-
-public class OrderUpdateDto : OrderCreateDto
-{
-    public string Status { get; set; } = string.Empty;
-}
-
-public class OrderDetailDto
-{
-    public long Id { get; set; }
-    public string OrderNo { get; set; } = string.Empty;
-    public string CustomerName { get; set; } = string.Empty;
-    public string Product { get; set; } = string.Empty;
-    public int Quantity { get; set; }
-    public decimal UnitPrice { get; set; }
-    public decimal Amount { get; set; }
-    public string Status { get; set; } = string.Empty;
-    public DateTime? DeliveryDate { get; set; }
-    public string? Remark { get; set; }
-    public DateTime CreatedTime { get; set; }
-    public DateTime? UpdatedTime { get; set; }
-    public string? CreatedBy { get; set; }
-}
-
-public class OrderQueryDto
-{
-    public string? OrderNo { get; set; }
-    public string? CustomerName { get; set; }
-    public string? Status { get; set; }
-    public DateTime? StartDate { get; set; }
-    public DateTime? EndDate { get; set; }
-    public int Page { get; set; } = 1;
-    public int PageSize { get; set; } = 10;
-}

+ 0 - 44
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/PlanDtos.cs

@@ -1,44 +0,0 @@
-namespace Admin.NET.Plugin.AiDOP.Dto;
-
-public class PlanCreateDto
-{
-    public string PlanType { get; set; } = string.Empty;
-    public string ProductName { get; set; } = string.Empty;
-    public int Quantity { get; set; }
-    public DateTime? StartDate { get; set; }
-    public DateTime? EndDate { get; set; }
-    public string Priority { get; set; } = "中";
-    public string? Remark { get; set; }
-}
-
-public class PlanUpdateDto : PlanCreateDto
-{
-    public string Status { get; set; } = string.Empty;
-}
-
-public class PlanDetailDto
-{
-    public long Id { get; set; }
-    public string PlanNo { get; set; } = string.Empty;
-    public string PlanType { get; set; } = string.Empty;
-    public string ProductName { get; set; } = string.Empty;
-    public int Quantity { get; set; }
-    public int CompletedQuantity { get; set; }
-    public DateTime? StartDate { get; set; }
-    public DateTime? EndDate { get; set; }
-    public string Status { get; set; } = string.Empty;
-    public string Priority { get; set; } = string.Empty;
-    public string? Remark { get; set; }
-    public DateTime CreatedTime { get; set; }
-    public DateTime? UpdatedTime { get; set; }
-    public string? CreatedBy { get; set; }
-}
-
-public class PlanQueryDto
-{
-    public string? PlanNo { get; set; }
-    public string? PlanType { get; set; }
-    public string? Status { get; set; }
-    public int Page { get; set; } = 1;
-    public int PageSize { get; set; } = 10;
-}

+ 0 - 53
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/WorkOrderDtos.cs

@@ -1,53 +0,0 @@
-namespace Admin.NET.Plugin.AiDOP.Dto;
-
-public class WorkOrderCreateDto
-{
-    public long? PlanId { get; set; }
-    public string Product { get; set; } = string.Empty;
-    public int Quantity { get; set; }
-    public string WorkCenter { get; set; } = string.Empty;
-    public string? Team { get; set; }
-    public string? Owner { get; set; }
-    public DateTime? StartDate { get; set; }
-    public DateTime? PlanEndDate { get; set; }
-    public string Priority { get; set; } = "中";
-    public string? Remark { get; set; }
-}
-
-public class WorkOrderUpdateDto : WorkOrderCreateDto
-{
-    public string Status { get; set; } = string.Empty;
-    public int CompletedQuantity { get; set; }
-}
-
-public class WorkOrderDetailDto
-{
-    public long Id { get; set; }
-    public string WorkOrderNo { get; set; } = string.Empty;
-    public string? PlanNo { get; set; }
-    public string Product { get; set; } = string.Empty;
-    public int Quantity { get; set; }
-    public int CompletedQuantity { get; set; }
-    public string WorkCenter { get; set; } = string.Empty;
-    public string? Team { get; set; }
-    public string? Owner { get; set; }
-    public DateTime? StartDate { get; set; }
-    public DateTime? PlanEndDate { get; set; }
-    public DateTime? ActualEndDate { get; set; }
-    public string Status { get; set; } = string.Empty;
-    public string Priority { get; set; } = string.Empty;
-    public string? Remark { get; set; }
-    public DateTime CreatedTime { get; set; }
-    public DateTime? UpdatedTime { get; set; }
-    public string? CreatedBy { get; set; }
-}
-
-public class WorkOrderQueryDto
-{
-    public string? WorkOrderNo { get; set; }
-    public string? PlanNo { get; set; }
-    public string? Status { get; set; }
-    public string? WorkCenter { get; set; }
-    public int Page { get; set; } = 1;
-    public int PageSize { get; set; } = 10;
-}

+ 0 - 53
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoOrder.cs

@@ -1,53 +0,0 @@
-namespace Admin.NET.Plugin.AiDOP.Entity;
-
-/// <summary>
-/// Demo 订单表(与 Gitee Ai-DOP Demo 表名 ado_orders 对齐,便于数据迁移)
-/// </summary>
-[SugarTable("ado_orders", "AiDOP Demo 订单")]
-public class AdoOrder
-{
-    [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "integer")]
-    public long Id { get; set; }
-
-    [SugarColumn(ColumnDescription = "订单号", Length = 50)]
-    public string OrderNo { get; set; } = string.Empty;
-
-    [SugarColumn(ColumnDescription = "客户Id", IsNullable = true)]
-    public long? CustomerId { get; set; }
-
-    [SugarColumn(ColumnDescription = "客户名称", Length = 200)]
-    public string CustomerName { get; set; } = string.Empty;
-
-    [SugarColumn(ColumnDescription = "产品Id", IsNullable = true)]
-    public long? ProductId { get; set; }
-
-    [SugarColumn(ColumnDescription = "产品名称", Length = 200)]
-    public string Product { get; set; } = string.Empty;
-
-    [SugarColumn(ColumnDescription = "数量")]
-    public int Quantity { get; set; }
-
-    [SugarColumn(ColumnDescription = "单价", DecimalDigits = 4)]
-    public decimal UnitPrice { get; set; }
-
-    [SugarColumn(ColumnDescription = "金额", DecimalDigits = 4)]
-    public decimal Amount { get; set; }
-
-    [SugarColumn(ColumnDescription = "状态", Length = 50)]
-    public string Status { get; set; } = "待处理";
-
-    [SugarColumn(ColumnDescription = "交货日期", IsNullable = true)]
-    public DateTime? DeliveryDate { get; set; }
-
-    [SugarColumn(ColumnDescription = "备注", Length = 1000, IsNullable = true)]
-    public string? Remark { get; set; }
-
-    [SugarColumn(ColumnDescription = "创建时间")]
-    public DateTime CreatedTime { get; set; }
-
-    [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)]
-    public DateTime? UpdatedTime { get; set; }
-
-    [SugarColumn(ColumnDescription = "创建人", Length = 100, IsNullable = true)]
-    public string? CreatedBy { get; set; }
-}

+ 0 - 53
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoPlan.cs

@@ -1,53 +0,0 @@
-namespace Admin.NET.Plugin.AiDOP.Entity;
-
-/// <summary>
-/// Demo 计划表 ado_plans
-/// </summary>
-[SugarTable("ado_plans", "AiDOP Demo 计划")]
-public class AdoPlan
-{
-    [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "integer")]
-    public long Id { get; set; }
-
-    [SugarColumn(ColumnDescription = "计划号", Length = 50)]
-    public string PlanNo { get; set; } = string.Empty;
-
-    [SugarColumn(ColumnDescription = "计划类型", Length = 50)]
-    public string PlanType { get; set; } = string.Empty;
-
-    [SugarColumn(ColumnDescription = "产品Id", IsNullable = true)]
-    public long? ProductId { get; set; }
-
-    [SugarColumn(ColumnDescription = "产品名称", Length = 200)]
-    public string ProductName { get; set; } = string.Empty;
-
-    [SugarColumn(ColumnDescription = "计划数量")]
-    public int Quantity { get; set; }
-
-    [SugarColumn(ColumnDescription = "已完成数量")]
-    public int CompletedQuantity { get; set; }
-
-    [SugarColumn(ColumnDescription = "开始日期", IsNullable = true)]
-    public DateTime? StartDate { get; set; }
-
-    [SugarColumn(ColumnDescription = "结束日期", IsNullable = true)]
-    public DateTime? EndDate { get; set; }
-
-    [SugarColumn(ColumnDescription = "状态", Length = 50)]
-    public string Status { get; set; } = "未开始";
-
-    [SugarColumn(ColumnDescription = "优先级", Length = 50)]
-    public string Priority { get; set; } = "中";
-
-    [SugarColumn(ColumnDescription = "备注", Length = 1000, IsNullable = true)]
-    public string? Remark { get; set; }
-
-    [SugarColumn(ColumnDescription = "创建时间")]
-    public DateTime CreatedTime { get; set; }
-
-    [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)]
-    public DateTime? UpdatedTime { get; set; }
-
-    [SugarColumn(ColumnDescription = "创建人", Length = 100, IsNullable = true)]
-    public string? CreatedBy { get; set; }
-}

+ 0 - 68
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoWorkOrder.cs

@@ -1,68 +0,0 @@
-namespace Admin.NET.Plugin.AiDOP.Entity;
-
-/// <summary>
-/// Demo 工单表 ado_work_orders
-/// </summary>
-[SugarTable("ado_work_orders", "AiDOP Demo 工单")]
-public class AdoWorkOrder
-{
-    [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "integer")]
-    public long Id { get; set; }
-
-    [SugarColumn(ColumnDescription = "工单号", Length = 50)]
-    public string WorkOrderNo { get; set; } = string.Empty;
-
-    [SugarColumn(ColumnDescription = "计划Id", IsNullable = true)]
-    public long? PlanId { get; set; }
-
-    [SugarColumn(ColumnDescription = "计划号", Length = 50, IsNullable = true)]
-    public string? PlanNo { get; set; }
-
-    [SugarColumn(ColumnDescription = "产品Id", IsNullable = true)]
-    public long? ProductId { get; set; }
-
-    [SugarColumn(ColumnDescription = "产品", Length = 200)]
-    public string Product { get; set; } = string.Empty;
-
-    [SugarColumn(ColumnDescription = "数量")]
-    public int Quantity { get; set; }
-
-    [SugarColumn(ColumnDescription = "已完成数量")]
-    public int CompletedQuantity { get; set; }
-
-    [SugarColumn(ColumnDescription = "工作中心", Length = 100)]
-    public string WorkCenter { get; set; } = string.Empty;
-
-    [SugarColumn(ColumnDescription = "班组", Length = 100, IsNullable = true)]
-    public string? Team { get; set; }
-
-    [SugarColumn(ColumnDescription = "负责人", Length = 100, IsNullable = true)]
-    public string? Owner { get; set; }
-
-    [SugarColumn(ColumnDescription = "开始日期", IsNullable = true)]
-    public DateTime? StartDate { get; set; }
-
-    [SugarColumn(ColumnDescription = "计划完工", IsNullable = true)]
-    public DateTime? PlanEndDate { get; set; }
-
-    [SugarColumn(ColumnDescription = "实际完工", IsNullable = true)]
-    public DateTime? ActualEndDate { get; set; }
-
-    [SugarColumn(ColumnDescription = "状态", Length = 50)]
-    public string Status { get; set; } = "待生产";
-
-    [SugarColumn(ColumnDescription = "优先级", Length = 50)]
-    public string Priority { get; set; } = "中";
-
-    [SugarColumn(ColumnDescription = "备注", Length = 1000, IsNullable = true)]
-    public string? Remark { get; set; }
-
-    [SugarColumn(ColumnDescription = "创建时间")]
-    public DateTime CreatedTime { get; set; }
-
-    [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)]
-    public DateTime? UpdatedTime { get; set; }
-
-    [SugarColumn(ColumnDescription = "创建人", Length = 100, IsNullable = true)]
-    public string? CreatedBy { get; set; }
-}

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

@@ -3,8 +3,11 @@
 global using Admin.NET.Core;
 global using Furion;
 global using Furion.DatabaseAccessor;
+global using Furion.DependencyInjection;
 global using Furion.DynamicApiController;
+global using Furion.FriendlyException;
 global using Furion.UnifyResult;
+global using Mapster;
 global using Microsoft.AspNetCore.Authorization;
 global using Microsoft.AspNetCore.Mvc;
 global using SqlSugar;

+ 100 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Dto/SeOrderDto.cs

@@ -0,0 +1,100 @@
+namespace Admin.NET.Plugin.AiDOP.Order;
+
+// ─────────────────────────── 查询入参 ───────────────────────────
+
+/// <summary>订单列表分页查询入参</summary>
+public class SeOrderListInput
+{
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 10;
+
+    /// <summary>订单号(模糊)</summary>
+    public string? BillNo { get; set; }
+
+    /// <summary>客户编码(模糊)</summary>
+    public string? CustomNo { get; set; }
+
+    /// <summary>状态:新建 / 评审 / 确认</summary>
+    public string? State { get; set; }
+
+    /// <summary>订单类别:1=销售 2=计划</summary>
+    public int? OrderType { get; set; }
+}
+
+/// <summary>关键词分页查询(客户 / 物料弹窗)</summary>
+public class KeywordPageInput
+{
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 10;
+    public string? Keyword { get; set; }
+}
+
+// ─────────────────────────── 保存入参 ───────────────────────────
+
+/// <summary>订单明细行入参</summary>
+public class SeOrderEntryInput
+{
+    /// <summary>明细主键(编辑时传入,新增行为 null)</summary>
+    public long? Id { get; set; }
+    public int? EntrySeq { get; set; }
+    public string? ItemNumber { get; set; }
+    public string? ItemName { get; set; }
+    public string? Specification { get; set; }
+    public decimal? Qty { get; set; }
+    public string? PlanDate { get; set; }
+    public string? SysCapacityDate { get; set; }
+    public string? Date { get; set; }
+    public string? Remark { get; set; }
+}
+
+/// <summary>新增 / 编辑销售订单入参</summary>
+public class SeOrderSaveInput
+{
+    public long? Id { get; set; }
+
+    [Required(ErrorMessage = "订单编号不能为空")]
+    public string BillNo { get; set; } = string.Empty;
+
+    public int OrderType { get; set; } = 2;
+    public long? CustomId { get; set; }
+    public string? CustomNo { get; set; }
+    public string? CustomName { get; set; }
+
+    /// <summary>签订日期(yyyy-MM-dd)</summary>
+    public string? Date { get; set; }
+    public int? CustomLevel { get; set; }
+    public int Urgent { get; set; } = 0;
+    public string? BillFrom { get; set; }
+    public string? Country { get; set; } = "中国";
+    public string? RDate { get; set; }
+
+    public List<SeOrderEntryInput> Entries { get; set; } = new();
+}
+
+/// <summary>变更申请保存入参</summary>
+public class SeOrderChangeSaveInput
+{
+    public long SeOrderId { get; set; }
+    public string? BillNo { get; set; }
+    public int OrderType { get; set; } = 2;
+    public long? CustomId { get; set; }
+    public string? CustomNo { get; set; }
+    public string? CustomName { get; set; }
+    public string? Date { get; set; }
+    public int? CustomLevel { get; set; }
+    public int Urgent { get; set; } = 0;
+    public string? BillFrom { get; set; }
+    public string? Country { get; set; }
+    public string? RDate { get; set; }
+
+    [Required(ErrorMessage = "变更原因不能为空")]
+    public string? ChangeReason { get; set; }
+
+    [Required(ErrorMessage = "变更要求不能为空")]
+    public string? ChangeType { get; set; }
+
+    [Required(ErrorMessage = "变更内容不能为空")]
+    public string? ChangeContent { get; set; }
+
+    public List<SeOrderEntryInput> Entries { get; set; } = new();
+}

+ 77 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Entity/SeOrder.cs

@@ -0,0 +1,77 @@
+namespace Admin.NET.Plugin.AiDOP.Order;
+
+/// <summary>
+/// 销售订单主表(crm_seorder)
+/// </summary>
+[IgnoreTable]
+[SugarTable("crm_seorder", "销售订单表")]
+public class SeOrder
+{
+    [SugarColumn(ColumnName = "Id", IsPrimaryKey = true, IsIdentity = false)]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "bill_no", Length = 64)]
+    public string? BillNo { get; set; }
+
+    /// <summary>1=销售 2=计划</summary>
+    [SugarColumn(ColumnName = "order_type")]
+    public int? OrderType { get; set; }
+
+    [SugarColumn(ColumnName = "custom_id")]
+    public long? CustomId { get; set; }
+
+    [SugarColumn(ColumnName = "custom_no", Length = 64)]
+    public string? CustomNo { get; set; }
+
+    [SugarColumn(ColumnName = "custom_name", Length = 128)]
+    public string? CustomName { get; set; }
+
+    /// <summary>签订日期</summary>
+    [SugarColumn(ColumnName = "date")]
+    public DateTime? Date { get; set; }
+
+    [SugarColumn(ColumnName = "custom_level")]
+    public int? CustomLevel { get; set; }
+
+    /// <summary>1=加急 0=普通</summary>
+    [SugarColumn(ColumnName = "urgent")]
+    public int? Urgent { get; set; }
+
+    /// <summary>客户订单号</summary>
+    [SugarColumn(ColumnName = "bill_from", Length = 128)]
+    public string? BillFrom { get; set; }
+
+    [SugarColumn(ColumnName = "country", Length = 64)]
+    public string? Country { get; set; }
+
+    /// <summary>客户下单日期</summary>
+    [SugarColumn(ColumnName = "rdate")]
+    public DateTime? RDate { get; set; }
+
+    [SugarColumn(ColumnName = "flowstate", Length = 64)]
+    public string? FlowState { get; set; }
+
+    [SugarColumn(ColumnName = "Reason", Length = 256)]
+    public string? Reason { get; set; }
+
+    [SugarColumn(ColumnName = "IsDeleted")]
+    public int IsDeleted { get; set; } = 0;
+
+    [SugarColumn(ColumnName = "create_by_name", Length = 64)]
+    public string? CreateByName { get; set; }
+
+    [SugarColumn(ColumnName = "create_time")]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "update_by_name", Length = 64)]
+    public string? UpdateByName { get; set; }
+
+    [SugarColumn(ColumnName = "update_time")]
+    public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id")]
+    public long? TenantId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_id")]
+    public long? FactoryId { get; set; }
+}

+ 81 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Entity/SeOrderChange.cs

@@ -0,0 +1,81 @@
+namespace Admin.NET.Plugin.AiDOP.Order;
+
+/// <summary>
+/// 销售订单变更表(crm_seorder_change)
+/// </summary>
+[IgnoreTable]
+[SugarTable("crm_seorder_change", "销售订单变更表")]
+public class SeOrderChange
+{
+    [SugarColumn(ColumnName = "Id", IsPrimaryKey = true, IsIdentity = false)]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "seorder_id")]
+    public long SeOrderId { get; set; }
+
+    [SugarColumn(ColumnName = "bill_no", Length = 64)]
+    public string? BillNo { get; set; }
+
+    /// <summary>1=销售 2=计划</summary>
+    [SugarColumn(ColumnName = "order_type")]
+    public int? OrderType { get; set; }
+
+    [SugarColumn(ColumnName = "custom_id")]
+    public long? CustomId { get; set; }
+
+    [SugarColumn(ColumnName = "custom_no", Length = 64)]
+    public string? CustomNo { get; set; }
+
+    [SugarColumn(ColumnName = "custom_name", Length = 128)]
+    public string? CustomName { get; set; }
+
+    [SugarColumn(ColumnName = "date")]
+    public DateTime? Date { get; set; }
+
+    [SugarColumn(ColumnName = "custom_level")]
+    public int? CustomLevel { get; set; }
+
+    [SugarColumn(ColumnName = "urgent")]
+    public int? Urgent { get; set; }
+
+    [SugarColumn(ColumnName = "bill_from", Length = 128)]
+    public string? BillFrom { get; set; }
+
+    [SugarColumn(ColumnName = "country", Length = 64)]
+    public string? Country { get; set; }
+
+    [SugarColumn(ColumnName = "rdate")]
+    public DateTime? RDate { get; set; }
+
+    /// <summary>变更原因:客户原因 / 公司原因</summary>
+    [SugarColumn(ColumnName = "change_Reason", Length = 64)]
+    public string? ChangeReason { get; set; }
+
+    /// <summary>变更要求:变更订单 / 取消订单</summary>
+    [SugarColumn(ColumnName = "change_Type", Length = 64)]
+    public string? ChangeType { get; set; }
+
+    [SugarColumn(ColumnName = "change_content", Length = 1024)]
+    public string? ChangeContent { get; set; }
+
+    [SugarColumn(ColumnName = "flowstate", Length = 64)]
+    public string? FlowState { get; set; }
+
+    [SugarColumn(ColumnName = "IsDeleted")]
+    public int IsDeleted { get; set; } = 0;
+
+    [SugarColumn(ColumnName = "create_by_name", Length = 64)]
+    public string? CreateByName { get; set; }
+
+    [SugarColumn(ColumnName = "create_time")]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "update_time")]
+    public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id")]
+    public long? TenantId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_id")]
+    public long? FactoryId { get; set; }
+}

+ 67 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Entity/SeOrderEntry.cs

@@ -0,0 +1,67 @@
+namespace Admin.NET.Plugin.AiDOP.Order;
+
+/// <summary>
+/// 销售订单明细表(crm_seorderentry)
+/// </summary>
+[IgnoreTable]
+[SugarTable("crm_seorderentry", "销售订单明细表")]
+public class SeOrderEntry
+{
+    [SugarColumn(ColumnName = "Id", IsPrimaryKey = true, IsIdentity = false)]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "seorder_id")]
+    public long SeOrderId { get; set; }
+
+    [SugarColumn(ColumnName = "bill_no", Length = 64)]
+    public string? BillNo { get; set; }
+
+    [SugarColumn(ColumnName = "entry_seq")]
+    public int? EntrySeq { get; set; }
+
+    [SugarColumn(ColumnName = "item_number", Length = 64)]
+    public string? ItemNumber { get; set; }
+
+    [SugarColumn(ColumnName = "item_name", Length = 128)]
+    public string? ItemName { get; set; }
+
+    [SugarColumn(ColumnName = "specification", Length = 256)]
+    public string? Specification { get; set; }
+
+    [SugarColumn(ColumnName = "unit", Length = 32)]
+    public string? Unit { get; set; }
+
+    [SugarColumn(ColumnName = "qty")]
+    public decimal? Qty { get; set; }
+
+    /// <summary>客户要求交期</summary>
+    [SugarColumn(ColumnName = "plan_date")]
+    public DateTime? PlanDate { get; set; }
+
+    /// <summary>最终交货日期</summary>
+    [SugarColumn(ColumnName = "date")]
+    public DateTime? Date { get; set; }
+
+    /// <summary>系统建议交期(产能)</summary>
+    [SugarColumn(ColumnName = "sys_capacity_date")]
+    public DateTime? SysCapacityDate { get; set; }
+
+    [SugarColumn(ColumnName = "remark", Length = 512)]
+    public string? Remark { get; set; }
+
+    /// <summary>0=再评审 1=新建 2=评审 3=确认</summary>
+    [SugarColumn(ColumnName = "progress")]
+    public int? Progress { get; set; }
+
+    [SugarColumn(ColumnName = "deliver_count")]
+    public decimal? DeliverCount { get; set; }
+
+    [SugarColumn(ColumnName = "IsDeleted")]
+    public int IsDeleted { get; set; } = 0;
+
+    [SugarColumn(ColumnName = "create_time")]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "update_time")]
+    public DateTime? UpdateTime { get; set; }
+}

+ 430 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/SeOrderService.cs

@@ -0,0 +1,430 @@
+using Yitter.IdGenerator;
+
+namespace Admin.NET.Plugin.AiDOP.Order;
+
+/// <summary>
+/// 销售订单评审服务 🗂️
+/// 路由前缀:/api/Order/seorder/...
+/// </summary>
+[ApiDescriptionSettings(Order = 300, Description = "销售订单评审")]
+[Route("api/Order")]
+[AllowAnonymous]
+[NonUnify]
+public class SeOrderService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+    private readonly SqlSugarRepository<SeOrder> _seOrderRep;
+    private readonly SqlSugarRepository<SeOrderEntry> _seOrderEntryRep;
+    private readonly SqlSugarRepository<SeOrderChange> _seOrderChangeRep;
+
+    public SeOrderService(
+        ISqlSugarClient db,
+        SqlSugarRepository<SeOrder> seOrderRep,
+        SqlSugarRepository<SeOrderEntry> seOrderEntryRep,
+        SqlSugarRepository<SeOrderChange> seOrderChangeRep)
+    {
+        _db = db;
+        _seOrderRep = seOrderRep;
+        _seOrderEntryRep = seOrderEntryRep;
+        _seOrderChangeRep = seOrderChangeRep;
+    }
+
+    // ══════════════════════════════════════════════════════════════
+    // 订单列表 GET /api/Order/seorder/list
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>获取订单评审分页列表 🗂️</summary>
+    [DisplayName("获取订单评审列表")]
+    [HttpGet("seorder/list")]
+    public async Task<object> GetSeOrderList([FromQuery] SeOrderListInput input)
+    {
+        var pars = new List<SugarParameter>();
+        var innerConditions = new List<string> { "a.IsDeleted = 0" };
+
+        if (!string.IsNullOrWhiteSpace(input.BillNo))
+        {
+            innerConditions.Add("a.bill_no LIKE @BillNo");
+            pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.CustomNo))
+        {
+            innerConditions.Add("a.custom_no LIKE @CustomNo");
+            pars.Add(new SugarParameter("@CustomNo", $"%{input.CustomNo.Trim()}%"));
+        }
+        if (input.OrderType.HasValue)
+        {
+            innerConditions.Add("a.order_type = @OrderType");
+            pars.Add(new SugarParameter("@OrderType", input.OrderType.Value));
+        }
+
+        var baseSql = BuildListBaseSql(string.Join(" AND ", innerConditions));
+
+        // state 是 CASE 计算列,需包一层再过滤
+        var outerWhere = "";
+        if (!string.IsNullOrWhiteSpace(input.State))
+        {
+            outerWhere = "WHERE t.State = @State";
+            pars.Add(new SugarParameter("@State", input.State.Trim()));
+        }
+
+        var offset = (input.Page - 1) * input.PageSize;
+        var total = await _db.Ado.GetIntAsync(
+            $"SELECT COUNT(*) FROM ({baseSql}) AS t {outerWhere}", pars);
+        var list = await _db.Ado.SqlQueryAsync<SeOrderListRow>(
+            $"SELECT * FROM ({baseSql}) AS t {outerWhere} ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}", pars);
+
+        return new { total, page = input.Page, pageSize = input.PageSize, list };
+    }
+
+    private static string BuildListBaseSql(string innerWhere) => $"""
+        SELECT
+            a.id               AS Id,
+            a.bill_no          AS BillNo,
+            a.order_type       AS OrderType,
+            a.custom_no        AS CustomNo,
+            b.SortName         AS SortName,
+            a.rdate            AS RDate,
+            a.flowstate        AS FlowState,
+            entry.progress     AS Progress,
+            CASE
+                WHEN entry.progress = 1 THEN '新建'
+                WHEN entry.progress = 2 THEN '评审'
+                WHEN entry.progress = 0 THEN '再评审'
+                WHEN entry.progress = 3 THEN '确认'
+                ELSE '确认'
+            END                AS State,
+            chg.change_content AS ChangeContent,
+            cN.changeNum       AS ChangeNum
+        FROM crm_seorder a
+        LEFT JOIN CustMaster b ON a.custom_no = b.Cust
+        LEFT JOIN (
+            SELECT sorderid, MAX(create_time) AS create_time
+            FROM b_examine_result
+            GROUP BY sorderid
+        ) d ON a.id = d.sorderid
+        LEFT JOIN (
+            SELECT bill_no, change_content
+            FROM (
+                SELECT bill_no, change_content, update_time,
+                       ROW_NUMBER() OVER (PARTITION BY bill_no ORDER BY update_time DESC) AS RowNum
+                FROM crm_seorder_change
+                WHERE bill_no IS NOT NULL
+            ) ranked
+            WHERE RowNum = 1
+        ) chg ON chg.bill_no = a.bill_no
+        LEFT JOIN (
+            SELECT COUNT(*) AS changeNum, bill_no
+            FROM crm_seorder_change
+            WHERE bill_no IS NOT NULL
+            GROUP BY bill_no
+        ) cN ON cN.bill_no = a.bill_no
+        LEFT JOIN (
+            SELECT seorder_id, progress,
+                   ROW_NUMBER() OVER (PARTITION BY seorder_id ORDER BY Id) AS rn
+            FROM crm_seorderentry
+            WHERE IsDeleted = 0
+        ) entry ON a.id = entry.seorder_id AND entry.rn = 1
+        WHERE {innerWhere}
+        """;
+
+    // ══════════════════════════════════════════════════════════════
+    // 客户列表 GET /api/Order/seorder/customers
+    // 注:字面量路由优先于 {id} 参数路由,无需额外排序
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>获取客户选择列表 🗂️</summary>
+    [DisplayName("获取客户列表")]
+    [HttpGet("seorder/customers")]
+    public async Task<object> GetCustomers([FromQuery] KeywordPageInput input)
+    {
+        var pars = new List<SugarParameter>();
+        var where = "1=1";
+        if (!string.IsNullOrWhiteSpace(input.Keyword))
+        {
+            where = "(Cust LIKE @Keyword OR SortName LIKE @Keyword)";
+            pars.Add(new SugarParameter("@Keyword", $"%{input.Keyword.Trim()}%"));
+        }
+
+        var offset = (input.Page - 1) * input.PageSize;
+        var total = await _db.Ado.GetIntAsync(
+            $"SELECT COUNT(*) FROM CustMaster WHERE {where}", pars);
+        var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(
+            $"SELECT Cust AS Value, SortName AS Label, NULL AS Extra, NULL AS Id FROM CustMaster WHERE {where} ORDER BY Cust LIMIT {input.PageSize} OFFSET {offset}",
+            pars);
+
+        return new { total, page = input.Page, pageSize = input.PageSize, list };
+    }
+
+    // ══════════════════════════════════════════════════════════════
+    // 物料列表 GET /api/Order/seorder/items
+    // TODO: 将 bd_material 替换为实际物料主数据表名
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>获取物料选择列表 🗂️</summary>
+    [DisplayName("获取物料列表")]
+    [HttpGet("seorder/items")]
+    public async Task<object> GetItems([FromQuery] KeywordPageInput input)
+    {
+        var pars = new List<SugarParameter>();
+        var where = "IsDeleted = 0";
+        if (!string.IsNullOrWhiteSpace(input.Keyword))
+        {
+            where += " AND (item_number LIKE @Keyword OR item_name LIKE @Keyword)";
+            pars.Add(new SugarParameter("@Keyword", $"%{input.Keyword.Trim()}%"));
+        }
+
+        var offset = (input.Page - 1) * input.PageSize;
+        var total = await _db.Ado.GetIntAsync(
+            $"SELECT COUNT(*) FROM bd_material WHERE {where}", pars);
+        var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(
+            $"SELECT item_number AS Value, item_name AS Label, CONCAT(IFNULL(specification,''),'|',IFNULL(unit,'')) AS Extra, NULL AS Id FROM bd_material WHERE {where} ORDER BY item_number LIMIT {input.PageSize} OFFSET {offset}",
+            pars);
+
+        return new { total, page = input.Page, pageSize = input.PageSize, list };
+    }
+
+    // ══════════════════════════════════════════════════════════════
+    // 订单详情 GET /api/Order/seorder/{id}
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>获取订单详情(含明细)🗂️</summary>
+    [DisplayName("获取订单详情")]
+    [HttpGet("seorder/{id}")]
+    public async Task<object> GetSeOrderDetail(long id)
+    {
+        const string orderSql = """
+            SELECT Id, bill_no AS BillNo, order_type AS OrderType,
+                   custom_id AS CustomId, custom_no AS CustomNo, custom_name AS CustomName,
+                   date AS Date, custom_level AS CustomLevel, urgent AS Urgent,
+                   bill_from AS BillFrom, country AS Country, rdate AS RDate, flowstate AS FlowState
+            FROM crm_seorder
+            WHERE Id = @Id AND IsDeleted = 0
+            LIMIT 1
+            """;
+        var order = (await _db.Ado.SqlQueryAsync<SeOrderDetailRow>(orderSql, new { Id = id }))
+                    .FirstOrDefault()
+                    ?? throw Oops.Oh("订单不存在");
+
+        const string entrySql = """
+            SELECT Id, entry_seq AS EntrySeq, item_number AS ItemNumber, item_name AS ItemName,
+                   specification AS Specification, qty AS Qty, plan_date AS PlanDate,
+                   sys_capacity_date AS SysCapacityDate, date AS Date, remark AS Remark,
+                   unit AS Unit, deliver_count AS DeliverCount, progress AS Progress
+            FROM crm_seorderentry
+            WHERE seorder_id = @Id AND IsDeleted = 0
+            ORDER BY entry_seq
+            """;
+        var entries = await _db.Ado.SqlQueryAsync<SeOrderEntryRow>(entrySql, new { Id = id });
+
+        return new
+        {
+            order.Id, order.BillNo, order.OrderType,
+            order.CustomId, order.CustomNo, order.CustomName,
+            order.Date, order.CustomLevel, order.Urgent,
+            order.BillFrom, order.Country, order.RDate, order.FlowState,
+            entries
+        };
+    }
+
+    // ══════════════════════════════════════════════════════════════
+    // 新增 / 编辑 POST /api/Order/seorder/save
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>保存销售订单(新增或编辑)🗂️</summary>
+    [DisplayName("保存销售订单")]
+    [ApiDescriptionSettings(Name = "SaveSeOrder"), HttpPost("seorder/save")]
+    public async Task<object> SaveSeOrder([FromBody] SeOrderSaveInput input)
+    {
+        if (input.Id is null or 0)
+        {
+            // ── 新增:参照 SysJobService.AddJobDetail ──
+            var isExist = await _seOrderRep.IsAnyAsync(u => u.BillNo == input.BillNo && u.IsDeleted == 0);
+            if (isExist) throw Oops.Oh("订单编号已存在");
+
+            var entity = input.Adapt<SeOrder>();
+            entity.Id = YitIdHelper.NextId();
+            entity.IsDeleted = 0;
+            entity.CreateTime = DateTime.Now;
+            await _seOrderRep.InsertAsync(entity);
+            await SaveEntriesAsync(entity.Id, input.Entries);
+            return new { id = entity.Id, message = "新增成功" };
+        }
+        else
+        {
+            // ── 编辑:参照 SysJobService.UpdateJobDetail ──
+            var entity = await _seOrderRep.GetFirstAsync(u => u.Id == input.Id.Value && u.IsDeleted == 0)
+                         ?? throw Oops.Oh("订单不存在");
+
+            input.Adapt(entity);
+            entity.UpdateTime = DateTime.Now;
+            await _seOrderRep.UpdateAsync(entity);
+
+            await SaveEntriesAsync(input.Id.Value, input.Entries);
+            return new { id = input.Id, message = "编辑成功" };
+        }
+    }
+
+    /// <summary>
+    /// 明细三路合并:
+    /// ① DB有且入参有(按 Id 匹配)→ 更新
+    /// ② DB无但入参有              → 新增
+    /// ③ DB有但入参无              → 删除
+    /// </summary>
+    private async Task SaveEntriesAsync(long orderId, List<SeOrderEntryInput> entries)
+    {
+        // 取 DB 现有明细,以 Id 为 key 建索引
+        var dbEntries = await _seOrderEntryRep.GetListAsync(u => u.SeOrderId == orderId && u.IsDeleted == 0);
+        var dbById = dbEntries.ToDictionary(u => u.Id);
+
+        // 入参中携带 Id 的集合(用于判断 DB 行是否需要删除)
+        var inputIds = new HashSet<long>(entries.Where(e => e.Id > 0).Select(e => e.Id!.Value));
+
+        for (var i = 0; i < entries.Count; i++)
+        {
+            var e = entries[i];
+            var seq = e.EntrySeq ?? (i + 1);
+
+            if (e.Id > 0 && dbById.TryGetValue(e.Id.Value, out var existing))
+            {
+                // ① DB有、参数有 → 更新(参照 SysJobService.UpdateJobDetail)
+                e.Adapt(existing);
+                existing.EntrySeq = seq;
+                existing.UpdateTime = DateTime.Now;
+                await _seOrderEntryRep.UpdateAsync(existing);
+            }
+            else
+            {
+                // ② DB无、参数有 → 新增(参照 SysJobService.AddJobDetail)
+                var entry = e.Adapt<SeOrderEntry>();
+                entry.Id = YitIdHelper.NextId();
+                entry.SeOrderId = orderId;
+                entry.EntrySeq = seq;
+                entry.IsDeleted = 0;
+                entry.CreateTime = DateTime.Now;
+                await _seOrderEntryRep.InsertAsync(entry);
+            }
+        }
+
+        // ③ DB有、参数无 → 删除(参照 SysJobService.DeleteJobDetail)
+        foreach (var toDelete in dbEntries.Where(u => !inputIds.Contains(u.Id)))
+        {
+            await _seOrderEntryRep.DeleteAsync(u => u.Id == toDelete.Id);
+        }
+    }
+
+    // ══════════════════════════════════════════════════════════════
+    // 订单评审 POST /api/Order/seorder/{id}/review
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>订单评审(设进度=2)⏰</summary>
+    [DisplayName("订单评审")]
+    [ApiDescriptionSettings(Name = "ReviewSeOrder"), HttpPost("seorder/{id}/review")]
+    public async Task<object> ReviewSeOrder(long id)
+    {
+        // 参照 SysJobService 中 AsUpdateable().SetColumns().Where() 模式
+        await _seOrderEntryRep.AsUpdateable()
+            .SetColumns(u => new SeOrderEntry { Progress = 2, UpdateTime = DateTime.Now })
+            .Where(u => u.SeOrderId == id && u.IsDeleted == 0)
+            .ExecuteCommandAsync();
+
+        return new { message = "评审完成", estimatedDeliveryDate = DateTime.Now.AddDays(30).ToString("yyyy-MM-dd") };
+    }
+
+    // ══════════════════════════════════════════════════════════════
+    // 交期确认 POST /api/Order/seorder/{id}/confirm-delivery
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>交期确认(设进度=3)⏰</summary>
+    [DisplayName("交期确认")]
+    [ApiDescriptionSettings(Name = "ConfirmSeOrderDelivery"), HttpPost("seorder/{id}/confirm-delivery")]
+    public async Task<object> ConfirmDelivery(long id)
+    {
+        await _seOrderEntryRep.AsUpdateable()
+            .SetColumns(u => new SeOrderEntry { Progress = 3, UpdateTime = DateTime.Now })
+            .Where(u => u.SeOrderId == id && u.IsDeleted == 0)
+            .ExecuteCommandAsync();
+
+        return new { message = "交期已确认" };
+    }
+
+    // ══════════════════════════════════════════════════════════════
+    // 资源解锁 POST /api/Order/seorder/{id}/unlock
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>资源解锁 🔓</summary>
+    [DisplayName("资源解锁")]
+    [ApiDescriptionSettings(Name = "UnlockSeOrder"), HttpPost("seorder/{id}/unlock")]
+    public async Task<object> UnlockSeOrder(long id)
+    {
+        return new { message = "解锁成功" };
+    }
+
+    // ══════════════════════════════════════════════════════════════
+    // 变更申请 POST /api/Order/seorder/{id}/change
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>提交订单变更申请 🔖</summary>
+    [DisplayName("提交订单变更申请")]
+    [ApiDescriptionSettings(Name = "CreateSeOrderChange"), HttpPost("seorder/{id}/change")]
+    public async Task<object> CreateChange(long id, [FromBody] SeOrderChangeSaveInput input)
+    {
+        // 参照 SysJobService.AddJobDetail:Adapt → 设主键与审计字段 → InsertAsync
+        var entity = input.Adapt<SeOrderChange>();
+        entity.Id = YitIdHelper.NextId();
+        entity.SeOrderId = id;
+        entity.IsDeleted = 0;
+        entity.CreateTime = DateTime.Now;
+        await _seOrderChangeRep.InsertAsync(entity);
+        return new { id = entity.Id, message = "变更申请已保存" };
+    }
+
+    // ──────────────── 内部查询结果映射类 ────────────────
+
+    private sealed class SeOrderListRow
+    {
+        public long Id { get; set; }
+        public string? BillNo { get; set; }
+        public int? OrderType { get; set; }
+        public string? CustomNo { get; set; }
+        public string? SortName { get; set; }
+        public DateTime? RDate { get; set; }
+        public int? Progress { get; set; }
+        public string? State { get; set; }
+        public string? ChangeContent { get; set; }
+        public int? ChangeNum { get; set; }
+        public string? FlowState { get; set; }
+    }
+
+    private sealed class SeOrderDetailRow
+    {
+        public long Id { get; set; }
+        public string? BillNo { get; set; }
+        public int? OrderType { get; set; }
+        public long? CustomId { get; set; }
+        public string? CustomNo { get; set; }
+        public string? CustomName { get; set; }
+        public DateTime? Date { get; set; }
+        public int? CustomLevel { get; set; }
+        public int? Urgent { get; set; }
+        public string? BillFrom { get; set; }
+        public string? Country { get; set; }
+        public DateTime? RDate { get; set; }
+        public string? FlowState { get; set; }
+    }
+
+    private sealed class SeOrderEntryRow
+    {
+        public long Id { get; set; }
+        public int? EntrySeq { get; set; }
+        public string? ItemNumber { get; set; }
+        public string? ItemName { get; set; }
+        public string? Specification { get; set; }
+        public decimal? Qty { get; set; }
+        public DateTime? PlanDate { get; set; }
+        public DateTime? SysCapacityDate { get; set; }
+        public DateTime? Date { get; set; }
+        public string? Remark { get; set; }
+        public string? Unit { get; set; }
+        public decimal? DeliverCount { get; set; }
+        public int? Progress { get; set; }
+    }
+
+    private sealed class SimpleKvRow
+    {
+        public string? Value { get; set; }
+        public string? Label { get; set; }
+        public string? Extra { get; set; }
+        public long? Id { get; set; }
+    }
+}

+ 5 - 0
server/global.json

@@ -0,0 +1,5 @@
+{
+  "sdk": {
+    "version": "10.0.x"
+  }
+}