fix: 优化工作人员重复信息提示

This commit is contained in:
czl231
2026-08-11 13:37:44 +08:00
parent 5738a43f3e
commit 4d79a4917e
12 changed files with 440 additions and 9 deletions

View File

@@ -205,7 +205,15 @@ func GetByIdentity[T any](ctx *gin.Context) {
infra.Response.Success(ctx, ProtectPreciseLocation(ctx, new(T), response))
}
// UpdateAllowedByIdentity 按白名单更新资源,并保持原有数据库错误响应行为。
func UpdateAllowedByIdentity(ctx *gin.Context, model any, values map[string]any, allowedFields []string) {
UpdateAllowedByIdentityWithError(ctx, model, values, allowedFields, nil)
}
// UpdateAllowedByIdentityWithError 按白名单更新资源,并允许调用方转换数据库写入错误。
// 参数transformError 为空时保持原有错误响应,非空时仅转换数据库更新错误。
// 返回值:无,处理结果通过统一 HTTP 响应写入。
func UpdateAllowedByIdentityWithError(ctx *gin.Context, model any, values map[string]any, allowedFields []string, transformError func(error) error) {
values = FilterFields(values, allowedFields)
if len(values) == 0 {
infra.Response.Success(ctx, gin.H{"updated": false})
@@ -214,6 +222,9 @@ func UpdateAllowedByIdentity(ctx *gin.Context, model any, values map[string]any,
result := ActiveRecords(impl.DBService.Model(model)).Where("identity = ?", ctx.Param("identity")).Updates(values)
if result.Error != nil {
if transformError != nil {
result.Error = transformError(result.Error)
}
infra.Response.Error(ctx, result.Error)
return
}

View File

@@ -0,0 +1,52 @@
// 功能描述:转换工作人员写入时的数据库错误,避免向客户端泄露数据库实现细节。
// 版本v1.0
package common
import (
"errors"
"log"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/jackc/pgx/v5/pgconn"
)
const (
staffUsernameUniqueConstraint = "idx_staff_account_username"
staffPhoneUniqueConstraint = "idx_staff_account_phone"
)
var (
errStaffUsernameExists = errors.New("该用户名已存在")
errStaffPhoneExists = errors.New("该联系电话已被其他工作人员使用")
errStaffSaveFailed = errors.New("保存失败,请稍后重试")
)
// CreateStaffRecord 创建工作人员记录,并统一转换数据库写入错误。
// 参数staff 为已经完成业务校验和密码哈希处理的工作人员模型。
// 返回值:创建成功返回 nil失败返回脱敏后的工作人员写入错误。
func CreateStaffRecord(staff *models.StaffAccount) error {
if err := impl.DBService.Create(staff).Error; err != nil {
return StaffWriteError(err)
}
return nil
}
// StaffWriteError 将工作人员写入错误转换为稳定的中文业务提示。
// 参数err 为工作人员创建或更新操作返回的数据库错误。
// 返回值:已知唯一约束冲突返回对应业务提示,其他错误脱敏后返回通用保存失败提示。
func StaffWriteError(err error) error {
var postgresError *pgconn.PgError
if errors.As(err, &postgresError) && postgresError.Code == "23505" {
switch postgresError.ConstraintName {
case staffUsernameUniqueConstraint:
return errStaffUsernameExists
case staffPhoneUniqueConstraint:
return errStaffPhoneExists
}
}
// 未识别错误仅写入服务端日志,客户端不能看到数据库表名、约束名或 SQLSTATE。
log.Printf("staff write failed: %v", err)
return errStaffSaveFailed
}

View File

@@ -0,0 +1,143 @@
// 功能描述:验证工作人员数据库写入错误的业务化转换与脱敏规则。
// 版本v1.0
package common
import (
"errors"
"fmt"
"net/http/httptest"
"strings"
"testing"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgconn"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// TestStaffWriteErrorKnownConflicts 验证用户名和联系电话冲突会返回明确中文提示。
// 参数t 为 Go 测试上下文。
// 返回值:无。
func TestStaffWriteErrorKnownConflicts(t *testing.T) {
tests := []struct {
name string
constraint string
message string
}{
{name: "用户名重复", constraint: staffUsernameUniqueConstraint, message: "该用户名已存在"},
{name: "联系电话重复", constraint: staffPhoneUniqueConstraint, message: "该联系电话已被其他工作人员使用"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
databaseError := &pgconn.PgError{Code: "23505", ConstraintName: test.constraint}
if actual := StaffWriteError(databaseError).Error(); actual != test.message {
t.Fatalf("期望提示 %q实际为 %q", test.message, actual)
}
})
}
}
// TestStaffWriteErrorWrappedConflict 验证被包装的 PostgreSQL 冲突仍能被准确识别。
// 参数t 为 Go 测试上下文。
// 返回值:无。
func TestStaffWriteErrorWrappedConflict(t *testing.T) {
databaseError := &pgconn.PgError{Code: "23505", ConstraintName: staffPhoneUniqueConstraint}
wrapped := fmt.Errorf("create staff: %w", databaseError)
if actual := StaffWriteError(wrapped).Error(); actual != "该联系电话已被其他工作人员使用" {
t.Fatalf("期望识别包装后的联系电话冲突,实际为 %q", actual)
}
}
// TestStaffWriteErrorSanitizesUnknownErrors 验证未知约束和普通数据库异常不会泄露原始信息。
// 参数t 为 Go 测试上下文。
// 返回值:无。
func TestStaffWriteErrorSanitizesUnknownErrors(t *testing.T) {
tests := []struct {
name string
err error
}{
{name: "未知唯一约束", err: &pgconn.PgError{Code: "23505", ConstraintName: "idx_private_constraint"}},
{name: "普通数据库异常", err: errors.New("database unavailable at private-host")},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if actual := StaffWriteError(test.err).Error(); actual != "保存失败,请稍后重试" {
t.Fatalf("期望通用脱敏提示,实际为 %q", actual)
}
})
}
}
// TestCreateStaffRecordTransformsConflict 验证三个入口共用的创建函数会转换唯一约束错误。
// 参数t 为 Go 测试上下文。
// 返回值:无。
func TestCreateStaffRecordTransformsConflict(t *testing.T) {
mock := setupStaffWriteDatabase(t)
mock.ExpectQuery(`INSERT INTO "staff_account"`).
WillReturnError(&pgconn.PgError{Code: "23505", ConstraintName: staffUsernameUniqueConstraint})
err := CreateStaffRecord(&models.StaffAccount{})
if err == nil || err.Error() != "该用户名已存在" {
t.Fatalf("期望工作人员创建返回用户名冲突,实际为 %v", err)
}
assertStaffWriteExpectations(t, mock)
}
// TestUpdateAllowedByIdentityWithErrorTransformsConflict 验证编辑联系电话冲突会经过调用方转换器。
// 参数t 为 Go 测试上下文。
// 返回值:无。
func TestUpdateAllowedByIdentityWithErrorTransformsConflict(t *testing.T) {
mock := setupStaffWriteDatabase(t)
mock.ExpectExec(`UPDATE "staff_account"`).
WillReturnError(&pgconn.PgError{Code: "23505", ConstraintName: staffPhoneUniqueConstraint})
gin.SetMode(gin.TestMode)
response := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(response)
ctx.Params = gin.Params{{Key: "identity", Value: "staff-identity"}}
UpdateAllowedByIdentityWithError(ctx, &models.StaffAccount{}, map[string]any{"phone": "13800000000"}, []string{"phone"}, StaffWriteError)
if !strings.Contains(response.Body.String(), "该联系电话已被其他工作人员使用") {
t.Fatalf("期望响应包含联系电话冲突提示,实际为 %s", response.Body.String())
}
assertStaffWriteExpectations(t, mock)
}
// setupStaffWriteDatabase 创建工作人员写入测试使用的 PostgreSQL 模拟连接。
// 参数t 为 Go 测试上下文。
// 返回值SQL 模拟器,测试结束后自动恢复全局数据库连接。
func setupStaffWriteDatabase(t *testing.T) sqlmock.Sqlmock {
t.Helper()
connection, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("创建数据库模拟连接失败:%v", err)
}
database, err := gorm.Open(postgres.New(postgres.Config{Conn: connection}), &gorm.Config{SkipDefaultTransaction: true})
if err != nil {
_ = connection.Close()
t.Fatalf("创建 GORM 测试连接失败:%v", err)
}
previous := impl.DBService
impl.DBService = database
t.Cleanup(func() {
impl.DBService = previous
_ = connection.Close()
})
return mock
}
// assertStaffWriteExpectations 验证工作人员写入测试声明的 SQL 均已执行。
// 参数t 为 Go 测试上下文mock 为 SQL 模拟器。
// 返回值:无。
func assertStaffWriteExpectations(t *testing.T, mock sqlmock.Sqlmock) {
t.Helper()
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("工作人员写入 SQL 未符合预期:%v", err)
}
}

