diff --git a/backend/api/internal/logic/platform/product/producer.go b/backend/api/internal/logic/platform/product/producer.go index 16f6ee1..dd9ec8b 100644 --- a/backend/api/internal/logic/platform/product/producer.go +++ b/backend/api/internal/logic/platform/product/producer.go @@ -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) { diff --git a/backend/api/internal/logic/platform/product/producer_test.go b/backend/api/internal/logic/platform/product/producer_test.go index 7230522..71d5ab2 100644 --- a/backend/api/internal/logic/platform/product/producer_test.go +++ b/backend/api/internal/logic/platform/product/producer_test.go @@ -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{ diff --git a/docs/操作日志_生产商角色固定_20260813.md b/docs/操作日志_生产商角色固定_20260813.md new file mode 100644 index 0000000..412e0b3 --- /dev/null +++ b/docs/操作日志_生产商角色固定_20260813.md @@ -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` 字段。历史非法角色与新数据会暂时存在显示差异,这是明确保留历史原值的结果。 diff --git a/docs/项目文档_生产商角色固定_v1.0.md b/docs/项目文档_生产商角色固定_v1.0.md new file mode 100644 index 0000000..f2d58a6 --- /dev/null +++ b/docs/项目文档_生产商角色固定_v1.0.md @@ -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` 文本输入实现权限扩展。 + diff --git a/frontend/platform_admin/src/api/resource-page-rules.ts b/frontend/platform_admin/src/api/resource-page-rules.ts index 1e61284..a9cfda9 100644 --- a/frontend/platform_admin/src/api/resource-page-rules.ts +++ b/frontend/platform_admin/src/api/resource-page-rules.ts @@ -84,7 +84,6 @@ const pageRules: Record = { 'address', 'password', 'display_name', - 'role_code', 'remark', ], editOptionalKeys: ['password'], diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index 4b75296..517082b 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -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', [