增加气站历史账户显式修复与审计
This commit is contained in:
@@ -3,6 +3,8 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -20,6 +22,7 @@ import (
|
||||
gaslogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/gas"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/repair"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/routers"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/seed"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -85,6 +88,11 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("mock refund reviewers repaired: %d\n", count)
|
||||
case "repair-legacy-gas-account":
|
||||
if err := repairLegacyGasAccount(os.Args[2:], os.Stdout); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "migrate":
|
||||
if err := migrateDatabase(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
@@ -99,7 +107,7 @@ func main() {
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|gas-resource-contract|delivery-resource-contract|migrate|mock-data|repair-mock-gasorder-status|repair-mock-ec-order|repair-mock-track-point|repair-mock-refund-reviewer>")
|
||||
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|gas-resource-contract|delivery-resource-contract|migrate|mock-data|repair-mock-gasorder-status|repair-mock-ec-order|repair-mock-track-point|repair-mock-refund-reviewer|repair-legacy-gas-account>")
|
||||
}
|
||||
|
||||
type route struct {
|
||||
@@ -293,6 +301,49 @@ func repairMockRefundReviewer() (int64, error) {
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// repairLegacyGasAccount 显式修复唯一历史 010 气站账号,并将前后值写入操作审计表。
|
||||
func repairLegacyGasAccount(arguments []string, output io.Writer) error {
|
||||
flags := flag.NewFlagSet("repair-legacy-gas-account", flag.ContinueOnError)
|
||||
flags.SetOutput(io.Discard)
|
||||
operator := flags.String("operator", "", "执行修复的平台账户用户名")
|
||||
displayName := flags.String("display-name", "", "修复后的显示名称")
|
||||
dryRun := flags.Bool("dry-run", false, "只读核对目标,不执行修复")
|
||||
if err := flags.Parse(arguments); err != nil {
|
||||
return fmt.Errorf("parse repair arguments: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(*operator) == "" || strings.TrimSpace(*displayName) == "" {
|
||||
return errors.New("repair-legacy-gas-account requires --operator and --display-name")
|
||||
}
|
||||
|
||||
config.New(serviceKey)
|
||||
if config.Spec.Databases == nil {
|
||||
return fmt.Errorf("database configuration is required")
|
||||
}
|
||||
databaseService, err := database.NewDatabase(
|
||||
config.Spec.Databases.Driver,
|
||||
config.Spec.Databases.Source,
|
||||
dbsql.SetOptions(nil),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect database: %w", err)
|
||||
}
|
||||
if *dryRun {
|
||||
result, err := repair.InspectLegacyGasAccount(databaseService, *operator, *displayName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect legacy gas account: %w", err)
|
||||
}
|
||||
return json.NewEncoder(output).Encode(result)
|
||||
}
|
||||
if err := databaseService.AutoMigrate(&models.AuditOperationLog{}); err != nil {
|
||||
return fmt.Errorf("migrate operation audit table: %w", err)
|
||||
}
|
||||
result, err := repair.RepairLegacyGasAccount(databaseService, *operator, *displayName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("repair legacy gas account: %w", err)
|
||||
}
|
||||
return json.NewEncoder(output).Encode(result)
|
||||
}
|
||||
|
||||
func migrateDatabase() error {
|
||||
config.New(serviceKey)
|
||||
options := &types.SqlOptions{
|
||||
|
||||
24
backend/api/internal/models/audit_operation_log.go
Normal file
24
backend/api/internal/models/audit_operation_log.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// AuditOperationLog 对应 audit_operation_log,保存平台敏感维护操作的不可变审计快照。
|
||||
type AuditOperationLog struct {
|
||||
Entity // 公共实体字段
|
||||
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;index" json:"operator_identity"` // 操作平台账户唯一标识
|
||||
OperatorName string `gorm:"column:operator_name;type:varchar(64);not null" json:"operator_name"` // 操作平台账户登录名快照
|
||||
ResourceType string `gorm:"column:resource_type;type:varchar(64);not null;index" json:"resource_type"` // 被维护资源类型
|
||||
ResourceIdentity string `gorm:"column:resource_identity;type:varchar(36);not null;index" json:"resource_identity"` // 被维护资源唯一标识
|
||||
Action string `gorm:"column:action;type:varchar(64);not null;index" json:"action"` // 审计动作编码
|
||||
BeforeValue string `gorm:"column:before_value;type:text;not null" json:"before_value"` // 修复前字段 JSON 快照
|
||||
AfterValue string `gorm:"column:after_value;type:text;not null" json:"after_value"` // 修复后字段 JSON 快照
|
||||
OccurredAt time.Time `gorm:"column:occurred_at;type:timestamptz;not null;index" json:"occurred_at"` // 操作实际发生时间
|
||||
Remark string `gorm:"column:remark;type:varchar(255);not null;default:''" json:"remark"` // 操作原因或补充说明
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&AuditOperationLog{}) }
|
||||
func (table *AuditOperationLog) TableName() string { return "audit_operation_log" }
|
||||
148
backend/api/internal/repair/gas_account.go
Normal file
148
backend/api/internal/repair/gas_account.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Package repair 提供必须显式执行且保留审计快照的一次性数据修复。
|
||||
package repair
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
legacyGasAccountRole = "010"
|
||||
gasAdminRole = "admin"
|
||||
gasAccountRepairAction = "repair_legacy_gas_account_role"
|
||||
)
|
||||
|
||||
// GasAccountRepairResult 返回本次修复定位到的账号及审计信息。
|
||||
type GasAccountRepairResult struct {
|
||||
AccountIdentity string `json:"account_identity"`
|
||||
Username string `json:"username"`
|
||||
OldRoleCode string `json:"old_role_code"`
|
||||
NewRoleCode string `json:"new_role_code"`
|
||||
OldDisplayName string `json:"old_display_name"`
|
||||
NewDisplayName string `json:"new_display_name"`
|
||||
Operator string `json:"operator"`
|
||||
RepairedAt *time.Time `json:"repaired_at,omitempty"`
|
||||
}
|
||||
|
||||
// InspectLegacyGasAccount 只读校验修复目标和操作者,不修改任何业务或审计数据。
|
||||
func InspectLegacyGasAccount(database *gorm.DB, operatorUsername string, displayName string) (*GasAccountRepairResult, error) {
|
||||
operatorUsername = strings.TrimSpace(operatorUsername)
|
||||
displayName = strings.TrimSpace(displayName)
|
||||
if database == nil || operatorUsername == "" || displayName == "" {
|
||||
return nil, errors.New("database, operator and display name are required")
|
||||
}
|
||||
account, operator, err := legacyGasAccountRepairTarget(database, operatorUsername)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GasAccountRepairResult{
|
||||
AccountIdentity: account.Identity,
|
||||
Username: account.Username,
|
||||
OldRoleCode: account.RoleCode,
|
||||
NewRoleCode: gasAdminRole,
|
||||
OldDisplayName: account.DisplayName,
|
||||
NewDisplayName: displayName,
|
||||
Operator: operator.Username,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func legacyGasAccountRepairTarget(database *gorm.DB, operatorUsername string) (models.GasAccount, models.PlatformAccount, error) {
|
||||
var accounts []models.GasAccount
|
||||
if err := database.Where("role_code = ?", legacyGasAccountRole).Limit(2).Find(&accounts).Error; err != nil {
|
||||
return models.GasAccount{}, models.PlatformAccount{}, fmt.Errorf("find legacy gas account: %w", err)
|
||||
}
|
||||
if len(accounts) == 0 {
|
||||
return models.GasAccount{}, models.PlatformAccount{}, errors.New("legacy gas account role 010 not found")
|
||||
}
|
||||
if len(accounts) > 1 {
|
||||
return models.GasAccount{}, models.PlatformAccount{}, fmt.Errorf("refuse ambiguous repair: found %d gas accounts with role 010", len(accounts))
|
||||
}
|
||||
|
||||
var operator models.PlatformAccount
|
||||
if err := database.Where("username = ? AND status = ?", operatorUsername, common.StatusEnable).First(&operator).Error; err != nil {
|
||||
return models.GasAccount{}, models.PlatformAccount{}, fmt.Errorf("find enabled platform operator %q: %w", operatorUsername, err)
|
||||
}
|
||||
return accounts[0], operator, nil
|
||||
}
|
||||
|
||||
type gasAccountSnapshot struct {
|
||||
RoleCode string `json:"role_code"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
// RepairLegacyGasAccount 将唯一一条历史 010 气站账号显式修复为管理员并写入审计日志。
|
||||
func RepairLegacyGasAccount(database *gorm.DB, operatorUsername string, displayName string) (*GasAccountRepairResult, error) {
|
||||
operatorUsername = strings.TrimSpace(operatorUsername)
|
||||
displayName = strings.TrimSpace(displayName)
|
||||
if database == nil || operatorUsername == "" || displayName == "" {
|
||||
return nil, errors.New("database, operator and display name are required")
|
||||
}
|
||||
|
||||
var result GasAccountRepairResult
|
||||
err := database.Transaction(func(tx *gorm.DB) error {
|
||||
account, operator, err := legacyGasAccountRepairTarget(tx, operatorUsername)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
before := gasAccountSnapshot{RoleCode: account.RoleCode, DisplayName: account.DisplayName}
|
||||
after := gasAccountSnapshot{RoleCode: gasAdminRole, DisplayName: displayName}
|
||||
beforeJSON, err := json.Marshal(before)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal repair before snapshot: %w", err)
|
||||
}
|
||||
afterJSON, err := json.Marshal(after)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal repair after snapshot: %w", err)
|
||||
}
|
||||
|
||||
repairedAt := time.Now()
|
||||
update := tx.Model(&models.GasAccount{}).
|
||||
Where("identity = ? AND role_code = ?", account.Identity, legacyGasAccountRole).
|
||||
Updates(map[string]any{"role_code": gasAdminRole, "display_name": displayName, "updated_at": repairedAt})
|
||||
if update.Error != nil {
|
||||
return fmt.Errorf("repair legacy gas account: %w", update.Error)
|
||||
}
|
||||
if update.RowsAffected != 1 {
|
||||
return errors.New("legacy gas account changed concurrently; repair aborted")
|
||||
}
|
||||
|
||||
audit := models.AuditOperationLog{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
|
||||
OperatorIdentity: operator.Identity,
|
||||
OperatorName: operator.Username,
|
||||
ResourceType: "gas_account",
|
||||
ResourceIdentity: account.Identity,
|
||||
Action: gasAccountRepairAction,
|
||||
BeforeValue: string(beforeJSON),
|
||||
AfterValue: string(afterJSON),
|
||||
OccurredAt: repairedAt,
|
||||
Remark: "显式修复历史非法气站角色编码 010,并补齐显示名称",
|
||||
}
|
||||
if err := tx.Create(&audit).Error; err != nil {
|
||||
return fmt.Errorf("write gas account repair audit: %w", err)
|
||||
}
|
||||
|
||||
result = GasAccountRepairResult{
|
||||
AccountIdentity: account.Identity,
|
||||
Username: account.Username,
|
||||
OldRoleCode: before.RoleCode,
|
||||
NewRoleCode: after.RoleCode,
|
||||
OldDisplayName: before.DisplayName,
|
||||
NewDisplayName: after.DisplayName,
|
||||
Operator: operator.Username,
|
||||
RepairedAt: &repairedAt,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
84
backend/api/internal/repair/gas_account_test.go
Normal file
84
backend/api/internal/repair/gas_account_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package repair
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func repairTestDatabase(t *testing.T) (*gorm.DB, sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
sqlDatabase, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("创建 SQL Mock 失败:%v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = sqlDatabase.Close() })
|
||||
database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("创建 GORM 数据库失败:%v", err)
|
||||
}
|
||||
return database, mock
|
||||
}
|
||||
|
||||
func TestRepairLegacyGasAccountWritesUpdateAndAuditInOneTransaction(t *testing.T) {
|
||||
database, mock := repairTestDatabase(t)
|
||||
now := time.Now()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "gas_account" WHERE role_code = $1 AND "gas_account"."deleted_at" IS NULL LIMIT $2`)).
|
||||
WithArgs(legacyGasAccountRole, 2).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "username", "display_name", "role_code", "status", "created_at", "updated_at"}).
|
||||
AddRow(9, "gas-account-identity", "xuehai", "", legacyGasAccountRole, 1, now, now))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_account" WHERE (username = $1 AND status = $2) AND "platform_account"."deleted_at" IS NULL ORDER BY "platform_account"."id" LIMIT $3`)).
|
||||
WithArgs("root", 1, 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "username", "platform_role_code", "status", "created_at", "updated_at"}).
|
||||
AddRow(1, "root-account-identity", "root", "root", 1, now, now))
|
||||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "gas_account" SET "display_name"=$1,"role_code"=$2,"updated_at"=$3 WHERE (identity = $4 AND role_code = $5) AND "gas_account"."deleted_at" IS NULL`)).
|
||||
WithArgs("薛海", gasAdminRole, sqlmock.AnyArg(), "gas-account-identity", legacyGasAccountRole).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectQuery(`INSERT INTO "audit_operation_log"`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
result, err := RepairLegacyGasAccount(database, "root", " 薛海 ")
|
||||
if err != nil {
|
||||
t.Fatalf("修复失败:%v", err)
|
||||
}
|
||||
if result.AccountIdentity != "gas-account-identity" || result.OldRoleCode != "010" || result.NewRoleCode != "admin" {
|
||||
t.Fatalf("修复结果不正确:%#v", result)
|
||||
}
|
||||
if result.OldDisplayName != "" || result.NewDisplayName != "薛海" || result.Operator != "root" || result.RepairedAt == nil {
|
||||
t.Fatalf("修复审计摘要不完整:%#v", result)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("数据库事务与审计写入不符合预期:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepairLegacyGasAccountRefusesAmbiguousTarget(t *testing.T) {
|
||||
database, mock := repairTestDatabase(t)
|
||||
now := time.Now()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "gas_account" WHERE role_code = $1 AND "gas_account"."deleted_at" IS NULL LIMIT $2`)).
|
||||
WithArgs(legacyGasAccountRole, 2).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "role_code", "created_at", "updated_at"}).
|
||||
AddRow(1, "first", legacyGasAccountRole, now, now).
|
||||
AddRow(2, "second", legacyGasAccountRole, now, now))
|
||||
mock.ExpectRollback()
|
||||
|
||||
if _, err := RepairLegacyGasAccount(database, "root", "薛海"); err == nil {
|
||||
t.Fatal("存在多个 010 账号时必须拒绝修复")
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("歧义目标不应产生写操作:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectLegacyGasAccountRejectsMissingArguments(t *testing.T) {
|
||||
if _, err := InspectLegacyGasAccount(nil, "root", "薛海"); err == nil {
|
||||
t.Fatal("缺少数据库时必须拒绝预检")
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@
|
||||
|
||||
气站只能由平台总后台新增,创建后直接进入启用状态,无需审核。已启用或已停用气站可继续切换启停状态。
|
||||
|
||||
气站账户与配送点账户当前均只有管理员角色。新建时服务端分别固定写入 `admin`,编辑时不允许变更角色;旧客户端继续提交 `role_code` 时服务端兼容接收但不采纳。列表、详情、新建和编辑页面统一显示“角色”,并将 `admin` 显示为“气站管理员”或“配送点管理员”。历史未知编码显示为“未知角色(原编码)”,不得自动改成管理员。
|
||||
气站账户与配送点账户当前均只有管理员角色。新建时服务端分别固定写入 `admin`,编辑时不允许变更角色;旧客户端继续提交 `role_code` 时服务端兼容接收但不采纳。列表、详情、新建和编辑页面统一显示“角色”,并将 `admin` 显示为“气站管理员”或“配送点管理员”。气站账户未填写显示名称时明确展示“未填写”,不使用含义不明的横线。历史未知编码显示为“未知角色(原编码)”,不得自动改成管理员;确需修复时必须通过定向维护命令校验唯一目标,并记录操作者、修复时间及修复前后值。
|
||||
|
||||
气站账户、配送点账户和生产商账户没有头像字段与受控头像接口。它们的详情和编辑页使用紧凑文字账户摘要,新建页直接显示基本信息表单,不展示无法上传或保存的默认头像。工作人员、用户和平台账户继续使用真实头像上传与受控读取链路。
|
||||
|
||||
@@ -308,6 +308,8 @@
|
||||
|
||||
平台账户必须关联角色编码。系统角色和 root 账户受保护,不能通过普通管理动作破坏。角色菜单通过 `/platform_role/:identity/menu` 整体读取和替换,菜单自身由系统初始化,不在后台任意增删改。
|
||||
|
||||
定向数据修复不开放为普通页面能力。修复命令必须校验唯一目标和有效平台操作者,并在同一事务向 `audit_operation_log` 追加资源标识、动作、操作者、发生时间及修复前后快照;审计记录不提供通用编辑或删除入口。
|
||||
|
||||
## 7. 通用页面与接口行为
|
||||
|
||||
- 列表资源提供分页、字段筛选、状态展示、详情和关联资源选择。
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
| 履约 | `ord_service_task`、`dsp_assignment`、`ord_delivery_track`、`dsp_delivery_track_point` | 任务状态转换受限;定位、轨迹和证据均有采集时间 |
|
||||
| 资金与押金 | `wal_wallet_ledger`、`wal_deposit`、`wal_deposit_refund`、`wal_settlement`、`wal_withdrawal`、`wal_reconciliation` | `wal_wallet_ledger` 为唯一钱包事实流水;金额方向、关联对象和余额快照可校验;退押金保留验收、扣减和退款去向 |
|
||||
| 用气统计 | `dev_usage_stat`、`dev_usage_report` | 明确统计周期、单位、来源、计算版本和最后更新时间;报表导出留痕 |
|
||||
| 内容审计 | `cnt_article`、`cnt_banner`、`cnt_notice`、`aud_operation_log` | 发布有版本;审计日志追加写入且设置留存期限 |
|
||||
| 内容审计 | `cms_content`、`audit_operation_log` | 内容发布有版本;敏感维护操作的审计日志追加写入并设置留存期限 |
|
||||
|
||||
## 2. API 约定
|
||||
|
||||
|
||||
36
docs/操作日志_气站历史账号角色显式修复_20260817.md
Normal file
36
docs/操作日志_气站历史账号角色显式修复_20260817.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 操作日志:气站历史账号角色显式修复
|
||||
|
||||
操作时间:2026-08-17 22:12:42(Asia/Shanghai)
|
||||
操作类型:定向数据修复与展示优化
|
||||
执行管理员:`root`
|
||||
|
||||
## 问题拆分
|
||||
|
||||
- 气站账户未填写显示名称时,列表和详情使用通用横线,无法区分“未填写”和加载异常。
|
||||
- 历史账号 `019fd0e4-6d29-7c54-b2fd-0e8acb2079e1` 保存了非法角色编码 `010`,不满足气站端仅允许 `admin` 登录的约束。
|
||||
|
||||
## 修复内容
|
||||
|
||||
- 气站账户 `display_name` 空值统一展示为“未填写”。
|
||||
- 新增显式维护命令 `repair-legacy-gas-account`,要求提供平台操作者和目标显示名称。
|
||||
- 命令仅允许修复唯一一条 `role_code = 010` 的气站账号;不存在或多条匹配时拒绝执行。
|
||||
- 账号“薛海”的角色由 `010` 修复为 `admin`,显示名称由空字符串补齐为“薛海”。
|
||||
- 同一数据库事务写入 `audit_operation_log`,记录操作者、资源标识、修复时间、修复前后 JSON 和原因。
|
||||
|
||||
## 审计快照
|
||||
|
||||
| 项目 | 修复前 | 修复后 |
|
||||
| --- | --- | --- |
|
||||
| 账号唯一标识 | `019fd0e4-6d29-7c54-b2fd-0e8acb2079e1` | 不变 |
|
||||
| 用户名 | 薛海 | 不变 |
|
||||
| 显示名称 | 空字符串 | 薛海 |
|
||||
| 角色编码 | `010` | `admin` |
|
||||
| 执行管理员 | `root` | `root` |
|
||||
| 修复时间 | - | `2026-08-17T22:12:42.6845244+08:00` |
|
||||
|
||||
审计记录唯一标识:`01a01011-48ad-7042-b57f-96173f4010d2`。
|
||||
|
||||
## 边界
|
||||
|
||||
- 不修改普通新增和编辑接口的角色规则;新建仍固定写入 `admin`,编辑仍不接受角色变更。
|
||||
- 不根据页面展示自动迁移未知角色,也不把其他历史未知编码升级为管理员。
|
||||
@@ -30,6 +30,11 @@ expectIncludes(
|
||||
"fixedAdminRole('gas_account')",
|
||||
'平台总后台缺少气站管理员中文映射',
|
||||
);
|
||||
expectIncludes(
|
||||
platformResources,
|
||||
"f('display_name', { emptyText: '未填写' }), fixedAdminRole('gas_account')",
|
||||
'气站账户空显示名称未明确展示为未填写',
|
||||
);
|
||||
expectIncludes(
|
||||
platformResources,
|
||||
"fixedAdminRole('delivery_account')",
|
||||
|
||||
@@ -490,7 +490,7 @@ const productStatusOptions = [
|
||||
|
||||
export const resources: ResourceUiDefinition[] = [
|
||||
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' },
|
||||
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('gas_account'), relation('gas_basic_identity', '/gas_basic', true)]),
|
||||
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name', { emptyText: '未填写' }), fixedAdminRole('gas_account'), relation('gas_basic_identity', '/gas_basic', true)]),
|
||||
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), f('gas_basic_identity', { label: '气站', listLabel: '气站名称', type: 'identity', relation: '/gas_basic', displayRelationLabel: true, emptyText: '平台直属', placeholder: '请选择气站,留空表示平台直属' }), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
|
||||
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('delivery_account'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
||||
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: resourceSearchEnumOptions('staff_account', 'role_code') }), f('gas_basic_identity', { label: '所属气站', type: 'identity', relation: '/gas_basic' }), f('delivery_basic_identity', { label: '所属配送点', type: 'identity', relation: '/delivery_basic', relationLinkage: { parentKey: 'gas_basic_identity', optionParentKey: 'gas_basic_identity', filterKey: 'gas_basic_identities', backfillParent: true } }), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
|
||||
|
||||
Reference in New Issue
Block a user