完善配送端资源搜索与资质导航

This commit is contained in:
czl231
2026-08-22 23:24:12 +08:00
parent 5d4bd61672
commit 4334719051
22 changed files with 432 additions and 58 deletions

View File

@@ -1,33 +1,44 @@
package delivery
import "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
type ResourceContract struct {
Domain string `json:"domain"`
Name string `json:"name"`
Path string `json:"path"`
PageKind string `json:"pageKind"`
Mode string `json:"mode"`
Domain string `json:"domain"`
Name string `json:"name"`
Path string `json:"path"`
PageKind string `json:"pageKind"`
Mode string `json:"mode"`
SearchFields []common.KeywordSearchField `json:"searchFields,omitempty"`
}
func ExpectedResources() []ResourceContract {
items := []ResourceContract{
{"profile", "delivery_profile", "/delivery_profile", "list", "readonly"},
{"staff", "staff_account", "/staff_account", "list", "writable"},
{"staff", "staff_credential", "/staff_credential", "list", "writable"},
{"user", "user_account", "/user_account", "list", "writable"},
{"user", "user_address", "/user_address", "list", "writable"},
{"contract", "gasorder_contract", "/gasorder_contract", "list", "managed"},
{"contract", "gasorder_contract_product", "/gasorder_contract_product", "list", "append_only"},
{"contract", "gasorder_contract_revision", "/gasorder_contract_revision", "list", "readonly"},
{"contract", "product_info", "/product_info", "list", "readonly"},
{"gasorder", "gasorder_basic", "/gasorder_basic", "list", "append_only"},
{"finance", "wallet_basic", "/wallet_basic", "list", "readonly"},
{"finance", "wallet_bank", "/wallet_bank", "list", "readonly"},
{"finance", "payment_order", "/payment_order", "list", "readonly"},
{"finance", "wallet_record", "/wallet_record", "list", "readonly"},
{"finance", "payment_refund", "/payment_refund", "list", "readonly"},
{"finance", "wallet_recharge", "/wallet_recharge", "list", "append_only"},
{"finance", "wallet_apply_cash", "/wallet_apply_cash", "list", "append_only"},
{"finance", "fin_settlement", "/fin_settlement", "list", "readonly"},
resourceContract("profile", "delivery_profile", "readonly"),
resourceContract("staff", "staff_account", "writable"),
resourceContract("staff", "staff_credential", "writable"),
resourceContract("user", "user_account", "writable"),
resourceContract("user", "user_address", "writable"),
resourceContract("contract", "gasorder_contract", "managed"),
resourceContract("contract", "gasorder_contract_product", "append_only"),
resourceContract("contract", "gasorder_contract_revision", "readonly"),
resourceContract("contract", "product_info", "readonly"),
resourceContract("gasorder", "gasorder_basic", "append_only"),
resourceContract("finance", "wallet_basic", "readonly"),
resourceContract("finance", "wallet_bank", "readonly"),
resourceContract("finance", "payment_order", "readonly"),
resourceContract("finance", "wallet_record", "readonly"),
resourceContract("finance", "payment_refund", "readonly"),
resourceContract("finance", "wallet_recharge", "append_only"),
resourceContract("finance", "wallet_apply_cash", "append_only"),
resourceContract("finance", "fin_settlement", "readonly"),
}
return items
}
// resourceContract 创建配送端标准资源契约,并附加显式搜索字段。
func resourceContract(domain, name, mode string) ResourceContract {
return ResourceContract{
Domain: domain, Name: name, Path: "/" + name, PageKind: "list", Mode: mode,
SearchFields: resourceSearchFields(name),
}
}

View File

@@ -0,0 +1,26 @@
// 功能描述验证配送端资源搜索契约与页面能力保持一致。版本v1.0.0。
package delivery
import "testing"
// TestExpectedResourcesExposeSearchContract 验证有效搜索与隐藏搜索的资源边界。
func TestExpectedResourcesExposeSearchContract(t *testing.T) {
resources := ExpectedResources()
byName := make(map[string]ResourceContract, len(resources))
for _, resource := range resources {
byName[resource.Name] = resource
}
credential := byName["staff_credential"].SearchFields
if len(credential) != 2 || credential[0].Key != "credential_type" || credential[1].Key != "credential_no" {
t.Fatalf("人员资质搜索契约不完整:%#v", credential)
}
for _, name := range []string{"delivery_profile", "user_address", "wallet_bank"} {
if len(byName[name].SearchFields) != 0 {
t.Fatalf("%s 不应显示无效搜索:%#v", name, byName[name].SearchFields)
}
}
channel := byName["payment_order"].SearchFields[2]
if channel.Kind != "enum" || len(channel.Values) != 3 {
t.Fatalf("支付渠道中英文搜索契约不完整:%#v", channel)
}
}

View File

