修复库房地址受控展示

为平台库房管理列表和详情增加专用响应处理,在保留全局敏感字段保护的前提下恢复库房自身地址。未填写地址时显示‘未填写’,创建和编辑继续复用原有白名单写入逻辑,不修改数据库及历史数据。补充回归测试、操作日志和项目文档。
This commit is contained in:
czl231
2026-08-13 02:25:42 +08:00
parent 27733b313e
commit 2252a9c2a0
6 changed files with 206 additions and 2 deletions

View File

@@ -0,0 +1,71 @@
// 功能描述:提供平台总后台库房列表与详情,并受控恢复库房自身地址。
// 版本v1.0
package product
import (
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
)
// ListProductWarehouse 查询库房分页列表,并向已鉴权的平台管理端恢复库房地址。
func ListProductWarehouse(ctx *gin.Context) {
page, size := common.PageSize(ctx)
var list []models.ProductWarehouse
var total int64
query := common.ApplyKeywordFilter(ctx, common.ActiveRecords(impl.DBService.Model(&models.ProductWarehouse{})), &models.ProductWarehouse{})
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
if err := query.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
response, err := common.PublicResourceResponse(list)
if err != nil {
infra.Response.Error(ctx, err)
return
}
// 先执行全局敏感字段保护,再仅为库房管理资源恢复库房自身地址。
protected := common.ProtectPreciseLocation(ctx, &models.ProductWarehouse{}, response)
infra.Response.Success(ctx, gin.H{"total": total, "list": restoreProductWarehouseAddresses(protected, list)})
}
// GetProductWarehouse 查询单个库房,并返回详情与编辑表单所需的库房地址。
func GetProductWarehouse(ctx *gin.Context) {
var warehouse models.ProductWarehouse
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&warehouse).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
response, err := common.PublicResourceResponse(warehouse)
if err != nil {
infra.Response.Error(ctx, err)
return
}
protected := common.ProtectPreciseLocation(ctx, &models.ProductWarehouse{}, response)
infra.Response.Success(ctx, restoreProductWarehouseAddresses(protected, []models.ProductWarehouse{warehouse}))
}
// restoreProductWarehouseAddresses 将已鉴权库房记录的地址恢复到安全响应中。
func restoreProductWarehouseAddresses(response any, warehouses []models.ProductWarehouse) any {
switch data := response.(type) {
case []any:
for index, item := range data {
if index >= len(warehouses) {
break
}
if record, ok := item.(map[string]any); ok {
record["address"] = warehouses[index].Address
}
}
case map[string]any:
if len(warehouses) > 0 {
data["address"] = warehouses[0].Address
}
}
return response
}

View File

@@ -0,0 +1,49 @@
// 功能描述:验证库房管理响应仅恢复库房自身地址。
// 版本v1.0
package product
import (
"testing"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
)
// TestRestoreProductWarehouseAddresses 验证库房列表按原顺序恢复地址。
func TestRestoreProductWarehouseAddresses(t *testing.T) {
response := []any{
map[string]any{"identity": "warehouse-1"},
map[string]any{"identity": "warehouse-2"},
}
warehouses := []models.ProductWarehouse{
{Address: "海口市龙华区库房路 1 号"},
{Address: "三亚市吉阳区库房路 2 号"},
}
restored := restoreProductWarehouseAddresses(response, warehouses).([]any)
for index, warehouse := range warehouses {
if restored[index].(map[string]any)["address"] != warehouse.Address {
t.Fatalf("第 %d 条库房地址未正确恢复:%#v", index+1, restored[index])
}
}
}
// TestRestoreProductWarehouseAddress 验证库房详情恢复编辑所需地址。
func TestRestoreProductWarehouseAddress(t *testing.T) {
response := map[string]any{"identity": "warehouse-1"}
warehouse := models.ProductWarehouse{Address: "海口市秀英区库房路 18 号"}
restored := restoreProductWarehouseAddresses(response, []models.ProductWarehouse{warehouse}).(map[string]any)
if restored["address"] != warehouse.Address {
t.Fatalf("库房详情地址未正确恢复:%#v", restored)
}
}
// TestRestoreProductWarehouseEmptyAddress 验证未填写地址时保留空值。
func TestRestoreProductWarehouseEmptyAddress(t *testing.T) {
response := map[string]any{"identity": "warehouse-1"}
restored := restoreProductWarehouseAddresses(response, []models.ProductWarehouse{{}}).(map[string]any)
if restored["address"] != "" {
t.Fatalf("未填写库房地址应保留空值:%#v", restored)
}
}

View File

