Эх сурвалжийг харах

fix(s0): make shelf list read-only + lock DomainCode on edit

货架列表(LocationShelfList)彻底只读:移除新增/编辑/删除入口与弹窗,仅保留查询/分页/展示;
s0LocationShelvesApi 移除 create/update/delete,仅留 list/get。
后端 location-shelves 控制器下线 POST/PUT/DELETE,仅保留 GET 查询(写入统一走库位维护主从保存)。
库位维护(LocationList)编辑态锁定域编码(:disabled=editingId),后端 Update 在赋新值前强校验 DomainCode
不可修改,杜绝 FULL Replace 按新域删除导致旧域货架 orphan。不改 S5/WMS 数据中台链路、不改表结构。

bump version Web 2.4.283 / server 1.0.329
YY968XX 3 өдөр өмнө
parent
commit
04de8bb07a

+ 1 - 1
Web/package.json

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

+ 1 - 17
Web/src/views/aidop/s0/api/s0WarehouseApi.ts

@@ -248,27 +248,11 @@ export interface S0LocationShelfRow {
 	updateTime?: string | null;
 }
 
-export interface S0LocationShelfUpsert {
-	companyRefId?: string;
-	factoryRefId?: string;
-	domainCode?: string;
-	location: string;
-	invShelf: string;
-	descr?: string;
-	area?: string;
-	createUser?: string;
-	updateUser?: string;
-}
-
+// 货架为只读查询:新增/修改/删除统一走库位维护主从保存(s0LocationsApi),此处仅保留 list/get。
 export const s0LocationShelvesApi = {
 	list: (params: Record<string, unknown>) =>
 		service.get<Paged<S0LocationShelfRow>>('/api/s0/warehouse/location-shelves', { params }).then(unwrap),
 	get: (id: number) => service.get<S0LocationShelfRow>(`/api/s0/warehouse/location-shelves/${id}`).then(unwrap),
-	create: (body: S0LocationShelfUpsert) =>
-		service.post<S0LocationShelfRow>('/api/s0/warehouse/location-shelves', body).then(unwrap),
-	update: (id: number, body: S0LocationShelfUpsert) =>
-		service.put<S0LocationShelfRow>(`/api/s0/warehouse/location-shelves/${id}`, body).then(unwrap),
-	delete: (id: number) => service.delete(`/api/s0/warehouse/location-shelves/${id}`).then(unwrap),
 };
 
 // ========== 条码规则 ==========

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

@@ -53,7 +53,7 @@
 					<el-col :span="12"><el-form-item label="货主/保管方"><el-input v-model="form.storer" /></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="库位类型"><el-input v-model="form.typed" placeholder="如 Supp" /></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="物理地址"><el-input v-model="form.physicalAddress" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="域编码"><el-input v-model="form.domainCode" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="域编码"><el-input v-model="form.domainCode" :disabled="!!editingId" /></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="启用"><el-switch v-model="form.isActive" /></el-form-item></el-col>
 				</el-row>
 			</el-form>

+ 8 - 100
Web/src/views/aidop/s0/warehouse/LocationShelfList.vue

@@ -1,5 +1,5 @@
 <template>
-	<AidopDemoShell :title="pageTitle" subtitle="S0 / 仓储建模 / 货架列表">
+	<AidopDemoShell :title="pageTitle" subtitle="S0 / 仓储建模 / 货架列表(只读)">
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
 			<el-form-item label="关键字">
 				<el-input v-model="query.keyword" placeholder="货架编码/库位编码" clearable style="width: 200px" />
@@ -10,23 +10,19 @@
 			<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%" max-height="calc(100vh - 260px)">
