完善气站资源列表搜索契约

This commit is contained in:
czl231
2026-08-19 22:07:00 +08:00
parent 72da8e9e47
commit 2f5b934037
10 changed files with 168 additions and 16 deletions

View File

@@ -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})

View File

@@ -1,4 +1,6 @@
package gas
package gas
import "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
type ResourceMode string
@@ -15,6 +17,7 @@ type ResourceContract struct {
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

View File

@@ -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)
}
}

View File

@@ -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}
}

View File

@@ -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)

View File

@@ -85,6 +85,8 @@
工作人员支持新增、编辑、启停和归档;资质支持新增、编辑、启停和归档。工作人员只能归属当前气站,可选归属当前气站的一个配送点。存在未完成订单时不得归档。删除工作人员和资质均更新为已归档。
安装人员、配送人员和运维人员列表统一支持按“用户名、角色”模糊搜索,输入框必须明确提示“可搜索:用户名、角色”。
### 4.4 用户管理
- 用户账户是平台全局账户;当前气站可管理由总后台分配到本站的用户,也可自行创建用户并在同一事务内建立本站服务关系。
@@ -163,6 +165,8 @@
标准列表资源使用独立记录页,不在列表抽屉中承载新建、详情或编辑:
- 列表搜索能力由后端资源契约声明;支持搜索时输入框必须逐项显示“可搜索:字段名”,不得使用“关键字段”等含义不明确的兜底文案。
- 后端资源契约未声明可搜索字段时,前端必须隐藏“模糊搜索”、输入框、“查询”和“重置”组成的完整搜索区域,刷新和新建操作不受影响。
- 新建页:`列表路径/new`,仅为支持新建的资源生成。
- 详情页:`列表路径/:identity`20 类标准资源全部提供,并支持刷新和直达。
- 编辑页:`列表路径/:identity/edit`,仅为支持通用编辑的资源生成。

View File

@@ -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 类。
- 前端类型检查:通过。
- 前端生产构建:通过。
## 风险评估
- 影响范围仅限标准资源列表的关键字搜索展示与过滤字段,不改变增删改查接口。
- 无契约资源将不再发送关键字,避免页面提示与服务端行为不一致。
- 搜索字段采用显式白名单,降低误搜敏感字段和未来模型字段变化带来的风险。

View File

@@ -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('<a-form v-if="searchEnabled"'))
throw new Error('不支持搜索的资源必须隐藏完整搜索区域');
const forbidden = [
'/platform/platform_', '/gas/gas_', '/delivery/delivery_', '/staff/',
'/user/', '/ec/ec_', '/finance/fin_', '/wallet/wallet',

File diff suppressed because one or more lines are too long

View File

@@ -30,19 +30,17 @@ export function useResourceListSearch(
);
return displayFields.value.filter((field) => searchableKeys.has(field.key));
});
// 气站 API 仍支持通用关键字查询;缺少细粒度搜索契约时保留原搜索入口。
const searchEnabled = computed(() => true);
const searchEnabled = computed(() => searchableFields.value.length > 0);
const searchPlaceholder = computed(
() => searchableFields.value.length
? `可搜索:${searchableFields.value
() =>
`可搜索:${searchableFields.value
.map((field) => field.listLabel ?? field.label)
.join('、')}`
: '关键字段模糊搜索',
.join('、')}`,
);
/** 仅在当前资源支持搜索时返回去除首尾空格的关键字。 */
function requestKeyword() {
return filters.keyword.trim();
return searchEnabled.value ? filters.keyword.trim() : '';
}
/** 将受支持的搜索状态写入 URL并移除无效或陈旧的 keyword。 */