@@ -0,0 +1,62 @@
// Package delivery 定义配送点管理端公开的资源搜索契约。
// 版本v1.0.0
package delivery
import (
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
)
// deliveryResourceSearchFields 只公开页面能够解释且服务端确实执行的搜索字段。
var deliveryResourceSearchFields = map[string][]common.KeywordSearchField{
"staff_account": {searchText("username")},
"staff_credential": {searchText("credential_type"), searchText("credential_no")},
"user_account": {searchText("username")},
"gasorder_contract": {searchText("contract_no"), searchText("title")},
"gasorder_contract_product": {searchText("product_code"), searchText("product_type_name")},
"gasorder_contract_revision": {searchEnum("action", searchOption("activate", "启用"), searchOption("renew", "续签"), searchOption("terminate", "终止"))},
"product_info": {searchText("code"), searchText("name")},
"gasorder_basic": {searchText("request_no"), searchEnum("creator_type",
searchOption("user", "用户"), searchOption("staff", "工作人员"),
searchOption("delivery", "配送点"), searchOption("gas", "气站"))},
"payment_order": {searchText("payment_no"), searchText("request_no"), searchEnum("channel",
searchOption("wechat", "微信"), searchOption("alipay", "支付宝"), searchOption("mock", "模拟支付"))},
"wallet_record": {searchText("record_no"), searchText("request_no")},
"payment_refund": {searchText("refund_no"), searchText("request_no")},
"wallet_recharge": {searchText("record_no"), searchText("request_no")},
"wallet_apply_cash": {searchText("cash_no"), searchText("request_no"), searchEnum("channel",
searchOption("bank", "银行卡"), searchOption("alipay", "支付宝"), searchOption("wechat", "微信"))},
"fin_settlement": {searchText("settlement_no")},
}
// init 只注册平台总后台尚未注册的模型;共享模型继续复用全局策略。
func init() {
common.RegisterKeywordSearchPolicy(&models.GasorderContractProduct{}, deliveryResourceSearchFields["gasorder_contract_product"])
common.RegisterKeywordSearchPolicy(&models.GasorderContractRevision{}, deliveryResourceSearchFields["gasorder_contract_revision"])
common.RegisterKeywordSearchPolicy(&models.PaymentOrder{}, deliveryResourceSearchFields["payment_order"])
common.RegisterKeywordSearchPolicy(&models.WalletRecord{}, deliveryResourceSearchFields["wallet_record"])
}
// resourceSearchFields 返回搜索契约副本,防止调用方修改全局定义。
func resourceSearchFields(name string) []common.KeywordSearchField {
fields := deliveryResourceSearchFields[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
}
func searchText(key string) common.KeywordSearchField {
return common.KeywordSearchField{Key: key, Kind: common.KeywordSearchText}
}
func searchEnum(key string, values ...common.KeywordSearchValue) common.KeywordSearchField {
return common.KeywordSearchField{Key: key, Kind: common.KeywordSearchEnum, Values: values}
}
func searchOption(value, label string) common.KeywordSearchValue {
return common.KeywordSearchValue{Value: value, Label: label}
}

View File

@@ -1,6 +1,7 @@
package delivery
import (
"strings"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
@@ -187,17 +188,38 @@ func ArchiveStaff(ctx *gin.Context) {
}
func credentialQuery(point models.DeliveryBasic) *gorm.DB {
return common.ActiveRecords(db().Model(&models.StaffCredential{})).
return credentialQueryWithDB(db(), point)
}
// credentialQueryWithDB 固定资质所属配送点、气站和配送角色范围。
func credentialQueryWithDB(databaseService *gorm.DB, point models.DeliveryBasic) *gorm.DB {
return common.ActiveRecords(databaseService.Model(&models.StaffCredential{})).
Joins("JOIN staff_account ON staff_account.id = staff_credential.staff_account_id").
Where("staff_account.delivery_basic_id = ? AND staff_account.gas_basic_id = ? AND staff_account.role_code = ?",
point.ID, point.GasBasicID, "delivery")
}
// scopedCredentialQuery 将资质列表进一步锁定到已验证的配送人员。
func scopedCredentialQuery(databaseService *gorm.DB, point models.DeliveryBasic, staffID uint64) *gorm.DB {
return credentialQueryWithDB(databaseService, point).
Where("staff_credential.staff_account_id = ?", staffID)
}
func ListCredential(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if ok {
listScoped(ctx, &models.StaffCredential{}, credentialQuery(point), "staff_credential.created_at desc")
if !ok {
return
}
staffIdentity := strings.TrimSpace(ctx.Query("staff_account_identity"))
if staffIdentity == "" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
staff, found := scopedStaff(ctx, staffIdentity, point)
if !found {
return
}
listScoped(ctx, &models.StaffCredential{}, scopedCredentialQuery(db(), point, staff.ID), "staff_credential.created_at desc")
}
func GetCredential(ctx *gin.Context) {
@@ -260,9 +282,13 @@ func UpdateCredential(ctx *gin.Context) {
if !ok {
return
}
if staff.ID != existing.StaffAccountID {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := db().Model(&existing).Updates(map[string]any{
"staff_account_id": staff.ID, "credential_type": request.CredentialType,
"credential_no": request.CredentialNo, "expired_at": request.ExpiredAt,
"credential_type": request.CredentialType, "credential_no": request.CredentialNo,
"expired_at": request.ExpiredAt,
}).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -31,3 +31,24 @@ func TestDeliveryStaffQueryKeepsRoleScope(t *testing.T) {
}
}
}
// TestScopedCredentialQueryKeepsOwnerAndRoleScope 验证资质列表同时限制人员和配送角色。
func TestScopedCredentialQueryKeepsOwnerAndRoleScope(t *testing.T) {
connection, _, err := sqlmock.New()
if err != nil {
t.Fatalf("创建 SQL mock 失败:%v", err)
}
t.Cleanup(func() { _ = connection.Close() })
databaseService, err := gorm.Open(postgres.New(postgres.Config{Conn: connection}), &gorm.Config{DryRun: true})
if err != nil {
t.Fatalf("打开 GORM 失败:%v", err)
}
point := models.DeliveryBasic{Entity: models.Entity{ID: 22}, GasBasicID: 11}
statement := scopedCredentialQuery(databaseService, point, 33).
Find(&[]models.StaffCredential{}).Statement.SQL.String()
for _, required := range []string{"delivery_basic_id", "gas_basic_id", "role_code", "staff_account_id"} {
if !strings.Contains(statement, required) {
t.Fatalf("配送人员资质范围缺少 %s%s", required, statement)
}
}
}