+		<el-alert type="info" :closable="false" show-icon class="mb12"
+			title="货架为只读展示。新增/修改/删除货架请在「库位维护」的货架明细中完成。" />
+
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" max-height="calc(100vh - 300px)">
 			<el-table-column prop="kwhjName" label="库位:货架" width="160" show-overflow-tooltip />
 			<el-table-column prop="location" label="所属库位" width="130" show-overflow-tooltip />
 			<el-table-column prop="locationDescr" label="库位说明" min-width="150" show-overflow-tooltip />
 			<el-table-column prop="invShelf" label="货架编码" width="130" show-overflow-tooltip />
 			<el-table-column prop="descr" label="货架说明" min-width="150" show-overflow-tooltip />
 			<el-table-column prop="area" label="区域" width="100" show-overflow-tooltip />
-			<el-table-column label="操作" width="160" 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>
-				</template>
-			</el-table-column>
 		</el-table>
 
 		<div class="pager">
@@ -34,40 +30,14 @@
 				:page-sizes="[20, 50, 100]" layout="total, sizes, prev, pager, next"
 				@current-change="loadList" @size-change="loadList" />
 		</div>
-
-		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close @closed="resetForm">
-			<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
-				<el-form-item v-if="false" label="公司" prop="companyRefId">
-					<el-select v-model="form.companyRefId" clearable filterable placeholder="请选择公司" style="width: 100%">
-						<el-option v-for="item in companyOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
-					</el-select>
-				</el-form-item>
-				<el-form-item v-if="false" label="工厂" prop="factoryRefId">
-					<el-select v-model="form.factoryRefId" clearable filterable placeholder="请选择工厂" style="width: 100%" :disabled="!form.companyRefId">
-						<el-option v-for="item in formFactoryOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
-					</el-select>
-				</el-form-item>
-				<el-form-item label="所属库位" prop="location"><el-input v-model="form.location" /></el-form-item>
-				<el-form-item label="货架编码" prop="invShelf"><el-input v-model="form.invShelf" :disabled="!!editingId" /></el-form-item>
-				<el-form-item label="货架说明"><el-input v-model="form.descr" /></el-form-item>
-				<el-form-item label="区域"><el-input v-model="form.area" /></el-form-item>
-				<el-form-item label="域编码"><el-input v-model="form.domainCode" /></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>
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopS0WhLocationShelf">
-import { computed, onMounted, reactive, ref, watch } from 'vue';
+import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
-import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
-import { s0LocationShelvesApi, type S0LocationShelfRow, type S0LocationShelfUpsert } from '../api/s0WarehouseApi';
-import { loadOrgList, type OrgOption } from '../api/s0SalesApi';
+import { s0LocationShelvesApi, type S0LocationShelfRow } from '../api/s0WarehouseApi';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '货架列表');
@@ -75,43 +45,7 @@ const query = reactive({ keyword: '', domainCode: '', location: '', page: 1, pag
 const loading = ref(false);
 const rows = ref<S0LocationShelfRow[]>([]);
 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<S0LocationShelfUpsert>({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', location: '', invShelf: '', descr: '', area: '' });
-
-// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 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;
-}
-const companyOptions = ref<OrgOption[]>([]);
-const factoryOptions = ref<OrgOption[]>([]);
-const formFactoryOptions = computed(() => {
-	if (!form.companyRefId) return factoryOptions.value;
-	return factoryOptions.value.filter((item) => item.pid === form.companyRefId);
-});
-
-const rules: FormRules = {
-	location: [{ required: true, message: '请填写所属库位', trigger: 'blur' }],
-	invShelf: [{ required: true, message: '请填写货架编码', trigger: 'blur' }],
-};
 
-watch(
-	() => form.companyRefId,
-	() => {
-		if (!formFactoryOptions.value.some((item) => item.id === form.factoryRefId)) {
-			form.factoryRefId = undefined;
-		}
-	},
-);
 async function loadList() {
 	loading.value = true;
 	try {
@@ -120,34 +54,8 @@ async function loadList() {
 	} catch { rows.value = []; total.value = 0; } finally { loading.value = false; }
 }
 function resetQuery() { Object.assign(query, { keyword: '', domainCode: '', location: '', page: 1 }); void loadList(); }
-function resetForm() { editingId.value = null; Object.assign(form, { companyRefId: undefined, factoryRefId: undefined, domainCode: '', location: '', invShelf: '', descr: '', area: '' }); formRef.value?.clearValidate(); }
-function openCreate() { resetForm(); dialogTitle.value = '新增货架'; dialogVisible.value = true; }
-function openEdit(row: S0LocationShelfRow) {
-	resetForm(); editingId.value = row.id; dialogTitle.value = `编辑货架 ${row.kwhjName || row.invShelf}`;
-	Object.assign(form, { companyRefId: row.companyRefId, factoryRefId: row.factoryRefId, domainCode: row.domainCode ?? '', location: row.location, invShelf: row.invShelf, descr: row.descr ?? '', area: row.area ?? '' });
-	dialogVisible.value = true;
-}
-async function submitForm() {
-	await formRef.value?.validate();
-	applyDefaultOrg();
-	saving.value = true;
-	try {
-		if (editingId.value) { await s0LocationShelvesApi.update(editingId.value, { ...form }); ElMessage.success('已保存'); }
-		else { await s0LocationShelvesApi.create({ ...form }); ElMessage.success('已创建'); }
-		dialogVisible.value = false; await loadList();
-	} finally { saving.value = false; }
-}
-function onDelete(row: S0LocationShelfRow) {
-	ElMessageBox.confirm(`确定删除货架「${row.kwhjName || row.invShelf}」?`, '确认', { type: 'warning' })
-		.then(async () => { await s0LocationShelvesApi.delete(row.id); ElMessage.success('已删除'); await loadList(); }).catch(() => {});
-}
-async function loadOrgOptions() {
-	const [companies, factories] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
-	companyOptions.value = companies.filter((item) => item.id && item.id !== '0');
-	factoryOptions.value = factories.filter((item) => item.id && item.id !== '0');
-}
 
-onMounted(() => { void loadList(); void loadOrgOptions(); });
+onMounted(() => { void loadList(); });
 </script>
 
 <style scoped lang="scss">

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 閫氱敤鏉冮檺寮€鍙戝钩鍙?/Description>
-    <AssemblyVersion>1.0.328</AssemblyVersion>
-    <FileVersion>1.0.328</FileVersion>
-    <Version>1.0.328</Version>
+    <AssemblyVersion>1.0.329</AssemblyVersion>
+    <FileVersion>1.0.329</FileVersion>
+    <Version>1.0.329</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 6 - 78
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LocationShelvesController.cs

@@ -5,8 +5,9 @@ using Admin.NET.Plugin.AiDOP.Infrastructure;
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
 /// <summary>
-/// S0 货架主数据(LocationShelfMaster 语义)。独立货架页 CRUD。
-/// 多租户隔离:所有读写显式限定当前请求租户;父库位存在性按 tenant+domain+location 校验;不依赖全局 AOP。
+/// S0 货架主数据(LocationShelfMaster 语义)。独立货架页只读查询。
+/// 货架的新增/修改/删除统一走「库位维护」主从保存(AdoS0LocationsController),本控制器不再暴露写入口。
+/// 多租户隔离:所有读取显式限定当前请求租户;不依赖全局 AOP。
 /// </summary>
 [ApiController]
 [Route("api/s0/warehouse/location-shelves")]
@@ -15,16 +16,13 @@ public class AdoS0LocationShelvesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0LocationShelfMaster> _rep;
     private readonly SqlSugarRepository<AdoS0LocationMaster> _locRep;
-    private readonly AdoS0ReferenceChecker _refChecker;
 
     public AdoS0LocationShelvesController(
         SqlSugarRepository<AdoS0LocationShelfMaster> rep,
-        SqlSugarRepository<AdoS0LocationMaster> locRep,
-        AdoS0ReferenceChecker refChecker)
+        SqlSugarRepository<AdoS0LocationMaster> locRep)
     {
         _rep = rep;
         _locRep = locRep;
-        _refChecker = refChecker;
     }
 
     [HttpGet]
@@ -66,78 +64,8 @@ public class AdoS0LocationShelvesController : ControllerBase
         return Ok(item);
     }
 
-    [HttpPost]
-    public async Task<IActionResult> CreateAsync([FromBody] AdoS0LocationShelfUpsertDto dto)
-    {
-        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
-
-        // 唯一性:当前租户 + domain + location + invShelf
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.DomainCode == dto.DomainCode && x.Location == dto.Location && x.InvShelf == dto.InvShelf))
-            return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "货架编码已存在");
-
-        // 父库位存在性:必须为当前租户下同 domain+location 的库位(不得为他租户库位建货架)
-        if (!await _refChecker.LocationExistsInTenantAsync(tenantId, dto.DomainCode, dto.Location))
-            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
-                $"库位编码 '{dto.Location}' 不存在于当前租户库位主数据");
-
-        var entity = new AdoS0LocationShelfMaster
-        {
-            TenantId = tenantId,
-            CompanyRefId = dto.CompanyRefId,
-            FactoryRefId = dto.FactoryRefId,
-            DomainCode = dto.DomainCode ?? string.Empty,
-            Location = dto.Location,
-            InvShelf = dto.InvShelf,
-            Descr = dto.Descr,
-            Area = dto.Area,
-            CreateUser = dto.CreateUser,
-            CreateTime = DateTime.Now
-        };
-
-        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
-        return Ok(entity);
-    }
-
-    [HttpPut("{id:long}")]
-    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0LocationShelfUpsertDto dto)
-    {
-        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();
-
-        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.DomainCode == dto.DomainCode && x.Location == dto.Location && x.InvShelf == dto.InvShelf))
-            return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "货架编码已存在");
-
-        if (!await _refChecker.LocationExistsInTenantAsync(tenantId, dto.DomainCode, dto.Location))
-            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
-                $"库位编码 '{dto.Location}' 不存在于当前租户库位主数据");
-
-        // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
-        entity.CompanyRefId = dto.CompanyRefId;
-        entity.FactoryRefId = dto.FactoryRefId;
-        entity.DomainCode = dto.DomainCode ?? string.Empty;
-        entity.Location = dto.Location;
-        entity.InvShelf = dto.InvShelf;
-        entity.Descr = dto.Descr;
-        entity.Area = dto.Area;
-        entity.UpdateUser = dto.UpdateUser;
-        entity.UpdateTime = DateTime.Now;
-
-        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
-        return Ok(entity);
-    }
-
-    [HttpDelete("{id:long}")]
-    public async Task<IActionResult> DeleteAsync(long id)
-    {
-        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
-
-        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
-        if (item == null) return NotFound();
-        await _rep.DeleteAsync(item);
-        return Ok(new { message = "删除成功" });
-    }
+    // 写入口(POST/PUT/DELETE)已下线:S0 货架的新增/修改/删除统一走「库位维护」主从保存
+    // (AdoS0LocationsController 的 locations 端点,随库位主表 FULL Replace)。本控制器仅保留 GET 查询。
 
     /// <summary>
     /// 补显库位说明 + 「库位:货架」拼接。关联 LocationMaster 时限定当前租户,避免同编码库位跨租户补错描述。

+ 5 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LocationsController.cs

@@ -199,6 +199,11 @@ public class AdoS0LocationsController : ControllerBase
         if (!string.Equals(entity.Location, dto.Location?.Trim(), StringComparison.Ordinal))
             return AdoS0ApiErrors.InvalidRequest("库位编码不可修改");
 
+        // 域编码不可修改:与库位编码同口径。必须在 entity.DomainCode 被赋新值之前判断,
+        // 否则 FULL Replace 会按新 DomainCode 删除货架,导致旧 DomainCode 下货架 orphan。
+        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))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "库位编码已存在");