From 7c95971a45e7ec575c7d4bb7fc74444866d75a5b Mon Sep 17 00:00:00 2001 From: czl231 <3286836406@qq.com> Date: Tue, 18 Aug 2026 20:52:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B0=94=E7=AB=99=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E9=85=8D=E9=80=81=E7=82=B9=E5=BD=92=E5=B1=9E=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/internal/logic/gas/user.go | 21 +++++++++--- backend/api/internal/logic/gas/user_test.go | 32 +++++++++++++++++++ .../操作日志_气站工作人员头像适配_20260818.md | 5 +++ .../scripts/check-resource-pages.mjs | 9 +++++- frontend/gas_admin/src/api/resources.ts | 2 +- 5 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 backend/api/internal/logic/gas/user_test.go diff --git a/backend/api/internal/logic/gas/user.go b/backend/api/internal/logic/gas/user.go index c4ceb76..82803f6 100644 --- a/backend/api/internal/logic/gas/user.go +++ b/backend/api/internal/logic/gas/user.go @@ -11,19 +11,30 @@ import ( "gorm.io/gorm" ) +// gasUserListItem 在用户主档上附加当前气站服务关系中的配送点归属。 +type gasUserListItem struct { + models.UserAccount + DeliveryBasicID uint64 `gorm:"column:delivery_basic_id" json:"delivery_basic_id"` +} + +// scopedUserList 将用户列表与当前气站唯一有效服务关系绑定,避免返回跨站归属。 +func scopedUserList(databaseService *gorm.DB, gasBasicID uint64) *gorm.DB { + return common.ActiveRecords(databaseService.Model(&models.UserAccount{})). + Select("user_account.*, user_service_relation.delivery_basic_id AS delivery_basic_id"). + Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_account.id AND user_service_relation.status <> ?", common.StatusArchived). + Where("user_service_relation.gas_basic_id = ?", gasBasicID) +} + func ListUser(ctx *gin.Context) { station, ok := currentGas(ctx) if !ok { return } page, size := common.PageSize(ctx) - var list []models.UserAccount + var list []gasUserListItem var total int64 query := common.ApplyKeywordFilter(ctx, - common.ActiveRecords(impl.DBService.Model(&models.UserAccount{})). - Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_account.id AND user_service_relation.status <> ?", - common.StatusArchived). - Where("user_service_relation.gas_basic_id = ?", station.ID), + scopedUserList(impl.DBService, station.ID), &models.UserAccount{}) if err := query.Count(&total).Error; err != nil { infra.Response.Error(ctx, err) diff --git a/backend/api/internal/logic/gas/user_test.go b/backend/api/internal/logic/gas/user_test.go new file mode 100644 index 0000000..d9fc2a5 --- /dev/null +++ b/backend/api/internal/logic/gas/user_test.go @@ -0,0 +1,32 @@ +package gas + +import ( + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// TestScopedUserListIncludesDeliveryRelation 验证用户列表带出当前气站服务关系的配送点主键。 +func TestScopedUserListIncludesDeliveryRelation(t *testing.T) { + connection, _, err := sqlmock.New() + if err != nil { + t.Fatalf("create 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("open GORM database: %v", err) + } + + statement := scopedUserList(databaseService, 42). + Find(&[]gasUserListItem{}).Statement.SQL.String() + if !strings.Contains(statement, "user_service_relation.delivery_basic_id AS delivery_basic_id") { + t.Fatalf("user list must select delivery relation, got: %s", statement) + } + if !strings.Contains(statement, "user_service_relation.gas_basic_id =") { + t.Fatalf("user list must keep gas scope, got: %s", statement) + } +} diff --git a/docs/操作日志_气站工作人员头像适配_20260818.md b/docs/操作日志_气站工作人员头像适配_20260818.md index 09ace42..8382de0 100644 --- a/docs/操作日志_气站工作人员头像适配_20260818.md +++ b/docs/操作日志_气站工作人员头像适配_20260818.md @@ -14,6 +14,7 @@ - 复用 5173 的账户摘要组件和头像组合式逻辑,在新建、详情、编辑页显示头像选择与预览。 - 启用工作人员与用户列表的受保护头像缩略图,替代头像存储路径文本。 +- 用户账户列表从当前气站服务关系带出配送点归属,修复配送点唯一标识显示为空。 - 从基础信息表单移除头像文本输入框,保存时仅在选择新头像或恢复默认头像后提交头像字段。 - 新增工作人员、用户头像读取路由,查询始终附加当前气站或服务关系范围。 - 将更新请求的头像字段改为可选指针,未修改头像时不更新数据库列。 @@ -22,6 +23,7 @@ - `/staff/add` 顶部显示账户摘要与头像选择区,布局和交互参考 5173。 - 工作人员与用户列表显示 32 像素圆形头像;无头像或加载失败时回退默认头像。 +- 用户账户列表以“所属配送点”为列名并显示配送点业务名称,唯一标识仅用于悬停查看和复制。 - 工作人员与用户的详情、编辑页可安全读取现有头像。 - 普通资料编辑不会误清空头像;主动恢复默认头像仍会提交空值。 @@ -30,7 +32,10 @@ - `frontend/gas_admin/src/views/resource/ResourceRecordPage.vue`:启用头像摘要、过滤文本字段、接入上传载荷与未保存检测。 - `frontend/gas_admin/src/api/avatar.ts`:使用气站 API 前缀读取受保护头像。 - `frontend/gas_admin/src/views/shared/protected-list-avatar-loader.ts`:开放工作人员与用户缩略图渲染。 +- `frontend/gas_admin/src/api/resources.ts`:用户账户的配送点关系列表以业务名称为主展示,并保留唯一标识复制能力。 +- `frontend/gas_admin/scripts/check-resource-pages.mjs`:新增用户账户配送点名称展示的静态回归检查。 - `backend/api/internal/logic/gas/staff.go`、`user.go`:新增范围受限头像读取并保护头像更新语义。 +- `backend/api/internal/logic/gas/user_test.go`:校验用户列表携带配送点关系且保留气站范围。 - `backend/api/internal/routers/gas_business.go`、`gas_test.go`:注册并校验头像路由。 ## 验证结果 diff --git a/frontend/gas_admin/scripts/check-resource-pages.mjs b/frontend/gas_admin/scripts/check-resource-pages.mjs index 6a1ad64..a701707 100644 --- a/frontend/gas_admin/scripts/check-resource-pages.mjs +++ b/frontend/gas_admin/scripts/check-resource-pages.mjs @@ -1,6 +1,6 @@ /** * 功能:静态检查气站管理端全部标准资源是否具备独立记录页配置。 - * 版本:v1.2.0 + * 版本:v1.3.0 */ import { readFileSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; @@ -83,6 +83,13 @@ for (const marker of [ if (!listSource.includes('recordEditReason(record)')) { fail('合同列表缺少编辑禁用原因提示'); } +if ( + !resourceSource.includes( + "relation('delivery_basic_identity', '/delivery_basic', true, { label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, showIdentityCopy: true })", + ) +) { + fail('用户账户列表未按文档以所属配送点名称为主展示'); +} const createCount = listResources.filter((item) => createModes.has(item.mode)).length; console.log( diff --git a/frontend/gas_admin/src/api/resources.ts b/frontend/gas_admin/src/api/resources.ts index 03359e7..4d0c390 100644 --- a/frontend/gas_admin/src/api/resources.ts +++ b/frontend/gas_admin/src/api/resources.ts @@ -513,7 +513,7 @@ const gasOverrides: ResourceUiDefinition[] = [ { name: '重置密码', resource: '/staff_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] }, ]), define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: 'context', lockPrefilled: true, showIdentityCopy: true } }), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]), - define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name'), relation('delivery_basic_identity', '/delivery_basic', true)], 'list', [ + define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name'), relation('delivery_basic_identity', '/delivery_basic', true, { label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, showIdentityCopy: true })], 'list', [ { name: '重置密码', resource: '/user_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] }, ]), define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),