固定生产商管理员角色
新建生产商时由服务端固定写入 admin,编辑时忽略并禁止修改角色。平台页面将 admin 只读显示为‘生产商管理员’,历史未知角色保持原值,不修改数据库或历史数据。补充后端回归测试、操作日志和项目文档。
This commit is contained in:
@@ -11,6 +11,9 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// producerAdminRoleCode 是生产商账户当前唯一支持的角色编码。
|
||||
const producerAdminRoleCode = "admin"
|
||||
|
||||
type producerAccountCreateRequest struct {
|
||||
ProducerCode string `json:"producer_code" binding:"required,max=64"`
|
||||
Name string `json:"name" binding:"required,max=128"`
|
||||
@@ -21,7 +24,7 @@ type producerAccountCreateRequest struct {
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -32,7 +35,7 @@ type producerAccountUpdateRequest struct {
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Address string `json:"address" binding:"max=255"`
|
||||
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"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
@@ -97,6 +100,37 @@ func restoreProducerAccountAddresses(response any, producers []models.ProducerAc
|
||||
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) {
|
||||
var request producerAccountCreateRequest
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.IsValidAccountPassword(request.Password) {
|
||||
@@ -108,11 +142,7 @@ func CreateProducerAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
roleCode := strings.TrimSpace(request.RoleCode)
|
||||
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}
|
||||
data := newProducerAccount(request, hash)
|
||||
if err := impl.DBService.Create(&data).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -126,7 +156,7 @@ func UpdateProducerAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
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 !common.IsValidAccountPassword(request.Password) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
@@ -139,7 +169,7 @@ func UpdateProducerAccount(ctx *gin.Context) {
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -9,6 +9,27 @@ import (
|
||||
"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 验证生产商列表按原顺序恢复企业地址。
|
||||
func TestRestoreProducerAccountAddresses(t *testing.T) {
|
||||
response := []any{
|
||||
|
||||
43
docs/操作日志_生产商角色固定_20260813.md
Normal file
43
docs/操作日志_生产商角色固定_20260813.md
Normal 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` 字段。历史非法角色与新数据会暂时存在显示差异,这是明确保留历史原值的结果。
|
||||
39
docs/项目文档_生产商角色固定_v1.0.md
Normal file
39
docs/项目文档_生产商角色固定_v1.0.md
Normal 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` 文本输入实现权限扩展。
|
||||
|
||||
@@ -84,7 +84,6 @@ const pageRules: Record<string, ResourcePageRule> = {
|
||||
'address',
|
||||
'password',
|
||||
'display_name',
|
||||
'role_code',
|
||||
'remark',
|
||||
],
|
||||
editOptionalKeys: ['password'],
|
||||
|
||||
@@ -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(
|
||||
name: 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_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', [
|
||||
|
||||
Reference in New Issue
Block a user