ソースを参照

fix(s0): enforce tenant org scope and repair UAT refs

B-1 止血:组织归属改由服务端裁决,停止产生跨租户组织引用。

背景:2026-06-14 a18c8a6c5 在「单部署」假设下把 DEFAULT_COMPANY_REF_ID=1329900200001 /
DEFAULT_FACTORY_REF_ID=1329900200002 写死进 S0 仓储页;其后 1.0.317.sql 把这两个 SysOrg
改挂到 Demo 租户 797403760988230,前端常量未同步,后端又 CompanyRefId = dto.CompanyRefId
照单全收,导致各租户新增都把本租户数据指向别租户的 SysOrg。

改动:
- 新增 AdoS0OrgScope:组织归属的服务端唯一裁决者。tenantId 只取自 JWT
  (AdoS0TenantScope),在当前租户内按 C1-C4 / F1-F4 解析公司与工厂,绝不跨租户查找、
  绝不新增组织;解析不出返回 400 由调用方转业务错误。实现自
  AdoS0ProcessFlowCardsController.ResolveDefaultOrgAsync 原样抽取,语义不变。
- 12 个 S0 Controller:新增时 stamp 解析出的组织并忽略 payload 组织值;编辑时 pin
  既有原值,既杜绝篡改成他租户组织,也不因普通编辑隐式迁移历史存量。6 处唯一性预检
  同步改用解析值/既有值,与落库口径一致。
- AdoS0ProcessFlowCardsController 改用共享 resolver,删除私有实现与常量(仅去重,
  无业务语义变化),全仓 resolver 收敛为 1 份。
- 11 个 Upsert DTO 移除组织字段的 [Range]:字段已由服务端接管,该校验是失效前置门,
  会把「租户未配置机构」误报成「公司不能为空」。
- 11 个 warehouse 前端页删除固定组织 ID 与 applyDefaultOrg,不再注入任何组织值。

B-2A 存量迁移(1.0.434):把 UAT 租户 838257186181189 的 91 行 NbrControl 组织引用
从 Demo 租户组织迁回本租户 OrgId 838257186320453。全 27 张含 company_ref_id 的表重新
扫描确认 UAT 命中仅此一表;WHERE 精确命中旧值并以 EXISTS 校验目标组织归属,幂等;
除两个组织列外业务字段校验和前后完全一致。租户 797403760988229 的 167 行不在本批范围。

验证:dotnet build 0 Errors;pnpm build 通过;vue-tsc 581 <= 597 基线且本批文件净增 0;
migration affected_rows=91 / verify Success;UAT CROSS-TENANT 91->0,全库 258->167;
UATAdminA 页面 total 仍 91、无 403、无 console error、91/91 行组织均为本租户。
YY968XX 1 ヶ月 前
親
コミット
0830d34988
33 ファイル変更、484 行追加、304 行削除
  1. 1 1
      Web/package.json
  2. 4 11
      Web/src/views/aidop/s0/warehouse/BarcodeRuleList.vue
  3. 4 11
      Web/src/views/aidop/s0/warehouse/CostCenterList.vue
  4. 4 11
      Web/src/views/aidop/s0/warehouse/DepartmentList.vue
  5. 4 11
      Web/src/views/aidop/s0/warehouse/EmpWorkDutyList.vue
  6. 4 11
      Web/src/views/aidop/s0/warehouse/EmployeeList.vue
  7. 4 11
      Web/src/views/aidop/s0/warehouse/ItemPackList.vue
  8. 4 11
      Web/src/views/aidop/s0/warehouse/LabelTypeList.vue
  9. 4 11
      Web/src/views/aidop/s0/warehouse/LocationList.vue
  10. 4 11
      Web/src/views/aidop/s0/warehouse/NbrControlList.vue
  11. 4 11
      Web/src/views/aidop/s0/warehouse/NbrTypeList.vue
  12. 4 11
      Web/src/views/aidop/s0/warehouse/TaskAssignmentList.vue
  13. 9 3
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  14. 53 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.434.sql
  15. 85 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.434.verify.sql
  16. 8 4
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Manufacturing/AdoS0MfgPersonSkillsController.cs
  17. 3 94
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Manufacturing/AdoS0ProcessFlowCardsController.cs
  18. 10 4
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0BarcodeRulesController.cs
  19. 12 6
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0CostCentersController.cs
  20. 12 6
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0DepartmentsController.cs
  21. 8 4
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0EmpWorkDutiesController.cs
  22. 14 6
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0EmployeesController.cs
  23. 8 4
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0ItemPacksController.cs
  24. 12 6
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LabelTypesController.cs
  25. 12 6
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LocationsController.cs
  26. 8 4
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0NbrControlsController.cs
  27. 8 4
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0NbrTypesController.cs
  28. 15 9
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0TaskAssignmentsController.cs
  29. 12 4
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Warehouse/AdoS0WarehouseBarcodeDtos.cs
  30. 24 8
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Warehouse/AdoS0WarehouseBasicDtos.cs
  31. 12 4
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Warehouse/AdoS0WarehouseNbrDtos.cs
  32. 18 6
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Warehouse/AdoS0WarehouseOtherDtos.cs
  33. 96 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AdoS0OrgScope.cs

+ 1 - 1
Web/package.json

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

+ 4 - 11
Web/src/views/aidop/s0/warehouse/BarcodeRuleList.vue

