固化七月迭代审计治理进展以隔离线上热修
Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。 Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。 Confidence: medium Scope-risk: broad Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。 Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。 Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
This commit is contained in:
@@ -2,8 +2,10 @@ package enterprise_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"time"
|
||||
|
||||
accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -18,6 +21,7 @@ type Service struct {
|
||||
enterpriseStore *postgres.EnterpriseStore
|
||||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
accessAudit accessauditapp.Writer
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -25,12 +29,14 @@ func New(
|
||||
enterpriseStore *postgres.EnterpriseStore,
|
||||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
accessAudit accessauditapp.Writer,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
enterpriseStore: enterpriseStore,
|
||||
enterpriseCardAuthStore: enterpriseCardAuthStore,
|
||||
iotCardStore: iotCardStore,
|
||||
accessAudit: accessAudit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,10 +210,20 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if s.db == nil || s.accessAudit == nil {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "企业卡授权审计接缝未配置")
|
||||
}
|
||||
if err := validateEnterpriseCardActor(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := middleware.CanManageEnterprise(ctx, enterpriseID, s.enterpriseStore); err != nil {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
enterprise, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
ownerShop := s.loadOwnerShop(ctx, enterprise.OwnerShopID)
|
||||
|
||||
iccids, err := s.resolveICCIDsForAllocate(ctx, req)
|
||||
if err != nil {
|
||||
@@ -231,6 +247,14 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
cardIDToICCID[card.IotCardID] = card.ICCID
|
||||
allCandidateIDs = append(allCandidateIDs, card.IotCardID)
|
||||
}
|
||||
auditCardList, err := s.iotCardStore.GetByIDs(ctx, allCandidateIDs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡审计快照失败")
|
||||
}
|
||||
cardIDMap := make(map[uint]*model.IotCard, len(auditCardList))
|
||||
for _, card := range auditCardList {
|
||||
cardIDMap[card.ID] = card
|
||||
}
|
||||
|
||||
// 检测已被其他企业授权的卡,阻止重复授权
|
||||
conflictAuths, err := s.enterpriseCardAuthStore.GetConflictingAuthsByCardIDs(ctx, enterpriseID, allCandidateIDs)
|
||||
@@ -239,7 +263,12 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
}
|
||||
|
||||
cardIDsToAllocate := make([]uint, 0, len(allCandidateIDs))
|
||||
seenAllocate := make(map[uint]struct{}, len(allCandidateIDs))
|
||||
for _, cardID := range allCandidateIDs {
|
||||
if _, seen := seenAllocate[cardID]; seen {
|
||||
continue
|
||||
}
|
||||
seenAllocate[cardID] = struct{}{}
|
||||
if _, conflict := conflictAuths[cardID]; conflict {
|
||||
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
|
||||
ICCID: cardIDToICCID[cardID],
|
||||
@@ -274,12 +303,41 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
}
|
||||
|
||||
if len(auths) > 0 {
|
||||
if err := s.enterpriseCardAuthStore.BatchCreate(ctx, auths); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建授权记录失败")
|
||||
auditResult := constants.AuditResultSuccess
|
||||
if resp.FailCount > 0 {
|
||||
auditResult = constants.AuditResultPartial
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.CreateInBatches(auths, 100).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建授权记录失败")
|
||||
}
|
||||
cards := make([]accessauditapp.IotCardChange, 0, len(auths))
|
||||
authorizations := make([]accessauditapp.EnterpriseCardAuthorizationChange, 0, len(auths))
|
||||
for _, auth := range auths {
|
||||
card := cardIDMap[auth.CardID]
|
||||
cards = append(cards, accessauditapp.IotCardChange{
|
||||
Card: card, BeforeData: map[string]any{"enterprise_id": nil, "authorized": false},
|
||||
AfterData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": true},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "卡已授权给企业",
|
||||
})
|
||||
authorizations = append(authorizations, accessauditapp.EnterpriseCardAuthorizationChange{
|
||||
Authorization: auth, AfterData: map[string]any{"authorized": true, "remark": auth.Remark},
|
||||
})
|
||||
}
|
||||
return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
|
||||
ActionCode: constants.AuditActionEnterpriseCardsAllocated, Summary: "向企业授权卡",
|
||||
Result: auditResult, OperatorID: currentUserID, Enterprise: enterprise, Shop: ownerShop,
|
||||
Cards: cards, CardAuthorizations: authorizations,
|
||||
BeforeData: map[string]any{"authorized_card_count": 0},
|
||||
AfterData: map[string]any{"authorized_card_count": len(auths)},
|
||||
})
|
||||
}); err != nil {
|
||||
s.recordFailure(ctx, constants.AuditActionEnterpriseCardsAllocated, "向企业授权卡失败", enterprise, ownerShop, cardChanges(cardIDMap, allCandidateIDs), err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp.SuccessCount = len(cardIDsToAllocate)
|
||||
resp.SuccessCount = len(auths)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -289,10 +347,20 @@ func (s *Service) RecallCards(ctx context.Context, enterpriseID uint, req *dto.R
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if s.db == nil || s.accessAudit == nil {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "企业卡授权审计接缝未配置")
|
||||
}
|
||||
if err := validateEnterpriseCardActor(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := middleware.CanManageEnterprise(ctx, enterpriseID, s.enterpriseStore); err != nil {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
enterprise, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
ownerShop := s.loadOwnerShop(ctx, enterprise.OwnerShopID)
|
||||
|
||||
iccids, err := s.resolveICCIDsForRecall(ctx, enterpriseID, req)
|
||||
if err != nil {
|
||||
@@ -324,6 +392,7 @@ func (s *Service) RecallCards(ctx context.Context, enterpriseID uint, req *dto.R
|
||||
}
|
||||
|
||||
cardIDsToRecall := make([]uint, 0)
|
||||
seenRecall := make(map[uint]struct{}, len(iccids))
|
||||
for _, iccid := range iccids {
|
||||
card, exists := cardMap[iccid]
|
||||
if !exists {
|
||||
@@ -340,20 +409,130 @@ func (s *Service) RecallCards(ctx context.Context, enterpriseID uint, req *dto.R
|
||||
})
|
||||
continue
|
||||
}
|
||||
if _, seen := seenRecall[card.ID]; seen {
|
||||
continue
|
||||
}
|
||||
seenRecall[card.ID] = struct{}{}
|
||||
cardIDsToRecall = append(cardIDsToRecall, card.ID)
|
||||
}
|
||||
|
||||
if len(cardIDsToRecall) > 0 {
|
||||
if err := s.enterpriseCardAuthStore.BatchUpdateStatus(ctx, enterpriseID, cardIDsToRecall, 0); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "回收授权失败")
|
||||
var recalled []*model.EnterpriseCardAuthorization
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("enterprise_id = ? AND card_id IN ? AND revoked_at IS NULL", enterpriseID, cardIDsToRecall).
|
||||
Find(&recalled).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询有效卡授权失败")
|
||||
}
|
||||
if len(recalled) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
ids := make([]uint, 0, len(recalled))
|
||||
cards := make([]accessauditapp.IotCardChange, 0, len(recalled))
|
||||
authorizations := make([]accessauditapp.EnterpriseCardAuthorizationChange, 0, len(recalled))
|
||||
for _, auth := range recalled {
|
||||
ids = append(ids, auth.ID)
|
||||
before := *auth
|
||||
auth.RevokedBy = ¤tUserID
|
||||
auth.RevokedAt = &now
|
||||
cards = append(cards, accessauditapp.IotCardChange{
|
||||
Card: cardIDMap[auth.CardID],
|
||||
BeforeData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": true},
|
||||
AfterData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": false},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "卡授权已回收",
|
||||
})
|
||||
authorizations = append(authorizations, accessauditapp.EnterpriseCardAuthorizationChange{
|
||||
Authorization: auth,
|
||||
BeforeData: map[string]any{"revoked_by": before.RevokedBy, "revoked_at": before.RevokedAt},
|
||||
AfterData: map[string]any{"revoked_by": currentUserID, "revoked_at": now},
|
||||
})
|
||||
}
|
||||
if err := tx.Model(&model.EnterpriseCardAuthorization{}).Where("id IN ? AND revoked_at IS NULL", ids).
|
||||
Updates(map[string]any{"revoked_by": currentUserID, "revoked_at": now}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "回收卡授权失败")
|
||||
}
|
||||
result := constants.AuditResultSuccess
|
||||
if len(resp.FailedItems) > 0 {
|
||||
result = constants.AuditResultPartial
|
||||
}
|
||||
return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
|
||||
ActionCode: constants.AuditActionEnterpriseCardsRecalled, Summary: "回收企业卡授权",
|
||||
Result: result, OperatorID: currentUserID, Enterprise: enterprise, Shop: ownerShop,
|
||||
Cards: cards, CardAuthorizations: authorizations,
|
||||
BeforeData: map[string]any{"authorized_card_count": len(recalled)},
|
||||
AfterData: map[string]any{"authorized_card_count": 0},
|
||||
})
|
||||
}); err != nil {
|
||||
s.recordFailure(ctx, constants.AuditActionEnterpriseCardsRecalled, "回收企业卡授权失败", enterprise, ownerShop, cardChanges(cardIDMap, cardIDsToRecall), err)
|
||||
return nil, err
|
||||
}
|
||||
resp.SuccessCount = len(recalled)
|
||||
}
|
||||
|
||||
resp.SuccessCount = len(cardIDsToRecall)
|
||||
resp.FailCount = len(resp.FailedItems)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func validateEnterpriseCardActor(ctx context.Context) error {
|
||||
switch middleware.GetUserTypeFromContext(ctx) {
|
||||
case constants.UserTypeSuperAdmin, constants.UserTypePlatform, constants.UserTypeAgent:
|
||||
return nil
|
||||
default:
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) loadOwnerShop(ctx context.Context, ownerShopID *uint) *model.Shop {
|
||||
if ownerShopID == nil {
|
||||
return nil
|
||||
}
|
||||
shop, err := postgres.NewShopStore(s.db, nil).GetByID(ctx, *ownerShopID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return shop
|
||||
}
|
||||
|
||||
func cardChanges(cardMap map[uint]*model.IotCard, ids []uint) []accessauditapp.IotCardChange {
|
||||
changes := make([]accessauditapp.IotCardChange, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if card := cardMap[id]; card != nil {
|
||||
changes = append(changes, accessauditapp.IotCardChange{Card: card})
|
||||
}
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func (s *Service) recordFailure(
|
||||
ctx context.Context,
|
||||
actionCode, summary string,
|
||||
enterprise *model.Enterprise,
|
||||
ownerShop *model.Shop,
|
||||
cards []accessauditapp.IotCardChange,
|
||||
originalErr error,
|
||||
) {
|
||||
accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{
|
||||
ActionCode: actionCode, Summary: summary, Result: enterpriseCardFailureResult(originalErr),
|
||||
OperatorID: middleware.GetUserIDFromContext(ctx), Enterprise: enterprise, Shop: ownerShop,
|
||||
Cards: cards, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}, originalErr)
|
||||
}
|
||||
|
||||
func enterpriseCardFailureResult(err error) string {
|
||||
var appErr *errors.AppError
|
||||
if stderrors.As(err, &appErr) {
|
||||
switch appErr.Code {
|
||||
case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeEnterpriseNotFound,
|
||||
errors.CodeIotCardNotFound, errors.CodeIotCardStatusNotAllowed, errors.CodeCannotAuthorizeToOthersEnterprise,
|
||||
errors.CodeCannotAuthorizeOthersCard, errors.CodeCannotAuthorizeBoundCard, errors.CodeCardAlreadyAuthorized,
|
||||
errors.CodeCardNotAuthorized, errors.CodeCannotRevokeOthersAuthorization:
|
||||
return constants.AuditResultDenied
|
||||
}
|
||||
}
|
||||
return constants.AuditResultFailed
|
||||
}
|
||||
|
||||
func (s *Service) ListCards(ctx context.Context, enterpriseID uint, req *dto.EnterpriseCardListReq) (*dto.EnterpriseCardPageResult, error) {
|
||||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user