View File

@@ -0,0 +1,71 @@
// 功能描述:验证三个管理后台的工作人员写入入口均接入公共错误转换链路。
// 版本v1.0
package common
import (
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"runtime"
"testing"
)
// TestStaffHandlersUseWriteErrorConversion 防止任一后台重新绕过工作人员错误转换器。
// 参数t 为 Go 测试上下文。
// 返回值:无。
func TestStaffHandlersUseWriteErrorConversion(t *testing.T) {
_, currentFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("无法定位工作人员接线测试文件")
}
logicDirectory := filepath.Dir(filepath.Dir(currentFile))
targets := []struct {
name string
path string
}{
{name: "平台总后台", path: filepath.Join(logicDirectory, "platform", "staff", "staff.go")},
{name: "气站后台", path: filepath.Join(logicDirectory, "gas", "staff.go")},
{name: "配送点后台", path: filepath.Join(logicDirectory, "delivery", "staff.go")},
}
for _, target := range targets {
t.Run(target.name, func(t *testing.T) {
calls := commonCallsInFile(t, target.path)
if calls["CreateStaffRecord"] == 0 {
t.Fatalf("%s工作人员创建入口未接入 CreateStaffRecord", target.name)
}
if calls["UpdateAllowedByIdentityWithError"] == 0 {
t.Fatalf("%s工作人员编辑入口未接入错误转换更新函数", target.name)
}
})
}
}
// commonCallsInFile 统计源文件中调用 common 包函数的次数。
// 参数t 为测试上下文path 为待检查的 Go 源文件。
// 返回值:以函数名为键、调用次数为值的映射。
func commonCallsInFile(t *testing.T, path string) map[string]int {
t.Helper()
file, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
if err != nil {
t.Fatalf("解析工作人员处理器失败:%v", err)
}
calls := map[string]int{}
ast.Inspect(file, func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return true
}
selector, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
packageName, ok := selector.X.(*ast.Ident)
if ok && packageName.Name == "common" {
calls[selector.Sel.Name]++
}
return true
})
return calls
}

