收口审计治理与套餐任务进展

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -12,6 +12,7 @@ import (
systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
@@ -90,7 +91,7 @@ func NewConnectionService(db *gorm.DB, repo ApplicationRepository, tokens Access
// Save 创建或更新企业微信应用配置。
func (s *ConnectionService) Save(ctx context.Context, request dto.SaveWeComApplicationRequest) (*dto.WeComApplicationResponse, error) {
if s == nil || s.db == nil || s.repo == nil {
if s == nil || s.db == nil || s.repo == nil || s.audit == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信连接服务未配置")
}
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
@@ -140,10 +141,12 @@ func (s *ConnectionService) Save(ctx context.Context, request dto.SaveWeComAppli
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
requestID = *value
}
resourceID := fmt.Sprintf("%d", existing.ID)
if err := s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
OperatorID: operatorID, OperationType: "wecom_application_save", Description: "保存企业微信应用安全配置",
OperatorID: operatorID, OperationType: constants.AuditOperationWeComApplicationSave, Description: "保存企业微信应用安全配置",
ConfigKey: fmt.Sprintf("wecom.application.%d", existing.ID), BeforeData: before,
AfterData: applicationAuditSnapshot(existing), RequestID: requestID, CorrelationID: requestID,
ResourceID: &resourceID, DisplayName: existing.Name, Identity: applicationAuditIdentity(existing),
}); err != nil {
return err
}
@@ -152,6 +155,14 @@ func (s *ConnectionService) Save(ctx context.Context, request dto.SaveWeComAppli
return nil
})
if err != nil {
recordConfigFailure(ctx, s.db, s.audit, systemconfigapp.ChangeAudit{
OperatorID: operatorID, OperationType: constants.AuditOperationWeComApplicationSave,
Description: "保存企业微信应用配置失败", ConfigKey: fmt.Sprintf("wecom.application.%s.%d", request.CorpID, request.AgentID),
DisplayName: request.Name, Identity: map[string]any{
"corp_id": request.CorpID, "agent_id": request.AgentID, "name": request.Name, "status": request.Status,
"credentials_configured": request.Secret != "" && request.CallbackToken != "" && request.EncodingAESKey != "",
}, Result: constants.AuditResultFailed, ErrorCode: fmt.Sprintf("%d", errors.CodeDatabaseError), ErrorSummary: "企业微信应用配置事务已回滚",
})
var appErr *errors.AppError
if stdErrors.As(err, &appErr) {
return nil, appErr
@@ -240,7 +251,7 @@ func (s *ConnectionService) Test(ctx context.Context, applicationID uint) error
// SaveDefaultCreator 从应用当前可见成员中保存代理等账号使用的默认审批发起人。
func (s *ConnectionService) SaveDefaultCreator(ctx context.Context, applicationID uint, request dto.SaveWeComDefaultCreatorRequest) (*dto.WeComApplicationResponse, error) {
if s == nil || s.db == nil || s.repo == nil || s.members == nil {
if s == nil || s.db == nil || s.repo == nil || s.members == nil || s.audit == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信默认审批发起人服务未配置")
}
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
@@ -256,6 +267,7 @@ func (s *ConnectionService) SaveDefaultCreator(ctx context.Context, applicationI
}
member, err := s.members.GetVisible(ctx, applicationID, request.UserID)
if err != nil {
recordApplicationFailure(ctx, s.db, s.audit, constants.AuditOperationWeComDefaultCreatorSave, "拒绝保存不可用的企业微信默认审批发起人", application, constants.AuditResultDenied, errors.CodeInvalidParam)
return nil, err
}
now := s.now().UTC()
@@ -273,15 +285,18 @@ func (s *ConnectionService) SaveDefaultCreator(ctx context.Context, applicationI
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
requestID = *value
}
resourceID := fmt.Sprintf("%d", applicationID)
return s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
OperatorID: operatorID, OperationType: "wecom_default_creator_save", Description: "保存企业微信默认审批发起人",
OperatorID: operatorID, OperationType: constants.AuditOperationWeComDefaultCreatorSave, Description: "保存企业微信默认审批发起人",
ConfigKey: fmt.Sprintf("wecom.application.%d.default_creator", applicationID), BeforeData: before,
AfterData: applicationAuditSnapshot(application), RequestID: requestID, CorrelationID: requestID,
ResourceID: &resourceID, DisplayName: application.Name, Identity: applicationAuditIdentity(application),
})
}
return nil
})
if err != nil {
recordApplicationFailure(ctx, s.db, s.audit, constants.AuditOperationWeComDefaultCreatorSave, "保存企业微信默认审批发起人失败", application, constants.AuditResultFailed, errors.CodeDatabaseError)
var appErr *errors.AppError
if stdErrors.As(err, &appErr) {
return nil, appErr
@@ -295,12 +310,52 @@ func (s *ConnectionService) SaveDefaultCreator(ctx context.Context, applicationI
func applicationAuditSnapshot(application *model.WeComApplication) map[string]any {
return map[string]any{
"id": application.ID, "corp_id": application.CorpID, "agent_id": application.AgentID,
"name": application.Name, "status": application.Status, "credentials_configured": true,
"name": application.Name, "status": application.Status,
"credentials_configured": application.Secret != "" && application.CallbackToken != "" && application.EncodingAESKey != "",
"default_creator_userid": application.DefaultCreatorUserID,
"default_creator_name": application.DefaultCreatorName,
}
}
func applicationAuditIdentity(application *model.WeComApplication) map[string]any {
if application == nil {
return nil
}
return map[string]any{
"id": application.ID, "corp_id": application.CorpID, "agent_id": application.AgentID,
"name": application.Name, "status": application.Status,
"credentials_configured": application.Secret != "" && application.CallbackToken != "" && application.EncodingAESKey != "",
}
}
func recordApplicationFailure(ctx context.Context, db *gorm.DB, audit systemconfigapp.AuditWriter, operation, description string, application *model.WeComApplication, result string, code int) {
if application == nil {
return
}
resourceID := fmt.Sprintf("%d", application.ID)
recordConfigFailure(ctx, db, audit, systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: operation, Description: description,
ConfigKey: fmt.Sprintf("wecom.application.%d", application.ID), ResourceID: &resourceID,
DisplayName: application.Name, Identity: applicationAuditIdentity(application), BeforeData: applicationAuditSnapshot(application),
Result: result, ErrorCode: fmt.Sprintf("%d", code), ErrorSummary: description,
})
}
func recordConfigFailure(ctx context.Context, db *gorm.DB, audit systemconfigapp.AuditWriter, change systemconfigapp.ChangeAudit) {
if db == nil || audit == nil || change.OperatorID == 0 || change.ConfigKey == "" {
return
}
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
change.RequestID = *value
change.CorrelationID = *value
}
if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return audit.WriteConfigChange(ctx, tx, change)
}); err != nil {
auditfailure.RecordSecondaryWriteFailure(change.OperationType, change.ConfigKey, change.RequestID, change.CorrelationID, change.ErrorCode, err)
}
}
func toApplicationResponse(application model.WeComApplication) dto.WeComApplicationResponse {
statusName := "禁用"
if application.Status == constants.StatusEnabled {

View File

@@ -2,10 +2,13 @@ package wecom
import (
"context"
"fmt"
"time"
"github.com/bytedance/sonic"
"gorm.io/gorm"
systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
@@ -27,30 +30,32 @@ type DirectoryProvider interface {
// MemberRepository 定义可见成员快照同步和分页查询边界。
type MemberRepository interface {
ReplaceVisible(ctx context.Context, applicationID uint, members []model.WeComMember, syncedAt time.Time) error
ReplaceVisible(ctx context.Context, tx *gorm.DB, applicationID uint, members []model.WeComMember, syncedAt time.Time) error
ListVisible(ctx context.Context, applicationID uint, page, pageSize int, keyword string) ([]model.WeComMember, int64, error)
}
// DirectoryService 同步并分页查询企业微信应用可见成员。
type DirectoryService struct {
db *gorm.DB
applications interface {
GetEnabled(ctx context.Context, applicationID uint) (*model.WeComApplication, error)
}
provider DirectoryProvider
members MemberRepository
audit systemconfigapp.AuditWriter
now func() time.Time
}
// NewDirectoryService 创建企业微信通讯录同步用例。
func NewDirectoryService(applications interface {
func NewDirectoryService(db *gorm.DB, applications interface {
GetEnabled(ctx context.Context, applicationID uint) (*model.WeComApplication, error)
}, provider DirectoryProvider, members MemberRepository) *DirectoryService {
return &DirectoryService{applications: applications, provider: provider, members: members, now: time.Now}
}, provider DirectoryProvider, members MemberRepository, audit systemconfigapp.AuditWriter) *DirectoryService {
return &DirectoryService{db: db, applications: applications, provider: provider, members: members, audit: audit, now: time.Now}
}
// Sync 拉取并替换指定应用当前可见成员快照。
func (s *DirectoryService) Sync(ctx context.Context, applicationID uint) (*dto.WeComMemberSyncResponse, error) {
if s == nil || s.applications == nil || s.provider == nil || s.members == nil || applicationID == 0 {
if s == nil || s.db == nil || s.applications == nil || s.provider == nil || s.members == nil || s.audit == nil || applicationID == 0 {
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信通讯录服务未配置")
}
if !canManageWeComDirectory(ctx) {
@@ -62,6 +67,7 @@ func (s *DirectoryService) Sync(ctx context.Context, applicationID uint) (*dto.W
}
remoteMembers, err := s.provider.ListVisibleMembers(ctx, applicationID)
if err != nil {
s.recordFailure(ctx, application, "同步企业微信应用可见成员失败")
return nil, err
}
syncedAt := s.now().UTC()
@@ -77,12 +83,46 @@ func (s *DirectoryService) Sync(ctx context.Context, applicationID uint) (*dto.W
CreatedAt: syncedAt, UpdatedAt: syncedAt,
})
}
if err := s.members.ReplaceVisible(ctx, applicationID, members, syncedAt); err != nil {
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.members.ReplaceVisible(ctx, tx, applicationID, members, syncedAt); err != nil {
return err
}
requestID := ""
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
requestID = *value
}
resourceID := fmt.Sprintf("%d", applicationID)
after := applicationAuditSnapshot(application)
after["synced_count"] = len(members)
after["synced_at"] = syncedAt
return s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: constants.AuditOperationWeComMembersSync,
Description: "同步企业微信应用可见成员", ConfigKey: fmt.Sprintf("wecom.application.%d.members", applicationID),
ResourceID: &resourceID, DisplayName: application.Name, Identity: applicationAuditIdentity(application),
AfterData: after, RequestID: requestID, CorrelationID: requestID,
})
})
if err != nil {
s.recordFailure(ctx, application, "保存企业微信应用可见成员快照失败")
return nil, err
}
return &dto.WeComMemberSyncResponse{ApplicationID: applicationID, SyncedCount: len(members), SyncedAt: syncedAt}, nil
}
func (s *DirectoryService) recordFailure(ctx context.Context, application *model.WeComApplication, description string) {
if application == nil {
return
}
resourceID := fmt.Sprintf("%d", application.ID)
recordConfigFailure(ctx, s.db, s.audit, systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: constants.AuditOperationWeComMembersSync,
Description: description, ConfigKey: fmt.Sprintf("wecom.application.%d.members", application.ID),
ResourceID: &resourceID, DisplayName: application.Name, Identity: applicationAuditIdentity(application),
BeforeData: applicationAuditSnapshot(application), Result: constants.AuditResultFailed,
ErrorCode: fmt.Sprintf("%d", errors.CodeInternalError), ErrorSummary: description,
})
}
// List 分页返回本地最近一次同步的应用可见成员。
func (s *DirectoryService) List(ctx context.Context, applicationID uint, request dto.WeComMemberListRequest) (*dto.WeComMemberListResponse, error) {
if s == nil || s.applications == nil || s.members == nil || applicationID == 0 {

View File

@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/hex"
stdErrors "errors"
"fmt"
"strings"
"time"
@@ -108,7 +109,7 @@ func (s *SceneService) ListBusinessFields(ctx context.Context, businessType stri
// Save 校验模板控件后创建或替换指定稳定业务场景映射。
func (s *SceneService) Save(ctx context.Context, businessType string, request dto.SaveWeComApprovalSceneRequest) (*dto.WeComApprovalSceneResponse, error) {
if s == nil || s.db == nil || s.provider == nil || s.repo == nil {
if s == nil || s.db == nil || s.provider == nil || s.repo == nil || s.audit == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信审批场景服务未配置")
}
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
@@ -124,9 +125,11 @@ func (s *SceneService) Save(ctx context.Context, businessType string, request dt
}
definition, err := s.provider.GetTemplateDetail(ctx, request.ApplicationID, strings.TrimSpace(request.TemplateID))
if err != nil {
s.recordFailure(ctx, businessType, request, constants.AuditResultFailed, errors.CodeInternalError, "校验企业微信审批模板失败")
return nil, err
}
if err := validateSceneMapping(businessType, request.ControlMapping, definition.Controls); err != nil {
s.recordFailure(ctx, businessType, request, constants.AuditResultDenied, errors.CodeInvalidParam, "拒绝保存非法企业微信审批场景映射")
return nil, err
}
request.ControlMapping = normalizeSceneMapping(request.ControlMapping)
@@ -177,23 +180,27 @@ func (s *SceneService) Save(ctx context.Context, businessType string, request dt
} else if err := s.repo.Update(ctx, tx, existing); err != nil {
return err
}
if s.audit != nil {
requestID := ""
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
requestID = *value
}
if err := s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
OperatorID: operatorID, OperationType: "wecom_approval_scene_save", Description: "保存企业微信审批模板映射",
ConfigKey: "wecom.approval_scene." + businessType, BeforeData: before,
AfterData: sceneAuditSnapshot(existing), RequestID: requestID, CorrelationID: requestID,
}); err != nil {
return err
}
requestID := ""
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
requestID = *value
}
resourceID := strings.TrimSpace(existing.BusinessType)
if existing.ID != 0 {
resourceID = fmt.Sprintf("%d", existing.ID)
}
if err := s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
OperatorID: operatorID, OperationType: constants.AuditOperationWeComApprovalSceneSave, Description: "保存企业微信审批模板映射",
ConfigKey: "wecom.approval_scene." + businessType, BeforeData: before,
AfterData: sceneAuditSnapshot(existing), RequestID: requestID, CorrelationID: requestID,
ResourceID: &resourceID, DisplayName: existing.TemplateName, Identity: sceneAuditIdentity(existing),
}); err != nil {
return err
}
saved = existing
return nil
})
if err != nil {
s.recordFailure(ctx, businessType, request, constants.AuditResultFailed, errors.CodeDatabaseError, "保存企业微信审批场景失败")
var appErr *errors.AppError
if stdErrors.As(err, &appErr) {
return nil, appErr
@@ -364,6 +371,28 @@ func sceneAuditSnapshot(scene *model.WeComApprovalScene) map[string]any {
}
}
func sceneAuditIdentity(scene *model.WeComApprovalScene) map[string]any {
if scene == nil {
return nil
}
return map[string]any{
"id": scene.ID, "business_type": scene.BusinessType, "application_id": scene.ApplicationID,
"template_id": scene.TemplateID, "template_name": scene.TemplateName, "status": scene.Status,
}
}
func (s *SceneService) recordFailure(ctx context.Context, businessType string, request dto.SaveWeComApprovalSceneRequest, result string, code int, description string) {
recordConfigFailure(ctx, s.db, s.audit, systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: constants.AuditOperationWeComApprovalSceneSave,
Description: description, ConfigKey: "wecom.approval_scene." + businessType,
DisplayName: businessType, Identity: map[string]any{
"business_type": businessType, "application_id": request.ApplicationID,
"template_id": strings.TrimSpace(request.TemplateID), "status": request.Status,
},
Result: result, ErrorCode: fmt.Sprintf("%d", code), ErrorSummary: description,
})
}
func sceneResponse(scene model.WeComApprovalScene) (*dto.WeComApprovalSceneResponse, error) {
var mapping []dto.WeComControlMappingItem
if err := sonic.Unmarshal(scene.ControlMapping, &mapping); err != nil {