固化七月迭代审计治理进展以隔离线上热修
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:
41
internal/infrastructure/audit/batch.go
Normal file
41
internal/infrastructure/audit/batch.go
Normal 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
|
||||
}
|
||||
410
internal/infrastructure/audit/registry.go
Normal file
410
internal/infrastructure/audit/registry.go
Normal 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
|
||||
}
|
||||
57
internal/infrastructure/audit/security_test.go
Normal file
57
internal/infrastructure/audit/security_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
1098
internal/infrastructure/audit/writer.go
Normal file
1098
internal/infrastructure/audit/writer.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: "使用原支付单号主动查单,确认不存在或关闭后才允许关闭本地支付单",
|
||||
})
|
||||
|
||||
@@ -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: "使用原支付单号主动查单,确认不存在或关闭后才允许关闭本地支付单",
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
11
internal/infrastructure/wecom/integration_linkage.go
Normal file
11
internal/infrastructure/wecom/integration_linkage.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user