View File

@@ -71,7 +71,7 @@ func CreateStaff(ctx *gin.Context) {
Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: "delivery",
GasBasicID: point.GasBasicID, DeliveryBasicID: point.ID, WorkStatus: request.WorkStatus,
}
if err := db().Create(&staff).Error; err != nil {
if err := common.CreateStaffRecord(&staff); err != nil {
infra.Response.Error(ctx, err)
return
}
@@ -92,9 +92,9 @@ func UpdateStaff(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.UpdateAllowedByIdentity(ctx, &models.StaffAccount{}, gin.H{
common.UpdateAllowedByIdentityWithError(ctx, &models.StaffAccount{}, gin.H{
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "work_status": request.WorkStatus,
}, []string{"name", "phone", "avatar", "work_status"})
}, []string{"name", "phone", "avatar", "work_status"}, common.StaffWriteError)
}
func ResetStaffPassword(ctx *gin.Context) {

View File

@@ -92,7 +92,7 @@ func CreateStaff(ctx *gin.Context) {
Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: request.RoleCode,
GasBasicID: station.ID, DeliveryBasicID: deliveryID, WorkStatus: request.WorkStatus,
}
if err := impl.DBService.Create(&staff).Error; err != nil {
if err := common.CreateStaffRecord(&staff); err != nil {
infra.Response.Error(ctx, err)
return
}
@@ -120,10 +120,10 @@ func UpdateStaff(ctx *gin.Context) {
}
deliveryID = delivery.ID
}
common.UpdateAllowedByIdentity(ctx, &models.StaffAccount{}, gin.H{
common.UpdateAllowedByIdentityWithError(ctx, &models.StaffAccount{}, gin.H{
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "role_code": request.RoleCode,
"delivery_basic_id": deliveryID, "work_status": request.WorkStatus,
}, []string{"name", "phone", "avatar", "role_code", "delivery_basic_id", "work_status"})
}, []string{"name", "phone", "avatar", "role_code", "delivery_basic_id", "work_status"}, common.StaffWriteError)
}
func UpdateStaffStatus(ctx *gin.Context) {

View File

@@ -104,7 +104,7 @@ func CreateStaff(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := impl.DBService.Create(&staff).Error; err != nil {
if err := common.CreateStaffRecord(&staff); err != nil {
infra.Response.Error(ctx, err)
return
}
@@ -145,7 +145,7 @@ func UpdateStaff(ctx *gin.Context) {
if request.Avatar != nil {
values["avatar"] = *request.Avatar
}
common.UpdateAllowedByIdentity(ctx, &models.StaffAccount{}, values, []string{"name", "phone", "avatar", "role_code", "gas_basic_id", "delivery_basic_id", "work_status"})
common.UpdateAllowedByIdentityWithError(ctx, &models.StaffAccount{}, values, []string{"name", "phone", "avatar", "role_code", "gas_basic_id", "delivery_basic_id", "work_status"}, common.StaffWriteError)
}
func validWorkStatus(status string) bool { return status == "on_duty" || status == "off_duty" }

View File

@@ -0,0 +1,67 @@
# 工作人员唯一冲突提示操作日志
操作时间2026-08-11 13:29:44
操作类型:修改
影响模块:平台总后台、气站后台、配送点后台工作人员新增与编辑
## 操作前状态
- `staff_account` 的用户名和非空联系电话分别由数据库唯一索引保护,但三个后台的工作人员写入入口都会把 PostgreSQL 原始错误直接返回前端。
- 用户遇到重复数据时会看到约束名、SQLSTATE 等数据库实现细节,无法判断需要修改用户名还是联系电话。
- 保存失败后各前端会保留现有表单5173平台后台若已上传头像重新保存时仍会再次上传同一文件可能产生重复或孤立资源。
- 公共 `UpdateAllowedByIdentity` 没有为特定资源转换数据库错误的扩展点。
## 具体操作
- 新增工作人员写入错误转换器:
- 精确识别 `idx_staff_account_username`,返回“该用户名已存在”。
- 精确识别 `idx_staff_account_phone`,返回“该联系电话已被其他工作人员使用”。
- 未识别数据库错误只写服务端日志,客户端统一收到“保存失败,请稍后重试”。
- 新增公共工作人员创建函数,让平台、气站、配送点三个入口统一执行错误转换。
- 以向下兼容方式扩展公共白名单更新函数:原函数签名和行为保持不变,工作人员编辑入口使用带错误转换器的新函数。
- 三个后台的工作人员创建均接入用户名和联系电话冲突转换;三个后台的工作人员编辑均接入联系电话冲突转换。
- 新增5173头像上传结果缓存
- 头像上传成功但资源保存失败时,下一次保存复用同一受控 URI。
- 选择新文件、清除头像或重新加载资料时清空缓存。
- 头像上传本身失败时自动清空缓存,允许再次上传。
- 新增头像失败重试行为检查脚本,并加入平台后台脚本命令。
## 操作后状态
- 三个后台均不再向工作人员表单暴露数据库表名、索引名或 SQLSTATE。
- 用户名重复只在新建时返回明确提示;联系电话重复在新建和编辑时均返回明确提示。
- 联系电话继续允许留空,非空值保持全表唯一;已归档工作人员继续占用用户名和非空联系电话。
- 保存失败后表单、下拉选择和头像预览保持不变5173再次保存不会重复上传已经成功上传的头像。
- 现有 HTTP 200 加 JSON 业务错误结构保持不变,没有新增保存前查重接口或数据库迁移。
## 代码变更
- `backend/api/internal/logic/common/staff_error.go`:新增 `CreateStaffRecord``StaffWriteError`,统一处理创建写入、唯一冲突与未知错误脱敏。
- `backend/api/internal/logic/common/staff_error_test.go`:覆盖已知冲突、包装错误、未知错误脱敏、创建和编辑写入路径。
- `backend/api/internal/logic/common/staff_wiring_test.go`:验证三个后台的创建和编辑入口均接入公共错误转换链路。
- `backend/api/internal/logic/common/base.go`:新增 `UpdateAllowedByIdentityWithError`,原 `UpdateAllowedByIdentity` 保持兼容。
- `backend/api/internal/logic/platform/staff/staff.go`:接入平台工作人员创建和编辑错误转换。
- `backend/api/internal/logic/gas/staff.go`:接入气站工作人员创建和编辑错误转换。
- `backend/api/internal/logic/delivery/staff.go`:接入配送点工作人员创建和编辑错误转换。
- `frontend/platform_admin/src/views/resource/avatar-upload-cache.ts`:新增单次保存流程头像 URI 缓存。
- `frontend/platform_admin/src/views/resource/use-resource-avatar.ts`:接入头像上传缓存及失效规则。
- `frontend/platform_admin/scripts/check-avatar-upload-cache.mjs``package.json`:新增头像失败重试行为检查命令。
## 验证结果
- `go test ./internal/logic/common`:通过,工作人员冲突转换、未知错误脱敏、创建和编辑路径均通过。
- `go test ./internal/logic/platform/staff ./internal/logic/gas ./internal/logic/delivery`:通过,三个工作人员处理器编译与既有测试正常。
- `go test ./...`:通过,后端完整回归测试正常。
- `npm.cmd run avatar-retry:check`:通过,同一头像保存重试仅上传一次,重置与上传失败后可以再次上传。
- `npm.cmd run type:check`:通过。
- `npm.cmd run resource-pages:check`通过详情46类、新建25类、编辑23类。
- `npm.cmd run build`通过Vite生产构建完成。
- `git diff --check`:通过,未发现空白符错误。
## 风险评估
- 唯一冲突识别依赖当前 PostgreSQL 约束名;模型继续沿用现有索引定义时行为稳定,相关常量和测试可防止无意改名。
- 未识别工作人员写入错误会对客户端脱敏,但原始错误仍写入服务端日志,便于定位数据库连接或约束异常。
- 头像 URI只在当前5173资料页实例中缓存页面卸载、选择新头像或清除头像后立即失效不跨账户复用。
- 气站后台和配送点后台当前没有真实头像文件上传能力,本次不为其增加头像缓存或改变现有表单结构。
- 本次不修改气站与配送点联动、详情唯一标识展示或编辑页冗余详情请求等独立问题。

View File

@@ -15,6 +15,7 @@
"contract:check": "node scripts/check-backend-contract.mjs",
"resource-pages:check": "node scripts/check-resource-pages.mjs",
"account-roles:check": "node scripts/check-account-role-presentation.mjs",
"avatar-retry:check": "node scripts/check-avatar-upload-cache.mjs",
"audit:platform": "node scripts/check-backend-contract.mjs",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",

View File

@@ -0,0 +1,46 @@
/**
* 功能:验证头像上传成功后的保存重试会复用受控资源地址。
* 版本v1.0.0
*/
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { transformWithOxc } from 'vite';
const sourceURL = new URL(
'../src/views/resource/avatar-upload-cache.ts',
import.meta.url,
);
const source = await readFile(sourceURL, 'utf8');
const transformed = await transformWithOxc(source, sourceURL.pathname);
const moduleURL = `data:text/javascript;base64,${Buffer.from(transformed.code).toString('base64')}`;
const { createAvatarUploadCache } = await import(moduleURL);
let uploadCount = 0;
const cache = createAvatarUploadCache(async () => {
uploadCount += 1;
return { uri: `/uploads/avatars/test-${uploadCount}.png` };
});
const file = { name: 'avatar.png', size: 128, lastModified: 1 };
const firstURI = await cache.resolve(file);
const retryURI = await cache.resolve(file);
assert.equal(firstURI, '/uploads/avatars/test-1.png');
assert.equal(retryURI, firstURI);
assert.equal(uploadCount, 1, '保存失败后重试不应重复上传同一头像');
cache.reset();
const replacedURI = await cache.resolve(file);
assert.equal(replacedURI, '/uploads/avatars/test-2.png');
assert.equal(uploadCount, 2, '重置缓存后应重新上传头像');
let failedCount = 0;
const retryableCache = createAvatarUploadCache(async () => {
failedCount += 1;
if (failedCount === 1) throw new Error('临时上传失败');
return { uri: '/uploads/avatars/retry.png' };
});
await assert.rejects(() => retryableCache.resolve(file), /临时上传失败/);
assert.equal(await retryableCache.resolve(file), '/uploads/avatars/retry.png');
assert.equal(failedCount, 2, '上传失败后应允许重新上传');
console.log('头像失败重试缓存检查通过');

View File

@@ -0,0 +1,35 @@
/**
* 功能:缓存一次资源保存流程中已经完成的头像上传结果。
* 版本v1.0.0
*/
export type AvatarUpload = (file: File) => Promise<{ uri: string }>;
/** 创建头像上传缓存;选择新文件或清除头像时由调用方主动重置。 */
export function createAvatarUploadCache(upload: AvatarUpload) {
let cachedFile: File | undefined;
let cachedURI: Promise<string> | undefined;
/** 返回当前文件的受控资源地址;同一文件重试时复用首次成功结果。 */
async function resolve(file: File) {
if (cachedFile !== file || !cachedURI) {
cachedFile = file;
cachedURI = upload(file)
.then((result) => result.uri)
.catch((error) => {
// 上传本身失败时清空缓存,允许用户直接重试。
if (cachedFile === file) cachedURI = undefined;
throw error;
});
}
return cachedURI;
}
/** 清空已上传地址,确保新选择的文件不会错误复用旧头像。 */
function reset() {
cachedFile = undefined;
cachedURI = undefined;
}
return { resolve, reset };
}

View File

@@ -6,12 +6,14 @@ import { Message } from '@arco-design/web-vue';
import { onBeforeUnmount, ref } from 'vue';
import { avatarApi } from '@/api/avatar';
import { DEFAULT_USER_AVATAR } from '@/constants/avatar';
import { createAvatarUploadCache } from './avatar-upload-cache';
export function useResourceAvatar() {
const url = ref(DEFAULT_USER_AVATAR);
const file = ref<File>();
const cleared = ref(false);
const canClear = ref(false);
const uploadCache = createAvatarUploadCache(avatarApi.upload);
let objectURL = '';
function revoke() {
@@ -22,6 +24,7 @@ export function useResourceAvatar() {
/** 加载现有受保护头像,记录没有头像时继续使用默认图。 */
async function load(resource: string, identity: string) {
revoke();
uploadCache.reset();
url.value = DEFAULT_USER_AVATAR;
canClear.value = false;
file.value = undefined;
@@ -45,6 +48,7 @@ export function useResourceAvatar() {
return;
}
revoke();
uploadCache.reset();
objectURL = URL.createObjectURL(next);
url.value = objectURL;
file.value = next;
@@ -55,6 +59,7 @@ export function useResourceAvatar() {
/** 标记清除头像,真正写入空值发生在保存资料时。 */
function clear() {
revoke();
uploadCache.reset();
url.value = DEFAULT_USER_AVATAR;
file.value = undefined;
cleared.value = true;
@@ -63,7 +68,7 @@ export function useResourceAvatar() {
/** 将头像变化写入资源更新载荷。 */
async function applyToPayload(payload: Record<string, unknown>) {
if (file.value) payload.avatar = (await avatarApi.upload(file.value)).uri;
if (file.value) payload.avatar = await uploadCache.resolve(file.value);
else if (cleared.value) payload.avatar = '';
}