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

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

@@ -0,0 +1,41 @@
package audit
import (
"context"
"gorm.io/gorm"
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
)
// BatchInput 描述一条批次根事件及每个已识别资源的子事件。
type BatchInput struct {
Root AppendInput
Children []AppendInput
}
// AppendBatch 在同一事务内追加批次根事件和资源子事件。
func (w *Writer) AppendBatch(ctx context.Context, tx *gorm.DB, input BatchInput) error {
if input.Root.EventID == "" {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "批次根事件缺少稳定事件ID")
}
if err := w.Append(ctx, tx, input.Root); err != nil {
return err
}
for index := range input.Children {
child := input.Children[index]
if child.EventID == "" {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "批次子事件缺少稳定事件ID")
}
if child.ParentEventID == "" {
child.ParentEventID = input.Root.EventID
}
if child.CorrelationID == "" {
child.CorrelationID = input.Root.CorrelationID
}
if err := w.Append(ctx, tx, child); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,410 @@
// Package audit 实现统一 Audit Event 的注册表与持久化 Adapter。
package audit
import "github.com/break/junhong_cmp_fiber/pkg/constants"
// ActionDefinition 是受控审计动作的写入契约。
type ActionDefinition struct {
Code string
Name string
Category string
Risk string
PrimaryResource string
AllowedActor string
Source string
RequireTransaction bool
DefaultVisibility string
AllowedVisibility []string
SubjectFields []string
SensitiveRead bool
}
// ResourceDefinition 是受控审计资源的快照契约。
type ResourceDefinition struct {
Type string
Name string
IdentityFields []string
}
// Registry 保存首批已评审的动作与资源定义。
type Registry struct {
actionsByOperation map[string]ActionDefinition
actionsByCode map[string]ActionDefinition
resources map[string]ResourceDefinition
}
// NewRegistry 创建首批统一审计注册表。
func NewRegistry() *Registry {
accountCreated := accountLifecycleAction(constants.AuditActionAccountCreated, "创建账号", constants.AuditRiskNormal)
accountUpdated := accountLifecycleAction(constants.AuditActionAccountUpdated, "更新账号", constants.AuditRiskNormal)
accountDeleted := accountLifecycleAction(constants.AuditActionAccountDeleted, "删除账号", constants.AuditRiskHigh)
accountPasswordReset := accountSecurityAction(constants.AuditActionAccountPasswordReset, "重置账号密码", constants.AuditRiskHigh)
accountPasswordChanged := accountSecurityAction(constants.AuditActionAccountPasswordChanged, "修改账号密码", constants.AuditRiskHigh)
accountWeComBound := accountSecurityAction(constants.AuditActionAccountWeComBound, "绑定账号企业微信身份", constants.AuditRiskNormal)
authLogin := accountSecurityAction(constants.AuditActionAuthLogin, "后台账号登录", constants.AuditRiskNormal)
authLogout := accountSecurityAction(constants.AuditActionAuthLogout, "后台账号退出登录", constants.AuditRiskNormal)
authTokenRefreshed := accountSecurityAction(constants.AuditActionAuthTokenRefreshed, "刷新后台访问令牌", constants.AuditRiskNormal)
accountRolesAssigned := accessAction(constants.AuditActionAccountRolesAssigned, "分配账号角色", constants.AuditResourceAccount)
accountRoleRemoved := accessAction(constants.AuditActionAccountRoleRemoved, "移除账号角色", constants.AuditResourceAccount)
shopRolesAssigned := accessAction(constants.AuditActionShopRolesAssigned, "分配店铺角色", constants.AuditResourceShop)
shopRoleDeleted := accessAction(constants.AuditActionShopRoleDeleted, "移除店铺角色", constants.AuditResourceShop)
shopRolesAssigned.Category = constants.AuditCategoryBusiness
shopRoleDeleted.Category = constants.AuditCategoryBusiness
shopCreated := shopIdentityAction(constants.AuditActionShopCreated, "创建店铺")
shopUpdated := shopIdentityAction(constants.AuditActionShopUpdated, "更新店铺基础资料")
shopEnabled := shopStateAction(constants.AuditActionShopEnabled, "启用店铺", constants.AuditRiskNormal)
shopDisabled := shopStateAction(constants.AuditActionShopDisabled, "禁用店铺", constants.AuditRiskNormal)
shopDeleted := shopStateAction(constants.AuditActionShopDeleted, "删除店铺", constants.AuditRiskHigh)
shopBusinessOwnerUpdated := shopStateAction(constants.AuditActionShopBusinessOwnerUpdated, "更新店铺业务员归属", constants.AuditRiskNormal)
shopClientLoginLimitUpdated := shopStateAction(constants.AuditActionShopClientLoginLimitUpdated, "更新店铺 C 端登录限制", constants.AuditRiskHigh)
enterpriseCreated := enterpriseAction(constants.AuditActionEnterpriseCreated, "创建企业", constants.AuditCategoryBusiness, constants.AuditRiskNormal)
enterpriseUpdated := enterpriseAction(constants.AuditActionEnterpriseUpdated, "更新企业基础资料", constants.AuditCategoryBusiness, constants.AuditRiskNormal)
enterpriseStatusUpdated := enterpriseAction(constants.AuditActionEnterpriseStatusUpdated, "更新企业状态", constants.AuditCategoryBusiness, constants.AuditRiskNormal)
enterprisePasswordUpdated := enterpriseAction(constants.AuditActionEnterprisePasswordUpdated, "更新企业账号密码", constants.AuditCategoryBusiness, constants.AuditRiskHigh)
enterpriseCardsAllocated := enterpriseCardAction(constants.AuditActionEnterpriseCardsAllocated, "向企业授权卡")
enterpriseCardsRecalled := enterpriseCardAction(constants.AuditActionEnterpriseCardsRecalled, "回收企业卡授权")
enterpriseCardRemarkUpdated := enterpriseCardAction(constants.AuditActionEnterpriseCardRemarkUpdated, "更新企业卡授权备注")
enterpriseDevicesAllocated := enterpriseCardAction(constants.AuditActionEnterpriseDevicesAllocated, "向企业授权设备")
enterpriseDevicesRecalled := enterpriseCardAction(constants.AuditActionEnterpriseDevicesRecalled, "回收企业设备授权")
personalProfileUpdated := personalAction(constants.AuditActionPersonalCustomerProfileUpdated, "更新个人资料", []string{"nickname", "avatar_url"})
personalPhoneBound := personalAction(constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号", []string{"phone"})
personalPhoneChanged := personalAction(constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号", []string{"phone"})
personalWechatIdentityUpdated := personalAction(constants.AuditActionPersonalCustomerWechatIdentityUpdated, "同步个人微信主体", []string{"app_id", "app_type"})
systemConfigUpdated := ActionDefinition{
Code: constants.AuditActionSystemConfigUpdated, Name: "更新受控系统配置",
Category: constants.AuditCategoryConfiguration, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceSystemConfig, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
outboxReplayed := outboxRecoveryAction(
constants.AuditActionOutboxReplayed,
"人工重放 Outbox 事件",
)
outboxExpiredLeaseReleased := outboxRecoveryAction(
constants.AuditActionOutboxExpiredLeaseReleased,
"人工释放 Outbox 过期租约",
)
deviceBatchCompleted := deviceBatchAction(
constants.AuditActionDeviceBatchAllocationCompleted,
"完成设备批量分配",
constants.AuditResourceDeviceBatchTask,
)
deviceBatchItem := deviceBatchAction(
constants.AuditActionDeviceBatchAllocationItem,
"处理设备批量分配项",
constants.AuditResourceDevice,
)
deviceBatchItem.AllowedVisibility = []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult}
wecomCredentialsRead := ActionDefinition{
Code: constants.AuditActionWeComCredentialsRead, Name: "读取企业微信应用明文凭据",
Category: constants.AuditCategorySecurity, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceWeComApplication, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly}, SensitiveRead: true,
}
roleCreated := accessAction(constants.AuditActionRoleCreated, "创建角色", constants.AuditResourceRole)
roleUpdated := accessAction(constants.AuditActionRoleUpdated, "更新角色", constants.AuditResourceRole)
roleStatusUpdated := accessAction(constants.AuditActionRoleStatusUpdated, "更新角色状态", constants.AuditResourceRole)
roleDefaultCreditUpdated := accessAction(constants.AuditActionRoleDefaultCreditUpdated, "更新角色默认信用额度", constants.AuditResourceRole)
roleDeleted := accessAction(constants.AuditActionRoleDeleted, "删除角色", constants.AuditResourceRole)
rolePermissionsAssigned := accessAction(constants.AuditActionRolePermissionsAssigned, "配置角色权限", constants.AuditResourceRole)
rolePermissionRemoved := accessAction(constants.AuditActionRolePermissionRemoved, "移除角色权限", constants.AuditResourceRole)
rolePermissionsBatchRemoved := accessAction(constants.AuditActionRolePermissionsBatchRemoved, "批量移除角色权限", constants.AuditResourceRole)
permissionCreated := accessAction(constants.AuditActionPermissionCreated, "创建权限", constants.AuditResourcePermission)
permissionUpdated := accessAction(constants.AuditActionPermissionUpdated, "更新权限", constants.AuditResourcePermission)
permissionDeleted := accessAction(constants.AuditActionPermissionDeleted, "删除权限", constants.AuditResourcePermission)
return &Registry{
actionsByOperation: map[string]ActionDefinition{
constants.AuditOperationSystemConfigUpdate: systemConfigUpdated,
constants.AuditOperationOutboxReplay: outboxReplayed,
constants.AuditOperationOutboxReleaseExpiredLease: outboxExpiredLeaseReleased,
},
actionsByCode: map[string]ActionDefinition{
constants.AuditActionAccountCreated: accountCreated,
constants.AuditActionAccountUpdated: accountUpdated,
constants.AuditActionAccountDeleted: accountDeleted,
constants.AuditActionAccountPasswordReset: accountPasswordReset,
constants.AuditActionAccountPasswordChanged: accountPasswordChanged,
constants.AuditActionAccountWeComBound: accountWeComBound,
constants.AuditActionAuthLogin: authLogin,
constants.AuditActionAuthLogout: authLogout,
constants.AuditActionAuthTokenRefreshed: authTokenRefreshed,
constants.AuditActionAccountRolesAssigned: accountRolesAssigned,
constants.AuditActionAccountRoleRemoved: accountRoleRemoved,
constants.AuditActionShopRolesAssigned: shopRolesAssigned,
constants.AuditActionShopRoleDeleted: shopRoleDeleted,
constants.AuditActionShopCreated: shopCreated,
constants.AuditActionShopUpdated: shopUpdated,
constants.AuditActionShopEnabled: shopEnabled,
constants.AuditActionShopDisabled: shopDisabled,
constants.AuditActionShopDeleted: shopDeleted,
constants.AuditActionShopBusinessOwnerUpdated: shopBusinessOwnerUpdated,
constants.AuditActionShopClientLoginLimitUpdated: shopClientLoginLimitUpdated,
constants.AuditActionEnterpriseCreated: enterpriseCreated,
constants.AuditActionEnterpriseUpdated: enterpriseUpdated,
constants.AuditActionEnterpriseStatusUpdated: enterpriseStatusUpdated,
constants.AuditActionEnterprisePasswordUpdated: enterprisePasswordUpdated,
constants.AuditActionEnterpriseCardsAllocated: enterpriseCardsAllocated,
constants.AuditActionEnterpriseCardsRecalled: enterpriseCardsRecalled,
constants.AuditActionEnterpriseCardRemarkUpdated: enterpriseCardRemarkUpdated,
constants.AuditActionEnterpriseDevicesAllocated: enterpriseDevicesAllocated,
constants.AuditActionEnterpriseDevicesRecalled: enterpriseDevicesRecalled,
constants.AuditActionPersonalCustomerProfileUpdated: personalProfileUpdated,
constants.AuditActionPersonalCustomerPhoneBound: personalPhoneBound,
constants.AuditActionPersonalCustomerPhoneChanged: personalPhoneChanged,
constants.AuditActionPersonalCustomerWechatIdentityUpdated: personalWechatIdentityUpdated,
constants.AuditActionSystemConfigUpdated: systemConfigUpdated,
constants.AuditActionOutboxReplayed: outboxReplayed,
constants.AuditActionOutboxExpiredLeaseReleased: outboxExpiredLeaseReleased,
constants.AuditActionDeviceBatchAllocationCompleted: deviceBatchCompleted,
constants.AuditActionDeviceBatchAllocationItem: deviceBatchItem,
constants.AuditActionWeComCredentialsRead: wecomCredentialsRead,
constants.AuditActionRoleCreated: roleCreated,
constants.AuditActionRoleUpdated: roleUpdated,
constants.AuditActionRoleStatusUpdated: roleStatusUpdated,
constants.AuditActionRoleDefaultCreditUpdated: roleDefaultCreditUpdated,
constants.AuditActionRoleDeleted: roleDeleted,
constants.AuditActionRolePermissionsAssigned: rolePermissionsAssigned,
constants.AuditActionRolePermissionRemoved: rolePermissionRemoved,
constants.AuditActionRolePermissionsBatchRemoved: rolePermissionsBatchRemoved,
constants.AuditActionPermissionCreated: permissionCreated,
constants.AuditActionPermissionUpdated: permissionUpdated,
constants.AuditActionPermissionDeleted: permissionDeleted,
},
resources: map[string]ResourceDefinition{
constants.AuditResourceAccount: {
Type: constants.AuditResourceAccount, Name: "账号",
IdentityFields: []string{"id", "username", "phone", "user_type", "shop_id", "enterprise_id", "wecom_userid", "wecom_name"},
},
constants.AuditResourceRole: {
Type: constants.AuditResourceRole, Name: "角色",
IdentityFields: []string{"id", "role_name", "role_type", "status", "default_credit_enabled", "default_credit_limit"},
},
constants.AuditResourcePermission: {
Type: constants.AuditResourcePermission, Name: "权限",
IdentityFields: []string{"id", "perm_name", "perm_code", "perm_type", "platform", "available_for_role_types", "parent_id", "status"},
},
constants.AuditResourceSystemConfig: {
Type: constants.AuditResourceSystemConfig, Name: "受控系统配置",
IdentityFields: []string{"config_key", "module"},
},
constants.AuditResourceOutboxEvent: {
Type: constants.AuditResourceOutboxEvent, Name: "Outbox 事件",
IdentityFields: []string{
"event_id", "event_type", "aggregate_type", "aggregate_id",
"resource_type", "resource_id", "business_key",
},
},
constants.AuditResourceDeviceBatchTask: {
Type: constants.AuditResourceDeviceBatchTask, Name: "设备批量分配任务",
IdentityFields: []string{"task_no", "operation_type"},
},
constants.AuditResourceDevice: {
Type: constants.AuditResourceDevice, Name: "设备",
IdentityFields: []string{"id", "virtual_no", "imei", "sn", "generation"},
},
constants.AuditResourceIotCard: {
Type: constants.AuditResourceIotCard, Name: "IoT卡",
IdentityFields: []string{"id", "iccid", "iccid_19", "iccid_20", "virtual_no", "msisdn", "carrier_type", "shop_id", "series_id", "generation"},
},
constants.AuditResourceShop: {
Type: constants.AuditResourceShop, Name: "店铺",
IdentityFields: []string{"id", "shop_code", "shop_name", "parent_id", "level"},
},
constants.AuditResourceOrder: {
Type: constants.AuditResourceOrder, Name: "订单",
IdentityFields: []string{"id", "order_no", "buyer_type", "buyer_id", "asset_identifier", "total_amount", "payment_method", "payment_status"},
},
constants.AuditResourceRefund: {
Type: constants.AuditResourceRefund, Name: "退款单",
IdentityFields: []string{"id", "refund_no", "order_id", "order_no", "asset_identifier", "shop_id", "requested_refund_amount", "status"},
},
constants.AuditResourceEnterprise: {
Type: constants.AuditResourceEnterprise, Name: "企业",
IdentityFields: []string{"id", "enterprise_code", "enterprise_name", "owner_shop_id"},
},
constants.AuditResourceDeviceSIMBinding: {
Type: constants.AuditResourceDeviceSIMBinding, Name: "设备卡槽绑定",
IdentityFields: []string{"id", "device_id", "device_virtual_no", "slot_position", "iot_card_id", "iccid", "virtual_no", "is_current"},
},
constants.AuditResourceAssetAllocationRecord: {
Type: constants.AuditResourceAssetAllocationRecord, Name: "资产分配记录",
IdentityFields: []string{"id", "allocation_no", "asset_type", "asset_id", "asset_identifier", "from_owner_type", "from_owner_id", "to_owner_type", "to_owner_id"},
},
constants.AuditResourceExchangeOrder: {
Type: constants.AuditResourceExchangeOrder, Name: "换货单",
IdentityFields: []string{"id", "exchange_no", "old_asset_type", "old_asset_id", "new_asset_type", "new_asset_id", "shop_id", "status"},
},
constants.AuditResourceAgentRecharge: {
Type: constants.AuditResourceAgentRecharge, Name: "代理充值单",
IdentityFields: []string{"id", "recharge_no", "shop_id", "agent_wallet_id", "approval_instance_id", "status"},
},
constants.AuditResourceAssetWallet: {
Type: constants.AuditResourceAssetWallet, Name: "资产钱包",
IdentityFields: []string{"id", "resource_type", "resource_id", "currency"},
},
constants.AuditResourceApprovalInstance: {
Type: constants.AuditResourceApprovalInstance, Name: "审批实例",
IdentityFields: []string{"id", "business_type", "business_id", "provider", "external_ref", "status"},
},
constants.AuditResourceWeComApplication: {
Type: constants.AuditResourceWeComApplication, Name: "企业微信应用配置",
IdentityFields: []string{"id", "corp_id", "agent_id", "name", "status", "credentials_configured"},
},
constants.AuditResourceAuthentication: {
Type: constants.AuditResourceAuthentication, Name: "认证状态",
IdentityFields: []string{"account_id", "device", "auth_method", "state", "wecom_corp_id", "wecom_userid", "wecom_name"},
},
constants.AuditResourceEnterpriseCardAuthorization: {
Type: constants.AuditResourceEnterpriseCardAuthorization, Name: "企业卡授权记录",
IdentityFields: []string{"id", "enterprise_id", "card_id", "authorized_by", "authorizer_type", "authorized_at", "revoked_by", "revoked_at", "device_auth_id"},
},
constants.AuditResourceEnterpriseDeviceAuthorization: {
Type: constants.AuditResourceEnterpriseDeviceAuthorization, Name: "企业设备授权记录",
IdentityFields: []string{"id", "enterprise_id", "device_id", "authorized_by", "authorizer_type", "authorized_at", "revoked_by", "revoked_at"},
},
constants.AuditResourcePersonalCustomer: {
Type: constants.AuditResourcePersonalCustomer, Name: "个人客户",
IdentityFields: []string{"id", "nickname", "wx_open_id", "wx_union_id", "status"},
},
constants.AuditResourcePersonalCustomerPhone: {
Type: constants.AuditResourcePersonalCustomerPhone, Name: "个人客户手机号",
IdentityFields: []string{"id", "customer_id", "phone", "is_primary", "verified_at", "status"},
},
constants.AuditResourcePersonalCustomerOpenID: {
Type: constants.AuditResourcePersonalCustomerOpenID, Name: "个人客户微信主体",
IdentityFields: []string{"id", "customer_id", "app_id", "open_id", "union_id", "app_type"},
},
},
}
}
func accountSecurityAction(code, name, risk string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategorySecurity, Risk: risk,
PrimaryResource: constants.AuditResourceAccount, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func accessAction(code, name, primaryResource string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategorySecurity, Risk: constants.AuditRiskHigh,
PrimaryResource: primaryResource, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func shopIdentityAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourceShop, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func shopStateAction(code, name, risk string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: risk,
PrimaryResource: constants.AuditResourceShop, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
}
}
func enterpriseAction(code, name, category, risk string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: category, Risk: risk,
PrimaryResource: constants.AuditResourceEnterprise, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func enterpriseCardAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourceEnterprise, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
}
}
func personalAction(code, name string, subjectFields []string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryIdentity, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourcePersonalCustomer, AllowedActor: constants.AuditActorPersonalCustomer,
Source: constants.AuditSourcePersonalAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectDetail,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectDetail},
SubjectFields: subjectFields,
}
}
func accountLifecycleAction(code, name, risk string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryIdentity, Risk: risk,
PrimaryResource: constants.AuditResourceAccount, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func deviceBatchAction(code, name, primaryResource string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskNormal,
PrimaryResource: primaryResource, AllowedActor: constants.AuditActorSystemTask,
Source: constants.AuditSourceWorker, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func outboxRecoveryAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryReliability, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceOutboxEvent, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
// Action 返回已注册动作定义。
func (r *Registry) Action(code string) (ActionDefinition, bool) {
if r == nil {
return ActionDefinition{}, false
}
action, ok := r.actionsByCode[code]
return action, ok
}
// ActionByOperation 返回旧应用接缝操作类型对应的受控动作。
func (r *Registry) ActionByOperation(operation string) (ActionDefinition, bool) {
if r == nil {
return ActionDefinition{}, false
}
action, ok := r.actionsByOperation[operation]
return action, ok
}
// Resource 返回已注册资源定义。
func (r *Registry) Resource(resourceType string) (ResourceDefinition, bool) {
if r == nil {
return ResourceDefinition{}, false
}
resource, ok := r.resources[resourceType]
return resource, ok
}

View File

@@ -0,0 +1,57 @@
package audit
import (
"testing"
"github.com/bytedance/sonic"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// TestSecurityActionsRegistered 验证账号安全动作不会绕过注册表。
func TestSecurityActionsRegistered(t *testing.T) {
registry := NewRegistry()
for _, code := range []string{
constants.AuditActionAccountPasswordReset,
constants.AuditActionAccountPasswordChanged,
constants.AuditActionAccountWeComBound,
constants.AuditActionAuthLogin,
constants.AuditActionAuthLogout,
constants.AuditActionAuthTokenRefreshed,
} {
action, ok := registry.Action(code)
if !ok {
t.Fatalf("安全动作未注册:%s", code)
}
if action.PrimaryResource != constants.AuditResourceAccount || action.Category != constants.AuditCategorySecurity {
t.Fatalf("安全动作注册错误:%s", code)
}
}
}
// TestSecurityAuditRemovesCredentials 验证安全凭据不会进入审计 JSON。
func TestSecurityAuditRemovesCredentials(t *testing.T) {
encoded, err := safeObject(map[string]any{
"password": "secret",
"verification_code": "123456",
"access_token": "token",
"cookie": "session=value",
"credentials_configured": true,
"state": "changed",
})
if err != nil {
t.Fatalf("清理审计 JSON 失败:%v", err)
}
var value map[string]any
if err := sonic.Unmarshal(encoded, &value); err != nil {
t.Fatalf("解析审计 JSON 失败:%v", err)
}
for _, field := range []string{"password", "verification_code", "access_token", "cookie"} {
if _, exists := value[field]; exists {
t.Fatalf("安全凭据未删除:%s", field)
}
}
if value["credentials_configured"] != true || value["state"] != "changed" {
t.Fatalf("安全业务事实被错误删除:%v", value)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ import (
"encoding/hex"
"strings"
"time"
"unicode/utf8"
"github.com/google/uuid"
"gorm.io/datatypes"
@@ -46,15 +47,18 @@ type Attempt struct {
// Completion 描述外部尝试从待处理状态进入终态的结果。
type Completion struct {
Result string
HTTPStatus int
ProviderCode string
ProviderMessage string
ResponseSummary any
DurationMS int64
StateChanged bool
AuditEventID *uint
RecoveryStrategy string
Result string
HTTPStatus int
ProviderCode string
ProviderMessage string
SafeProviderMessage string
ResponseSummary any
DurationMS int64
StateChanged bool
ResourceID *string
ResourceKey *string
AuditEventID *uint
RecoveryStrategy string
}
// InboundAttempt 描述业务处理前必须保存的入站回调安全事实。
@@ -105,6 +109,13 @@ func (r *Repository) Start(ctx context.Context, input Attempt) (*model.Integrati
}
if input.Attempt <= 0 {
input.Attempt = 1
if input.TriggerSeries != nil {
if err := r.db.WithContext(ctx).Model(&model.IntegrationLog{}).
Select("COALESCE(MAX(attempt), 0) + 1").
Where("trigger_series = ?", *input.TriggerSeries).Scan(&input.Attempt).Error; err != nil {
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "计算 Integration Log 尝试序号失败")
}
}
}
if input.StartedAt == nil {
startedAt := r.now().UTC()
@@ -136,12 +147,19 @@ func (r *Repository) Complete(ctx context.Context, integrationID string, complet
if r == nil || r.db == nil {
return nil, pkgerrors.New(pkgerrors.CodeInvalidStatus, "Integration Log 数据库未配置")
}
if integrationID == "" || !isTerminalResult(completion.Result) {
if !validRequiredString(integrationID, constants.IntegrationIDMaxLength) ||
!validOptionalString(completion.ResourceID, constants.IntegrationResourceIDMaxLength) ||
!validOptionalString(completion.ResourceKey, constants.IntegrationResourceKeyMaxLength) ||
!isTerminalResult(completion.Result) {
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 终态参数无效")
}
if completion.Result == constants.IntegrationResultUnknown && strings.TrimSpace(completion.RecoveryStrategy) == "" {
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "结果未知必须记录明确恢复策略")
}
safeProviderMessage := strings.TrimSpace(completion.SafeProviderMessage)
if safeProviderMessage != "" && utf8.RuneCountInString(constants.IntegrationSafeMessagePrefix+safeProviderMessage) > constants.IntegrationProviderMessageMaxLength {
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 安全结果摘要过长")
}
responseSummary, err := marshalSummary(completion.ResponseSummary)
if err != nil {
return nil, pkgerrors.Wrap(pkgerrors.CodeInvalidParam, err, "Integration Log 响应摘要无效")
@@ -157,12 +175,20 @@ func (r *Repository) Complete(ctx context.Context, integrationID string, complet
if completion.ProviderCode != "" {
updates["provider_code"] = completion.ProviderCode
}
if completion.ProviderMessage != "" {
if safeProviderMessage != "" {
updates["provider_message"] = constants.IntegrationSafeMessagePrefix + safeProviderMessage
} else if completion.ProviderMessage != "" {
updates["provider_message"] = sanitizer.TextSummary(completion.ProviderMessage)
}
if completion.AuditEventID != nil {
updates["audit_event_id"] = completion.AuditEventID
}
if completion.ResourceID != nil {
updates["resource_id"] = completion.ResourceID
}
if completion.ResourceKey != nil {
updates["resource_key"] = completion.ResourceKey
}
if completion.RecoveryStrategy != "" {
updates["recovery_strategy"] = completion.RecoveryStrategy
}
@@ -187,12 +213,17 @@ func (r *Repository) RecordInbound(ctx context.Context, input InboundAttempt) (*
if r == nil || r.db == nil {
return nil, false, pkgerrors.New(pkgerrors.CodeInvalidStatus, "Integration Log 数据库未配置")
}
if input.Provider == "" || input.Operation == "" || input.IdempotencyKey == "" {
if input.Provider == "" || input.Operation == "" || input.IdempotencyKey == "" ||
!validGeneratedString(input.IntegrationID, constants.IntegrationIDMaxLength) ||
!validOptionalString(input.ResourceID, constants.IntegrationResourceIDMaxLength) ||
!validOptionalString(input.ResourceKey, constants.IntegrationResourceKeyMaxLength) ||
!validOptionalString(input.CorrelationID, constants.IntegrationCorrelationIDMaxLength) {
return nil, false, pkgerrors.New(pkgerrors.CodeInvalidParam, "入站 Integration Log 参数无效")
}
if input.IntegrationID == "" {
input.IntegrationID = uuid.NewString()
}
triggerSeries := input.IntegrationID
hash := sha256.Sum256(input.RawPayload)
summary, err := marshalSummary(map[string]any{
"content_type": input.ContentType,
@@ -208,7 +239,8 @@ func (r *Repository) RecordInbound(ctx context.Context, input InboundAttempt) (*
Provider: input.Provider, Direction: constants.IntegrationDirectionInbound, Operation: input.Operation,
ExternalID: optionalString(input.ExternalID), ResourceType: optionalString(input.ResourceType),
ResourceID: input.ResourceID, ResourceKey: input.ResourceKey, StartedAt: &now, Attempt: 1,
Result: constants.IntegrationResultPending, RequestSummary: summary,
TriggerSeries: &triggerSeries,
Result: constants.IntegrationResultPending, RequestSummary: summary,
ContentHash: hex.EncodeToString(hash[:]), RequestID: input.RequestID, CorrelationID: input.CorrelationID,
}
result := r.db.WithContext(ctx).Clauses(clause.OnConflict{
@@ -289,9 +321,28 @@ func validateAttempt(input Attempt) error {
if input.InitialResult != "" && input.InitialResult != constants.IntegrationResultPending && !isUnsentResult(input.InitialResult) {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 初始结果只能是待处理或未发送终态")
}
if !validGeneratedString(input.IntegrationID, constants.IntegrationIDMaxLength) ||
!validOptionalString(input.TriggerSeries, constants.IntegrationTriggerSeriesMaxLength) ||
!validOptionalString(input.CorrelationID, constants.IntegrationCorrelationIDMaxLength) ||
!validOptionalString(input.ResourceID, constants.IntegrationResourceIDMaxLength) ||
!validOptionalString(input.ResourceKey, constants.IntegrationResourceKeyMaxLength) {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 链路或资源标识无效")
}
return nil
}
func validGeneratedString(value string, maxLength int) bool {
return value == "" || validRequiredString(value, maxLength)
}
func validRequiredString(value string, maxLength int) bool {
return strings.TrimSpace(value) == value && value != "" && utf8.RuneCountInString(value) <= maxLength
}
func validOptionalString(value *string, maxLength int) bool {
return value == nil || validRequiredString(*value, maxLength)
}
func isTerminalResult(result string) bool {
switch result {
case constants.IntegrationResultSuccess, constants.IntegrationResultFailed, constants.IntegrationResultUnknown,

View File

@@ -14,6 +14,7 @@ import (
"gorm.io/gorm/clause"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
@@ -29,6 +30,7 @@ type DeliveryEnvelope struct {
BusinessKey string `json:"business_key,omitempty"`
RequestID string `json:"request_id,omitempty"`
CorrelationID string `json:"correlation_id,omitempty"`
ParentEventID string `json:"parent_event_id,omitempty"`
Payload sonic.NoCopyRawMessage `json:"payload"`
}
@@ -253,7 +255,7 @@ func deliveryEnvelope(event model.OutboxEvent) DeliveryEnvelope {
EventID: event.EventID, EventType: event.EventType, PayloadVersion: event.PayloadVersion,
AggregateType: event.AggregateType, AggregateID: event.AggregateID,
ResourceType: event.ResourceType, ResourceID: event.ResourceID, BusinessKey: event.BusinessKey,
RequestID: event.RequestID, CorrelationID: event.CorrelationID,
RequestID: event.RequestID, CorrelationID: event.CorrelationID, ParentEventID: event.ParentEventID,
Payload: sonic.NoCopyRawMessage(event.Payload),
}
}
@@ -326,5 +328,10 @@ func (h *Handler) Handle(ctx context.Context, task *asynq.Task) error {
if envelope.EventID == "" || envelope.EventType == "" || envelope.PayloadVersion <= 0 {
return stderrors.New("Outbox 事件信封不完整")
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: envelope.EventType,
ActorName: "Outbox 消费任务", Source: constants.AuditSourceWorker,
RequestID: envelope.RequestID, CorrelationID: envelope.CorrelationID, ParentEventID: envelope.ParentEventID,
})
return h.consumer.Consume(ctx, envelope)
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/asynctask"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
@@ -30,6 +31,7 @@ type Envelope struct {
BusinessKey string `json:"business_key,omitempty"`
RequestID string `json:"request_id,omitempty"`
CorrelationID string `json:"correlation_id,omitempty"`
ParentEventID string `json:"parent_event_id,omitempty"`
Payload any `json:"payload"`
}
@@ -73,13 +75,24 @@ func (r *Repository) append(ctx context.Context, tx *gorm.DB, envelope Envelope,
if envelope.PayloadVersion <= 0 {
envelope.PayloadVersion = 1
}
linkage := auditcontext.From(ctx)
if envelope.RequestID == "" {
envelope.RequestID = linkage.RequestID
}
if envelope.CorrelationID == "" {
envelope.CorrelationID = linkage.CorrelationID
}
if envelope.ParentEventID == "" {
envelope.ParentEventID = linkage.ParentEventID
}
now := time.Now().UTC()
event := &model.OutboxEvent{
EventID: envelope.EventID, EventType: envelope.EventType, PayloadVersion: envelope.PayloadVersion,
AggregateType: envelope.AggregateType, AggregateID: envelope.AggregateID,
ResourceType: envelope.ResourceType, ResourceID: envelope.ResourceID, BusinessKey: envelope.BusinessKey,
RequestID: envelope.RequestID, CorrelationID: envelope.CorrelationID, Payload: datatypes.JSON(payload),
Status: constants.OutboxStatusPending, MaxRetries: constants.OutboxDefaultMaxRetries, NextAttemptAt: now,
RequestID: envelope.RequestID, CorrelationID: envelope.CorrelationID, ParentEventID: envelope.ParentEventID,
Payload: datatypes.JSON(payload),
Status: constants.OutboxStatusPending, MaxRetries: constants.OutboxDefaultMaxRetries, NextAttemptAt: now,
}
create := tx.WithContext(ctx)
if idempotent {

View File

@@ -32,7 +32,8 @@ func (w *AgentRechargePaymentEventWriter) Append(ctx context.Context, tx *gorm.D
PayloadVersion: constants.AgentRechargePaymentConfirmedPayloadVersionV1,
AggregateType: "agent_recharge", AggregateID: strconv.FormatUint(uint64(event.RechargeID), 10),
ResourceType: "payment", ResourceID: strconv.FormatUint(uint64(event.PaymentID), 10),
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event,
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID,
ParentEventID: event.ParentEventID, Payload: event,
})
return err
}

View File

@@ -6,6 +6,8 @@ import (
"github.com/hibiken/asynq"
agentrecharge "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
@@ -20,10 +22,18 @@ func NewAgentRechargeRecoveryTaskHandler(service *agentrecharge.RecoverOnlinePay
}
// Handle 扫描长期待支付记录并复用原支付单号收敛渠道状态。
func (h *AgentRechargeRecoveryTaskHandler) Handle(ctx context.Context, _ *asynq.Task) error {
func (h *AgentRechargeRecoveryTaskHandler) Handle(ctx context.Context, task *asynq.Task) error {
if h == nil || h.service == nil {
return errors.New(errors.CodeServiceUnavailable, "代理在线充值支付恢复任务未配置")
}
taskType := constants.TaskTypeAgentRechargeRecovery
if task != nil && task.Type() != "" {
taskType = task.Type()
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorScheduledJob, ActorID: taskType,
ActorName: "代理在线充值支付恢复计划任务", Source: constants.AuditSourceScheduler,
})
_, err := h.service.ProcessBatch(ctx)
return err
}

View File

@@ -118,7 +118,7 @@ func (a *AlipayWapAdapter) completeUnknown(ctx context.Context, integrationID st
a.logger.Warn("支付宝支付请求结果未知", zap.String("integration_id", integrationID), zap.Error(cause))
}
_, err := a.integration.Complete(ctx, integrationID, integrationlog.Completion{
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", ProviderMessage: "支付宝支付请求结果未知",
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", SafeProviderMessage: "支付宝支付请求结果未知",
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
RecoveryStrategy: "使用原支付单号主动查单,确认不存在或关闭后才允许关闭本地支付单",
})

View File

@@ -184,7 +184,7 @@ func (a *WechatWebAdapter) completeUnknown(ctx context.Context, integrationID st
a.logger.Warn("微信支付请求结果未知", zap.String("integration_id", integrationID), zap.Error(cause))
}
_, err := a.integration.Complete(ctx, integrationID, integrationlog.Completion{
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", ProviderMessage: "微信支付请求结果未知",
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", SafeProviderMessage: "微信支付请求结果未知",
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
RecoveryStrategy: "使用原支付单号主动查单,确认不存在或关闭后才允许关闭本地支付单",
})

View File

@@ -92,10 +92,13 @@ func (u *ApprovalAttachmentUploader) Upload(ctx context.Context, applicationID,
return "", err
}
resourceID := strconv.FormatUint(uint64(instanceID), 10)
integrationID, triggerSeries, correlationID := singleIntegrationLinkage(nil)
attempt, err := u.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
IntegrationID: integrationID,
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComAttachmentUpload, ResourceType: constants.WeComApprovalInstanceResourceType,
ResourceID: &resourceID, RequestSummary: map[string]any{
ResourceID: &resourceID, TriggerSeries: triggerSeries, CorrelationID: correlationID,
RequestSummary: map[string]any{
"application_id": applicationID, "file_name": fileName, "file_bytes": info.Size(),
},
})

View File

@@ -42,7 +42,7 @@ func NewApprovalDetailClient(tokens DirectoryTokenProvider, integration TokenInt
}
// Get 获取审批详情并记录一次真实外呼。
func (c *ApprovalDetailClient) Get(ctx context.Context, applicationID uint, spNo string) (ApprovalDetail, error) {
func (c *ApprovalDetailClient) Get(ctx context.Context, applicationID uint, spNo string, resourceID *string, correlationID string) (ApprovalDetail, error) {
token, err := c.tokens.GetAccessToken(ctx, applicationID)
if err != nil {
return ApprovalDetail{}, err
@@ -51,11 +51,14 @@ func (c *ApprovalDetailClient) Get(ctx context.Context, applicationID uint, spNo
if err != nil {
return ApprovalDetail{}, err
}
resourceID := strconv.FormatUint(uint64(applicationID), 10)
resourceKey := strings.TrimSpace(spNo)
triggerSeries := "wecom-approval-detail:" + resourceKey
attempt, err := c.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComApprovalDetail, ResourceType: constants.WeComApprovalInstanceResourceType,
ResourceID: &resourceID, ExternalID: &spNo, RequestSummary: map[string]any{"application_id": applicationID, "sp_no": spNo},
ResourceID: resourceID, ResourceKey: &resourceKey, ExternalID: &spNo,
TriggerSeries: &triggerSeries, CorrelationID: optionalIntegrationString(correlationID),
RequestSummary: map[string]any{"application_id": applicationID, "sp_no": spNo},
})
if err != nil {
return ApprovalDetail{}, err

View File

@@ -2,6 +2,7 @@ package wecom
import (
"context"
"strconv"
"strings"
"time"
@@ -39,11 +40,18 @@ func (h *ApprovalDetailTaskHandler) Handle(ctx context.Context, task *asynq.Task
if !validApprovalSyncSource(payload.Source) {
return errors.New(errors.CodeInvalidParam, "企业微信审批详情同步来源无效")
}
detail, err := h.details.Get(ctx, payload.ApplicationID, payload.SPNo)
record, err := h.contexts.FindBySPNo(ctx, payload.ApplicationID, payload.SPNo)
if err != nil {
return err
}
record, err := h.contexts.FindBySPNo(ctx, payload.ApplicationID, payload.SPNo)
correlationID := payload.SPNo
var resourceID *string
if record != nil {
value := strconv.FormatUint(uint64(record.Instance.ID), 10)
resourceID = &value
correlationID = record.Instance.CorrelationID
}
detail, err := h.details.Get(ctx, payload.ApplicationID, payload.SPNo, resourceID, correlationID)
if err != nil {
return err
}
@@ -68,11 +76,14 @@ func (h *ApprovalDetailTaskHandler) Handle(ctx context.Context, task *asynq.Task
}
}
if strings.TrimSpace(payload.IntegrationID) != "" {
resolvedResourceID := strconv.FormatUint(uint64(record.Instance.ID), 10)
resolvedResourceKey := strings.TrimSpace(payload.SPNo)
_, err = h.integration.Complete(ctx, payload.IntegrationID, integrationlog.Completion{
Result: constants.IntegrationResultCompleted, ProviderCode: "processed",
ProviderMessage: "企业微信审批回调已完成权威详情同步",
ResponseSummary: map[string]any{"sp_no": payload.SPNo, "sp_status": detail.SPStatus, "decisions": decisions},
DurationMS: 0, StateChanged: len(decisions) > 0,
ResourceID: &resolvedResourceID, ResourceKey: &resolvedResourceKey,
})
}
return err

View File

@@ -71,14 +71,16 @@ func (c *ApprovalInfoClient) List(ctx context.Context, input ApprovalInfoQuery)
return ApprovalInfoPage{}, err
}
resourceID := strconv.FormatUint(uint64(input.ApplicationID), 10)
integrationID, triggerSeries, correlationID := singleIntegrationLinkage(nil)
attempt, err := c.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComApprovalInfo, ResourceType: constants.WeComApprovalInstanceResourceType,
IntegrationID: integrationID,
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComApprovalInfo, ResourceType: constants.WeComApplicationResourceType,
ResourceID: &resourceID, RequestSummary: map[string]any{
"application_id": input.ApplicationID, "starttime": input.StartTime.Unix(), "endtime": input.EndTime.Unix(),
"template_id": input.TemplateID, "creator_userid": input.CreatorUserID, "size": input.Size,
"cursor_present": strings.TrimSpace(input.Cursor) != "",
},
}, TriggerSeries: triggerSeries, CorrelationID: correlationID,
})
if err != nil {
return ApprovalInfoPage{}, err

View File

@@ -76,13 +76,14 @@ func (c *ApprovalSubmissionClient) Submit(ctx context.Context, input ApprovalSub
}
resourceID := strconv.FormatUint(uint64(input.InstanceID), 10)
correlationID := strings.TrimSpace(input.CorrelationID)
triggerSeries := "wecom-approval-submit:" + resourceID
attempt, err := c.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComApprovalSubmit, ResourceType: constants.WeComApprovalInstanceResourceType,
ResourceID: &resourceID, RequestSummary: map[string]any{
"application_id": input.ApplicationID, "template_id": input.TemplateID,
"creator_source_configured": input.CreatorUserID != "", "control_count": len(input.Contents),
}, CorrelationID: optionalIntegrationString(correlationID),
}, CorrelationID: optionalIntegrationString(correlationID), TriggerSeries: &triggerSeries,
})
if err != nil {
return ApprovalSubmitResult{Outcome: submissionOutcomeFailed, Message: "写入企业微信审批提交日志失败", SafeToRetry: true}, err

View File

@@ -10,6 +10,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/queue"
"go.uber.org/zap"
)
@@ -84,12 +85,13 @@ func (s *CallbackService) Receive(ctx context.Context, applicationID uint, signa
if err := xml.Unmarshal(plaintext, &event); err != nil || event.Event != "sys_approval_change" || strings.TrimSpace(event.SPNo) == "" {
return errors.New(errors.CodeInvalidParam, "企业微信审批回调事件无效")
}
resourceID := strconv.FormatUint(uint64(applicationID), 10)
resourceKey := strings.TrimSpace(event.SPNo)
requestID := middleware.GetRequestIDFromContext(ctx)
log, created, err := s.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
IdempotencyKey: applicationCallbackIdempotencyKey(applicationID, signature),
Provider: constants.IntegrationProviderWeCom, Operation: constants.IntegrationOperationWeComApprovalCallback,
ExternalID: event.SPNo, ResourceType: constants.WeComApprovalInstanceResourceType, ResourceID: &resourceID,
RawPayload: body, ContentType: "application/xml",
ExternalID: event.SPNo, ResourceType: constants.WeComApprovalInstanceResourceType, ResourceKey: &resourceKey,
RawPayload: body, ContentType: "application/xml", RequestID: requestID, CorrelationID: &resourceKey,
})
if err != nil {
return err

View File

@@ -75,11 +75,13 @@ func (c *DirectoryClient) listVisibleDepartments(ctx context.Context, applicatio
}
resourceID := strconv.FormatUint(uint64(applicationID), 10)
requestID := middleware.GetRequestIDFromContext(ctx)
integrationID, triggerSeries, correlationID := singleIntegrationLinkage(requestID)
attempt, err := c.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
IntegrationID: integrationID,
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComVisibleDepartments, ResourceType: constants.WeComApplicationResourceType,
ResourceID: &resourceID, RequestSummary: map[string]any{"application_id": applicationID},
RequestID: requestID, CorrelationID: requestID,
RequestID: requestID, CorrelationID: correlationID, TriggerSeries: triggerSeries,
})
if err != nil {
return nil, err
@@ -115,12 +117,14 @@ func (c *DirectoryClient) listDepartmentMembers(ctx context.Context, application
}
resourceID := strconv.FormatUint(uint64(applicationID), 10)
requestID := middleware.GetRequestIDFromContext(ctx)
integrationID, triggerSeries, correlationID := singleIntegrationLinkage(requestID)
attempt, err := c.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
IntegrationID: integrationID,
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComVisibleMembers, ResourceType: constants.WeComApplicationResourceType,
ResourceID: &resourceID, RequestSummary: map[string]any{
"application_id": applicationID, "department_id": departmentID, "fetch_child": true,
}, RequestID: requestID, CorrelationID: requestID,
}, RequestID: requestID, CorrelationID: correlationID, TriggerSeries: triggerSeries,
})
if err != nil {
return nil, err

View File

@@ -0,0 +1,11 @@
package wecom
import "github.com/google/uuid"
func singleIntegrationLinkage(correlationID *string) (string, *string, *string) {
integrationID := uuid.NewString()
if correlationID == nil {
correlationID = &integrationID
}
return integrationID, &integrationID, correlationID
}

View File

@@ -54,12 +54,14 @@ func (c *TemplateClient) GetTemplateDetail(ctx context.Context, applicationID ui
}
resourceID := strconv.FormatUint(uint64(applicationID), 10)
requestID := middleware.GetRequestIDFromContext(ctx)
integrationID, triggerSeries, correlationID := singleIntegrationLinkage(requestID)
attempt, err := c.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
IntegrationID: integrationID,
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComTemplateDetail, ResourceType: constants.WeComApprovalSceneResourceType,
ResourceID: &resourceID, RequestSummary: map[string]any{
"application_id": applicationID, "template_id": templateID,
}, RequestID: requestID, CorrelationID: requestID,
}, RequestID: requestID, CorrelationID: correlationID, TriggerSeries: triggerSeries,
})
if err != nil {
return wecomapp.TemplateDefinition{}, err

View File

@@ -161,12 +161,14 @@ func (p *TokenProvider) fetchAndCache(ctx context.Context, applicationID uint, c
}
resourceID := strconv.FormatUint(uint64(applicationID), 10)
requestID := middleware.GetRequestIDFromContext(ctx)
integrationID, triggerSeries, correlationID := singleIntegrationLinkage(requestID)
attempt, err := p.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
IntegrationID: integrationID,
Provider: constants.IntegrationProviderWeCom, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationWeComAccessToken, ResourceType: constants.WeComApplicationResourceType,
ResourceID: &resourceID, RequestSummary: map[string]any{
"application_id": applicationID, "corp_id": application.CorpID, "agent_id": application.AgentID,
}, RequestID: requestID, CorrelationID: requestID,
}, RequestID: requestID, CorrelationID: correlationID, TriggerSeries: triggerSeries,
})
if err != nil {
return "", err