From 2f5b934037ecc66941137fab858ed2eafc0a2345 Mon Sep 17 00:00:00 2001 From: czl231 <3286836406@qq.com> Date: Wed, 19 Aug 2026 22:07:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=B0=94=E7=AB=99=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=88=97=E8=A1=A8=E6=90=9C=E7=B4=A2=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/cmd/cli/main.go | 2 +- .../internal/logic/gas/resource_contract.go | 16 +++--- .../logic/gas/resource_contract_test.go | 20 +++++++ .../api/internal/logic/gas/resource_search.go | 51 ++++++++++++++++++ backend/api/internal/routers/gas.go | 2 + docs/06-气站管理系统需求.md | 4 ++ ...作日志_气站列表搜索能力显式化_20260819.md | 53 +++++++++++++++++++ .../scripts/check-backend-contract.mjs | 20 +++++++ .../src/contracts/gas-resources.json | 2 +- .../views/shared/use-resource-list-search.ts | 14 +++-- 10 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 backend/api/internal/logic/gas/resource_contract_test.go create mode 100644 backend/api/internal/logic/gas/resource_search.go create mode 100644 docs/操作日志_气站列表搜索能力显式化_20260819.md diff --git a/backend/api/cmd/cli/main.go b/backend/api/cmd/cli/main.go index 13189e8..1e91258 100644 --- a/backend/api/cmd/cli/main.go +++ b/backend/api/cmd/cli/main.go @@ -167,7 +167,7 @@ func writeGasResourceContract(output io.Writer) error { for _, item := range expected { contracts = append(contracts, contract{ Domain: item.Domain, Name: item.Name, Path: item.Path, - PageKind: item.PageKind, Mode: string(item.Mode), + PageKind: item.PageKind, Mode: string(item.Mode), SearchFields: item.SearchFields, }) } return json.NewEncoder(output).Encode(manifest{Resources: contracts, Routes: routes}) diff --git a/backend/api/internal/logic/gas/resource_contract.go b/backend/api/internal/logic/gas/resource_contract.go index 0de0099..b59f0fe 100644 --- a/backend/api/internal/logic/gas/resource_contract.go +++ b/backend/api/internal/logic/gas/resource_contract.go @@ -1,4 +1,6 @@ -package gas +package gas + +import "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" type ResourceMode string @@ -10,11 +12,12 @@ const ( ) type ResourceContract struct { - Domain string `json:"domain"` - Name string `json:"name"` - Path string `json:"path"` - PageKind string `json:"pageKind"` - Mode ResourceMode `json:"mode"` + Domain string `json:"domain"` + Name string `json:"name"` + Path string `json:"path"` + PageKind string `json:"pageKind"` + Mode ResourceMode `json:"mode"` + SearchFields []common.KeywordSearchField `json:"searchFields,omitempty"` } func ExpectedResources() []ResourceContract { @@ -39,6 +42,7 @@ func ExpectedResources() []ResourceContract { for _, item := range items { result = append(result, ResourceContract{ Domain: item.domain, Name: item.name, Path: "/" + item.name, PageKind: "list", Mode: item.mode, + SearchFields: resourceSearchFields(item.name), }) } return result diff --git a/backend/api/internal/logic/gas/resource_contract_test.go b/backend/api/internal/logic/gas/resource_contract_test.go new file mode 100644 index 0000000..0804411 --- /dev/null +++ b/backend/api/internal/logic/gas/resource_contract_test.go @@ -0,0 +1,20 @@ +package gas + +import "testing" + +// TestExpectedResourcesExposeExplicitSearchContract 验证支持搜索和不支持搜索的资源边界明确。 +func TestExpectedResourcesExposeExplicitSearchContract(t *testing.T) { + resources := ExpectedResources() + byName := make(map[string]ResourceContract, len(resources)) + for _, resource := range resources { + byName[resource.Name] = resource + } + + staffFields := byName["staff_account"].SearchFields + if len(staffFields) != 2 || staffFields[0].Key != "username" || staffFields[1].Key != "role_code" { + t.Fatalf("工作人员搜索字段 = %#v,期望 username、role_code", staffFields) + } + if len(byName["user_address"].SearchFields) != 0 { + t.Fatalf("用户地址不应公开模糊搜索字段:%#v", byName["user_address"].SearchFields) + } +} diff --git a/backend/api/internal/logic/gas/resource_search.go b/backend/api/internal/logic/gas/resource_search.go new file mode 100644 index 0000000..916b21d --- /dev/null +++ b/backend/api/internal/logic/gas/resource_search.go @@ -0,0 +1,51 @@ +// Package gas 定义气站管理端各资源公开的可见字段搜索契约。 +// 版本:v1.0.0 +package gas + +import "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" + +// gasResourceSearchFields 仅声明页面能够准确解释且服务端允许匹配的字段。 +var gasResourceSearchFields = map[string][]common.KeywordSearchField{ + "delivery_basic": {textSearchField("delivery_code"), textSearchField("name")}, + "delivery_account": {textSearchField("username"), textSearchField("display_name"), enumSearchField("role_code", searchValue("admin", "配送点管理员"))}, + "staff_account": {textSearchField("username"), enumSearchField("role_code", + searchValue("installer", "安装人员"), searchValue("delivery", "配送人员"), searchValue("operations", "运维人员"))}, + "staff_credential": {textSearchField("credential_type")}, + "user_account": {textSearchField("username")}, + "gasorder_contract": {textSearchField("contract_no"), textSearchField("title")}, + "gasorder_basic": {textSearchField("request_no"), enumSearchField("creator_type", + searchValue("user", "用户"), searchValue("staff", "工作人员"), searchValue("delivery", "配送站"), searchValue("gas", "气站"))}, + "product_info": {textSearchField("code"), textSearchField("name")}, + "wallet_basic": {textSearchField("owner_type")}, + "payment_refund": {textSearchField("refund_no")}, + "wallet_apply_cash": {textSearchField("cash_no")}, + "fin_settlement": {textSearchField("settlement_no"), textSearchField("subject_type")}, + "cs_ticket": {textSearchField("ticket_no"), textSearchField("category"), textSearchField("priority")}, +} + +// resourceSearchFields 返回资源搜索契约副本;未配置资源明确不支持搜索。 +func resourceSearchFields(name string) []common.KeywordSearchField { + fields := gasResourceSearchFields[name] + result := make([]common.KeywordSearchField, 0, len(fields)) + for _, field := range fields { + copied := field + copied.Values = append([]common.KeywordSearchValue(nil), field.Values...) + result = append(result, copied) + } + return result +} + +// textSearchField 创建文本包含匹配字段。 +func textSearchField(key string) common.KeywordSearchField { + return common.KeywordSearchField{Key: key, Kind: common.KeywordSearchText} +} + +// enumSearchField 创建仅按中文展示名称匹配的枚举字段。 +func enumSearchField(key string, values ...common.KeywordSearchValue) common.KeywordSearchField { + return common.KeywordSearchField{Key: key, Kind: common.KeywordSearchEnum, Values: values} +} + +// searchValue 绑定枚举编码和页面中文名称。 +func searchValue(code, label string) common.KeywordSearchValue { + return common.KeywordSearchValue{Value: code, Label: label} +} diff --git a/backend/api/internal/routers/gas.go b/backend/api/internal/routers/gas.go index 96c5248..ea79c29 100644 --- a/backend/api/internal/routers/gas.go +++ b/backend/api/internal/routers/gas.go @@ -4,6 +4,7 @@ import ( "fmt" sdkmiddleware "git.apinb.com/bsm-sdk/core/middleware" + "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" gaslogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/gas" "github.com/gin-gonic/gin" ) @@ -17,6 +18,7 @@ func RegisterGas(serviceKey string, engine *gin.Engine) { protected := engine.Group(basePath) protected.Use(sdkmiddleware.JwtAuth(true)) protected.Use(gaslogic.RequireGasAdmin()) + protected.Use(common.EnableConfiguredKeywordSearch()) protected.GET("/auth/profile", gaslogic.CurrentProfile) protected.PUT("/auth/password", gaslogic.ChangePassword) protected.GET("/gas_menu", gaslogic.ListMenu) diff --git a/docs/06-气站管理系统需求.md b/docs/06-气站管理系统需求.md index 5bfe6fa..daf02cf 100644 --- a/docs/06-气站管理系统需求.md +++ b/docs/06-气站管理系统需求.md @@ -85,6 +85,8 @@ 工作人员支持新增、编辑、启停和归档;资质支持新增、编辑、启停和归档。工作人员只能归属当前气站,可选归属当前气站的一个配送点。存在未完成订单时不得归档。删除工作人员和资质均更新为已归档。 +安装人员、配送人员和运维人员列表统一支持按“用户名、角色”模糊搜索,输入框必须明确提示“可搜索:用户名、角色”。 + ### 4.4 用户管理 - 用户账户是平台全局账户;当前气站可管理由总后台分配到本站的用户,也可自行创建用户并在同一事务内建立本站服务关系。 @@ -163,6 +165,8 @@ 标准列表资源使用独立记录页,不在列表抽屉中承载新建、详情或编辑: +- 列表搜索能力由后端资源契约声明;支持搜索时输入框必须逐项显示“可搜索:字段名”,不得使用“关键字段”等含义不明确的兜底文案。 +- 后端资源契约未声明可搜索字段时,前端必须隐藏“模糊搜索”、输入框、“查询”和“重置”组成的完整搜索区域,刷新和新建操作不受影响。 - 新建页:`列表路径/new`,仅为支持新建的资源生成。 - 详情页:`列表路径/:identity`,20 类标准资源全部提供,并支持刷新和直达。 - 编辑页:`列表路径/:identity/edit`,仅为支持通用编辑的资源生成。 diff --git a/docs/操作日志_气站列表搜索能力显式化_20260819.md b/docs/操作日志_气站列表搜索能力显式化_20260819.md new file mode 100644 index 0000000..889c4cc --- /dev/null +++ b/docs/操作日志_气站列表搜索能力显式化_20260819.md @@ -0,0 +1,53 @@ +# 操作日志:气站列表搜索能力显式化 + +操作时间:2026-08-19 +操作类型:扩展 +影响模块:气站管理系统标准资源列表、后端资源契约 + +## 操作前状态 + +- 气站端所有标准资源列表都会显示搜索区域。 +- 未配置搜索字段的资源使用“关键字段模糊搜索”兜底提示,用户无法判断实际可搜索内容。 +- 工作人员列表的接口虽可接收关键字,但前后端契约未明确限定和展示“用户名、角色”。 + +## 具体操作 + +- 为气站资源契约增加可搜索字段声明,并在生成的前端契约中同步输出。 +- 工作人员账号统一声明“用户名、角色”为可搜索字段,覆盖安装人员、配送人员和运维人员列表。 +- 前端仅在契约存在可搜索字段时显示完整搜索区域,并按实际字段生成“可搜索:……”提示。 +- 为气站路由启用已配置关键字搜索策略,确保提示字段与服务端过滤语义一致。 +- 增加契约测试和静态检查,防止重新出现模糊兜底提示或无能力页面错误展示搜索区域。 + +## 操作后状态 + +- 工作人员列表显示“可搜索:用户名、角色”。 +- 其他支持搜索的资源按契约逐项显示具体字段。 +- 未声明搜索能力的资源隐藏“模糊搜索”、输入框、“查询”和“重置”,保留刷新与新建操作。 +- 现有资源接口、路由路径和非搜索操作保持兼容。 + +## 代码变更 + +- `backend/api/internal/logic/gas/resource_contract.go`:资源契约增加搜索字段。 +- `backend/api/internal/logic/gas/resource_search.go`:集中声明气站资源可搜索字段。 +- `backend/api/internal/logic/gas/resource_contract_test.go`:验证工作人员及无搜索资源契约。 +- `backend/api/internal/routers/gas.go`:启用已配置关键字搜索中间件。 +- `backend/api/cmd/cli/main.go`:生成前端契约时输出搜索字段。 +- `frontend/gas_admin/src/views/shared/use-resource-list-search.ts`:按契约显示或隐藏搜索并生成明确提示。 +- `frontend/gas_admin/scripts/check-backend-contract.mjs`:增加搜索展示规则回归检查。 +- `frontend/gas_admin/src/contracts/gas-resources.json`:同步生成气站资源搜索契约。 +- `docs/06-气站管理系统需求.md`:补充列表搜索展示规则。 + +## 验证结果 + +- 后端全量测试 `go test ./...`:通过。 +- 前端契约同步:通过。 +- 前端契约检查:通过,20 个资源与后端契约一致。 +- 前端资源页面检查:通过,详情 20 类、新建 11 类、编辑 8 类。 +- 前端类型检查:通过。 +- 前端生产构建:通过。 + +## 风险评估 + +- 影响范围仅限标准资源列表的关键字搜索展示与过滤字段,不改变增删改查接口。 +- 无契约资源将不再发送关键字,避免页面提示与服务端行为不一致。 +- 搜索字段采用显式白名单,降低误搜敏感字段和未来模型字段变化带来的风险。 diff --git a/frontend/gas_admin/scripts/check-backend-contract.mjs b/frontend/gas_admin/scripts/check-backend-contract.mjs index 0296cb6..6eff020 100644 --- a/frontend/gas_admin/scripts/check-backend-contract.mjs +++ b/frontend/gas_admin/scripts/check-backend-contract.mjs @@ -5,6 +5,14 @@ import { fileURLToPath } from 'node:url'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const source = readFileSync(resolve(root, 'src/api/resources.ts'), 'utf8'); const routes = readFileSync(resolve(root, 'src/router/routes/modules/platform.ts'), 'utf8'); +const searchSource = readFileSync( + resolve(root, 'src/views/shared/use-resource-list-search.ts'), + 'utf8', +); +const listPageSource = readFileSync( + resolve(root, 'src/views/shared/CrudListPage.vue'), + 'utf8', +); const contract = JSON.parse( readFileSync(resolve(root, 'src/contracts/gas-resources.json'), 'utf8'), ); @@ -35,6 +43,18 @@ for (const item of contract.resources) { throw new Error(`缺少后端资源路由:${item.name}`); } +const staffSearchFields = backend.get('staff_account')?.searchFields ?? []; +if (staffSearchFields.map((field) => field.key).join(',') !== 'username,role_code') + throw new Error('工作人员搜索契约必须明确为:用户名、角色'); +if ((backend.get('user_address')?.searchFields ?? []).length > 0) + throw new Error('不支持模糊搜索的用户地址资源不得公开搜索字段'); +if (!searchSource.includes('searchableFields.value.length > 0')) + throw new Error('列表搜索必须由资源搜索契约决定是否显示'); +if (searchSource.includes('关键字段模糊搜索')) + throw new Error('列表搜索不得使用含义不明确的兜底提示'); +if (!listPageSource.includes(' searchableKeys.has(field.key)); }); - // 气站 API 仍支持通用关键字查询;缺少细粒度搜索契约时保留原搜索入口。 - const searchEnabled = computed(() => true); + const searchEnabled = computed(() => searchableFields.value.length > 0); const searchPlaceholder = computed( - () => searchableFields.value.length - ? `可搜索:${searchableFields.value - .map((field) => field.listLabel ?? field.label) - .join('、')}` - : '关键字段模糊搜索', + () => + `可搜索:${searchableFields.value + .map((field) => field.listLabel ?? field.label) + .join('、')}`, ); /** 仅在当前资源支持搜索时返回去除首尾空格的关键字。 */ function requestKeyword() { - return filters.keyword.trim(); + return searchEnabled.value ? filters.keyword.trim() : ''; } /** 将受支持的搜索状态写入 URL,并移除无效或陈旧的 keyword。 */