@@ -113,16 +113,10 @@ function emptyForm() {
 }
 const form = reactive(emptyForm());
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const formFactoryOptions = computed(() => {
@@ -161,7 +155,6 @@ function openEdit(row: S0BarcodeRuleRow) {
 }
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		if (editingId.value) { await s0BarcodeRulesApi.update(editingId.value, { ...form }); ElMessage.success('已保存'); }

+ 4 - 11
Web/src/views/aidop/s0/warehouse/CostCenterList.vue

@@ -82,16 +82,10 @@ const saving = ref(false);
 const formRef = ref<FormInstance>();
 const form = reactive<S0CostCtrUpsert>({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', costCtr: '', descr: '', ufld1: '', effTime: null });
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const formFactoryOptions = computed(() => {
@@ -129,7 +123,6 @@ function openEdit(row: S0CostCtrRow) {
 }
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		if (editingId.value) { await s0CostCentersApi.update(editingId.value, { ...form }); ElMessage.success('已保存'); }

+ 4 - 11
Web/src/views/aidop/s0/warehouse/DepartmentList.vue

@@ -97,16 +97,10 @@ const formRef = ref<FormInstance>();
 
 const form = reactive<S0DepartmentUpsert>({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', department: '', descr: '', isActive: true });
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const formFactoryOptions = computed(() => {
@@ -159,7 +153,6 @@ function openEdit(row: S0DepartmentRow) {
 
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		if (editingId.value) { await s0DepartmentsApi.update(editingId.value, { ...form }); ElMessage.success('已保存'); }

+ 4 - 11
Web/src/views/aidop/s0/warehouse/EmpWorkDutyList.vue

@@ -271,16 +271,10 @@ async function loadDutyOptions() {
 	}
 }
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const formFactoryOptions = computed(() => {
@@ -340,7 +334,6 @@ function openEdit(row: S0EmpWorkDutyRow) {
 }
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		// 职责数组 → 逗号分隔字符串(存储口径不变;Up-Shelf 等大小写原样保留)

+ 4 - 11
Web/src/views/aidop/s0/warehouse/EmployeeList.vue

@@ -194,16 +194,10 @@ const lineOptions = ref<Option[]>([]);
 const workCtrOptions = ref<Option[]>([]);
 const jobTitleOptions = ref<Option[]>([]);
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const singleOrgMode = ref(false);
@@ -271,7 +265,6 @@ function enumOrNull(v?: string | null): string | null { return v == null || v ==
 
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		const payload: S0EmployeeUpsert = { ...form, sex: enumOrNull(form.sex), employmentStatus: enumOrNull(form.employmentStatus), maritalStatus: enumOrNull(form.maritalStatus) };

+ 4 - 11
Web/src/views/aidop/s0/warehouse/ItemPackList.vue

@@ -115,16 +115,10 @@ const formRef = ref<FormInstance>();
 const emptyF = (): S0ItemPackUpsert => ({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', itemNum: '', packingQty: null, smallPackingQty: null, packingType: '', netWeight: null, weightUM: '', length: null, width: null, high: null, issSpecific: '', custItem: '', remark: '', isActive: true });
 const form = reactive<S0ItemPackUpsert>(emptyF());
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const formFactoryOptions = computed(() => {
@@ -162,7 +156,6 @@ function openEdit(row: S0ItemPackRow) {
 }
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		if (editingId.value) { await s0ItemPacksApi.update(editingId.value, { ...form }); ElMessage.success('已保存'); }

+ 4 - 11
Web/src/views/aidop/s0/warehouse/LabelTypeList.vue

@@ -79,16 +79,10 @@ const saving = ref(false);
 const formRef = ref<FormInstance>();
 const form = reactive<S0LabelTypeUpsert>({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', barType: '', class: '', inputString: '' });
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const formFactoryOptions = computed(() => {
@@ -126,7 +120,6 @@ function openEdit(row: S0LabelTypeRow) {
 }
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		if (editingId.value) { await s0LabelTypesApi.update(editingId.value, { ...form }); ElMessage.success('已保存'); }

+ 4 - 11
Web/src/views/aidop/s0/warehouse/LocationList.vue

@@ -132,16 +132,10 @@ const form = reactive<S0LocationUpsert>({ companyRefId: undefined, factoryRefId:
 const shelves = ref<S0LocationShelfInput[]>([]);
 const gen = reactive<{ prefix: string; startNum?: number; endNum?: number; shelfRow?: number; shelfCol?: number }>({ prefix: '', startNum: undefined, endNum: undefined, shelfRow: 1, shelfCol: 1 });
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 
 const rules: FormRules = {
 	location: [{ required: true, message: '请填写库位编码', trigger: 'blur' }],
@@ -255,7 +249,6 @@ async function submitForm() {
 	await formRef.value?.validate();
 	const cleanedShelves = validateShelvesForSubmit();
 	if (cleanedShelves === null) return;
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		const payload: S0LocationUpsert = { ...form, shelves: cleanedShelves };

+ 4 - 11
Web/src/views/aidop/s0/warehouse/NbrControlList.vue

@@ -106,16 +106,10 @@ const formRef = ref<FormInstance>();
 const emptyForm = (): S0NbrControlUpsert => ({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', nbrType: '', description: '', nbrPre1: '', nbrPre2: '', nbrPre3: '', iniValue: null, minValue: null, maxValue: null, allowReset: false, allowSkip: false, allowManual: false, dateType: '', isDateType: false });
 const form = reactive<S0NbrControlUpsert>(emptyForm());
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const formFactoryOptions = computed(() => {
@@ -153,7 +147,6 @@ function openEdit(row: S0NbrControlRow) {
 }
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		if (editingId.value) { await s0NbrControlsApi.update(editingId.value, { ...form }); ElMessage.success('已保存'); }

+ 4 - 11
Web/src/views/aidop/s0/warehouse/NbrTypeList.vue

@@ -94,16 +94,10 @@ const saving = ref(false);
 const formRef = ref<FormInstance>();
 const form = reactive<S0NbrTypeUpsert>({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', nbrType: '', descr1: '', nbrClass: '', dept: '', isActive: true });
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const formFactoryOptions = computed(() => {
@@ -147,7 +141,6 @@ function openEdit(row: S0NbrTypeRow) {
 }
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	// 域编码兜底:隐藏字段后仍确保域非空(单号类型域为后端必填业务键),缺则取默认工厂 code。
 	if (!form.domainCode) form.domainCode = getDefaultS0OrgScope(companyOptions.value, factoryOptions.value).domainCode ?? form.domainCode;
 	saving.value = true;

+ 4 - 11
Web/src/views/aidop/s0/warehouse/TaskAssignmentList.vue

@@ -125,16 +125,10 @@ const formRef = ref<FormInstance>();
 const emptyF = (): S0TaskAssignmentUpsert => ({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', tcrq: null, sqr: '', rwlx: '', wlbm: '', sl: null, rqpc: '', yskw: '', mdkw: '', xqsj: null, clr: '', bz: '', zt: '' });
 const form = reactive<S0TaskAssignmentUpsert>(emptyF());
 
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
-const DEFAULT_COMPANY_REF_ID = '1329900200001';
-const DEFAULT_FACTORY_REF_ID = '1329900200002';
-function isMissingOrLegacyOrgRef(v: unknown): boolean {
-	return v == null || v === '' || v === 0 || v === '0' || v === 1 || v === '1';
-}
-function applyDefaultOrg() {
-	if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
-	if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
-}
+// 公司/工厂归属由**后端**在当前租户内解析并强制 stamp(AdoS0OrgScope.ResolveAsync):
+// 新增忽略 payload 组织值、编辑 pin 既有原值。前端不再注入任何固定组织 ID——
+// 原 DEFAULT_COMPANY_REF_ID/DEFAULT_FACTORY_REF_ID 写死的是租户 797403760988230 的 SysOrg,
+// 在多租户下会给本租户数据写入他租户组织引用(S0-ORG-REFERENCE-MODEL-FIX-B1)。
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const formFactoryOptions = computed(() => {
@@ -173,7 +167,6 @@ function openEdit(row: S0TaskAssignmentRow) {
 }
 async function submitForm() {
 	await formRef.value?.validate();
-	applyDefaultOrg();
 	saving.value = true;
 	try {
 		if (editingId.value) { await s0TaskAssignmentsApi.update(editingId.value, { ...form }); ElMessage.success('已保存'); }

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.433</AssemblyVersion>
-    <FileVersion>1.0.433</FileVersion>
-    <Version>1.0.433</Version>
+    <AssemblyVersion>1.0.434</AssemblyVersion>
+    <FileVersion>1.0.434</FileVersion>
+    <Version>1.0.434</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -601,6 +601,12 @@
     <None Update="UpdateScripts\1.0.433.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.434.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.434.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\UAT-PLACEHOLDER-MENU-HIDE.ops.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>

+ 53 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.434.sql

@@ -0,0 +1,53 @@
+-- ============================================================================
+-- 1.0.434:B-2A —— 把 UAT 租户 838257186181189 的 91 行 NbrControl 组织引用
+--          从 Demo 租户组织迁回本租户自己的组织。
+--
+-- 背景(S0-ORG-REFERENCE-MODEL-FIX / B-1 已止血):
+--   2026-06-14 commit a18c8a6c5 在「单部署」假设下把 DEFAULT_COMPANY_REF_ID=1329900200001 /
+--   DEFAULT_FACTORY_REF_ID=1329900200002 写死进 S0 仓储页;其后 1.0.317.sql 把这两个 SysOrg
+--   改挂到 Demo 租户 797403760988230,前端常量未同步 → 各租户新增都写入别租户的组织 Id。
+--   B-1 已让服务端(AdoS0OrgScope.ResolveAsync)成为组织归属的唯一裁决者,新增不再产生污染;
+--   本脚本只清理**当前 UAT 租户**的历史存量。
+--
+-- 迁移口径(2026-09-01 只读实测):
+--   源:tenant_id=838257186181189 AND company_ref_id=1329900200001 AND factory_ref_id=1329900200002
+--   目标:company_ref_id = factory_ref_id = 838257186320453
+--        (SysOrg.Id=838257186320453 / TenantId=838257186181189 / Code=UATTEST_CHL / Pid=0,
+--          即 SysTenant(838257186181189).OrgId,与 B-1 后 AdoS0OrgScope 在该租户下的实际解析
+--          结果 C4→F4 完全一致,故迁移后与新增行同口径。)
+--   全 27 张含 company_ref_id 的表扫描结果:UAT 命中**只有 NbrControl 91 行**,
+--   且 91 行的 (company_ref_id, factory_ref_id) 完全一致,无需分类处理。
+--   DepartmentMaster 在 UAT 下无跨租户行 → 不触碰,S8 App.json 的
+--   DepartmentFactoryRefId=1329900200002 本轮不改。
+--
+-- 唯一键 dry run(迁移前只读预演):
+--   NbrControl 唯一索引只有 PRIMARY(RecID) 与 uk_NbrControl_tenant_domain_nbrtype
+--   (tenant_id, domain_code, NbrType)——**不含 org 列**,改 org 结构上不可能撞唯一键;
+--   模拟迁移后按该唯一键 GROUP BY HAVING COUNT(*)>1 实测 conflicts = 0。
+--
+-- 明确不做(本批边界):
+--   - 不动租户 797403760988229 的另外 167 行(B-2B 另批,口径待业务确认)
+--   - 不动 LEGACY-NUMERIC(1000 / 0 / 1)
+--   - 不改 SysOrg / SysTenant / 前端 / B-1 后端逻辑 / sysOrg:list / 1.0.431 / GrantMenu
+--   - 不改 Type=501 语义、不补 CustMaster/StdOpMaster 缺 tenant_id 的唯一键
+--   - 除两个 org 列外不写任何业务字段(NbrControl.UpdateTime 无 ON UPDATE CURRENT_TIMESTAMP,
+--     本 UPDATE 不列它 → 业务字段与审计字段保持原值)
+--
+-- 安全护栏:EXISTS 子句确保目标组织确实属于该租户;目标组织不存在或归属不符时整条语句 0 行,
+--   绝不把业务数据指向一个不属于本租户的组织。
+--
+-- 幂等:WHERE 精确命中旧值;首次 affected_rows=91,重复执行 affected_rows=0
+--   (旧值已不存在)。无该数据的库上同样为 0 行空操作。
+-- ============================================================================
+
+UPDATE `NbrControl`
+SET `company_ref_id` = 838257186320453,
+    `factory_ref_id` = 838257186320453
+WHERE `tenant_id` = 838257186181189
+  AND `company_ref_id` = 1329900200001
+  AND `factory_ref_id` = 1329900200002
+  AND EXISTS (
+    SELECT 1 FROM `SysOrg` o
+     WHERE o.`Id` = 838257186320453
+       AND o.`TenantId` = 838257186181189
+  );

+ 85 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.434.verify.sql

@@ -0,0 +1,85 @@
+-- ============================================================================
+-- 1.0.434 verify:UAT 租户 838257186181189 的组织引用必须已全部迁回本租户组织,
+--                 且租户 797403760988229 的 167 行必须一行未动。
+--
+-- 约定(AutoVersionUpdate.RunVerifyScriptIfExists):每条语句取「首行首列」当布尔断言,
+--   必须为真;返回 0 行会得到 NULL 判假并中断启动。故每条断言一律返回标量真值,
+--   且不使用 SET(SET 不返回行 → 必然判假)。
+--
+-- 环境容错:目标租户/组织不存在的库上主脚本为空操作,断言以「该库本就没有这些数据」
+--   的形式自然为真(COUNT=0 / 比较成立),无需额外短路。
+--
+-- CROSS-TENANT 统计口径:与 B-1 / B-2A 审计一致——业务行 tenant_id 与其
+--   company_ref_id/factory_ref_id 所指 SysOrg.TenantId 不一致即计一行。
+--   统计范围为**实测存在跨租户引用的 6 张表**(其余 21 张含 company_ref_id 的表
+--   在本脚本编写时实测均为 0,见 V5 注释)。
+-- ============================================================================
+
+-- V1:目标组织确实属于 UAT 租户(防止把业务数据指向他租户组织)
+SELECT (SELECT COUNT(*) FROM `SysOrg`
+         WHERE `Id` = 838257186320453 AND `TenantId` = 838257186181189) = 1 AS v1_target_org_belongs_to_uat;
+
+-- V2:UAT 租户在 NbrControl 中不再引用这两个 Demo 租户固定组织 ID
+SELECT (SELECT COUNT(*) FROM `NbrControl`
+         WHERE `tenant_id` = 838257186181189
+           AND (`company_ref_id` IN (1329900200001, 1329900200002)
+             OR `factory_ref_id` IN (1329900200001, 1329900200002))) = 0 AS v2_uat_no_demo_org_ref;
+
+-- V3:UAT 租户 NbrControl 全部行的组织归属正确(company 与 factory 都必须同租户)
+SELECT (SELECT COUNT(*) FROM `NbrControl` t
+          LEFT JOIN `SysOrg` oc ON oc.`Id` = t.`company_ref_id`
+          LEFT JOIN `SysOrg` ofa ON ofa.`Id` = t.`factory_ref_id`
+         WHERE t.`tenant_id` = 838257186181189
+           AND ((oc.`Id` IS NOT NULL AND oc.`TenantId` <> t.`tenant_id`)
+             OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId` <> t.`tenant_id`))) = 0 AS v3_uat_org_scope_consistent;
+
+-- V4:租户 797403760988229 的 167 行跨租户引用**完全未变**(本批边界的关键断言)
+SELECT (
+  SELECT SUM(n) FROM (
+    SELECT COUNT(*) AS n FROM `NbrControl` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE t.`tenant_id`=797403760988229 AND ((oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`))
+    UNION ALL
+    SELECT COUNT(*) FROM `NbrTypeMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE t.`tenant_id`=797403760988229 AND ((oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`))
+    UNION ALL
+    SELECT COUNT(*) FROM `LocationShelfMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE t.`tenant_id`=797403760988229 AND ((oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`))
+    UNION ALL
+    SELECT COUNT(*) FROM `EmployeeMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE t.`tenant_id`=797403760988229 AND ((oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`))
+    UNION ALL
+    SELECT COUNT(*) FROM `CostCtrMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE t.`tenant_id`=797403760988229 AND ((oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`))
+    UNION ALL
+    SELECT COUNT(*) FROM `DepartmentMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE t.`tenant_id`=797403760988229 AND ((oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`))
+  ) x
+) = 167 AS v4_other_tenant_167_untouched;
+
+-- V5:全库 CROSS-TENANT 迁移后应恰为 167(= 258 − 91)。
+--     统计范围为编写时实测存在跨租户引用的 6 张表;其余 21 张含 company_ref_id 的表当时实测为 0。
+--     **剩余 167 行是本批有意保留的范围,不是失败。**
+SELECT (
+  SELECT SUM(n) FROM (
+    SELECT COUNT(*) AS n FROM `NbrControl` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE (oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`)
+    UNION ALL
+    SELECT COUNT(*) FROM `NbrTypeMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE (oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`)
+    UNION ALL
+    SELECT COUNT(*) FROM `LocationShelfMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE (oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`)
+    UNION ALL
+    SELECT COUNT(*) FROM `EmployeeMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE (oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`)
+    UNION ALL
+    SELECT COUNT(*) FROM `CostCtrMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE (oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`)
+    UNION ALL
+    SELECT COUNT(*) FROM `DepartmentMaster` t LEFT JOIN `SysOrg` oc ON oc.`Id`=t.`company_ref_id` LEFT JOIN `SysOrg` ofa ON ofa.`Id`=t.`factory_ref_id`
+      WHERE (oc.`Id` IS NOT NULL AND oc.`TenantId`<>t.`tenant_id`) OR (ofa.`Id` IS NOT NULL AND ofa.`TenantId`<>t.`tenant_id`)
+  ) x
+) = 167 AS v5_global_cross_tenant_167;
+
+-- V6:UAT NbrControl 行数守恒(迁移只改 org 列,不增删行)
+SELECT (SELECT COUNT(*) FROM `NbrControl` WHERE `tenant_id` = 838257186181189) = 91 AS v6_uat_rowcount_preserved;

+ 8 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Manufacturing/AdoS0MfgPersonSkillsController.cs

@@ -89,11 +89,15 @@ public class AdoS0MfgPersonSkillsController : ControllerBase
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0MfgPersonSkillUpsertDto dto)
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("人员技能");
         var entity = new AdoS0MfgPersonSkill
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             Code = dto.Code,
             Name = dto.Name,
             SkillLevel = dto.SkillLevel,
@@ -111,8 +115,8 @@ public class AdoS0MfgPersonSkillsController : ControllerBase
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
         var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.Code = dto.Code;
         entity.Name = dto.Name;
         entity.SkillLevel = dto.SkillLevel;

+ 3 - 94
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Manufacturing/AdoS0ProcessFlowCardsController.cs

@@ -22,30 +22,15 @@ namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Manufacturing;
 [NonUnify]
 public class AdoS0ProcessFlowCardsController : ControllerBase
 {
-    /// <summary>租户内优先匹配的公司编码(仅作优先条件,取不到即按机构类型/根节点在本租户内兜底)。</summary>
-    private const string PreferredCompanyCode = "S0-C001";
-
-    /// <summary>租户内优先匹配的工厂编码(同上,绝不跨租户查找)。</summary>
-    private const string PreferredFactoryCode = "S0-F001";
-
-    /// <summary>机构类型-公司(SysOrg.Type 数据字典值)。</summary>
-    private const string CompanyOrgType = "201";
-
-    /// <summary>机构类型-工厂(SysOrg.Type 数据字典值)。</summary>
-    private const string FactoryOrgType = "501";
-
     private readonly SqlSugarRepository<AdoS0ProcessFlowCard> _rep;
     private readonly SqlSugarRepository<AdoS0MfgRoutingOpDetail> _routingRep;
-    private readonly SqlSugarRepository<SysOrg> _orgRep;
 
     public AdoS0ProcessFlowCardsController(
         SqlSugarRepository<AdoS0ProcessFlowCard> rep,
-        SqlSugarRepository<AdoS0MfgRoutingOpDetail> routingRep,
-        SqlSugarRepository<SysOrg> orgRep)
+        SqlSugarRepository<AdoS0MfgRoutingOpDetail> routingRep)
     {
         _rep = rep;
         _routingRep = routingRep;
-        _orgRep = orgRep;
     }
 
     [HttpGet]
@@ -92,9 +77,8 @@ public class AdoS0ProcessFlowCardsController : ControllerBase
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0ProcessFlowCardUpsertDto dto)
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
-        var (companyId, factoryId) = await ResolveDefaultOrgAsync(tenantId);
-        if (companyId <= 0 || factoryId <= 0)
-            return AdoS0ApiErrors.InvalidRequest("当前租户未配置机构,无法确定流转卡的公司/工厂归属,请先在机构管理中维护");
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("流转卡");
 
         var flowCardNo = dto.FlowCardNo?.Trim() ?? string.Empty;
         if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == factoryId && x.FlowCardNo == flowCardNo))
@@ -186,79 +170,4 @@ public class AdoS0ProcessFlowCardsController : ControllerBase
         return Ok(new { message = "删除成功" });
     }
 
-    /// <summary>
-    /// 工艺路线联动选项:按所选物料编码过滤该物料关联的 RouteCode(去重)。
-    /// 用稳定物料编码 ItemNum 关联(RoutingOpDetail.MaterialCode),不用物料名称。
-    /// 返回空 → 前端允许手工填写兜底;返回非空 → 前端强制下拉。
-    /// </summary>
-    [HttpGet("routing-options")]
-    public async Task<IActionResult> GetRoutingOptionsAsync([FromQuery] string? itemNum)
-    {
-        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
-        var key = itemNum?.Trim();
-        if (string.IsNullOrWhiteSpace(key))
-            return Ok(Array.Empty<object>());
-
-        var list = await _routingRep.AsQueryable()
-            .Where(x => x.MaterialCode == key)
-            .GroupBy(x => new { x.RouteCode, x.RouteName })
-            .Select(x => new { routeCode = x.RouteCode, routeName = x.RouteName })
-            .ToListAsync();
-
-        var options = list
-            .Where(x => !string.IsNullOrWhiteSpace(x.routeCode))
-            .OrderBy(x => x.routeCode)
-            .ToList();
-        return Ok(options);
-    }
-
-    /// <summary>
-    /// 解析当前租户的归属组织(公司 / 工厂)。
-    ///
-    /// 租户边界:只在 <paramref name="tenantId"/> 内解析,唯一查询显式带 <c>TenantId == tenantId</c>;
-    ///   **禁止 ClearFilter** —— SysOrg 的 Code 没有唯一约束(index_SysOrg_C 是普通索引、非 UNIQUE),
-    ///   且 S0-C001 / S0-F001 只是 S0OrgSeedData 灌给 AIDOP-Demo 租户的示范种子、其余租户并无此组织;
-    ///   清掉租户过滤器按 Code 取组织,会把他租户的 SysOrg.Id 写进本租户单据(跨租户组织引用)。
-    ///
-    /// Code 定位:S0-C001 / S0-F001 仅作"租户内优先匹配条件",取不到即在本租户内按机构类型 / 根节点兜底;
-    ///   不跨租户查找、不复制种子、不新增组织。
-    ///
-    /// 优先级——公司:
-    ///   C1 本租户 Code=S0-C001 → C3 本租户根节点(Pid=0)且 Type=201 → C2 本租户任意 Type=201 → C4 本租户根节点(Pid=0)。
-    /// 优先级——工厂:
-    ///   F1 本租户 Code=S0-F001 且父级为已解析公司 → F2 本租户 Type=501 且父级为已解析公司
-    ///   → F3 本租户任意 Type=501 → F4 回落到公司自身(兼容未建工厂节点的租户,不新增组织)。
-    ///
-    /// 确定性:一次取本租户机构并按 (OrderNo, Id) 稳定排序,各级用 FirstOrDefault 在有序集合上取首个,
-    ///   避免多候选时结果随执行计划漂移(原实现的无序 FirstAsync 是不确定的)。
-    ///
-    /// 无解:返回 (0, 0) 交由调用方转 400 业务错误,**不抛异常** ——
-    ///   AdoS0ExceptionFilter 会把 /api/s0/* 上的未处理异常统一映射为 500 S01999
-    ///   (见 AdoS0ApiErrors.TryMapUnhandledException),抛异常会把"租户未配置机构"这类业务态误报成系统故障。
-    /// </summary>
-    /// <param name="tenantId">AdoS0TenantScope 解析出的可信租户(JWT claim),不接受 payload 传入。</param>
-    private async Task<(long CompanyId, long FactoryId)> ResolveDefaultOrgAsync(long tenantId)
-    {
-        // 单次取数 + 内存分级,避免逐级往返;排序在 SQL 侧一次确定,后续各级取首个即稳定。
-        var orgs = await _orgRep.AsQueryable()
-            .Where(x => x.TenantId == tenantId)
-            .OrderBy(x => new { x.OrderNo, x.Id })
-            .Select(x => new { x.Id, x.Code, x.Type, x.Pid })
-            .ToListAsync();
-        if (orgs.Count == 0) return (0, 0);
-
-        var company =
-            orgs.FirstOrDefault(o => o.Code == PreferredCompanyCode)                    // C1
-            ?? orgs.FirstOrDefault(o => o.Pid == 0 && o.Type == CompanyOrgType)         // C3
-            ?? orgs.FirstOrDefault(o => o.Type == CompanyOrgType)                       // C2
-            ?? orgs.FirstOrDefault(o => o.Pid == 0);                                    // C4
-        if (company == null) return (0, 0);
-
-        var factory =
-            orgs.FirstOrDefault(o => o.Code == PreferredFactoryCode && o.Pid == company.Id)  // F1
-            ?? orgs.FirstOrDefault(o => o.Type == FactoryOrgType && o.Pid == company.Id)     // F2
-            ?? orgs.FirstOrDefault(o => o.Type == FactoryOrgType);                           // F3
-
-        return (company.Id, factory?.Id ?? company.Id);                                      // F4
-    }
 }

+ 10 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0BarcodeRulesController.cs

@@ -77,8 +77,15 @@ public class AdoS0BarcodeRulesController : ControllerBase
                 && x.Customer == dto.Customer && x.Type == dto.Type))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.BarcodeRuleCodeExists, "条码规则已存在");
 
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("条码规则");
+
         var entity = MapToEntity(dto);
         entity.TenantId = tenantId;
+        // 组织字段后端强制 stamp 当前租户内解析出的组织,覆盖 MapToEntity 从 payload 带过来的值
+        // (杜绝跨租户组织引用)。
+        entity.CompanyRefId = companyId;
+        entity.FactoryRefId = factoryId;
         entity.CreateTime = DateTime.Now;
         await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
         return Ok(entity);
@@ -97,8 +104,8 @@ public class AdoS0BarcodeRulesController : ControllerBase
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.BarcodeRuleCodeExists, "条码规则已存在");
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.Customer = dto.Customer;
         entity.Type = dto.Type;
@@ -126,10 +133,9 @@ public class AdoS0BarcodeRulesController : ControllerBase
 
     private static AdoS0BarCodeNbr MapToEntity(AdoS0BarcodeRuleUpsertDto dto)
     {
+        // 组织字段不在此映射:由 CreateAsync 用 AdoS0OrgScope 解析出的当前租户组织 stamp(不读 payload)。
         var e = new AdoS0BarCodeNbr
         {
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
             DomainCode = dto.DomainCode ?? string.Empty,
             Customer = dto.Customer,
             Type = dto.Type,

+ 12 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0CostCentersController.cs

@@ -57,14 +57,19 @@ public class AdoS0CostCentersController : ControllerBase
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.CostCtr == dto.CostCtr))
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("成本中心");
+
+        // 唯一性预检以后端解析出的工厂为界(与落库口径一致,不用 payload 组织值)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == factoryId && x.CostCtr == dto.CostCtr))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "成本中心编码已存在");
 
         var entity = new AdoS0CostCtrMaster
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             DomainCode = dto.DomainCode ?? string.Empty,
             CostCtr = dto.CostCtr,
             Descr = dto.Descr,
@@ -86,12 +91,13 @@ public class AdoS0CostCentersController : ControllerBase
         var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.CostCtr == dto.CostCtr))
+        // 唯一性预检以既有记录的工厂为界(组织 pin 原值,不接受 payload 迁移工厂)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == entity.FactoryRefId && x.CostCtr == dto.CostCtr))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "成本中心编码已存在");
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.CostCtr = dto.CostCtr;
         entity.Descr = dto.Descr;

+ 12 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0DepartmentsController.cs

@@ -60,14 +60,19 @@ public class AdoS0DepartmentsController : ControllerBase
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.Department == dto.Department))
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("部门");
+
+        // 唯一性预检以后端解析出的工厂为界(与落库口径一致,不用 payload 组织值)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == factoryId && x.Department == dto.Department))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "部门编码已存在");
 
         var entity = new AdoS0DepartmentMaster
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             DomainCode = dto.DomainCode ?? string.Empty,
             Department = dto.Department,
             Descr = dto.Descr,
@@ -88,12 +93,13 @@ public class AdoS0DepartmentsController : ControllerBase
         var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Department == dto.Department))
+        // 唯一性预检以既有记录的工厂为界(组织 pin 原值,不接受 payload 迁移工厂)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == entity.FactoryRefId && x.Department == dto.Department))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "部门编码已存在");
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.Department = dto.Department;
         entity.Descr = dto.Descr;

+ 8 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0EmpWorkDutiesController.cs

@@ -80,6 +80,9 @@ public class AdoS0EmpWorkDutiesController : ControllerBase
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
 
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("物料职责");
+
         var scopeError = ValidateMaterialScope(dto, out var itemNum, out var itemNum1, out var itemNum2);
         if (scopeError != null) return AdoS0ApiErrors.InvalidRequest(scopeError);
 
@@ -89,8 +92,9 @@ public class AdoS0EmpWorkDutiesController : ControllerBase
         var entity = new AdoS0EmpWorkDutyMaster
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             DomainCode = dto.DomainCode ?? string.Empty,
             Employee = dto.Employee,
             ItemNum = itemNum,
@@ -124,8 +128,8 @@ public class AdoS0EmpWorkDutiesController : ControllerBase
         if (relError != null) return relError;
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.Employee = dto.Employee;
         entity.ItemNum = itemNum;

+ 14 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0EmployeesController.cs

@@ -102,7 +102,11 @@ public class AdoS0EmployeesController : ControllerBase
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.Employee == dto.Employee))
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("雇员");
+
+        // 唯一性预检以后端解析出的工厂为界(与落库口径一致,不用 payload 组织值)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == factoryId && x.Employee == dto.Employee))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "员工编码已存在");
 
         // 所属部门必须存在于当前租户(同 domain + department),杜绝员工绑定他租户部门
@@ -113,6 +117,10 @@ public class AdoS0EmployeesController : ControllerBase
 
         var entity = MapToEntity(dto);
         entity.TenantId = tenantId;
+        // 组织字段后端强制 stamp 当前租户内解析出的组织,覆盖 MapToEntity 从 payload 带过来的值
+        // (杜绝跨租户组织引用)。
+        entity.CompanyRefId = companyId;
+        entity.FactoryRefId = factoryId;
         entity.CreateTime = DateTime.Now;
         await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
         return Ok(entity);
@@ -126,7 +134,8 @@ public class AdoS0EmployeesController : ControllerBase
         var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Employee == dto.Employee))
+        // 唯一性预检以既有记录的工厂为界(组织 pin 原值,不接受 payload 迁移工厂)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == entity.FactoryRefId && x.Employee == dto.Employee))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "员工编码已存在");
 
         if (!string.IsNullOrWhiteSpace(dto.Department)
@@ -135,8 +144,8 @@ public class AdoS0EmployeesController : ControllerBase
                 $"部门编码 '{dto.Department}' 不存在于当前租户部门主数据");
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.Employee = dto.Employee;
         entity.Name = dto.Name;
@@ -181,10 +190,9 @@ public class AdoS0EmployeesController : ControllerBase
         return Ok(new { message = "删除成功" });
     }
 
+    // 组织字段不在此映射:由 CreateAsync 用 AdoS0OrgScope 解析出的当前租户组织 stamp(不读 payload)。
     private static AdoS0EmployeeMaster MapToEntity(AdoS0EmployeeUpsertDto dto) => new()
     {
-        CompanyRefId = dto.CompanyRefId,
-        FactoryRefId = dto.FactoryRefId,
         DomainCode = dto.DomainCode ?? string.Empty,
         Employee = dto.Employee,
         Name = dto.Name,

+ 8 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0ItemPacksController.cs

@@ -67,6 +67,9 @@ public class AdoS0ItemPacksController : ControllerBase
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
 
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("零件包装规格");
+
         // 唯一口径对齐数据库 uk_ItemPackMaster_tenant_domain_itemnum (tenant_id, Domain, ItemNum)
         if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.DomainCode == (dto.DomainCode ?? string.Empty) && x.ItemNum == dto.ItemNum))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "物料包装规格已存在");
@@ -74,8 +77,9 @@ public class AdoS0ItemPacksController : ControllerBase
         var entity = new AdoS0ItemPackMaster
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             DomainCode = dto.DomainCode ?? string.Empty,
             ItemNum = dto.ItemNum,
             PackingQty = dto.PackingQty,
@@ -110,8 +114,8 @@ public class AdoS0ItemPacksController : ControllerBase
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "物料包装规格已存在");
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.ItemNum = dto.ItemNum;
         entity.PackingQty = dto.PackingQty;

+ 12 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LabelTypesController.cs

@@ -58,14 +58,19 @@ public class AdoS0LabelTypesController : ControllerBase
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.BarType == dto.BarType))
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("标签格式");
+
+        // 唯一性预检以后端解析出的工厂为界(与落库口径一致,不用 payload 组织值)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == factoryId && x.BarType == dto.BarType))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "标签类型编码已存在");
 
         var entity = new AdoS0BarCodeType
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             DomainCode = dto.DomainCode ?? string.Empty,
             BarType = dto.BarType,
             Class = dto.Class,
@@ -86,12 +91,13 @@ public class AdoS0LabelTypesController : ControllerBase
         var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.BarType == dto.BarType))
+        // 唯一性预检以既有记录的工厂为界(组织 pin 原值,不接受 payload 迁移工厂)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == entity.FactoryRefId && x.BarType == dto.BarType))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "标签类型编码已存在");
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.BarType = dto.BarType;
         entity.Class = dto.Class;

+ 12 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LocationsController.cs

@@ -137,15 +137,20 @@ public class AdoS0LocationsController : ControllerBase
         var (shelfError, shelfItems) = ValidateShelves(dto.Shelves);
         if (shelfError != null) return shelfError;
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.Location == dto.Location))
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("库位");
+
+        // 唯一性预检以后端解析出的工厂为界(与落库口径一致,不用 payload 组织值)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == factoryId && x.Location == dto.Location))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "库位编码已存在");
 
         var now = DateTime.Now;
         var entity = new AdoS0LocationMaster
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             DomainCode = dto.DomainCode ?? string.Empty,
             Location = dto.Location,
             Descr = dto.Descr,
@@ -204,13 +209,14 @@ public class AdoS0LocationsController : ControllerBase
         if (!string.Equals(entity.DomainCode, (dto.DomainCode ?? string.Empty).Trim(), StringComparison.Ordinal))
             return AdoS0ApiErrors.InvalidRequest("域编码不可修改");
 
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Location == dto.Location))
+        // 唯一性预检以既有记录的工厂为界(组织 pin 原值,不接受 payload 迁移工厂)。
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == entity.FactoryRefId && x.Location == dto.Location))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "库位编码已存在");
 
         var now = DateTime.Now;
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         // entity.Location 保持不变(不可修改)
         entity.Descr = dto.Descr;

+ 8 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0NbrControlsController.cs

@@ -57,6 +57,9 @@ public class AdoS0NbrControlsController : ControllerBase
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
 
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("单号规则");
+
         // 唯一口径对齐数据库 uk_NbrControl_tenant_domain_nbrtype (tenant_id, domain_code, NbrType)
         if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.DomainCode == (dto.DomainCode ?? string.Empty) && x.NbrType == dto.NbrType))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "单号类型编码已存在");
@@ -64,8 +67,9 @@ public class AdoS0NbrControlsController : ControllerBase
         var entity = new AdoS0NbrControl
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             DomainCode = dto.DomainCode ?? string.Empty,
             NbrType = dto.NbrType,
             Description = dto.Description,
@@ -100,8 +104,8 @@ public class AdoS0NbrControlsController : ControllerBase
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "单号类型编码已存在");
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.NbrType = dto.NbrType;
         entity.Description = dto.Description;

+ 8 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0NbrTypesController.cs

@@ -70,6 +70,9 @@ public class AdoS0NbrTypesController : ControllerBase
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0NbrTypeUpsertDto dto)
     {
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("单号类型");
         if (string.IsNullOrWhiteSpace(dto.DomainCode))
             return AdoS0ApiErrors.InvalidRequest("请填写域编码");
 
@@ -81,8 +84,9 @@ public class AdoS0NbrTypesController : ControllerBase
         var entity = new AdoS0NbrTypeMaster
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             DomainCode = dto.DomainCode ?? string.Empty,
             Dept = dto.Dept,
             NbrClass = dto.NbrClass,
@@ -110,8 +114,8 @@ public class AdoS0NbrTypesController : ControllerBase
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "单号类型编码已存在");
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.Dept = dto.Dept;
         entity.NbrClass = dto.NbrClass;

+ 15 - 9
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0TaskAssignmentsController.cs

@@ -68,16 +68,21 @@ public class AdoS0TaskAssignmentsController : ControllerBase
         if (string.IsNullOrWhiteSpace(dto.DomainCode))
             return AdoS0ApiErrors.InvalidRequest("请填写域编码");
 
+        var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
+        if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("任务指派");
+
         // 唯一口径对齐数据库 uk_wms_rwzp_tenant_org_task
         // (tenant_id, company_ref_id, factory_ref_id, domain_code, rwlx, wlbm, rqpc, yskw, mdkw)
-        if (await IsDuplicateAsync(tenantId, dto, null))
+        // 组织维度用后端解析值(与落库口径一致,不用 payload 组织值)。
+        if (await IsDuplicateAsync(tenantId, companyId, factoryId, dto, null))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "相同工厂域下该物料任务指派已存在");
 
         var entity = new AdoS0TaskAssignment
         {
             TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
+            // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
+            CompanyRefId = companyId,
+            FactoryRefId = factoryId,
             DomainCode = dto.DomainCode ?? string.Empty,
             Tcrq = dto.Tcrq,
             Sqr = dto.Sqr,
@@ -109,12 +114,13 @@ public class AdoS0TaskAssignmentsController : ControllerBase
         var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await IsDuplicateAsync(tenantId, dto, id))
+        // 唯一性预检的组织维度用既有记录原值(组织 pin 原值,不接受 payload 迁移工厂)。
+        if (await IsDuplicateAsync(tenantId, entity.CompanyRefId, entity.FactoryRefId, dto, id))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "相同工厂域下该物料任务指派已存在");
 
         // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
+        // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
+        // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量(存量迁移属 B-2,另批处理)。
         entity.DomainCode = dto.DomainCode ?? string.Empty;
         entity.Tcrq = dto.Tcrq;
         entity.Sqr = dto.Sqr;
@@ -150,13 +156,13 @@ public class AdoS0TaskAssignmentsController : ControllerBase
     /// (tenant_id, company_ref_id, factory_ref_id, domain_code, rwlx, wlbm, rqpc, yskw, mdkw)。
     /// excludeId 非空时排除自身(更新场景)。
     /// </summary>
-    private async Task<bool> IsDuplicateAsync(long tenantId, AdoS0TaskAssignmentUpsertDto dto, long? excludeId)
+    private async Task<bool> IsDuplicateAsync(long tenantId, long companyRefId, long factoryRefId, AdoS0TaskAssignmentUpsertDto dto, long? excludeId)
     {
         var domainCode = dto.DomainCode ?? string.Empty;
         return await _rep.AsQueryable()
             .Where(x => x.TenantId == tenantId
-                && x.CompanyRefId == dto.CompanyRefId
-                && x.FactoryRefId == dto.FactoryRefId
+                && x.CompanyRefId == companyRefId
+                && x.FactoryRefId == factoryRefId
                 && x.DomainCode == domainCode
                 && x.Rwlx == dto.Rwlx
                 && x.Wlbm == dto.Wlbm

+ 12 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Warehouse/AdoS0WarehouseBarcodeDtos.cs

@@ -16,10 +16,14 @@ public class AdoS0BarcodeRuleQueryDto
 
 public class AdoS0BarcodeRuleUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(8)]
@@ -128,10 +132,14 @@ public class AdoS0LabelTypeQueryDto
 
 public class AdoS0LabelTypeUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(50)]

+ 24 - 8
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Warehouse/AdoS0WarehouseBasicDtos.cs

@@ -15,10 +15,14 @@ public class AdoS0DepartmentQueryDto
 
 public class AdoS0DepartmentUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(50)]
@@ -53,10 +57,14 @@ public class AdoS0EmployeeQueryDto
 
 public class AdoS0EmployeeUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(50)]
@@ -118,10 +126,14 @@ public class AdoS0CostCtrQueryDto
 
 public class AdoS0CostCtrUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(50)]
@@ -158,10 +170,14 @@ public class AdoS0LocationQueryDto
 
 public class AdoS0LocationUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(50)]

+ 12 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Warehouse/AdoS0WarehouseNbrDtos.cs

@@ -17,10 +17,14 @@ public class AdoS0NbrTypeQueryDto
 
 public class AdoS0NbrTypeUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(8)]
@@ -57,10 +61,14 @@ public class AdoS0NbrControlQueryDto
 
 public class AdoS0NbrControlUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(50)]

+ 18 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Warehouse/AdoS0WarehouseOtherDtos.cs

@@ -16,10 +16,14 @@ public class AdoS0ItemPackQueryDto
 
 public class AdoS0ItemPackUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(50)]
@@ -69,10 +73,14 @@ public class AdoS0EmpWorkDutyQueryDto
 
 public class AdoS0EmpWorkDutyUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(50)]
@@ -121,10 +129,14 @@ public class AdoS0TaskAssignmentQueryDto
 
 public class AdoS0TaskAssignmentUpsertDto
 {
-    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    /// <summary>
+    /// 公司。**服务端忽略**:新增由 AdoS0OrgScope.ResolveAsync(tenantId) 在当前租户内解析后强制 stamp,
+    /// 编辑 pin 既有原值。保留字段仅为 payload 向后兼容,不做校验——原 [Range(1, long.MaxValue)]
+    /// 是失效前置门:值不参与落库,却会在模型绑定阶段把"租户未配置机构"误报成"公司不能为空",故移除。
+    /// </summary>
     public long CompanyRefId { get; set; }
 
-    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    /// <summary>工厂。**服务端忽略**,同 <see cref="CompanyRefId"/>。</summary>
     public long FactoryRefId { get; set; }
 
     [MaxLength(50)]

+ 96 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AdoS0OrgScope.cs

@@ -0,0 +1,96 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Admin.NET.Plugin.AiDOP.Infrastructure;
+
+/// <summary>
+/// S0 组织归属(company_ref_id / factory_ref_id)的服务端唯一裁决者。
+///
+/// 背景(S0-ORG-REFERENCE-MODEL-FIX-B1):
+///   2026-06-14 commit a18c8a6c5 在「单部署」假设下给 S0 仓储页写死了
+///   DEFAULT_COMPANY_REF_ID=1329900200001 / DEFAULT_FACTORY_REF_ID=1329900200002;
+///   其后 1.0.317.sql 把这两个 SysOrg 改挂到 Demo 租户 797403760988230,
+///   前端常量却从未同步 → 任何租户新增都写入别租户的 SysOrg.Id,形成跨租户组织引用。
+///
+/// 铁律:
+///   - 组织归属**只由服务端**在当前租户内解析,一律忽略 payload 的组织值;
+///     前端 payload 不可信(老前端 / 手工 API / 其它客户端都可能带错值)。
+///   - tenantId 必须来自 <see cref="AdoS0TenantScope.TryResolveRequired"/>(JWT claim),
+///     绝不接受前端传入的 tenantId。
+///   - **绝不跨租户查找**:候选集一律先按 TenantId 收窄。
+///   - **绝不新增组织**:解析不出就返回 (0,0),由调用方转 400 业务错误,不自动建公司/工厂。
+///
+/// 本实现自 <c>AdoS0ProcessFlowCardsController.ResolveDefaultOrgAsync</c> 原样抽取
+/// (该实现已实测产出 8/8 SAME-TENANT),语义保持不变,避免多份重复 resolver 漂移。
+/// </summary>
+public static class AdoS0OrgScope
+{
+    /// <summary>租户内优先匹配的公司编码(仅作优先条件,取不到即按机构类型/根节点在本租户内兜底)。</summary>
+    private const string PreferredCompanyCode = "S0-C001";
+
+    /// <summary>租户内优先匹配的工厂编码(同上,绝不跨租户查找)。</summary>
+    private const string PreferredFactoryCode = "S0-F001";
+
+    /// <summary>机构类型-公司(SysOrg.Type 数据字典值)。</summary>
+    private const string CompanyOrgType = "201";
+
+    /// <summary>
+    /// 机构类型-工厂。
+    /// ⚠️ S0 legacy compatibility: Type=501 used as factory.
+    /// 平台数据字典 org_type 中 501 的官方语义是「组」(101=集团 / 201=公司 / 301=部门 / 501=组),
+    /// S0 自 S0OrgSeedData 起单方面把 501 复用为「工厂」。当前全库 Type=501 的组织均为 AiDOP
+    /// 自建工厂节点,无真实「组」,故保持兼容不重构;后续若出现真实「组」需另开批次消歧。
+    /// </summary>
+    private const string FactoryOrgType = "501";
+
+    /// <summary>
+    /// 在**当前租户内**解析 S0 默认公司/工厂组织。
+    ///
+    /// 优先级——公司:
+    ///   C1 本租户 Code=S0-C001 → C3 本租户根节点(Pid=0)且 Type=201
+    ///   → C2 本租户任意 Type=201 → C4 本租户任意根节点(Pid=0)
+    /// 优先级——工厂:
+    ///   F1 本租户 Code=S0-F001 且父级为已解析公司 → F2 本租户 Type=501 且父级为已解析公司
+    ///   → F3 本租户任意 Type=501 → F4 回落到公司自身(兼容未建工厂节点的租户,不新增组织)。
+    ///
+    /// 确定性:一次取本租户机构并按 (OrderNo, Id) 稳定排序,各级用 FirstOrDefault 在有序集合上取首个,
+    ///   避免多候选时结果随执行计划漂移。
+    ///
+    /// 无解:返回 (0, 0),由调用方转 400 业务错误,**不抛异常**——
+    ///   AdoS0ExceptionFilter 会把 /api/s0/* 上的未处理异常统一映射为 500 S01999,
+    ///   抛异常会把「租户未配置机构」这类业务态误报成系统故障。
+    /// </summary>
+    /// <param name="db">SqlSugar 上下文(调用方一般传 <c>_rep.Context</c>,无需额外注入 SysOrg 仓储)。</param>
+    /// <param name="tenantId">AdoS0TenantScope 解析出的可信租户(JWT claim),不接受 payload 传入。</param>
+    public static async Task<(long CompanyId, long FactoryId)> ResolveAsync(ISqlSugarClient db, long tenantId)
+    {
+        if (tenantId <= 0) return (0, 0);
+
+        // 单次取数 + 内存分级,避免逐级往返;排序在 SQL 侧一次确定,后续各级取首个即稳定。
+        var orgs = await db.Queryable<SysOrg>()
+            .Where(x => x.TenantId == tenantId)
+            .OrderBy(x => new { x.OrderNo, x.Id })
+            .Select(x => new { x.Id, x.Code, x.Type, x.Pid })
+            .ToListAsync();
+        if (orgs.Count == 0) return (0, 0);
+
+        var company =
+            orgs.FirstOrDefault(o => o.Code == PreferredCompanyCode)                    // C1
+            ?? orgs.FirstOrDefault(o => o.Pid == 0 && o.Type == CompanyOrgType)         // C3
+            ?? orgs.FirstOrDefault(o => o.Type == CompanyOrgType)                       // C2
+            ?? orgs.FirstOrDefault(o => o.Pid == 0);                                    // C4
+        if (company == null) return (0, 0);
+
+        var factory =
+            orgs.FirstOrDefault(o => o.Code == PreferredFactoryCode && o.Pid == company.Id)  // F1
+            ?? orgs.FirstOrDefault(o => o.Type == FactoryOrgType && o.Pid == company.Id)     // F2
+            ?? orgs.FirstOrDefault(o => o.Type == FactoryOrgType);                           // F3
+
+        return (company.Id, factory?.Id ?? company.Id);                                      // F4
+    }
+
+    /// <summary>
+    /// 解析失败时的统一业务错误(各 Controller 复用同一文案,便于前端与运维识别)。
+    /// </summary>
+    public static IActionResult NotConfiguredError(string entityLabel)
+        => AdoS0ApiErrors.InvalidRequest($"当前租户未配置机构,无法确定{entityLabel}的公司/工厂归属,请先在机构管理中维护");
+}