|
|
@@ -1,6 +1,7 @@
|
|
|
import { ref } from 'vue';
|
|
|
import { useUserInfo } from '/@/stores/userInfo';
|
|
|
import { loadOrgList, type OrgOption } from '../api/s0SalesApi';
|
|
|
+import { s0DepartmentsApi } from '../api/s0WarehouseApi';
|
|
|
|
|
|
/**
|
|
|
* S0 制造页共用:公司(201) / 工厂(501) 下拉
|
|
|
@@ -8,22 +9,91 @@ import { loadOrgList, type OrgOption } from '../api/s0SalesApi';
|
|
|
* 组织 ID 全链路为 string 语义(雪花 ID 超 JS number 精度,禁止转 number)。
|
|
|
* 未选状态统一为 undefined / null / '',不使用 0。
|
|
|
*/
|
|
|
+/**
|
|
|
+ * 公司(201)/工厂(501) 选项的统一加载器(含单-org 兼容回落)。
|
|
|
+ *
|
|
|
+ * 契约:
|
|
|
+ * typed 列表非空 → 直接返回 typed 列表(多组织租户,行为与历史一致)
|
|
|
+ * typed 为空 且 本租户仅 1 个 org → 该 org 同时作为公司与工厂候选(singleOrgMode=true)
|
|
|
+ * typed 为空 且 本租户多个 org → 返回空列表,暴露歧义,**不臆造** Company/Factory
|
|
|
+ *
|
|
|
+ * 回落**只**适用于单-org 租户;绝不把多-org 租户的任意 org 冒充公司/工厂。
|
|
|
+ */
|
|
|
+export interface S0OrgSelectorOptions {
|
|
|
+ companies: OrgOption[];
|
|
|
+ factories: OrgOption[];
|
|
|
+ singleOrgMode: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 对**已加载**的 typed 组织列表施加单-org 兼容回落。
|
|
|
+ * 页面可保留自己的 Promise.all 批量取数,取完后调用本函数做后处理,避免重复请求。
|
|
|
+ */
|
|
|
+export async function applySingleOrgFallback(c: OrgOption[], f: OrgOption[]): Promise<S0OrgSelectorOptions> {
|
|
|
+ const companies = c.filter((x) => x.id && x.id !== '0');
|
|
|
+ const factories = f.filter((x) => x.id && x.id !== '0');
|
|
|
+ if (companies.length > 0 || factories.length > 0) {
|
|
|
+ return { companies, factories, singleOrgMode: false };
|
|
|
+ }
|
|
|
+ const all = (await loadOrgList()).filter((x) => x.id && x.id !== '0');
|
|
|
+ if (all.length === 1) {
|
|
|
+ return { companies: all, factories: all, singleOrgMode: true };
|
|
|
+ }
|
|
|
+ // 多-org 租户却取不到 201/501:暴露歧义,不臆造。
|
|
|
+ return { companies: [], factories: [], singleOrgMode: false };
|
|
|
+}
|
|
|
+
|
|
|
+export async function loadCompanyFactoryOptions(): Promise<S0OrgSelectorOptions> {
|
|
|
+ const [c, f] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
|
|
|
+ return applySingleOrgFallback(c, f);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 表单工厂候选:正常按 pid 归属过滤;单-org 兼容模式下额外允许 self-match。
|
|
|
+ * self-match 只在 singleOrgMode 为真时生效,不做全局放宽。
|
|
|
+ */
|
|
|
+export function filterFactoriesForCompany(
|
|
|
+ factories: OrgOption[],
|
|
|
+ companyId: string | undefined | null,
|
|
|
+ singleOrgMode: boolean
|
|
|
+): OrgOption[] {
|
|
|
+ if (companyId == null || companyId === '') return factories;
|
|
|
+ return factories.filter((x) => x.pid === companyId || (singleOrgMode && x.id === companyId));
|
|
|
+}
|
|
|
+
|
|
|
export function useS0MfgOrgScope() {
|
|
|
const companyOptions = ref<OrgOption[]>([]);
|
|
|
const factoryOptions = ref<OrgOption[]>([]);
|
|
|
+ /** 单-org 兼容模式:本租户 SysOrg 只有一个节点,它同时充当公司与工厂 selector 值。 */
|
|
|
+ const singleOrgMode = ref(false);
|
|
|
|
|
|
+ /**
|
|
|
+ * 公司(201)/工厂(501) 下拉。
|
|
|
+ *
|
|
|
+ * 单-org 兼容回落(S0-WORKCENTER 之后的 org selector 契约):
|
|
|
+ * typed 列表非空 → 直接使用 typed 列表(正常多组织租户,行为不变)
|
|
|
+ * typed 为空 且 本租户仅 1 个 org → 该 org 同时作为公司与工厂候选(单-org 兼容模式)
|
|
|
+ * typed 为空 且 本租户多个 org → 不臆造,返回空列表并暴露歧义(沿用既有 UI 契约)
|
|
|
+ *
|
|
|
+ * 边界:回落**只**适用于单-org 租户;绝不允许把多-org 租户的任意 org 冒充公司/工厂。
|
|
|
+ */
|
|
|
async function loadOrgs() {
|
|
|
- const [c, f] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
|
|
|
- companyOptions.value = c;
|
|
|
- factoryOptions.value = f;
|
|
|
+ const r = await loadCompanyFactoryOptions();
|
|
|
+ companyOptions.value = r.companies;
|
|
|
+ factoryOptions.value = r.factories;
|
|
|
+ singleOrgMode.value = r.singleOrgMode;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 正常模式:工厂必须挂在所选公司下(factory.pid === companyId)。
|
|
|
+ * 单-org 兼容模式:同一个 org 既是公司又是工厂,额外允许 self-match(factory.id === companyId)。
|
|
|
+ * self-match 规则仅在 singleOrgMode 下生效,不做全局放宽。
|
|
|
+ */
|
|
|
function factoriesForCompany(cid: string | undefined | null) {
|
|
|
- if (cid == null || cid === '') return factoryOptions.value;
|
|
|
- return factoryOptions.value.filter((x) => x.pid === cid);
|
|
|
+ return filterFactoriesForCompany(factoryOptions.value, cid, singleOrgMode.value);
|
|
|
}
|
|
|
|
|
|
- return { companyOptions, factoryOptions, loadOrgs, factoriesForCompany };
|
|
|
+ return { companyOptions, factoryOptions, singleOrgMode, loadOrgs, factoriesForCompany };
|
|
|
}
|
|
|
|
|
|
// ── S0 公司/工厂后台归属治理(前台无感化 + 归属保值)────────────────────────────
|
|
|
@@ -41,19 +111,19 @@ export function useS0MfgOrgScope() {
|
|
|
export const S0_DEFAULT_COMPANY_CODE = 'S0-C001';
|
|
|
export const S0_DEFAULT_FACTORY_CODE = 'S0-F001';
|
|
|
|
|
|
-/**
|
|
|
- * 兜底工厂域编码(当前版本临时口径)。
|
|
|
- *
|
|
|
- * 未铺 S0-C001 / S0-F001 种子组织的租户取不到工厂,domainCode 便无来源;而部分后端
|
|
|
- * DTO(AdoS0LineMasterUpsertDto / AdoS0StdOpMasterUpsertDto 的 Domain)为 [Required]
|
|
|
- * 非空,域编码为空会被直接拒绝,页面无法保存。
|
|
|
- *
|
|
|
- * ⚠️ 已知不一致:这类租户的历史数据域编码并不统一(实测 UATTEST_CHL:
|
|
|
- * LineMaster / StdOpMaster 为 '8010',CustMaster 为 '2410'),前端无可靠依据按页面区分,
|
|
|
- * 故当前版本统一取 '2410'。新记录与部分历史记录的域编码会不同,属已知取舍。
|
|
|
- * 后续若为租户补齐真实工厂组织,本兜底会自动让位于工厂 code(见 getDefaultS0OrgScope)。
|
|
|
- */
|
|
|
-export const S0_FALLBACK_DOMAIN_CODE = '2410';
|
|
|
+// ── domainCode 与 Factory.Code 解耦(S0-FULL-DEMO-COVERAGE-CLOSURE-1 STEP 1)──────
|
|
|
+//
|
|
|
+// 旧口径:domainCode = 选中工厂的 Code(getDefaultS0OrgScope / syncDomainCodeFromFactory),
|
|
|
+// 并在取不到工厂时回落到硬编码常量 '2410'。该耦合导致:
|
|
|
+// ① 组织选择会改写业务数据域;② 硬编码常量与各表既有域(2410/8010/8011/WB8A)冲突。
|
|
|
+//
|
|
|
+// 新契约:Company/Factory 只负责**组织归属**,Domain 只负责**业务数据域**,两者不再互相推导。
|
|
|
+// CREATE:A 目标表有效业务数据只有一个非空 Domain → 用该 Domain
|
|
|
+// B 目标表存在多个 Domain → 用租户域锚 DepartmentMaster.Domain
|
|
|
+// C 目标表为空 → 用租户域锚
|
|
|
+// D 两者都解析不到 → 保持空,由后端 required 校验暴露配置缺失
|
|
|
+// EDIT :永远保留记录自身的 Domain,不重算(见 preserveOrgScopeForEdit)
|
|
|
+// 禁止:Factory.Code → Domain;禁止全局硬编码域;禁止取首行(rows[0])。
|
|
|
|
|
|
/** 组织归属三件套(string 语义;未定为 undefined)。 */
|
|
|
export interface S0OrgScope {
|
|
|
@@ -95,26 +165,64 @@ function getCurrentTenantOrgId(): string | undefined {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 从已加载组织列表按 code 求默认归属;取不到种子组织时回落到当前租户自身组织。
|
|
|
- * 两者都取不到才返回空对象(不脑补 id/domainCode)。
|
|
|
- * domainCode 口径:正常路径取工厂 code;兜底路径无工厂可取,用 S0_FALLBACK_DOMAIN_CODE。
|
|
|
+ * 从已加载组织列表按 code 求默认**组织归属**;取不到种子组织时回落到当前租户自身组织。
|
|
|
+ * 两者都取不到才返回空对象(不脑补 id)。
|
|
|
+ *
|
|
|
+ * ⚠️ 本函数**不再返回 domainCode** —— Domain 由 resolveCreateDomain 独立解析(见上方契约)。
|
|
|
*/
|
|
|
export function getDefaultS0OrgScope(companyOptions: OrgOption[], factoryOptions: OrgOption[]): S0OrgScope {
|
|
|
const company = companyOptions.find((item) => item.code === S0_DEFAULT_COMPANY_CODE);
|
|
|
if (!company) {
|
|
|
const tenantOrgId = getCurrentTenantOrgId();
|
|
|
- return tenantOrgId
|
|
|
- ? { companyRefId: tenantOrgId, factoryRefId: tenantOrgId, domainCode: S0_FALLBACK_DOMAIN_CODE }
|
|
|
- : {};
|
|
|
+ return tenantOrgId ? { companyRefId: tenantOrgId, factoryRefId: tenantOrgId } : {};
|
|
|
}
|
|
|
const factory = factoryOptions.find((item) => item.code === S0_DEFAULT_FACTORY_CODE && item.pid === company.id);
|
|
|
return {
|
|
|
companyRefId: company.id,
|
|
|
factoryRefId: factory?.id,
|
|
|
- domainCode: factory?.code ?? undefined,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 租户域锚:本租户 DepartmentMaster 的 Domain。
|
|
|
+ *
|
|
|
+ * 与服务端 CLOSURE-1 迁移脚本的 `@dom := (SELECT Domain FROM DepartmentMaster WHERE tenant_id=@dst LIMIT 1)`
|
|
|
+ * 同源,是本租户已确立的业务域锚点;数据派生,非硬编码。
|
|
|
+ */
|
|
|
+export async function loadTenantDomainAnchor(): Promise<string | undefined> {
|
|
|
+ try {
|
|
|
+ const data = await s0DepartmentsApi.list({ page: 1, pageSize: 1 });
|
|
|
+ const d = (data?.list ?? [])[0]?.domainCode;
|
|
|
+ const v = d == null ? '' : String(d).trim();
|
|
|
+ return v === '' ? undefined : v;
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('[s0] loadTenantDomainAnchor failed', e);
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * CREATE 时的 Domain 解析(契约 A/B/C/D)。
|
|
|
+ *
|
|
|
+ * @param domains 目标表**已完整观测到**的 domainCode 集合(见 seenWholeTable)
|
|
|
+ * @param seenWholeTable 是否已看到该表全部业务行(total <= 已加载行数)。
|
|
|
+ * 只有看全了才能断言「唯一域」,否则分页可能只覆盖到其中一个域,
|
|
|
+ * 会把多域表误判成单域表(例:EmployeeMaster 首页 20 行全是 8010,
|
|
|
+ * 但整表还有 3 行 WB8A)——这正是禁止使用 rows[0] 的同一类风险。
|
|
|
+ * @param tenantAnchor 租户域锚(DepartmentMaster.Domain)
|
|
|
+ */
|
|
|
+export function resolveCreateDomain(
|
|
|
+ domains: Array<string | null | undefined>,
|
|
|
+ seenWholeTable: boolean,
|
|
|
+ tenantAnchor?: string
|
|
|
+): string | undefined {
|
|
|
+ if (seenWholeTable) {
|
|
|
+ const distinct = Array.from(new Set(domains.map((d) => (d == null ? '' : String(d).trim())).filter((d) => d !== '')));
|
|
|
+ if (distinct.length === 1) return distinct[0]; // A:目标表唯一有效域
|
|
|
+ }
|
|
|
+ return tenantAnchor || undefined; // B / C:多域或空表 → 租户域锚;D:锚也没有 → undefined
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 新增:仅当归属字段为空时补默认,已有值不覆盖。就地写回 form。
|
|
|
*/
|
|
|
@@ -126,7 +234,7 @@ export function applyDefaultOrgScopeForCreate(
|
|
|
const d = getDefaultS0OrgScope(companyOptions, factoryOptions);
|
|
|
if (isEmptyScopeValue(form.companyRefId)) form.companyRefId = d.companyRefId;
|
|
|
if (isEmptyScopeValue(form.factoryRefId)) form.factoryRefId = d.factoryRefId;
|
|
|
- if (isEmptyScopeValue(form.domainCode) && d.domainCode != null) form.domainCode = d.domainCode;
|
|
|
+ // domainCode 不再由组织归属推导(去 Factory.Code 耦合);由各页 resolveCreateDomain 独立解析。
|
|
|
}
|
|
|
|
|
|
/** 接口返回记录里归属字段的只读形状(domainCode 可能为 null)。 */
|
|
|
@@ -160,7 +268,7 @@ export function normalizeOrgScopeForSubmit(
|
|
|
const d = getDefaultS0OrgScope(companyOptions, factoryOptions);
|
|
|
if (isEmptyScopeValue(form.companyRefId)) form.companyRefId = d.companyRefId;
|
|
|
if (isEmptyScopeValue(form.factoryRefId)) form.factoryRefId = d.factoryRefId;
|
|
|
- if (isEmptyScopeValue(form.domainCode) && d.domainCode != null) form.domainCode = d.domainCode;
|
|
|
+ // domainCode 不再由组织归属推导(去 Factory.Code 耦合);由各页 resolveCreateDomain 独立解析。
|
|
|
}
|
|
|
|
|
|
/**
|