固定生产商管理员角色

新建生产商时由服务端固定写入 admin,编辑时忽略并禁止修改角色。平台页面将 admin 只读显示为‘生产商管理员’,历史未知角色保持原值,不修改数据库或历史数据。补充后端回归测试、操作日志和项目文档。
This commit is contained in:
czl231
2026-08-13 02:07:55 +08:00
parent 6b5f2cb103
commit 27733b313e
6 changed files with 155 additions and 11 deletions

View File

@@ -11,6 +11,9 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// producerAdminRoleCode 是生产商账户当前唯一支持的角色编码。
const producerAdminRoleCode = "admin"
type producerAccountCreateRequest struct { type producerAccountCreateRequest struct {
ProducerCode string `json:"producer_code" binding:"required,max=64"` ProducerCode string `json:"producer_code" binding:"required,max=64"`
Name string `json:"name" binding:"required,max=128"` Name string `json:"name" binding:"required,max=128"`
@@ -21,7 +24,7 @@ type producerAccountCreateRequest struct {
Username string `json:"username" binding:"required,max=64"` Username string `json:"username" binding:"required,max=64"`
Password string `json:"password" binding:"required"` Password string `json:"password" binding:"required"`
DisplayName string `json:"display_name" binding:"max=64"` DisplayName string `json:"display_name" binding:"max=64"`
RoleCode string `json:"role_code" binding:"max=64"` RoleCode string `json:"role_code" binding:"max=64"` // 兼容旧客户端,服务端固定使用 admin。
Remark string `json:"remark"` Remark string `json:"remark"`
} }
@@ -32,7 +35,7 @@ type producerAccountUpdateRequest struct {
Phone string `json:"phone" binding:"max=32"` Phone string `json:"phone" binding:"max=32"`
Address string `json:"address" binding:"max=255"` Address string `json:"address" binding:"max=255"`
DisplayName string `json:"display_name" binding:"max=64"` DisplayName string `json:"display_name" binding:"max=64"`
RoleCode string `json:"role_code" binding:"max=64"` RoleCode string `json:"role_code" binding:"max=64"` // 兼容旧客户端,更新时不采纳。
Password string `json:"password"` Password string `json:"password"`
Remark string `json:"remark"` Remark string `json:"remark"`
} }
@@ -97,6 +100,37 @@ func restoreProducerAccountAddresses(response any, producers []models.ProducerAc
return response return response
} }
// newProducerAccount 构造平台创建的生产商账户,并固定为生产商管理员角色。
func newProducerAccount(request producerAccountCreateRequest, passwordHash string) models.ProducerAccount {
return models.ProducerAccount{
Entity: common.NewEntity(common.StatusEnable),
ProducerCode: strings.TrimSpace(request.ProducerCode),
Name: strings.TrimSpace(request.Name),
CreditCode: strings.TrimSpace(request.CreditCode),
Principal: strings.TrimSpace(request.Principal),
Phone: strings.TrimSpace(request.Phone),
Address: strings.TrimSpace(request.Address),
Username: strings.TrimSpace(request.Username),
DisplayName: strings.TrimSpace(request.DisplayName),
PasswordHash: passwordHash,
RoleCode: producerAdminRoleCode,
Remark: request.Remark,
}
}
// producerAccountUpdateValues 生成生产商更新白名单值,刻意忽略客户端角色编码。
func producerAccountUpdateValues(request producerAccountUpdateRequest) gin.H {
return gin.H{
"name": strings.TrimSpace(request.Name),
"credit_code": strings.TrimSpace(request.CreditCode),
"principal": strings.TrimSpace(request.Principal),
"phone": strings.TrimSpace(request.Phone),
"address": strings.TrimSpace(request.Address),
"display_name": strings.TrimSpace(request.DisplayName),
"remark": request.Remark,
}
}
func CreateProducerAccount(ctx *gin.Context) { func CreateProducerAccount(ctx *gin.Context) {
var request producerAccountCreateRequest var request producerAccountCreateRequest
if ctx.ShouldBindJSON(&request) != nil || !common.IsValidAccountPassword(request.Password) { if ctx.ShouldBindJSON(&request) != nil || !common.IsValidAccountPassword(request.Password) {
@@ -108,11 +142,7 @@ func CreateProducerAccount(ctx *gin.Context) {
infra.Response.Error(ctx, err) infra.Response.Error(ctx, err)
return return
} }
roleCode := strings.TrimSpace(request.RoleCode) data := newProducerAccount(request, hash)
if roleCode == "" {
roleCode = "admin"
}
data := models.ProducerAccount{Entity: common.NewEntity(common.StatusEnable), ProducerCode: strings.TrimSpace(request.ProducerCode), Name: strings.TrimSpace(request.Name), CreditCode: strings.TrimSpace(request.CreditCode), Principal: strings.TrimSpace(request.Principal), Phone: strings.TrimSpace(request.Phone), Address: strings.TrimSpace(request.Address), Username: strings.TrimSpace(request.Username), DisplayName: strings.TrimSpace(request.DisplayName), PasswordHash: hash, RoleCode: roleCode, Remark: request.Remark}
if err := impl.DBService.Create(&data).Error; err != nil { if err := impl.DBService.Create(&data).Error; err != nil {
infra.Response.Error(ctx, err) infra.Response.Error(ctx, err)
return return
@@ -126,7 +156,7 @@ func UpdateProducerAccount(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument) infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return return
} }
values := gin.H{"name": strings.TrimSpace(request.Name), "credit_code": strings.TrimSpace(request.CreditCode), "principal": strings.TrimSpace(request.Principal), "phone": strings.TrimSpace(request.Phone), "address": strings.TrimSpace(request.Address), "display_name": strings.TrimSpace(request.DisplayName), "role_code": strings.TrimSpace(request.RoleCode), "remark": request.Remark} values := producerAccountUpdateValues(request)
if request.Password != "" { if request.Password != "" {
if !common.IsValidAccountPassword(request.Password) { if !common.IsValidAccountPassword(request.Password) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument) infra.Response.Error(ctx, errcode.ErrInvalidArgument)
@@ -139,7 +169,7 @@ func UpdateProducerAccount(ctx *gin.Context) {
} }
values["password_hash"] = hash values["password_hash"] = hash
} }
common.UpdateAllowedByIdentity(ctx, &models.ProducerAccount{}, values, []string{"name", "credit_code", "principal", "phone", "address", "display_name", "role_code", "remark", "password_hash"}) common.UpdateAllowedByIdentity(ctx, &models.ProducerAccount{}, values, []string{"name", "credit_code", "principal", "phone", "address", "display_name", "remark", "password_hash"})
} }
func DeleteProducerAccount(ctx *gin.Context) { func DeleteProducerAccount(ctx *gin.Context) {

View File

@@ -9,6 +9,27 @@ import (
"git.apinb.com/heqiapp/platforms/backend/api/internal/models" "git.apinb.com/heqiapp/platforms/backend/api/internal/models"
) )
// TestNewProducerAccountUsesAdminRole 验证客户端角色不会改变新生产商的管理员权限。
func TestNewProducerAccountUsesAdminRole(t *testing.T) {
producer := newProducerAccount(producerAccountCreateRequest{
ProducerCode: " producer-1 ",
Name: " 示例生产商 ",
Username: " producer-admin ",
RoleCode: "1",
}, "password-hash")
if producer.RoleCode != producerAdminRoleCode {
t.Fatalf("生产商角色编码必须为 %q实际为 %q", producerAdminRoleCode, producer.RoleCode)
}
}
// TestProducerAccountUpdateIgnoresRole 验证编辑生产商时不会写入客户端角色编码。
func TestProducerAccountUpdateIgnoresRole(t *testing.T) {
values := producerAccountUpdateValues(producerAccountUpdateRequest{Name: "生产商", RoleCode: "1"})
if _, exists := values["role_code"]; exists {
t.Fatalf("生产商更新值不应包含角色编码:%#v", values)
}
}
// TestRestoreProducerAccountAddresses 验证生产商列表按原顺序恢复企业地址。 // TestRestoreProducerAccountAddresses 验证生产商列表按原顺序恢复企业地址。
func TestRestoreProducerAccountAddresses(t *testing.T) { func TestRestoreProducerAccountAddresses(t *testing.T) {
response := []any{ response := []any{

View File

@@ -0,0 +1,43 @@
# 生产商角色固定操作日志
操作时间2026-08-13
操作类型:修改
影响模块:平台总后台生产商管理、平台管理 API
## 操作前状态
生产商角色字段为自由文本。客户端提交 `1` 等任意字符串时,服务端会直接写入 `producer_account.role_code`,但系统并没有对应的生产商多角色权限实现。
## 具体操作
1. 新建生产商时忽略客户端角色,服务端固定写入 `admin`
2. 编辑生产商时忽略客户端角色,并从更新白名单移除 `role_code`
3. 新建和编辑页面将角色设为只读,`admin` 显示为“生产商管理员”。
4. 历史未知角色继续按数据库原值展示,不修改数据库及历史数据。
5. 增加新建角色固定和编辑角色忽略测试。
## 操作后状态
- 新生产商角色固定为 `admin`
- 编辑生产商不会改变已有角色。
- `admin` 在页面显示为“生产商管理员”。
- 历史值(如 `1`)保持原样,不执行迁移或数据修正。
## 代码变更
- `backend/api/internal/logic/platform/product/producer.go`:固定新建角色并禁止编辑角色。
- `backend/api/internal/logic/platform/product/producer_test.go`:新增角色规则测试。
- `frontend/platform_admin/src/api/resources.ts`:新增生产商管理员中文只读选项。
- `frontend/platform_admin/src/api/resource-page-rules.ts`:移除角色编辑能力。
## 验证结果
- `go test ./internal/logic/platform/product`:通过。
- `npm.cmd run resource-pages:check`:通过,详情 46 类、新建 25 类、编辑 23 类。
- `npm.cmd run build`通过TypeScript 检查与 Vite 生产构建成功。
- 本地后端重新编译并重启,健康接口返回 `platform-api``ok`
- `git diff --check`:通过。
## 风险评估
不修改数据库结构或历史数据,接口继续兼容旧客户端提交的 `role_code` 字段。历史非法角色与新数据会暂时存在显示差异,这是明确保留历史原值的结果。

View File

@@ -0,0 +1,39 @@
# 项目文档:生产商角色固定 v1.0
## 1. 项目概述
当前系统尚未实现生产商端及其多角色权限体系,因此生产商账户仅支持稳定角色编码 `admin`,平台页面显示为“生产商管理员”。本次修改不改变数据库结构和历史数据。
## 2. 目录结构说明
```text
platforms/
├── backend/api/internal/logic/platform/product/
│ ├── producer.go # 生产商创建与编辑角色规则
│ └── producer_test.go # 生产商角色回归测试
├── frontend/platform_admin/src/api/
│ ├── resources.ts # 生产商管理员中文展示配置
│ └── resource-page-rules.ts # 生产商编辑字段白名单
└── docs/
├── 项目文档_生产商角色固定_v1.0.md
└── 操作日志_生产商角色固定_20260813.md
```
## 3. 核心文件说明
- `producer.go``newProducerAccount` 固定角色为 `admin``producerAccountUpdateValues` 不生成角色更新值。
- `producer_test.go`:验证客户端提交其他角色不会影响新建和编辑结果。
- `resources.ts`:将 `admin` 映射为“生产商管理员”,新建时只读。
- `resource-page-rules.ts`:角色不属于生产商可编辑字段。
## 4. 变更记录
- 新建生产商固定使用 `admin`
- 编辑生产商不能修改角色。
- 新建、详情和编辑页面支持“生产商管理员”中文展示。
- 历史未知角色保持数据库原值,不执行 SQL 或迁移。
## 5. 维护指南
未来若建设生产商端多角色体系,应先设计独立角色、菜单权限、登录鉴权和企业账户关系,再开放角色编辑。不得仅通过放开 `role_code` 文本输入实现权限扩展。

View File

@@ -84,7 +84,6 @@ const pageRules: Record<string, ResourcePageRule> = {
'address', 'address',
'password', 'password',
'display_name', 'display_name',
'role_code',
'remark', 'remark',
], ],
editOptionalKeys: ['password'], editOptionalKeys: ['password'],

View File

@@ -337,6 +337,18 @@ function fixedAdminRole(resource: string): ResourceField {
}); });
} }
/** 生产商当前只有管理员角色;历史未知编码继续按原值展示。 */
function producerAdminRole(): ResourceField {
return f('role_code', {
label: '角色',
type: 'select',
required: true,
options: [{ label: '生产商管理员', value: 'admin' }],
defaultValue: 'admin',
readonlyOnCreate: true,
});
}
function define( function define(
name: string, name: string,
title: string, title: string,
@@ -432,7 +444,7 @@ export const resources: ResourceUiDefinition[] = [
}), }),
]), ]),
define('producer_account', '生产商管理', 'writable', [f('producer_code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('phone'), f('address', { emptyText: '未填写', placeholder: '请输入地址' }), f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), f('remark')]), define('producer_account', '生产商管理', 'writable', [f('producer_code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('phone'), f('address', { emptyText: '未填写', placeholder: '请输入地址' }), f('username', { required: true }), f('password', { required: true }), f('display_name'), producerAdminRole(), f('remark')]),
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]), define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]), define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]),
define('product_info', '智能气阀', 'editable', [f('code', { required: true }), f('name', { required: true }), relation('producer_account_identity', '/producer_account', true), relation('product_type_identity', '/product_type', true), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('produced_at', { required: true })], 'list', [ define('product_info', '智能气阀', 'editable', [f('code', { required: true }), f('name', { required: true }), relation('producer_account_identity', '/producer_account', true), relation('product_type_identity', '/product_type', true), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('produced_at', { required: true })], 'list', [