固化七月迭代审计治理进展以隔离线上热修

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:
2026-08-03 09:47:22 +08:00
parent cf2ff0ac1c
commit b3499adfca
114 changed files with 16961 additions and 2782 deletions

View File

@@ -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 = &currentUserID
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 {