完善气站资源列表搜索契约
This commit is contained in:
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
20
backend/api/internal/logic/gas/resource_contract_test.go
Normal file
20
backend/api/internal/logic/gas/resource_contract_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
51
backend/api/internal/logic/gas/resource_search.go
Normal file
51
backend/api/internal/logic/gas/resource_search.go
Normal 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}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -85,6 +85,8 @@
|
||||
|
||||
工作人员支持新增、编辑、启停和归档;资质支持新增、编辑、启停和归档。工作人员只能归属当前气站,可选归属当前气站的一个配送点。存在未完成订单时不得归档。删除工作人员和资质均更新为已归档。
|
||||
|
||||
安装人员、配送人员和运维人员列表统一支持按“用户名、角色”模糊搜索,输入框必须明确提示“可搜索:用户名、角色”。
|
||||
|
||||
### 4.4 用户管理
|
||||
|
||||
- 用户账户是平台全局账户;当前气站可管理由总后台分配到本站的用户,也可自行创建用户并在同一事务内建立本站服务关系。
|
||||
@@ -163,6 +165,8 @@
|
||||
|
||||
标准列表资源使用独立记录页,不在列表抽屉中承载新建、详情或编辑:
|
||||
|
||||
- 列表搜索能力由后端资源契约声明;支持搜索时输入框必须逐项显示“可搜索:字段名”,不得使用“关键字段”等含义不明确的兜底文案。
|
||||
- 后端资源契约未声明可搜索字段时,前端必须隐藏“模糊搜索”、输入框、“查询”和“重置”组成的完整搜索区域,刷新和新建操作不受影响。
|
||||
- 新建页:`列表路径/new`,仅为支持新建的资源生成。
|
||||
- 详情页:`列表路径/:identity`,20 类标准资源全部提供,并支持刷新和直达。
|
||||
- 编辑页:`列表路径/:identity/edit`,仅为支持通用编辑的资源生成。
|
||||
|
||||
53
docs/操作日志_气站列表搜索能力显式化_20260819.md
Normal file
53
docs/操作日志_气站列表搜索能力显式化_20260819.md
Normal 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 类。
|
||||
- 前端类型检查:通过。
|
||||
- 前端生产构建:通过。
|
||||
|
||||
## 风险评估
|
||||
|
||||
- 影响范围仅限标准资源列表的关键字搜索展示与过滤字段,不改变增删改查接口。
|
||||
- 无契约资源将不再发送关键字,避免页面提示与服务端行为不一致。
|
||||
- 搜索字段采用显式白名单,降低误搜敏感字段和未来模型字段变化带来的风险。
|
||||
@@ -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
@@ -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。 */
|
||||
|
||||
Reference in New Issue
Block a user