From 5d4bd61672e8febb147e88e9db688c9384b7c52a Mon Sep 17 00:00:00 2001 From: czl231 <3286836406@qq.com> Date: Sat, 22 Aug 2026 22:23:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=85=8D=E9=80=81=E7=AB=AF?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=A4=B4=E5=83=8F=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/internal/logic/delivery/staff.go | 16 ++- .../api/internal/logic/delivery/staff_test.go | 33 ++++++ docs/07-配送点管理系统需求.md | 2 + docs/操作日志_配送点独立资源页面_20260822.md | 9 +- docs/项目文档_配送点独立资源页面_v1.0.md | 10 +- .../scripts/check-resource-pages.mjs | 7 ++ frontend/delivery_admin/src/api/avatar.ts | 8 +- .../views/shared/ProtectedAvatarThumbnail.vue | 103 ++++++++++++++++ .../src/views/shared/ResourceListPage.vue | 39 ++++-- .../shared/protected-list-avatar-loader.ts | 111 ++++++++++++++++++ 10 files changed, 317 insertions(+), 21 deletions(-) create mode 100644 backend/api/internal/logic/delivery/staff_test.go create mode 100644 frontend/delivery_admin/src/views/shared/ProtectedAvatarThumbnail.vue create mode 100644 frontend/delivery_admin/src/views/shared/protected-list-avatar-loader.ts diff --git a/backend/api/internal/logic/delivery/staff.go b/backend/api/internal/logic/delivery/staff.go index f25477b..4b1a83a 100644 --- a/backend/api/internal/logic/delivery/staff.go +++ b/backend/api/internal/logic/delivery/staff.go @@ -14,21 +14,26 @@ import ( func scopedStaff(ctx *gin.Context, identity string, point models.DeliveryBasic) (models.StaffAccount, bool) { var staff models.StaffAccount - if err := common.ActiveRecords(db()).Where("identity = ? AND gas_basic_id = ? AND delivery_basic_id = ?", - identity, point.GasBasicID, point.ID).First(&staff).Error; err != nil { + if err := deliveryStaffQuery(db(), point).Where("identity = ?", identity).First(&staff).Error; err != nil { common.RespondRecordError(ctx, err) return staff, false } return staff, true } +// deliveryStaffQuery 固定配送人员的气站、配送点及角色范围。 +func deliveryStaffQuery(databaseService *gorm.DB, point models.DeliveryBasic) *gorm.DB { + return common.ActiveRecords(databaseService.Model(&models.StaffAccount{})). + Where("gas_basic_id = ? AND delivery_basic_id = ? AND role_code = ?", + point.GasBasicID, point.ID, "delivery") +} + func ListStaff(ctx *gin.Context) { point, _, ok := currentScope(ctx) if !ok { return } - query := common.ActiveRecords(db().Model(&models.StaffAccount{})). - Where("gas_basic_id = ? AND delivery_basic_id = ? AND role_code = ?", point.GasBasicID, point.ID, "delivery") + query := deliveryStaffQuery(db(), point) listScoped(ctx, &models.StaffAccount{}, query, "staff_account.created_at desc") } @@ -38,8 +43,7 @@ func GetStaff(ctx *gin.Context) { return } var staff models.StaffAccount - respondRecord(ctx, common.ActiveRecords(db()).Where("identity = ? AND gas_basic_id = ? AND delivery_basic_id = ? AND role_code = ?", - ctx.Param("identity"), point.GasBasicID, point.ID, "delivery"), &staff) + respondRecord(ctx, deliveryStaffQuery(db(), point).Where("identity = ?", ctx.Param("identity")), &staff) } // GetStaffAvatar 返回当前配送点范围内配送人员的受保护头像。 diff --git a/backend/api/internal/logic/delivery/staff_test.go b/backend/api/internal/logic/delivery/staff_test.go new file mode 100644 index 0000000..a5ad285 --- /dev/null +++ b/backend/api/internal/logic/delivery/staff_test.go @@ -0,0 +1,33 @@ +// 功能描述:验证配送点人员查询始终限制为当前配送点的配送角色。版本:v1.0.0。 +package delivery + +import ( + "strings" + "testing" + + "git.apinb.com/heqiapp/platforms/backend/api/internal/models" + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// TestDeliveryStaffQueryKeepsRoleScope 验证列表、详情和头像共用的范围包含 delivery 角色。 +func TestDeliveryStaffQueryKeepsRoleScope(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 := deliveryStaffQuery(databaseService, point). + Where("identity = ?", "staff-identity").Find(&models.StaffAccount{}).Statement.SQL.String() + for _, required := range []string{"gas_basic_id", "delivery_basic_id", "role_code"} { + if !strings.Contains(statement, required) { + t.Fatalf("配送人员范围缺少 %s:%s", required, statement) + } + } +} diff --git a/docs/07-配送点管理系统需求.md b/docs/07-配送点管理系统需求.md index 56f8286..f030a04 100644 --- a/docs/07-配送点管理系统需求.md +++ b/docs/07-配送点管理系统需求.md @@ -93,6 +93,7 @@ Global: - 资质支持新增、编辑、启停和归档。 - 删除均为归档;存在未完成订单时禁止归档人员。 - 不允许创建安装、运维、仓管、调度员或质控角色账号。 +- 配送人员列表不得展示数据库自增 ID;联系电话之后显示 32px 圆形头像缩略图,头像仅通过当前配送点 JWT 范围内的受保护接口懒加载。无头像或读取异常时显示默认头像,不得从列表响应读取或暴露头像 URI。 ### 4.4 用户管理 @@ -102,6 +103,7 @@ Global: - 配送点不能将用户迁移到其他配送点或其他气站。 - 归档用户时同时归档地址及服务关系。 - 用户存在未完成订单、未关闭工单、钱包余额、可提现余额或待处理提现时禁止归档。 +- 用户账户列表沿用配送人员列表的受控头像缩略图、默认头像、限流缓存和自增 ID 隐藏规则。 ### 4.5 合同管理 diff --git a/docs/操作日志_配送点独立资源页面_20260822.md b/docs/操作日志_配送点独立资源页面_20260822.md index f9e71d3..8cf3049 100644 --- a/docs/操作日志_配送点独立资源页面_20260822.md +++ b/docs/操作日志_配送点独立资源页面_20260822.md @@ -19,6 +19,9 @@ 7. 依据 5173 页面重新对齐灰色页面底、面包屑、分区卡片、详情信息网格、双列表单和底部操作区。 8. 将配送人员和用户账户的头像文本框替换为 5173 头像摘要卡片,新增上传、预览、恢复默认头像和配送点范围鉴权读取能力。 9. 修正配送人员、用户编辑接口:没有提交头像字段时保留旧头像,避免编辑其他资料时误清空。 +10. 配送人员和用户账户列表增加 32px 受控头像缩略图,复用 5173 的懒加载、并发 6、当前页缓存、取消和 Blob URL 回收策略。 +11. 删除全部标准资源列表的数据库自增 ID 列,只保留系统唯一标识;同时把该规则加入自动检查。 +12. 配送人员头像读取补充 `role_code = delivery` 范围限制,并新增 SQL 范围测试。 ## 操作后状态 @@ -28,6 +31,7 @@ - 新增:`src/views/resource/` 下 5 个记录页组件与组合函数。 - 新增:`src/views/shared/ResourceListPage.vue`。 +- 新增:`ProtectedAvatarThumbnail.vue`、`protected-list-avatar-loader.ts` 和配送人员角色范围测试。 - 新增:`src/router/routes/modules/resource-route-builder.ts`。 - 新增:`src/api/resource-display.ts`、`resource-navigation.ts`、`resource-record-form.ts`。 - 修改:`platform.ts`、`ResourcePage.vue`、`resources.ts`、路由类型、资源契约和包脚本。 @@ -45,12 +49,13 @@ - `contract:check`:通过,18 个资源与后端契约一致。 - `profile:check`:通过,资料专用只读页未回退。 - `type:check`:通过。 -- `build`:通过,2628 个模块完成生产构建。 -- `go test ./internal/logic/delivery`:通过。 +- `build`:通过,2632 个模块完成生产构建。 +- `go test ./internal/logic/delivery ./internal/routers`:通过,包含配送人员头像角色范围测试。 - `lint`:通过;仅报告项目既有警告,未产生失败项。 - 浏览器回归:工作人员列表、详情、编辑、正式新建地址、返回链路、未保存保护、配送订单新建页、支付列表均通过。 - 布局回归:工作人员详情、编辑和配送订单新建页已在应用内浏览器逐页截图检查,与 5173 的页面结构和响应式断点一致。 - 头像回归:配送人员新建、编辑页已确认不再显示头像文本框,头像选择按钮、格式大小提示、默认头像和身份摘要均正常;后端头像路由测试通过。 +- 列表头像回归:配送人员、用户账户显示受控圆形头像,其他资源不生成无效头像列;数据库 ID 列已从全部标准列表移除;点击刷新后头像重新加载正常。 ## 风险评估 diff --git a/docs/项目文档_配送点独立资源页面_v1.0.md b/docs/项目文档_配送点独立资源页面_v1.0.md index 4f95526..39b698d 100644 --- a/docs/项目文档_配送点独立资源页面_v1.0.md +++ b/docs/项目文档_配送点独立资源页面_v1.0.md @@ -24,6 +24,7 @@ frontend/delivery_admin/ └── src/ ├── api/ │ ├── resource-display.ts # 中文字段、状态和详情值展示 + │ ├── avatar.ts # 头像上传与配送点鉴权读取 │ ├── resource-navigation.ts # 独立页面路由和安全返回地址 │ └── resource-record-form.ts # 表单初始化、字段白名单和校验 ├── router/routes/modules/ @@ -37,7 +38,9 @@ frontend/delivery_admin/ │ ├── use-resource-avatar.ts # 头像预览、上传和清除状态 │ └── use-unsaved-record.ts # 未保存离开保护 └── views/shared/ - └── ResourceListPage.vue # 跳转独立页面的标准列表 + ├── ResourceListPage.vue # 跳转独立页面的标准列表 + ├── ProtectedAvatarThumbnail.vue # 32px 受控头像缩略图 + └── protected-list-avatar-loader.ts # 懒加载、限流、缓存和取消 ``` ## 4. 核心实现 @@ -50,6 +53,8 @@ frontend/delivery_admin/ 配送人员和用户账户参考 5173 使用头像身份摘要卡片。新建、编辑时点击头像可选择不超过 2 MB 的 JPG/PNG 文件,保存时先上传到受控头像目录,再把返回 URI 写入资源;详情页通过配送点范围鉴权接口读取头像。未选择新头像时不会清空已有头像。 +配送人员和用户账户列表同样参考 5173:联系电话后显示 32px 圆形头像,最多并发 6 个鉴权请求,接近可视区域才加载;当前页缓存结果,刷新或离页时取消请求并释放 Blob URL。404、网络错误和图片解码失败均回退本地默认头像。全部标准资源列表隐藏数据库自增 ID,只展示可复制的系统唯一标识。 + `resource-record-form.ts` 为五类可编辑资源声明后端更新字段白名单,防止用户名、合同编号等只读字段出现在编辑页或被无效提交。 支付与退款资源统一使用后端正式名称 `payment_order`、`payment_refund`,菜单地址仍保持 `/finance/payments`、`/finance/refunds`,避免接口路径不一致导致 404。 @@ -73,8 +78,9 @@ npm.cmd run build - 删除已停用的抽屉式 `CrudListPage.vue`。 - 修复支付、退款资源与后端契约名称不一致的问题。 - 新增 17/9/5 页面能力矩阵自动检查。 +- 新增配送人员、用户账户的受控头像缩略图,并移除全部标准列表的数据库自增 ID。 - 保持配送点资料专用只读页面和现有公共后端接口不变。 ## 7. 已知边界 -独立页面只使用配送端已存在的关系查询和 CRUD 能力,不引入平台端头像上传、合同附件上传、平台角色或跨组织账户摘要接口。普通头像和附件 URI 字段仍按原后端文本契约展示或填写。 +独立页面只使用配送端数据范围内的关系查询和 CRUD 能力;头像复用公共受控上传入口,并通过配送点专属鉴权接口读取。不引入合同附件上传、平台角色或跨组织账户摘要接口,附件 URI 字段仍按原后端文本契约展示或填写。 diff --git a/frontend/delivery_admin/scripts/check-resource-pages.mjs b/frontend/delivery_admin/scripts/check-resource-pages.mjs index 76e4e5a..3172abd 100644 --- a/frontend/delivery_admin/scripts/check-resource-pages.mjs +++ b/frontend/delivery_admin/scripts/check-resource-pages.mjs @@ -65,5 +65,12 @@ assert(routeKeys.has('GET /user_account/:identity/avatar'), '用户账户缺少 const recordPage = read('src/views/resource/ResourceRecordPage.vue'); assert(recordPage.includes('ResourceAccountSummary'), '账户资源页尚未接入 5173 头像摘要卡片'); assert(recordPage.includes("field.key !== 'avatar'"), '头像字段仍可能显示为普通文本框'); +const listPage = read('src/views/shared/ResourceListPage.vue'); +assert(!listPage.includes('title="ID"'), '标准列表仍暴露数据库自增 ID'); +assert(listPage.includes('ProtectedAvatarThumbnail'), '账户列表尚未接入受控头像缩略图'); +assert(listPage.includes('avatarLoader.reset()'), '列表刷新未清理头像缓存和请求'); +const avatarLoader = read('src/views/shared/protected-list-avatar-loader.ts'); +assert(avatarLoader.includes('MAX_CONCURRENT_REQUESTS = 6'), '头像请求并发上限未与 5173 对齐'); +assert(avatarLoader.includes("new Set(['staff_account', 'user_account'])"), '头像列表资源白名单不正确'); console.log(`独立资源页面契约通过:详情 ${listResources.length},新建 ${creatable.size},编辑 ${editable.size}`); diff --git a/frontend/delivery_admin/src/api/avatar.ts b/frontend/delivery_admin/src/api/avatar.ts index f018267..3e99de4 100644 --- a/frontend/delivery_admin/src/api/avatar.ts +++ b/frontend/delivery_admin/src/api/avatar.ts @@ -37,10 +37,14 @@ async function upload(file: File): Promise { } /** 读取配送点权限范围内的受保护头像。 */ -async function load(resource: string, identity: string): Promise { +async function load( + resource: string, + identity: string, + signal?: AbortSignal, +): Promise { const response = await fetch( `${deliveryApiBaseURL}${resource}/${encodeURIComponent(identity)}/avatar`, - { headers: authorizationHeaders() }, + { headers: authorizationHeaders(), signal }, ); if (response.status === 404) return undefined; if (!response.ok) throw new Error('头像读取失败'); diff --git a/frontend/delivery_admin/src/views/shared/ProtectedAvatarThumbnail.vue b/frontend/delivery_admin/src/views/shared/ProtectedAvatarThumbnail.vue new file mode 100644 index 0000000..bebbd28 --- /dev/null +++ b/frontend/delivery_admin/src/views/shared/ProtectedAvatarThumbnail.vue @@ -0,0 +1,103 @@ + + + + + + diff --git a/frontend/delivery_admin/src/views/shared/ResourceListPage.vue b/frontend/delivery_admin/src/views/shared/ResourceListPage.vue index 4f83ac1..68ec20b 100644 --- a/frontend/delivery_admin/src/views/shared/ResourceListPage.vue +++ b/frontend/delivery_admin/src/views/shared/ResourceListPage.vue @@ -30,21 +30,26 @@