@@ -119,7 +119,9 @@ func registerProductRoute(group *gin.RouterGroup) {
producer.DELETE("/:identity", product.DeleteProducerAccount)
registerRestrictedNoDeleteResource(group, "/product_type", &models.ProductType{}, []string{"code", "name"})
registerRestrictedNoDeleteResource(group, "/product_warehouse", &models.ProductWarehouse{}, []string{"code", "name", "address", "manager", "phone"})
warehouseFields := []string{"code", "name", "address", "manager", "phone"}
_, warehouseCreate, _, warehouseUpdate := common.ResourceHandlers(&models.ProductWarehouse{}, warehouseFields, warehouseFields)
registerNoDeleteResource(group, "/product_warehouse", product.ListProductWarehouse, warehouseCreate, product.GetProductWarehouse, warehouseUpdate, &models.ProductWarehouse{})
infoRelations := []common.ResourceRelation{
requiredRelation("producer_account_identity", "producer_account_id", &models.ProducerAccount{}),

View File

@@ -0,0 +1,43 @@
# 库房地址受控展示操作日志
操作时间2026-08-13
操作类型:修改
影响模块:平台总后台库房管理、平台管理 API
## 操作前状态
库房地址已保存于 `product_warehouse.address`,新建与编辑接口也允许写入地址,但通用敏感字段保护会删除列表和详情响应中的 `address`,导致页面统一显示 `-`
## 具体操作
1. 为库房管理增加专用列表与详情处理器。
2. 保留全局敏感字段保护,并在保护后仅恢复当前库房资源自身的地址。
3. 创建与编辑继续复用原有通用白名单写入逻辑。
4. 未填写地址时在页面显示“未填写”。
5. 新增列表、详情和空地址回归测试。
## 操作后状态
- 库房列表、详情和编辑页面可取得已保存地址。
- 未填写地址时显示“未填写”。
- 其他资源和公共接口继续执行原有地址保护。
- 数据库结构与历史数据均未修改。
## 代码变更
- `backend/api/internal/logic/platform/product/warehouse.go`:新增库房地址受控列表、详情和恢复逻辑。
- `backend/api/internal/logic/platform/product/warehouse_test.go`:新增地址恢复测试。
- `backend/api/internal/routers/platform.go`:库房 GET 路由改用专用处理器POST/PUT 保持通用处理器。
- `frontend/platform_admin/src/api/resources.ts`:配置库房地址空值文案。
## 验证结果
- `go test ./internal/logic/platform/product ./internal/routers`:通过。
- `npm.cmd run resource-pages:check`:通过,详情 46 类、新建 25 类、编辑 23 类。
- `npm.cmd run build`通过TypeScript 检查与 Vite 生产构建成功。
- 本地后端重新编译并重启,健康接口返回 `platform-api``ok`
- `git diff --check`:通过。
## 风险评估
完整库房地址仅在已鉴权的平台库房管理接口中恢复。全局敏感字段规则未放宽,数据库与其他终端不受影响。

View File

@@ -0,0 +1,39 @@
# 项目文档:库房地址受控展示 v1.0
## 1. 项目概述
本次修改解决平台总后台库房地址已保存但无法显示的问题。库房继续使用自身的 `product_warehouse.address` 字段,地址保持可选,不修改数据库结构或历史数据。
## 2. 目录结构说明
```text
platforms/
├── backend/api/internal/logic/platform/product/
│ ├── warehouse.go # 库房列表、详情及地址受控恢复
│ └── warehouse_test.go # 库房地址响应测试
├── backend/api/internal/routers/
│ └── platform.go # 平台库房管理路由
├── frontend/platform_admin/src/api/
│ └── resources.ts # 库房地址空值展示配置
└── docs/
├── 项目文档_库房地址受控展示_v1.0.md
└── 操作日志_库房地址受控展示_20260813.md
```
## 3. 核心文件说明
- `warehouse.go`:列表与详情先执行通用响应投影和敏感字段保护,再按当前查询记录恢复库房自身地址。
- `warehouse_test.go`:覆盖列表顺序、单条详情及空地址场景。
- `platform.go`GET 使用库房专用处理器;创建、更新和状态修改继续沿用通用资源能力。
- `resources.ts`:空地址显示“未填写”,编辑输入提示为“请输入地址”。
## 4. 变更记录
- 修复库房列表地址显示 `-`
- 支持详情和编辑页面读取已保存库房地址。
- 保持地址可选,并为未填写状态提供明确文案。
- 未新增依赖、数据库迁移或历史数据修改。
## 5. 维护指南
新增需要受控展示地址的管理资源时,应采用资源专用恢复逻辑,不得从全局敏感字段集合中移除 `address`。修改后至少运行平台产品逻辑测试、路由测试和平台前端构建。

View File

@@ -446,7 +446,7 @@ export const resources: ResourceUiDefinition[] = [
define('producer_account', '生产商管理', 'writable', [f('producer_code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('phone'), f('address', { emptyText: '未填写', placeholder: '请输入地址' }), f('username', { required: true }), f('password', { required: true }), f('display_name'), producerAdminRole(), f('remark')]),
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]),
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address', { emptyText: '未填写', placeholder: '请输入地址' }), f('manager'), f('phone')]),
define('product_info', '智能气阀', 'editable', [f('code', { required: true }), f('name', { required: true }), relation('producer_account_identity', '/producer_account', true), relation('product_type_identity', '/product_type', true), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('produced_at', { required: true })], 'list', [
{ name: '修改智能气阀状态', resource: '/product_info/:identity/lifecycle', method: 'PATCH', fields: [f('product_status', { required: true, type: 'select', options: [{ label: '待处理', value: 10 }, { label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '维修中', value: 31 }, { label: '已报废', value: 27 }] })] },
{ name: '变更智能气阀归属', resource: '/product_info/:identity', method: 'PUT', fields: [relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('action', { required: true, type: 'select', options: [{ label: '入库', value: 'warehouse' }, { label: '分配', value: 'assigned' }, { label: '归还', value: 'returned' }, { label: '人工调整', value: 'manual' }] }), f('reason', { required: true }), f('remark')] },