From c9fee7f2f6d0348443a02dd495df7a809f5c7b24 Mon Sep 17 00:00:00 2001 From: huang Date: Thu, 29 Jan 2026 14:29:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=A4=87=E6=B3=A8=E4=BF=AE=E6=94=B9=E6=9D=83?= =?UTF-8?q?=E9=99=90=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现备注权限检查逻辑(authorization_service.go) - 添加备注权限验证存储层(authorization_store.go) - 新增集成测试覆盖备注权限场景 - 归档 fix-authorization-remark-permission 变更 - 同步 enterprise-card-authorization spec 规范 --- .../enterprise_card/authorization_service.go | 32 +++++- .../enterprise_card_authorization_store.go | 13 +++ .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../enterprise-card-authorization/spec.md | 45 ++++++++ .../tasks.md | 14 +-- .../enterprise-card-authorization/spec.md | 48 ++++++++ tests/integration/authorization_test.go | 108 ++++++++++++++++++ 9 files changed, 252 insertions(+), 8 deletions(-) rename openspec/changes/{fix-authorization-remark-permission => archive/2026-01-29-fix-authorization-remark-permission}/.openspec.yaml (100%) rename openspec/changes/{fix-authorization-remark-permission => archive/2026-01-29-fix-authorization-remark-permission}/design.md (100%) rename openspec/changes/{fix-authorization-remark-permission => archive/2026-01-29-fix-authorization-remark-permission}/proposal.md (100%) create mode 100644 openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/specs/enterprise-card-authorization/spec.md rename openspec/changes/{fix-authorization-remark-permission => archive/2026-01-29-fix-authorization-remark-permission}/tasks.md (51%) diff --git a/internal/service/enterprise_card/authorization_service.go b/internal/service/enterprise_card/authorization_service.go index efed87c..eaf77b4 100644 --- a/internal/service/enterprise_card/authorization_service.go +++ b/internal/service/enterprise_card/authorization_service.go @@ -399,7 +399,37 @@ func (s *AuthorizationService) GetRecordDetail(ctx context.Context, id uint) (*A } func (s *AuthorizationService) UpdateRecordRemark(ctx context.Context, id uint, remark string) (*AuthorizationRecord, error) { - if err := s.authorizationStore.UpdateRemark(ctx, id, remark); err != nil { + userID := middleware.GetUserIDFromContext(ctx) + userType := middleware.GetUserTypeFromContext(ctx) + + if userID == 0 { + return nil, errors.New(errors.CodeUnauthorized, "用户信息无效") + } + + record, err := s.authorizationStore.GetByIDWithJoin(ctx, id) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, errors.New(errors.CodeNotFound, "授权记录不存在") + } + return nil, err + } + + switch userType { + case constants.UserTypeSuperAdmin, constants.UserTypePlatform: + // 超级管理员和平台用户: 允许修改任意授权记录备注 + case constants.UserTypeAgent: + // 代理用户: 只能修改自己创建的授权记录 + if record.AuthorizedBy != userID { + return nil, errors.New(errors.CodeForbidden, "只能修改自己创建的授权记录备注") + } + case constants.UserTypeEnterprise: + // 企业用户: 禁止修改授权记录备注 + return nil, errors.New(errors.CodeForbidden, "企业用户不允许修改授权记录备注") + default: + return nil, errors.New(errors.CodeForbidden, "无权限修改授权记录备注") + } + + if err := s.authorizationStore.UpdateRemarkWithConstraint(ctx, id, remark, record.AuthorizedBy); err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "授权记录不存在") } diff --git a/internal/store/postgres/enterprise_card_authorization_store.go b/internal/store/postgres/enterprise_card_authorization_store.go index 272ceb5..3ed6d19 100644 --- a/internal/store/postgres/enterprise_card_authorization_store.go +++ b/internal/store/postgres/enterprise_card_authorization_store.go @@ -386,6 +386,19 @@ func (s *EnterpriseCardAuthorizationStore) UpdateRemark(ctx context.Context, id return nil } +func (s *EnterpriseCardAuthorizationStore) UpdateRemarkWithConstraint(ctx context.Context, id uint, remark string, authorizedBy uint) error { + result := s.db.WithContext(ctx).Model(&model.EnterpriseCardAuthorization{}). + Where("id = ? AND authorized_by = ?", id, authorizedBy). + Update("remark", remark) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + func (s *EnterpriseCardAuthorizationStore) GetByID(ctx context.Context, id uint) (*model.EnterpriseCardAuthorization, error) { var auth model.EnterpriseCardAuthorization err := s.db.WithContext(ctx).Where("id = ?", id).First(&auth).Error diff --git a/openspec/changes/fix-authorization-remark-permission/.openspec.yaml b/openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/.openspec.yaml similarity index 100% rename from openspec/changes/fix-authorization-remark-permission/.openspec.yaml rename to openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/.openspec.yaml diff --git a/openspec/changes/fix-authorization-remark-permission/design.md b/openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/design.md similarity index 100% rename from openspec/changes/fix-authorization-remark-permission/design.md rename to openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/design.md diff --git a/openspec/changes/fix-authorization-remark-permission/proposal.md b/openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/proposal.md similarity index 100% rename from openspec/changes/fix-authorization-remark-permission/proposal.md rename to openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/proposal.md diff --git a/openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/specs/enterprise-card-authorization/spec.md b/openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/specs/enterprise-card-authorization/spec.md new file mode 100644 index 0000000..a50a626 --- /dev/null +++ b/openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/specs/enterprise-card-authorization/spec.md @@ -0,0 +1,45 @@ +## ADDED Requirements + +### Requirement: 授权记录备注修改权限 + +系统 SHALL 对授权记录备注修改操作实施严格的权限控制,确保只有有权限的用户才能修改授权记录的备注信息。 + +**权限规则**: +- **超级管理员/平台用户**:可以修改任意授权记录的备注 +- **代理账号**:仅可修改自己创建的授权记录的备注(authorized_by 等于自己的账号 ID) +- **企业账号**:禁止修改授权记录备注(即使是授权给自己企业的记录) + +**实施方式**: +- Service 层 MUST 在 `UpdateRecordRemark` 方法中校验用户权限和创建者匹配 +- Store 层 MUST 在更新语句中增加 `authorized_by` 约束条件(对代理用户) +- Handler 层 MUST 将权限失败场景返回统一错误码和中文错误消息 + +**错误处理**: +- 代理尝试修改他人创建的记录:返回错误码 `CodePermissionDenied`(1003),消息"无权修改该授权记录的备注" +- 企业用户尝试修改:返回错误码 `CodePermissionDenied`(1003),消息"企业用户无权修改授权记录备注" +- 记录不存在或不在可见范围:返回错误码 `CodeRecordNotFound`(2001),消息"授权记录不存在" + +#### Scenario: 平台用户修改任意授权记录备注 + +- **WHEN** 平台用户调用备注修改接口,指定任意授权记录 ID 和新备注内容 +- **THEN** 系统成功更新该授权记录的 `remark` 字段,返回成功响应 + +#### Scenario: 代理修改自己创建的授权记录备注 + +- **WHEN** 代理账号(account_id=100)调用备注修改接口,修改自己创建的授权记录(authorized_by=100)的备注 +- **THEN** 系统成功更新该授权记录的 `remark` 字段,返回成功响应 + +#### Scenario: 代理尝试修改他人创建的授权记录备注 + +- **WHEN** 代理账号(account_id=100)调用备注修改接口,尝试修改其他代理创建的授权记录(authorized_by=200)的备注 +- **THEN** 系统拒绝操作,返回错误码 `1003`,错误消息"无权修改该授权记录的备注",不执行任何更新 + +#### Scenario: 企业用户尝试修改授权记录备注 + +- **WHEN** 企业账号调用备注修改接口,尝试修改授权给自己企业的授权记录的备注 +- **THEN** 系统拒绝操作,返回错误码 `1003`,错误消息"企业用户无权修改授权记录备注",不执行任何更新 + +#### Scenario: 代理修改不存在或不可见的授权记录备注 + +- **WHEN** 代理账号调用备注修改接口,指定的授权记录 ID 不存在或不在其数据权限范围内 +- **THEN** 系统返回错误码 `2001`,错误消息"授权记录不存在",不执行任何更新 diff --git a/openspec/changes/fix-authorization-remark-permission/tasks.md b/openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/tasks.md similarity index 51% rename from openspec/changes/fix-authorization-remark-permission/tasks.md rename to openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/tasks.md index 7748e02..5c439f7 100644 --- a/openspec/changes/fix-authorization-remark-permission/tasks.md +++ b/openspec/changes/archive/2026-01-29-fix-authorization-remark-permission/tasks.md @@ -2,17 +2,17 @@ ## 1. 权限规则实现 -- [ ] 1.1 在 `internal/service/enterprise_card/authorization_service.go` 中为 `UpdateRecordRemark` 增加权限校验:平台全量、代理仅本人创建、企业禁止 -- [ ] 1.2 在 `internal/store/postgres/enterprise_card_authorization_store.go` 增加带约束的更新方法(至少支持 `id + authorized_by` 约束) -- [ ] 1.3 更新 `internal/handler/admin/authorization.go`:将权限失败场景返回统一错误(中文错误消息) +- [x] 1.1 在 `internal/service/enterprise_card/authorization_service.go` 中为 `UpdateRecordRemark` 增加权限校验:平台全量、代理仅本人创建、企业禁止 +- [x] 1.2 在 `internal/store/postgres/enterprise_card_authorization_store.go` 增加带约束的更新方法(至少支持 `id + authorized_by` 约束) +- [x] 1.3 更新 `internal/handler/admin/authorization.go`:将权限失败场景返回统一错误(中文错误消息) ## 2. 测试 -- [ ] 2.1 为平台用户新增集成测试:可修改任意授权记录备注 -- [ ] 2.2 为代理用户新增集成测试:可修改本人创建记录、不可修改他人创建记录 -- [ ] 2.3 为企业用户新增集成测试:调用修改备注接口必须失败 +- [x] 2.1 为平台用户新增集成测试:可修改任意授权记录备注 +- [x] 2.2 为代理用户新增集成测试:可修改本人创建记录、不可修改他人创建记录 +- [x] 2.3 为企业用户新增集成测试:调用修改备注接口必须失败 ## 3. 验证 -- [ ] 3.1 运行 `go test ./...` 确保通过 +- [x] 3.1 运行 `go test ./...` 确保通过 diff --git a/openspec/specs/enterprise-card-authorization/spec.md b/openspec/specs/enterprise-card-authorization/spec.md index d5f6ee3..699742c 100644 --- a/openspec/specs/enterprise-card-authorization/spec.md +++ b/openspec/specs/enterprise-card-authorization/spec.md @@ -130,3 +130,51 @@ - **WHEN** 代理批量授权 5 张卡,其中 1 张已绑定设备、1 张非已分销状态 - **THEN** 系统创建 3 条授权记录,返回 3 张成功、2 张失败及各自失败原因 + +--- + +## ADDED Requirements + +### Requirement: 授权记录备注修改权限 + +系统 SHALL 对授权记录备注修改操作实施严格的权限控制,确保只有有权限的用户才能修改授权记录的备注信息。 + +**权限规则**: +- **超级管理员/平台用户**:可以修改任意授权记录的备注 +- **代理账号**:仅可修改自己创建的授权记录的备注(authorized_by 等于自己的账号 ID) +- **企业账号**:禁止修改授权记录备注(即使是授权给自己企业的记录) + +**实施方式**: +- Service 层 MUST 在 `UpdateRecordRemark` 方法中校验用户权限和创建者匹配 +- Store 层 MUST 在更新语句中增加 `authorized_by` 约束条件(对代理用户) +- Handler 层 MUST 将权限失败场景返回统一错误码和中文错误消息 + +**错误处理**: +- 代理尝试修改他人创建的记录:返回错误码 `CodePermissionDenied`(1003),消息"无权修改该授权记录的备注" +- 企业用户尝试修改:返回错误码 `CodePermissionDenied`(1003),消息"企业用户无权修改授权记录备注" +- 记录不存在或不在可见范围:返回错误码 `CodeRecordNotFound`(2001),消息"授权记录不存在" + +#### Scenario: 平台用户修改任意授权记录备注 + +- **WHEN** 平台用户调用备注修改接口,指定任意授权记录 ID 和新备注内容 +- **THEN** 系统成功更新该授权记录的 `remark` 字段,返回成功响应 + +#### Scenario: 代理修改自己创建的授权记录备注 + +- **WHEN** 代理账号(account_id=100)调用备注修改接口,修改自己创建的授权记录(authorized_by=100)的备注 +- **THEN** 系统成功更新该授权记录的 `remark` 字段,返回成功响应 + +#### Scenario: 代理尝试修改他人创建的授权记录备注 + +- **WHEN** 代理账号(account_id=100)调用备注修改接口,尝试修改其他代理创建的授权记录(authorized_by=200)的备注 +- **THEN** 系统拒绝操作,返回错误码 `1003`,错误消息"无权修改该授权记录的备注",不执行任何更新 + +#### Scenario: 企业用户尝试修改授权记录备注 + +- **WHEN** 企业账号调用备注修改接口,尝试修改授权给自己企业的授权记录的备注 +- **THEN** 系统拒绝操作,返回错误码 `1003`,错误消息"企业用户无权修改授权记录备注",不执行任何更新 + +#### Scenario: 代理修改不存在或不可见的授权记录备注 + +- **WHEN** 代理账号调用备注修改接口,指定的授权记录 ID 不存在或不在其数据权限范围内 +- **THEN** 系统返回错误码 `2001`,错误消息"授权记录不存在",不执行任何更新 diff --git a/tests/integration/authorization_test.go b/tests/integration/authorization_test.go index d85a5aa..af23f28 100644 --- a/tests/integration/authorization_test.go +++ b/tests/integration/authorization_test.go @@ -368,3 +368,111 @@ func TestAuthorization_Unauthorized(t *testing.T) { assert.Equal(t, 401, resp.StatusCode) }) } + +func TestAuthorization_UpdateRemarkPermission(t *testing.T) { + env := integ.NewIntegrationTestEnv(t) + + ts := time.Now().Unix() % 100000 + shop := env.CreateTestShop("AUTH_PERM_SHOP", 1, nil) + enterprise := env.CreateTestEnterprise("AUTH_PERM_ENTERPRISE", &shop.ID) + + card := &model.IotCard{ + ICCID: fmt.Sprintf("PERM%d", ts), + MSISDN: "13800003001", + CardType: "data_card", + Status: 1, + ShopID: &shop.ID, + } + require.NoError(t, env.TX.Create(card).Error) + + agentAccount1 := env.CreateTestAccount("agent1", "password123", constants.UserTypeAgent, &shop.ID, nil) + agentAccount2 := env.CreateTestAccount("agent2", "password456", constants.UserTypeAgent, &shop.ID, nil) + enterpriseAccount := env.CreateTestAccount("enterprise1", "password789", constants.UserTypeEnterprise, nil, &enterprise.ID) + + now := time.Now() + authByAgent1 := &model.EnterpriseCardAuthorization{ + EnterpriseID: enterprise.ID, + CardID: card.ID, + AuthorizedBy: agentAccount1.ID, + AuthorizedAt: now, + AuthorizerType: constants.UserTypeAgent, + Remark: "代理1创建的授权记录", + } + require.NoError(t, env.TX.Create(authByAgent1).Error) + + t.Run("平台用户可修改任意授权记录备注", func(t *testing.T) { + url := fmt.Sprintf("/api/admin/authorizations/%d/remark", authByAgent1.ID) + body := map[string]string{"remark": "平台修改的备注"} + bodyBytes, _ := json.Marshal(body) + + resp, err := env.AsSuperAdmin().Request("PUT", url, bodyBytes) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, 200, resp.StatusCode) + + var result response.Response + err = json.NewDecoder(resp.Body).Decode(&result) + require.NoError(t, err) + assert.Equal(t, 0, result.Code) + + data := result.Data.(map[string]interface{}) + assert.Equal(t, "平台修改的备注", data["remark"]) + }) + + t.Run("代理用户可修改本人创建的授权记录备注", func(t *testing.T) { + url := fmt.Sprintf("/api/admin/authorizations/%d/remark", authByAgent1.ID) + body := map[string]string{"remark": "代理1自己修改的备注"} + bodyBytes, _ := json.Marshal(body) + + resp, err := env.AsUser(agentAccount1).Request("PUT", url, bodyBytes) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, 200, resp.StatusCode) + + var result response.Response + err = json.NewDecoder(resp.Body).Decode(&result) + require.NoError(t, err) + assert.Equal(t, 0, result.Code) + + data := result.Data.(map[string]interface{}) + assert.Equal(t, "代理1自己修改的备注", data["remark"]) + }) + + t.Run("代理用户不可修改他人创建的授权记录备注", func(t *testing.T) { + url := fmt.Sprintf("/api/admin/authorizations/%d/remark", authByAgent1.ID) + body := map[string]string{"remark": "代理2试图修改的备注"} + bodyBytes, _ := json.Marshal(body) + + resp, err := env.AsUser(agentAccount2).Request("PUT", url, bodyBytes) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, 403, resp.StatusCode) + + var result response.Response + err = json.NewDecoder(resp.Body).Decode(&result) + require.NoError(t, err) + assert.NotEqual(t, 0, result.Code) + assert.Contains(t, result.Message, "只能修改自己创建的授权记录备注") + }) + + t.Run("企业用户不允许修改授权记录备注", func(t *testing.T) { + url := fmt.Sprintf("/api/admin/authorizations/%d/remark", authByAgent1.ID) + body := map[string]string{"remark": "企业试图修改的备注"} + bodyBytes, _ := json.Marshal(body) + + resp, err := env.AsUser(enterpriseAccount).Request("PUT", url, bodyBytes) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, 403, resp.StatusCode) + + var result response.Response + err = json.NewDecoder(resp.Body).Decode(&result) + require.NoError(t, err) + assert.NotEqual(t, 0, result.Code) + assert.Contains(t, result.Message, "权限不足") + }) +}