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

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

View File

@@ -0,0 +1,189 @@
package audit
import (
"context"
"strconv"
"strings"
"github.com/bytedance/sonic"
"gorm.io/gorm"
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// WriteApproval 将通用审批状态变化及其 Integration/Outbox 引用写入统一 Audit Event。
func (w *Writer) WriteApproval(ctx context.Context, tx *gorm.DB, change approvalapp.AuditChange) error {
if change.InstanceID == 0 || change.BusinessID == 0 || change.BusinessType == "" || change.SubmitterAccountID == 0 {
return errors.New(errors.CodeInvalidParam, "通用审批审计资源不完整")
}
resources, err := approvalResources(ctx, tx, change)
if err != nil {
return err
}
result := change.Result
if result == "" {
result = constants.AuditResultSuccess
}
return w.Append(ctx, tx, AppendInput{
EventID: change.EventID, ActionCode: change.ActionCode, Summary: change.Summary,
Actor: ActorInput{Kind: change.ActorKind, ID: change.ActorID, Name: change.ActorName}, Source: change.Source,
ScopeType: constants.AuditScopePlatform, Result: result, ErrorSummary: change.ErrorSummary,
CorrelationID: change.CorrelationID, ParentEventID: change.ParentEventID,
Metadata: map[string]any{"provider": change.Provider, "decision": change.Decision}, Resources: resources,
})
}
func approvalResources(ctx context.Context, tx *gorm.DB, change approvalapp.AuditChange) ([]ResourceInput, error) {
instanceID := strconv.FormatUint(uint64(change.InstanceID), 10)
resources := []ResourceInput{{
Type: constants.AuditResourceApprovalInstance, ID: &instanceID, Key: instanceID, DisplayName: "审批实例 " + instanceID,
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleApprovalTarget,
IdentitySnapshot: map[string]any{
"id": change.InstanceID, "business_type": change.BusinessType, "business_id": change.BusinessID,
"submitter_account_id": change.SubmitterAccountID, "provider": change.Provider,
"external_ref": change.AfterExternalRef, "correlation_id": change.CorrelationID,
"status": statusValue(change.AfterStatus),
},
BeforeData: approvalState(change.BeforeStatus, change.BeforeExternalRef),
AfterData: approvalState(change.AfterStatus, change.AfterExternalRef),
}}
business, err := approvalBusinessResource(ctx, tx, change.BusinessType, change.BusinessID, change.InstanceID)
if err != nil {
return nil, err
}
resources = append(resources, business)
resources = append(resources, approvalSubmitterResource(change))
seenIntegrationIDs := make(map[string]struct{}, len(change.IntegrationIDs))
for _, integrationID := range change.IntegrationIDs {
integrationID = strings.TrimSpace(integrationID)
if integrationID == "" {
continue
}
if _, exists := seenIntegrationIDs[integrationID]; exists {
continue
}
seenIntegrationIDs[integrationID] = struct{}{}
resource, err := approvalIntegrationResource(ctx, tx, integrationID)
if err != nil {
return nil, err
}
resources = append(resources, resource)
}
if strings.TrimSpace(change.OutboxEventID) != "" {
resource, err := approvalOutboxResource(ctx, tx, change.OutboxEventID)
if err != nil {
return nil, err
}
resources = append(resources, resource)
}
return resources, nil
}
func approvalBusinessResource(ctx context.Context, tx *gorm.DB, businessType string, businessID, instanceID uint) (ResourceInput, error) {
id := strconv.FormatUint(uint64(businessID), 10)
switch businessType {
case constants.ApprovalBusinessTypeRefund:
var refund model.RefundRequest
if err := tx.WithContext(ctx).First(&refund, businessID).Error; err != nil {
return ResourceInput{}, errors.Wrap(errors.CodeDatabaseError, err, "查询审批关联退款单失败")
}
return ResourceInput{
Type: constants.AuditResourceRefund, ID: &id, Key: refund.RefundNo, DisplayName: refund.RefundNo,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleApprovalBusiness,
IdentitySnapshot: map[string]any{
"id": refund.ID, "refund_no": refund.RefundNo, "order_id": refund.OrderID, "order_no": refund.OrderNo,
"order_type": refund.OrderType, "asset_identifier": refund.AssetIdentifier, "shop_id": refund.ShopID,
"requested_refund_amount": refund.RequestedRefundAmount, "actual_received_amount": refund.ActualReceivedAmount,
"approval_instance_id": instanceID, "status": refund.Status,
},
}, nil
case constants.ApprovalBusinessTypeOfflineRecharge:
var recharge model.AgentRechargeRecord
if err := tx.WithContext(ctx).First(&recharge, businessID).Error; err != nil {
return ResourceInput{}, errors.Wrap(errors.CodeDatabaseError, err, "查询审批关联充值单失败")
}
return ResourceInput{
Type: constants.AuditResourceAgentRecharge, ID: &id, Key: recharge.RechargeNo, DisplayName: recharge.RechargeNo,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleApprovalBusiness,
IdentitySnapshot: map[string]any{
"id": recharge.ID, "recharge_no": recharge.RechargeNo, "user_id": recharge.UserID,
"shop_id": recharge.ShopID, "agent_wallet_id": recharge.AgentWalletID, "amount": recharge.Amount,
"payment_method": recharge.PaymentMethod, "payment_channel": recharge.PaymentChannel,
"approval_instance_id": instanceID, "status": recharge.Status,
},
}, nil
default:
return ResourceInput{}, errors.New(errors.CodeInvalidParam, "审批业务类型尚未注册审计资源")
}
}
func approvalSubmitterResource(change approvalapp.AuditChange) ResourceInput {
accountID := strconv.FormatUint(uint64(change.SubmitterAccountID), 10)
identity := map[string]any{"id": change.SubmitterAccountID}
var snapshot map[string]any
if sonic.Unmarshal(change.SubmitterSnapshot, &snapshot) == nil {
identity["username"] = snapshot["account_name"]
identity["user_type"] = snapshot["user_type"]
}
displayName, _ := identity["username"].(string)
if displayName == "" {
displayName = "账号 " + accountID
}
return ResourceInput{
Type: constants.AuditResourceAccount, ID: &accountID, Key: accountID, DisplayName: displayName,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleApprovalSubmitter,
IdentitySnapshot: identity,
}
}
func approvalIntegrationResource(ctx context.Context, tx *gorm.DB, integrationID string) (ResourceInput, error) {
var record model.IntegrationLog
if err := tx.WithContext(ctx).Where("integration_id = ?", integrationID).First(&record).Error; err != nil {
return ResourceInput{}, errors.Wrap(errors.CodeDatabaseError, err, "查询审批关联 Integration Log 失败")
}
id := strconv.FormatUint(uint64(record.ID), 10)
return ResourceInput{
Type: constants.AuditResourceIntegrationLog, ID: &id, Key: record.IntegrationID, DisplayName: record.IntegrationID,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleApprovalIntegration,
IdentitySnapshot: map[string]any{
"integration_id": record.IntegrationID, "provider": record.Provider, "direction": record.Direction,
"operation": record.Operation, "external_id": record.ExternalID,
"resource_type": record.ResourceType, "resource_id": record.ResourceID, "resource_key": record.ResourceKey,
"correlation_id": record.CorrelationID,
},
}, nil
}
func approvalOutboxResource(ctx context.Context, tx *gorm.DB, eventID string) (ResourceInput, error) {
var event model.OutboxEvent
if err := tx.WithContext(ctx).Where("event_id = ?", eventID).First(&event).Error; err != nil {
return ResourceInput{}, errors.Wrap(errors.CodeDatabaseError, err, "查询审批关联 Outbox 事件失败")
}
id := strconv.FormatUint(uint64(event.ID), 10)
return ResourceInput{
Type: constants.AuditResourceOutboxEvent, ID: &id, Key: event.EventID, DisplayName: event.EventID,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleApprovalOutbox,
IdentitySnapshot: map[string]any{
"event_id": event.EventID, "event_type": event.EventType, "aggregate_type": event.AggregateType,
"aggregate_id": event.AggregateID, "resource_type": event.ResourceType,
"resource_id": event.ResourceID, "business_key": event.BusinessKey,
},
}, nil
}
func approvalState(status *int, externalRef string) map[string]any {
if status == nil {
return nil
}
return map[string]any{"status": *status, "external_ref": externalRef}
}
func statusValue(status *int) any {
if status == nil {
return nil
}
return *status
}

View File

@@ -0,0 +1,46 @@
package audit
import (
"strconv"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// CommissionWithdrawalResource 构造佣金提现单审计资源,不记录收款账户信息。
func CommissionWithdrawalResource(withdrawal *model.CommissionWithdrawalRequest, relation, role string, beforeData, afterData map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(withdrawal.ID), 10)
key := withdrawal.WithdrawalNo
if key == "" {
key = id
}
return ResourceInput{
Type: constants.AuditResourceCommissionWithdrawal, ID: optionalResourceID(withdrawal.ID),
Key: key, DisplayName: "佣金提现单 " + key, Relation: relation, Role: role,
IdentitySnapshot: map[string]any{
"id": withdrawal.ID, "withdrawal_no": withdrawal.WithdrawalNo,
"shop_id": withdrawal.ShopID, "applicant_id": withdrawal.ApplicantID,
"amount": withdrawal.Amount, "fee": withdrawal.Fee, "fee_rate": withdrawal.FeeRate,
"actual_amount": withdrawal.ActualAmount, "withdrawal_method": withdrawal.WithdrawalMethod,
"payment_type": withdrawal.PaymentType, "status": withdrawal.Status,
"processor_id": withdrawal.ProcessorID, "processed_at": withdrawal.ProcessedAt, "paid_at": withdrawal.PaidAt,
},
BeforeData: beforeData, AfterData: afterData,
}
}
// AgentWalletResource 构造代理钱包审计资源及余额前后值。
func AgentWalletResource(wallet *model.AgentWallet, relation, role string, beforeData, afterData map[string]any) ResourceInput {
resource := agentWalletAuditResource(wallet, relation, role)
resource.BeforeData = beforeData
resource.AfterData = afterData
return resource
}
// AgentWalletTransactionResource 构造代理钱包流水审计资源。
func AgentWalletTransactionResource(transaction *model.AgentWalletTransaction, relation, role string) ResourceInput {
resource := agentWalletTransactionResource(transaction)
resource.Relation = relation
resource.Role = role
return resource
}

View File

@@ -0,0 +1,62 @@
package audit
import (
"context"
stderrors "errors"
"strconv"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
"github.com/break/junhong_cmp_fiber/pkg/constants"
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
)
// RecordFailure 在业务回滚后使用独立短事务记录失败或拒绝事实。
func (w *Writer) RecordFailure(ctx context.Context, db *gorm.DB, input AppendInput, originalErr error) {
fillFailureInput(&input, originalErr)
if w == nil || db == nil {
recordFailureWriteError(ctx, input, pkgerrors.New(pkgerrors.CodeInvalidStatus, "统一审计失败记录接缝未配置"))
return
}
if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return w.Append(ctx, tx, input)
}); err != nil {
recordFailureWriteError(ctx, input, err)
}
}
func fillFailureInput(input *AppendInput, originalErr error) {
var appErr *pkgerrors.AppError
if stderrors.As(originalErr, &appErr) && appErr != nil {
if input.Result == "" {
input.Result = constants.AuditResultDenied
if appErr.Code == pkgerrors.CodeDatabaseError || appErr.Code == pkgerrors.CodeInternalError {
input.Result = constants.AuditResultFailed
}
}
input.ErrorCode = strconv.Itoa(appErr.Code)
input.ErrorSummary = appErr.Message
return
}
if input.Result == "" {
input.Result = constants.AuditResultFailed
}
input.ErrorCode = strconv.Itoa(pkgerrors.CodeInternalError)
input.ErrorSummary = "业务操作失败"
}
func recordFailureWriteError(ctx context.Context, input AppendInput, err error) {
linkage := auditcontext.From(ctx)
resourceKey := ""
for _, resource := range input.Resources {
if resource.Relation == constants.AuditResourceRelationPrimary {
resourceKey = resource.Key
break
}
}
auditfailure.RecordSecondaryWriteFailure(
input.ActionCode, resourceKey, linkage.RequestID, linkage.CorrelationID, input.ErrorCode, err,
)
}

View File

@@ -0,0 +1,24 @@
package audit
import (
"strconv"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// NotificationResource 构造不包含通知正文的安全资源快照。
func NotificationResource(notification *model.Notification, relation, role string, before, after map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(notification.ID), 10)
return ResourceInput{
Type: constants.AuditResourceNotification, ID: &id, Key: notification.EventID + ":" + notification.RecipientKind + ":" + id,
DisplayName: notification.Type, Relation: relation, Role: role,
IdentitySnapshot: map[string]any{
"id": notification.ID, "event_id": notification.EventID,
"recipient_kind": notification.RecipientKind, "recipient_id": notification.RecipientID,
"category": notification.Category, "type": notification.Type, "severity": notification.Severity,
"ref_type": notification.RefType, "ref_id": notification.RefID, "ref_key": notification.RefKey,
},
BeforeData: before, AfterData: after, SubjectVisibility: constants.AuditSubjectInternalOnly,
}
}

View File

@@ -0,0 +1,218 @@
package audit
import (
"strconv"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// PackageSeriesResource 构造套餐系列审计资源。
func PackageSeriesResource(series *model.PackageSeries, relation, role string, beforeData, afterData map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(series.ID), 10)
var resourceID *string
if series.ID > 0 {
resourceID = &id
}
return ResourceInput{
Type: constants.AuditResourcePackageSeries, ID: resourceID, Key: series.SeriesCode, DisplayName: series.SeriesName,
Relation: relation, Role: role, IdentitySnapshot: map[string]any{
"id": series.ID, "series_code": series.SeriesCode, "series_name": series.SeriesName,
"status": series.Status, "enable_one_time_commission": series.EnableOneTimeCommission,
},
BeforeData: beforeData, AfterData: afterData,
}
}
// PackageResource 构造套餐商品审计资源。
func PackageResource(pkg *model.Package, relation, role string, beforeData, afterData map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(pkg.ID), 10)
var resourceID *string
if pkg.ID > 0 {
resourceID = &id
}
return ResourceInput{
Type: constants.AuditResourcePackage, ID: resourceID, Key: pkg.PackageCode, DisplayName: pkg.PackageName,
Relation: relation, Role: role, IdentitySnapshot: map[string]any{
"id": pkg.ID, "package_code": pkg.PackageCode, "package_name": pkg.PackageName,
"series_id": pkg.SeriesID, "package_type": pkg.PackageType, "duration_months": pkg.DurationMonths,
"duration_days": pkg.DurationDays, "price_config_status": pkg.PriceConfigStatus,
"is_gift": pkg.IsGift, "status": pkg.Status, "shelf_status": pkg.ShelfStatus,
},
BeforeData: beforeData, AfterData: afterData,
}
}
// ShopResource 构造套餐配置关联的店铺审计资源。
func ShopResource(shop *model.Shop, relation, role string) ResourceInput {
id := strconv.FormatUint(uint64(shop.ID), 10)
key := shop.ShopCode
if key == "" {
key = id
}
return ResourceInput{
Type: constants.AuditResourceShop, ID: &id, Key: key, DisplayName: shop.ShopName,
Relation: relation, Role: role, IdentitySnapshot: map[string]any{
"id": shop.ID, "shop_code": shop.ShopCode, "shop_name": shop.ShopName,
"parent_id": shop.ParentID, "level": shop.Level,
},
}
}
// ShopSeriesAllocationResource 构造店铺系列授权审计资源。
func ShopSeriesAllocationResource(allocation *model.ShopSeriesAllocation, relation, role string, beforeData, afterData map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(allocation.ID), 10)
var resourceID *string
if allocation.ID > 0 {
resourceID = &id
}
key := id
if allocation.ID == 0 {
key = "shop-series-" + strconv.FormatUint(uint64(allocation.ShopID), 10) + "-" + strconv.FormatUint(uint64(allocation.SeriesID), 10)
}
return ResourceInput{
Type: constants.AuditResourceShopSeriesAllocation, ID: resourceID, Key: key, DisplayName: "系列授权 " + key,
Relation: relation, Role: role, IdentitySnapshot: map[string]any{
"id": allocation.ID, "shop_id": allocation.ShopID, "series_id": allocation.SeriesID,
"allocator_shop_id": allocation.AllocatorShopID, "status": allocation.Status,
},
BeforeData: beforeData, AfterData: afterData,
}
}
// ShopPackageAllocationResource 构造店铺套餐授权审计资源。
func ShopPackageAllocationResource(allocation *model.ShopPackageAllocation, relation, role string, beforeData, afterData map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(allocation.ID), 10)
return ResourceInput{
Type: constants.AuditResourceShopPackageAllocation, ID: &id, Key: id, DisplayName: "套餐授权 " + id,
Relation: relation, Role: role, IdentitySnapshot: map[string]any{
"id": allocation.ID, "shop_id": allocation.ShopID, "package_id": allocation.PackageID,
"allocator_shop_id": allocation.AllocatorShopID, "series_allocation_id": allocation.SeriesAllocationID,
"status": allocation.Status, "shelf_status": allocation.ShelfStatus,
"retail_price_config_status": allocation.RetailPriceConfigStatus,
},
BeforeData: beforeData, AfterData: afterData,
}
}
// ShopPackagePriceHistoryResource 构造套餐价格历史审计资源。
func ShopPackagePriceHistoryResource(history *model.ShopPackageAllocationPriceHistory) ResourceInput {
id := strconv.FormatUint(uint64(history.ID), 10)
return ResourceInput{
Type: constants.AuditResourceShopPackagePriceHistory, ID: &id, Key: id, DisplayName: "价格历史 " + id,
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePackagePriceHistory,
IdentitySnapshot: map[string]any{
"id": history.ID, "allocation_id": history.AllocationID, "changed_by": history.ChangedBy,
"effective_from": history.EffectiveFrom,
},
AfterData: map[string]any{
"old_cost_price": history.OldCostPrice, "new_cost_price": history.NewCostPrice,
"change_reason": history.ChangeReason,
},
}
}
// PackageConfigBatchResource 构造套餐配置批次根资源。
func PackageConfigBatchResource(batchKey, operation string, shopID, seriesID uint) ResourceInput {
return ResourceInput{
Type: constants.AuditResourcePackageConfigBatch, Key: batchKey, DisplayName: batchKey,
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRolePackageConfigBatch,
IdentitySnapshot: map[string]any{
"batch_key": batchKey, "operation": operation, "shop_id": shopID, "series_id": seriesID,
},
}
}
// PackageUsageResource 构造套餐权益生命周期审计资源。
func PackageUsageResource(usage *model.PackageUsage, relation, role string, beforeData, afterData map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(usage.ID), 10)
return ResourceInput{
Type: constants.AuditResourcePackageUsage, ID: &id, Key: id, DisplayName: usage.PackageName,
Relation: relation, Role: role, IdentitySnapshot: map[string]any{
"id": usage.ID, "order_id": usage.OrderID, "order_no": usage.OrderNo,
"refund_id": usage.RefundID, "refund_no": usage.RefundNo,
"package_id": usage.PackageID, "package_name": usage.PackageName, "usage_type": usage.UsageType,
"iot_card_id": usage.IotCardID, "device_id": usage.DeviceID,
"data_limit_mb": usage.DataLimitMB, "data_usage_mb": usage.DataUsageMB,
"activated_at": usage.ActivatedAt, "expires_at": usage.ExpiresAt, "status": usage.Status,
"pending_realname_activation": usage.PendingRealnameActivation,
"last_reset_at": usage.LastResetAt, "next_reset_at": usage.NextResetAt, "generation": usage.Generation,
},
BeforeData: beforeData, AfterData: afterData,
}
}
// OrderResource 构造订单审计资源。
func OrderResource(order *model.Order, relation, role string) ResourceInput {
id := strconv.FormatUint(uint64(order.ID), 10)
var resourceID *string
if order.ID > 0 {
resourceID = &id
}
key := order.OrderNo
if key == "" {
key = id
}
return ResourceInput{
Type: constants.AuditResourceOrder, ID: resourceID, Key: key, DisplayName: key,
Relation: relation, Role: role, IdentitySnapshot: map[string]any{
"id": order.ID, "order_no": order.OrderNo, "order_type": order.OrderType,
"buyer_type": order.BuyerType, "buyer_id": order.BuyerID,
"iot_card_id": order.IotCardID, "device_id": order.DeviceID,
"asset_identifier": order.AssetIdentifier, "total_amount": order.TotalAmount,
"actual_paid_amount": order.ActualPaidAmount, "payment_method": order.PaymentMethod,
"payment_status": order.PaymentStatus, "purchase_role": order.PurchaseRole,
"source": order.Source, "operator_account_id": order.OperatorAccountID,
"operator_account_type": order.OperatorAccountType, "operator_account_name": order.OperatorAccountName,
"seller_shop_id": order.SellerShopID, "expires_at": order.ExpiresAt,
},
}
}
// PaymentResource 构造支付记录审计资源。
func PaymentResource(payment *model.Payment, relation, role string, beforeData, afterData map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(payment.ID), 10)
var resourceID *string
if payment.ID > 0 {
resourceID = &id
}
key := payment.PaymentNo
if key == "" {
key = id
}
return ResourceInput{
Type: constants.AuditResourcePayment, ID: resourceID, Key: key, DisplayName: key,
Relation: relation, Role: role, IdentitySnapshot: map[string]any{
"id": payment.ID, "payment_no": payment.PaymentNo, "order_id": payment.OrderID,
"order_type": payment.OrderType, "payment_method": payment.PaymentMethod,
"amount": payment.Amount, "status": payment.Status,
"third_party_trade_no": payment.ThirdPartyTradeNo, "payment_config_id": payment.PaymentConfigID,
},
BeforeData: beforeData, AfterData: afterData,
}
}
// RefundResource 构造退款单审计资源。
func RefundResource(refund *model.RefundRequest, relation, role string) ResourceInput {
id := strconv.FormatUint(uint64(refund.ID), 10)
var resourceID *string
if refund.ID > 0 {
resourceID = &id
}
key := refund.RefundNo
if key == "" {
key = id
}
return ResourceInput{
Type: constants.AuditResourceRefund, ID: resourceID, Key: key, DisplayName: key,
Relation: relation, Role: role, IdentitySnapshot: map[string]any{
"id": refund.ID, "refund_no": refund.RefundNo, "order_id": refund.OrderID,
"order_no": refund.OrderNo, "order_type": refund.OrderType,
"package_usage_id": refund.PackageUsageID, "asset_identifier": refund.AssetIdentifier,
"shop_id": refund.ShopID, "requested_refund_amount": refund.RequestedRefundAmount,
"actual_received_amount": refund.ActualReceivedAmount, "refund_reason": refund.RefundReason,
"approved_refund_amount": refund.ApprovedRefundAmount, "approval_instance_id": refund.ApprovalInstanceID,
"status": refund.Status, "commission_deducted": refund.CommissionDeducted, "asset_reset": refund.AssetReset,
},
}
}

View File

@@ -0,0 +1,84 @@
package audit
import (
"context"
"strconv"
"gorm.io/gorm"
agentrecharge "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// WriteAgentRechargePayment 将代理充值支付生命周期写入统一 Audit Event。
func (w *Writer) WriteAgentRechargePayment(ctx context.Context, tx *gorm.DB, change agentrecharge.PaymentAudit) error {
if change.Payment == nil || change.Payment.ID == 0 || change.Payment.PaymentNo == "" || change.Recharge == nil || change.Recharge.ID == 0 {
return errors.New(errors.CodeInvalidParam, "代理充值支付审计资源不完整")
}
payment := PaymentResource(change.Payment, constants.AuditResourceRelationPrimary, constants.AuditResourceRolePaymentTarget, change.BeforeData, change.AfterData)
rechargeID := strconv.FormatUint(uint64(change.Recharge.ID), 10)
rechargeRelation := constants.AuditResourceRelationReference
if len(change.RechargeBeforeData) > 0 || len(change.RechargeAfterData) > 0 {
rechargeRelation = constants.AuditResourceRelationAffected
}
rechargeStatus := any(change.Recharge.Status)
if status, ok := change.RechargeAfterData["status"]; ok {
rechargeStatus = status
}
recharge := ResourceInput{
Type: constants.AuditResourceAgentRecharge, ID: &rechargeID,
Key: change.Recharge.RechargeNo, DisplayName: change.Recharge.RechargeNo,
Relation: rechargeRelation, Role: constants.AuditResourceRolePaymentBusinessOrder,
IdentitySnapshot: map[string]any{
"id": change.Recharge.ID, "recharge_no": change.Recharge.RechargeNo,
"user_id": change.Recharge.UserID,
"shop_id": change.Recharge.ShopID, "agent_wallet_id": change.Recharge.AgentWalletID,
"amount": change.Recharge.Amount, "payment_method": change.Recharge.PaymentMethod,
"payment_channel": change.Recharge.PaymentChannel,
"approval_instance_id": change.Recharge.ApprovalInstanceID, "status": rechargeStatus,
},
BeforeData: change.RechargeBeforeData, AfterData: change.RechargeAfterData,
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: change.Summary,
}
resources := []ResourceInput{payment, recharge}
if change.Recharge.UserID > 0 {
var account model.Account
if err := tx.WithContext(ctx).Unscoped().First(&account, change.Recharge.UserID).Error; err != nil && err != gorm.ErrRecordNotFound {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值提交人审计快照失败")
}
if account.ID > 0 {
accountID := strconv.FormatUint(uint64(account.ID), 10)
resources = append(resources, ResourceInput{
Type: constants.AuditResourceAccount, ID: &accountID, Key: accountID, DisplayName: account.Username,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleRechargeSubmitter,
IdentitySnapshot: accountIdentity(&account),
})
}
}
var shop model.Shop
if err := tx.WithContext(ctx).Unscoped().First(&shop, change.Recharge.ShopID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值店铺审计快照失败")
}
resources = append(resources, ShopResource(&shop, constants.AuditResourceRelationReference, constants.AuditResourceRoleRechargeShop))
var wallet model.AgentWallet
if err := tx.WithContext(ctx).First(&wallet, change.Recharge.AgentWalletID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值钱包审计快照失败")
}
walletID := strconv.FormatUint(uint64(wallet.ID), 10)
resources = append(resources, ResourceInput{
Type: constants.AuditResourceAgentWallet, ID: &walletID, Key: walletID, DisplayName: "代理主钱包 " + walletID,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleRechargeWallet,
IdentitySnapshot: map[string]any{
"id": wallet.ID, "shop_id": wallet.ShopID, "wallet_type": wallet.WalletType,
"currency": wallet.Currency, "status": wallet.Status,
},
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: change.Summary,
})
return w.Append(ctx, tx, AppendInput{
ActionCode: change.ActionCode, Summary: change.Summary,
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
CorrelationID: change.Payment.PaymentNo, Resources: resources,
})
}

View File

@@ -0,0 +1,73 @@
package audit
import (
"context"
"strconv"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
// PollingInput 描述轮询配置、规则或人工任务的审计事实。
type PollingInput struct {
EventID string
ActionCode string
Summary string
ResourceType string
ResourceID uint
ResourceKey string
DisplayName string
OperatorID uint
IdentitySnapshot map[string]any
BeforeData map[string]any
AfterData map[string]any
Metadata map[string]any
Cards []*model.IotCard
Result string
ErrorCode string
ErrorSummary string
}
// WritePolling 将轮询配置、规则或人工任务转换为统一 Audit Event。
func (w *Writer) WritePolling(ctx context.Context, tx *gorm.DB, input PollingInput) error {
if input.OperatorID == 0 || input.ResourceType == "" || input.ResourceKey == "" {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "轮询审计资源或操作者不完整")
}
resourceID := optionalResourceID(input.ResourceID)
resources := []ResourceInput{{
Type: input.ResourceType, ID: resourceID, Key: input.ResourceKey, DisplayName: input.DisplayName,
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRolePollingTarget,
IdentitySnapshot: input.IdentitySnapshot, BeforeData: input.BeforeData, AfterData: input.AfterData,
SubjectVisibility: constants.AuditSubjectInternalOnly,
}}
for index, card := range input.Cards {
if card == nil || card.ID == 0 {
continue
}
resources = append(resources, ResourceInput{
Type: constants.AuditResourceIotCard, ID: optionalResourceID(card.ID),
Key: iotCardResourceKey(card), DisplayName: card.ICCID,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRolePollingCard,
IdentitySnapshot: iotCardIdentity(card), SubjectVisibility: constants.AuditSubjectInternalOnly,
SortOrder: index + 1,
})
}
result := input.Result
if result == "" {
result = constants.AuditResultSuccess
}
return w.Append(ctx, tx, AppendInput{
EventID: input.EventID, ActionCode: input.ActionCode, Summary: input.Summary,
Actor: ActorInput{
Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(input.OperatorID), 10),
Name: middleware.GetUsernameFromContext(ctx),
},
Source: constants.AuditSourceAdminAPI, ScopeType: constants.AuditScopePlatform,
Result: result, ErrorCode: input.ErrorCode, ErrorSummary: input.ErrorSummary,
Metadata: input.Metadata, Resources: resources,
})
}

View File

@@ -0,0 +1,187 @@
package audit
import (
"context"
"strconv"
"gorm.io/gorm"
agentrecharge "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// WriteAgentRecharge 将代理充值申请、终态和实际入账写入统一 Audit Event。
func (w *Writer) WriteAgentRecharge(ctx context.Context, tx *gorm.DB, change agentrecharge.RechargeAudit) error {
if change.Record == nil || change.Record.ID == 0 || change.Record.RechargeNo == "" {
return errors.New(errors.CodeInvalidParam, "代理充值审计资源不完整")
}
resources, err := agentRechargeResources(ctx, tx, change)
if err != nil {
return err
}
return w.Append(ctx, tx, AppendInput{
ActionCode: change.ActionCode, Summary: change.Summary,
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
CorrelationID: change.Record.RechargeNo, Resources: resources,
})
}
func agentRechargeResources(ctx context.Context, tx *gorm.DB, change agentrecharge.RechargeAudit) ([]ResourceInput, error) {
record := change.Record
id := strconv.FormatUint(uint64(record.ID), 10)
primary := ResourceInput{
Type: constants.AuditResourceAgentRecharge, ID: &id, Key: record.RechargeNo, DisplayName: record.RechargeNo,
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleRechargeTarget,
IdentitySnapshot: map[string]any{
"id": record.ID, "recharge_no": record.RechargeNo, "user_id": record.UserID,
"shop_id": record.ShopID, "agent_wallet_id": record.AgentWalletID, "amount": record.Amount,
"payment_method": record.PaymentMethod, "payment_channel": record.PaymentChannel,
"payment_transaction_id": record.PaymentTransactionID, "approval_instance_id": record.ApprovalInstanceID,
"status": record.Status,
},
BeforeData: change.BeforeData, AfterData: change.AfterData,
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: change.Summary,
}
resources := []ResourceInput{primary}
var account model.Account
if record.UserID > 0 {
if err := tx.WithContext(ctx).Unscoped().First(&account, record.UserID).Error; err != nil && err != gorm.ErrRecordNotFound {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值提交人审计快照失败")
}
if account.ID > 0 {
accountID := strconv.FormatUint(uint64(account.ID), 10)
resources = append(resources, ResourceInput{
Type: constants.AuditResourceAccount, ID: &accountID, Key: accountID, DisplayName: account.Username,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleRechargeSubmitter,
IdentitySnapshot: accountIdentity(&account),
})
}
}
var shop model.Shop
if err := tx.WithContext(ctx).Unscoped().First(&shop, record.ShopID).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值店铺审计快照失败")
}
resources = append(resources, ShopResource(&shop, constants.AuditResourceRelationReference, constants.AuditResourceRoleRechargeShop))
if change.Payment != nil {
resources = append(resources, PaymentResource(change.Payment, constants.AuditResourceRelationReference, constants.AuditResourceRolePaymentTarget, nil, nil))
}
if change.Approval != nil {
approvalID := strconv.FormatUint(uint64(change.Approval.ID), 10)
resources = append(resources, ResourceInput{
Type: constants.AuditResourceApprovalInstance, ID: &approvalID, Key: approvalID, DisplayName: "审批实例 " + approvalID,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleRechargeApproval,
IdentitySnapshot: map[string]any{
"id": change.Approval.ID, "business_type": change.Approval.BusinessType,
"business_id": change.Approval.BusinessID, "submitter_account_id": change.Approval.SubmitterAccountID,
"provider": change.Approval.Provider, "external_ref": change.Approval.ExternalRef,
"correlation_id": change.Approval.CorrelationID, "status": change.Approval.Status,
},
})
}
if change.Wallet != nil {
walletID := strconv.FormatUint(uint64(change.Wallet.ID), 10)
relation := constants.AuditResourceRelationReference
if change.Transaction != nil {
relation = constants.AuditResourceRelationAffected
}
wallet := ResourceInput{
Type: constants.AuditResourceAgentWallet, ID: &walletID, Key: walletID, DisplayName: "代理主钱包 " + walletID,
Relation: relation, Role: constants.AuditResourceRoleRechargeWallet,
IdentitySnapshot: map[string]any{
"id": change.Wallet.ID, "shop_id": change.Wallet.ShopID, "wallet_type": change.Wallet.WalletType,
"currency": change.Wallet.Currency, "status": change.Wallet.Status,
},
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: change.Summary,
}
if change.Transaction != nil {
wallet.BeforeData = map[string]any{"balance": change.Transaction.BalanceBefore}
wallet.AfterData = map[string]any{"balance": change.Transaction.BalanceAfter}
}
resources = append(resources, wallet)
}
if change.Transaction != nil {
transactionID := strconv.FormatUint(uint64(change.Transaction.ID), 10)
resources = append(resources, ResourceInput{
Type: constants.AuditResourceAgentWalletTransaction, ID: &transactionID, Key: transactionID, DisplayName: "代理钱包流水 " + transactionID,
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleRechargeWalletTransaction,
IdentitySnapshot: map[string]any{
"id": change.Transaction.ID, "agent_wallet_id": change.Transaction.AgentWalletID,
"shop_id": change.Transaction.ShopID, "transaction_type": change.Transaction.TransactionType,
"transaction_subtype": change.Transaction.TransactionSubtype,
"reference_type": change.Transaction.ReferenceType, "reference_id": change.Transaction.ReferenceID,
"status": change.Transaction.Status,
},
AfterData: map[string]any{
"amount": change.Transaction.Amount, "balance_before": change.Transaction.BalanceBefore,
"balance_after": change.Transaction.BalanceAfter,
},
})
}
return resources, nil
}
// AssetRechargeReferences 构造个人资产充值关联的提交人、钱包和资产资源。
func AssetRechargeReferences(ctx context.Context, tx *gorm.DB, recharge *model.RechargeOrder) ([]ResourceInput, error) {
if recharge == nil || recharge.ID == 0 {
return nil, errors.New(errors.CodeInvalidParam, "资产充值审计资源不完整")
}
resources := make([]ResourceInput, 0, 3)
var customer model.PersonalCustomer
if err := tx.WithContext(ctx).Unscoped().First(&customer, recharge.UserID).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产充值提交人审计快照失败")
}
customerID := strconv.FormatUint(uint64(customer.ID), 10)
resources = append(resources, ResourceInput{
Type: constants.AuditResourcePersonalCustomer, ID: &customerID, Key: customerID, DisplayName: customer.Nickname,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleRechargeSubmitter,
IdentitySnapshot: map[string]any{
"id": customer.ID, "nickname": customer.Nickname, "wx_open_id": customer.WxOpenID,
"wx_union_id": customer.WxUnionID, "status": customer.Status,
},
})
var wallet model.AssetWallet
if err := tx.WithContext(ctx).First(&wallet, recharge.AssetWalletID).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产充值钱包审计快照失败")
}
walletID := strconv.FormatUint(uint64(wallet.ID), 10)
resources = append(resources, ResourceInput{
Type: constants.AuditResourceAssetWallet, ID: &walletID, Key: walletID, DisplayName: "资产钱包 " + walletID,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleRechargeWallet,
IdentitySnapshot: map[string]any{
"id": wallet.ID, "resource_type": wallet.ResourceType, "resource_id": wallet.ResourceID,
"currency": wallet.Currency, "shop_id_tag": wallet.ShopIDTag, "enterprise_id_tag": wallet.EnterpriseIDTag,
},
})
switch recharge.ResourceType {
case constants.AssetWalletResourceTypeIotCard:
var card model.IotCard
if err := tx.WithContext(ctx).Unscoped().First(&card, recharge.ResourceID).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产充值卡审计快照失败")
}
cardID := strconv.FormatUint(uint64(card.ID), 10)
resources = append(resources, ResourceInput{
Type: constants.AuditResourceIotCard, ID: &cardID, Key: IotCardResourceKey(&card), DisplayName: card.ICCID,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleOrderAsset,
IdentitySnapshot: IotCardIdentitySnapshot(&card), SubjectVisibility: constants.AuditSubjectResult,
SubjectSummary: "资产充值状态已更新",
})
case constants.AssetWalletResourceTypeDevice:
var device model.Device
if err := tx.WithContext(ctx).Unscoped().First(&device, recharge.ResourceID).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产充值设备审计快照失败")
}
deviceID := strconv.FormatUint(uint64(device.ID), 10)
resources = append(resources, ResourceInput{
Type: constants.AuditResourceDevice, ID: &deviceID, Key: DeviceResourceKey(&device), DisplayName: device.VirtualNo,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleOrderAsset,
IdentitySnapshot: DeviceIdentitySnapshot(&device), SubjectVisibility: constants.AuditSubjectResult,
SubjectSummary: "资产充值状态已更新",
})
}
return resources, nil
}

View File

@@ -0,0 +1,128 @@
package audit
import (
"context"
"strconv"
"gorm.io/gorm"
refundapprovalapp "github.com/break/junhong_cmp_fiber/internal/application/refundapproval"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
)
// WriteRefundApplication 将退款申请、审批和关联业务资源写入同一事务。
func (w *Writer) WriteRefundApplication(ctx context.Context, tx *gorm.DB, input refundapprovalapp.ApplicationAudit) error {
if input.Refund == nil || input.Order == nil || input.Approval == nil || input.Submitter == nil {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "退款申请审计资源不完整")
}
primary := RefundResource(input.Refund, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleRefundTarget)
primary.AfterData = refundStateData(input.Refund)
primary.SubjectVisibility = constants.AuditSubjectResult
primary.SubjectSummary = "退款申请已提交"
resources := []ResourceInput{
primary,
OrderResource(input.Order, constants.AuditResourceRelationReference, constants.AuditResourceRoleRefundOrder),
ApprovalInstanceResource(input.Approval, constants.AuditResourceRelationAffected, constants.AuditResourceRoleRefundApproval, nil, map[string]any{"status": input.Approval.Status}),
AccountResource(input.Submitter, constants.AuditResourceRelationReference, constants.AuditResourceRoleRefundSubmitter),
}
for index := 1; index < len(resources); index++ {
resources[index].SubjectVisibility = constants.AuditSubjectInternalOnly
}
asset, err := RefundAssetResource(ctx, tx, input.Order, "退款申请已提交")
if err != nil {
return err
}
if asset != nil {
resources = append(resources, *asset)
}
return w.Append(ctx, tx, AppendInput{
EventID: "refund:" + strconv.FormatUint(uint64(input.Refund.ID), 10) + ":created",
ActionCode: constants.AuditActionRefundCreated, Summary: "提交退款申请",
Actor: ActorInput{Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(input.Submitter.ID), 10), Name: input.Submitter.Username},
Source: constants.AuditSourceAdminAPI, ScopeType: constants.AuditScopePlatform,
Result: constants.AuditResultSuccess, CorrelationID: input.Refund.RefundNo,
Metadata: map[string]any{"requested_refund_amount": input.Refund.RequestedRefundAmount},
Resources: resources,
})
}
// ApprovalInstanceResource 构造审批实例审计资源。
func ApprovalInstanceResource(instance *model.ApprovalInstance, relation, role string, beforeData, afterData map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(instance.ID), 10)
return ResourceInput{
Type: constants.AuditResourceApprovalInstance, ID: &id, Key: id, DisplayName: "审批实例 " + id,
Relation: relation, Role: role,
IdentitySnapshot: map[string]any{
"id": instance.ID, "business_type": instance.BusinessType, "business_id": instance.BusinessID,
"provider": instance.Provider, "external_ref": instance.ExternalRef, "status": instance.Status,
},
BeforeData: beforeData, AfterData: afterData,
}
}
// AccountResource 构造退款链路中的后台账号资源。
func AccountResource(account *model.Account, relation, role string) ResourceInput {
id := strconv.FormatUint(uint64(account.ID), 10)
return ResourceInput{
Type: constants.AuditResourceAccount, ID: &id, Key: id, DisplayName: account.Username,
Relation: relation, Role: role, IdentitySnapshot: accountIdentity(account),
}
}
// RefundAssetResource 构造退款订单实际关联的卡或设备资源。
func RefundAssetResource(ctx context.Context, tx *gorm.DB, order *model.Order, subjectSummary string) (*ResourceInput, error) {
if order.IotCardID != nil {
var card model.IotCard
if err := tx.WithContext(ctx).First(&card, *order.IotCardID).Error; err != nil {
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "查询退款关联卡审计快照失败")
}
id := strconv.FormatUint(uint64(card.ID), 10)
return &ResourceInput{
Type: constants.AuditResourceIotCard, ID: &id, Key: IotCardResourceKey(&card), DisplayName: card.ICCID,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleRefundAsset,
IdentitySnapshot: IotCardIdentitySnapshot(&card), SubjectVisibility: constants.AuditSubjectResult,
SubjectSummary: subjectSummary,
}, nil
}
if order.DeviceID != nil {
var device model.Device
if err := tx.WithContext(ctx).First(&device, *order.DeviceID).Error; err != nil {
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "查询退款关联设备审计快照失败")
}
id := strconv.FormatUint(uint64(device.ID), 10)
return &ResourceInput{
Type: constants.AuditResourceDevice, ID: &id, Key: DeviceResourceKey(&device), DisplayName: device.VirtualNo,
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleRefundAsset,
IdentitySnapshot: DeviceIdentitySnapshot(&device), SubjectVisibility: constants.AuditSubjectResult,
SubjectSummary: subjectSummary,
}, nil
}
return nil, nil
}
// CommissionRecordResource 构造退款失效的佣金记录资源。
func CommissionRecordResource(record *model.CommissionRecord, beforeData, afterData map[string]any) ResourceInput {
id := strconv.FormatUint(uint64(record.ID), 10)
return ResourceInput{
Type: constants.AuditResourceCommissionRecord, ID: &id, Key: id, DisplayName: "佣金记录 " + id,
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleRefundCommission,
IdentitySnapshot: map[string]any{
"id": record.ID, "shop_id": record.ShopID, "order_id": record.OrderID,
"iot_card_id": record.IotCardID, "device_id": record.DeviceID,
"commission_source": record.CommissionSource, "amount": record.Amount,
"status": record.Status, "released_at": record.ReleasedAt,
},
BeforeData: beforeData, AfterData: afterData,
}
}
func refundStateData(refund *model.RefundRequest) map[string]any {
return map[string]any{
"status": refund.Status, "approved_refund_amount": refund.ApprovedRefundAmount,
"approval_instance_id": refund.ApprovalInstanceID, "processor_id": refund.ProcessorID,
"processed_at": refund.ProcessedAt, "commission_deducted": refund.CommissionDeducted,
"asset_reset": refund.AssetReset, "reject_reason": refund.RejectReason, "remark": refund.Remark,
}
}

View File

@@ -17,6 +17,13 @@ type ActionDefinition struct {
AllowedVisibility []string
SubjectFields []string
SensitiveRead bool
AllowedOrigins []ActionOrigin
}
// ActionOrigin 定义动作允许的操作者与入口组合。
type ActionOrigin struct {
Actor string
Source string
}
// ResourceDefinition 是受控审计资源的快照契约。
@@ -70,6 +77,76 @@ func NewRegistry() *Registry {
personalPhoneBound := personalAction(constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号", []string{"phone"})
personalPhoneChanged := personalAction(constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号", []string{"phone"})
personalWechatIdentityUpdated := personalAction(constants.AuditActionPersonalCustomerWechatIdentityUpdated, "同步个人微信主体", []string{"app_id", "app_type"})
personalAssetBound := personalAction(constants.AuditActionPersonalCustomerAssetBound, "绑定个人客户资产", []string{"asset_type", "asset_id"})
personalAssetBound.Category = constants.AuditCategoryAsset
personalAssetUnbound := customerAssetAdminAction(constants.AuditActionPersonalCustomerAssetUnbound, "解除个人客户资产绑定")
personalAssetBindingMigrated := customerAssetAdminAction(constants.AuditActionPersonalCustomerAssetBindingMigrated, "迁移个人客户资产绑定")
iotCardCreated := iotCardAction(constants.AuditActionIotCardCreated, "创建 IoT 卡", constants.AuditActorSystemTask, constants.AuditSourceWorker)
iotCardDeleted := iotCardAction(constants.AuditActionIotCardDeleted, "删除 IoT 卡", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardDeleted.Risk = constants.AuditRiskHigh
iotCardBatchDeleted := ActionDefinition{
Code: constants.AuditActionIotCardBatchDeleted, Name: "批量删除 IoT 卡",
Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceIotCardBatch, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
iotCardAllocationBatch := iotCardBatchAction(constants.AuditActionIotCardAllocationBatch, "批量分配 IoT 卡")
iotCardAllocated := iotCardAction(constants.AuditActionIotCardAllocated, "分配 IoT 卡", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardRecallBatch := iotCardBatchAction(constants.AuditActionIotCardRecallBatch, "批量回收 IoT 卡")
iotCardRecalled := iotCardAction(constants.AuditActionIotCardRecalled, "回收 IoT 卡", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardSeriesBindingBatch := iotCardBatchAction(constants.AuditActionIotCardSeriesBindingBatch, "批量设置 IoT 卡系列绑定")
iotCardSeriesBound := iotCardAction(constants.AuditActionIotCardSeriesBound, "设置 IoT 卡系列绑定", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardSpeedTierSet := iotCardAction(constants.AuditActionIotCardSpeedTierSet, "设置 IoT 卡固定限速档位", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardRealnamePolicyBatchUpdated := iotCardBatchAction(constants.AuditActionIotCardRealnamePolicyBatchUpdated, "批量更新 IoT 卡实名策略")
iotCardRealnamePolicyUpdated := iotCardAction(constants.AuditActionIotCardRealnamePolicyUpdated, "更新 IoT 卡实名策略", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardRealnameStatusUpdated := iotCardAction(constants.AuditActionIotCardRealnameStatusUpdated, "人工更新 IoT 卡实名状态", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardRealnameCallbackSynced := iotCardAction(constants.AuditActionIotCardRealnameCallbackSynced, "运营商回调同步 IoT 卡实名状态", constants.AuditActorExternalSystem, constants.AuditSourceCallback)
iotCardManualRefreshed := iotCardAction(constants.AuditActionIotCardManualRefreshed, "人工刷新 IoT 卡", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardPersonalRefreshed := iotCardAction(constants.AuditActionIotCardPersonalRefreshed, "个人客户刷新 IoT 卡", constants.AuditActorPersonalCustomer, constants.AuditSourcePersonalAPI)
iotCardManualStopped := iotCardAction(constants.AuditActionIotCardManualStopped, "人工停用 IoT 卡网络", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardManualStarted := iotCardAction(constants.AuditActionIotCardManualStarted, "人工恢复 IoT 卡网络", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardAutoStopped := iotCardAction(constants.AuditActionIotCardAutoStopped, "自动停用 IoT 卡网络", constants.AuditActorSystemTask, constants.AuditSourceWorker)
iotCardAutoStarted := iotCardAction(constants.AuditActionIotCardAutoStarted, "自动恢复 IoT 卡网络", constants.AuditActorSystemTask, constants.AuditSourceWorker)
iotCardOpenAPIStarted := iotCardAction(constants.AuditActionIotCardOpenAPIStarted, "OpenAPI 恢复 IoT 卡网络", constants.AuditActorOpenAPI, constants.AuditSourceOpenAPI)
iotCardAutoStopReasonUpdated := iotCardAction(constants.AuditActionIotCardAutoStopReasonUpdated, "自动更新 IoT 卡停机原因", constants.AuditActorSystemTask, constants.AuditSourceWorker)
deviceCreated := deviceAction(constants.AuditActionDeviceCreated, "导入创建设备", constants.AuditActorSystemTask, constants.AuditSourceWorker)
deviceDeleted := deviceAction(constants.AuditActionDeviceDeleted, "删除设备", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
deviceDeleted.Risk = constants.AuditRiskHigh
deviceAllocationBatch := deviceMultiOriginBatchAction(constants.AuditActionDeviceAllocationBatch, "批量分配设备")
deviceAllocated := deviceMultiOriginAction(constants.AuditActionDeviceAllocated, "分配设备")
deviceRecallBatch := deviceMultiOriginBatchAction(constants.AuditActionDeviceRecallBatch, "批量回收设备")
deviceRecalled := deviceMultiOriginAction(constants.AuditActionDeviceRecalled, "回收设备")
deviceSeriesBindingBatch := deviceMultiOriginBatchAction(constants.AuditActionDeviceSeriesBindingBatch, "批量设置设备系列绑定")
deviceSeriesBound := deviceMultiOriginAction(constants.AuditActionDeviceSeriesBound, "设置设备系列绑定")
deviceRealnamePolicyBatchUpdated := deviceAccountBatchAction(constants.AuditActionDeviceRealnamePolicyBatchUpdated, "批量更新设备实名策略")
deviceRealnamePolicyUpdated := deviceAction(constants.AuditActionDeviceRealnamePolicyUpdated, "更新设备实名策略", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
deviceStopped := deviceAction(constants.AuditActionDeviceStopped, "停用设备绑定卡网络", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
deviceStarted := deviceAction(constants.AuditActionDeviceStarted, "恢复设备绑定卡网络", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
deviceWiFiSet := deviceExternalAction(constants.AuditActionDeviceWiFiSet, "设置设备 Wi-Fi", false)
deviceSwitchModeSet := deviceAction(constants.AuditActionDeviceSwitchModeSet, "设置设备切卡模式", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
deviceRebooted := deviceExternalAction(constants.AuditActionDeviceRebooted, "重启设备", true)
deviceReset := deviceExternalAction(constants.AuditActionDeviceReset, "恢复设备出厂设置", true)
deviceCardBound := deviceAction(constants.AuditActionDeviceCardBound, "设备绑定 IoT 卡", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
deviceCardUnbound := deviceAction(constants.AuditActionDeviceCardUnbound, "设备解绑 IoT 卡", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
deviceCurrentCardSwitched := deviceExternalAction(constants.AuditActionDeviceCurrentCardSwitched, "切换设备当前卡", true)
cardExchangeCreated := cardExchangeAction(constants.AuditActionCardExchangeCreated, "创建卡换货单", constants.AuditRiskNormal, false)
cardExchangeShippingInfoSubmitted := cardExchangeAction(constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息", constants.AuditRiskHigh, true)
cardExchangeShipped := cardExchangeAction(constants.AuditActionCardExchangeShipped, "卡换货发货", constants.AuditRiskNormal, false)
cardExchangeCompleted := cardExchangeAction(constants.AuditActionCardExchangeCompleted, "完成卡换货", constants.AuditRiskHigh, false)
cardExchangeCancelled := cardExchangeAction(constants.AuditActionCardExchangeCancelled, "取消卡换货", constants.AuditRiskNormal, false)
cardExchangeRenewed := cardExchangeAction(constants.AuditActionCardExchangeRenewed, "换出旧卡转新", constants.AuditRiskHigh, false)
cardExchangeRenewed.DefaultVisibility = constants.AuditSubjectInternalOnly
cardExchangeRenewed.AllowedVisibility = []string{constants.AuditSubjectInternalOnly}
deviceExchangeCreated := cardExchangeAction(constants.AuditActionDeviceExchangeCreated, "创建设备换货单", constants.AuditRiskNormal, false)
deviceExchangeShippingInfoSubmitted := cardExchangeAction(constants.AuditActionDeviceExchangeShippingInfoSubmitted, "提交设备换货收货信息", constants.AuditRiskHigh, true)
deviceExchangeShipped := cardExchangeAction(constants.AuditActionDeviceExchangeShipped, "设备换货发货", constants.AuditRiskNormal, false)
deviceExchangeCompleted := cardExchangeAction(constants.AuditActionDeviceExchangeCompleted, "完成设备换货", constants.AuditRiskHigh, false)
deviceExchangeCancelled := cardExchangeAction(constants.AuditActionDeviceExchangeCancelled, "取消设备换货", constants.AuditRiskNormal, false)
deviceExchangeRenewed := cardExchangeAction(constants.AuditActionDeviceExchangeRenewed, "换出旧设备转新", constants.AuditRiskHigh, false)
deviceExchangeRenewed.DefaultVisibility = constants.AuditSubjectInternalOnly
deviceExchangeRenewed.AllowedVisibility = []string{constants.AuditSubjectInternalOnly}
systemConfigUpdated := ActionDefinition{
Code: constants.AuditActionSystemConfigUpdated, Name: "更新受控系统配置",
Category: constants.AuditCategoryConfiguration, Risk: constants.AuditRiskHigh,
@@ -78,6 +155,19 @@ func NewRegistry() *Registry {
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
paymentConfigCreated := connectionConfigAction(constants.AuditActionPaymentConfigCreated, "创建支付连接配置", constants.AuditResourcePaymentConfig, constants.AuditRiskHigh)
paymentConfigUpdated := connectionConfigAction(constants.AuditActionPaymentConfigUpdated, "更新支付连接配置", constants.AuditResourcePaymentConfig, constants.AuditRiskHigh)
paymentConfigDeleted := connectionConfigAction(constants.AuditActionPaymentConfigDeleted, "删除支付连接配置", constants.AuditResourcePaymentConfig, constants.AuditRiskHigh)
paymentConfigActivated := connectionConfigAction(constants.AuditActionPaymentConfigActivated, "激活支付连接配置", constants.AuditResourcePaymentConfig, constants.AuditRiskHigh)
paymentConfigDeactivated := connectionConfigAction(constants.AuditActionPaymentConfigDeactivated, "停用支付连接配置", constants.AuditResourcePaymentConfig, constants.AuditRiskHigh)
carrierCreated := connectionConfigAction(constants.AuditActionCarrierCreated, "创建运营商配置", constants.AuditResourceCarrier, constants.AuditRiskNormal)
carrierUpdated := connectionConfigAction(constants.AuditActionCarrierUpdated, "更新运营商配置", constants.AuditResourceCarrier, constants.AuditRiskNormal)
carrierDeleted := connectionConfigAction(constants.AuditActionCarrierDeleted, "删除运营商配置", constants.AuditResourceCarrier, constants.AuditRiskHigh)
carrierStatusUpdated := connectionConfigAction(constants.AuditActionCarrierStatusUpdated, "更新运营商配置状态", constants.AuditResourceCarrier, constants.AuditRiskHigh)
wecomApplicationSaved := connectionConfigAction(constants.AuditActionWeComApplicationSaved, "保存企业微信应用配置", constants.AuditResourceWeComApplication, constants.AuditRiskHigh)
wecomDefaultCreatorSaved := connectionConfigAction(constants.AuditActionWeComDefaultCreatorSaved, "保存企业微信默认审批发起人", constants.AuditResourceWeComApplication, constants.AuditRiskHigh)
wecomMembersSynced := connectionConfigAction(constants.AuditActionWeComMembersSynced, "同步企业微信应用可见成员", constants.AuditResourceWeComApplication, constants.AuditRiskNormal)
wecomApprovalSceneSaved := connectionConfigAction(constants.AuditActionWeComApprovalSceneSaved, "保存企业微信审批场景配置", constants.AuditResourceWeComApprovalScene, constants.AuditRiskHigh)
outboxReplayed := outboxRecoveryAction(
constants.AuditActionOutboxReplayed,
"人工重放 Outbox 事件",
@@ -97,6 +187,37 @@ func NewRegistry() *Registry {
constants.AuditResourceDevice,
)
deviceBatchItem.AllowedVisibility = []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult}
iotCardImportTaskCreated := taskAction(constants.AuditActionIotCardImportTaskCreated, "创建 IoT 卡导入任务", constants.AuditResourceIotCardImportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
iotCardImportTaskCompleted := taskAction(constants.AuditActionIotCardImportTaskCompleted, "完成 IoT 卡导入任务", constants.AuditResourceIotCardImportTask, constants.AuditActorSystemTask, constants.AuditSourceWorker)
deviceImportTaskCreated := taskAction(constants.AuditActionDeviceImportTaskCreated, "创建设备导入任务", constants.AuditResourceDeviceImportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
deviceImportTaskCompleted := taskAction(constants.AuditActionDeviceImportTaskCompleted, "完成设备导入任务", constants.AuditResourceDeviceImportTask, constants.AuditActorSystemTask, constants.AuditSourceWorker)
assetPackageBatchOrderTaskCreated := taskAction(constants.AuditActionAssetPackageBatchOrderTaskCreated, "创建资产套餐批量订购任务", constants.AuditResourceAssetPackageBatchOrderTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
assetPackageBatchOrderTaskCompleted := taskAction(constants.AuditActionAssetPackageBatchOrderTaskCompleted, "完成资产套餐批量订购任务", constants.AuditResourceAssetPackageBatchOrderTask, constants.AuditActorSystemTask, constants.AuditSourceWorker)
orderPackageInvalidateTaskCreated := taskAction(constants.AuditActionOrderPackageInvalidateTaskCreated, "创建订单套餐批量失效任务", constants.AuditResourceOrderPackageInvalidateTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
orderPackageInvalidateTaskCompleted := taskAction(constants.AuditActionOrderPackageInvalidateTaskCompleted, "完成订单套餐批量失效任务", constants.AuditResourceOrderPackageInvalidateTask, constants.AuditActorSystemTask, constants.AuditSourceWorker)
orderPackageInvalidateItem := taskAction(constants.AuditActionOrderPackageInvalidateItem, "失效订单套餐权益", constants.AuditResourceOrder, constants.AuditActorSystemTask, constants.AuditSourceWorker)
exportTaskCreated := taskAction(constants.AuditActionExportTaskCreated, "创建业务导出任务", constants.AuditResourceExportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
exportTaskCancelled := taskAction(constants.AuditActionExportTaskCancelled, "取消业务导出任务", constants.AuditResourceExportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
notificationDelivered := notificationAction(constants.AuditActionNotificationDelivered, "生成站内通知", constants.AuditResourceNotification, constants.AuditActorSystemTask, constants.AuditSourceWorker)
notificationRead := notificationAction(constants.AuditActionNotificationRead, "标记通知已读", constants.AuditResourceNotification, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
notificationRead.AllowedOrigins = []ActionOrigin{{Actor: constants.AuditActorPersonalCustomer, Source: constants.AuditSourcePersonalAPI}}
notificationReadAll := notificationAction(constants.AuditActionNotificationReadAll, "批量标记通知已读", constants.AuditResourceNotificationReadBatch, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
notificationReadAll.AllowedOrigins = []ActionOrigin{{Actor: constants.AuditActorPersonalCustomer, Source: constants.AuditSourcePersonalAPI}}
notificationCleanup := notificationAction(constants.AuditActionNotificationCleanup, "清理过期通知", constants.AuditResourceNotificationCleanupBatch, constants.AuditActorSystemTask, constants.AuditSourceWorker)
notificationCleanupItem := notificationAction(constants.AuditActionNotificationCleanupItem, "清理单条过期通知", constants.AuditResourceNotification, constants.AuditActorSystemTask, constants.AuditSourceWorker)
pollingConfigCreated := pollingAction(constants.AuditActionPollingConfigCreated, "创建轮询配置", constants.AuditResourcePollingConfig, constants.AuditRiskHigh)
pollingConfigUpdated := pollingAction(constants.AuditActionPollingConfigUpdated, "更新轮询配置", constants.AuditResourcePollingConfig, constants.AuditRiskHigh)
pollingConfigDeleted := pollingAction(constants.AuditActionPollingConfigDeleted, "删除轮询配置", constants.AuditResourcePollingConfig, constants.AuditRiskHigh)
pollingConfigStatusUpdated := pollingAction(constants.AuditActionPollingConfigStatusUpdated, "更新轮询配置状态", constants.AuditResourcePollingConfig, constants.AuditRiskHigh)
pollingConcurrencyUpdated := pollingAction(constants.AuditActionPollingConcurrencyUpdated, "更新轮询并发配置", constants.AuditResourcePollingConcurrencyConfig, constants.AuditRiskNormal)
pollingConcurrencyReset := pollingAction(constants.AuditActionPollingConcurrencyReset, "重置轮询并发计数", constants.AuditResourcePollingConcurrencyConfig, constants.AuditRiskNormal)
pollingAlertRuleCreated := pollingAction(constants.AuditActionPollingAlertRuleCreated, "创建轮询告警规则", constants.AuditResourcePollingAlertRule, constants.AuditRiskNormal)
pollingAlertRuleUpdated := pollingAction(constants.AuditActionPollingAlertRuleUpdated, "更新轮询告警规则", constants.AuditResourcePollingAlertRule, constants.AuditRiskNormal)
pollingAlertRuleDeleted := pollingAction(constants.AuditActionPollingAlertRuleDeleted, "删除轮询告警规则", constants.AuditResourcePollingAlertRule, constants.AuditRiskNormal)
pollingManualTriggerSingle := pollingAction(constants.AuditActionPollingManualTriggerSingle, "单卡手动触发", constants.AuditResourcePollingManualTrigger, constants.AuditRiskNormal)
pollingManualTriggerBatch := pollingAction(constants.AuditActionPollingManualTriggerBatch, "批量手动触发", constants.AuditResourcePollingManualTrigger, constants.AuditRiskNormal)
pollingManualTriggerByCondition := pollingAction(constants.AuditActionPollingManualTriggerByCondition, "条件筛选触发", constants.AuditResourcePollingManualTrigger, constants.AuditRiskNormal)
pollingManualCancelled := pollingAction(constants.AuditActionPollingManualCancelled, "取消手动触发任务", constants.AuditResourcePollingManualTrigger, constants.AuditRiskNormal)
wecomCredentialsRead := ActionDefinition{
Code: constants.AuditActionWeComCredentialsRead, Name: "读取企业微信应用明文凭据",
Category: constants.AuditCategorySecurity, Risk: constants.AuditRiskHigh,
@@ -115,9 +236,121 @@ func NewRegistry() *Registry {
permissionCreated := accessAction(constants.AuditActionPermissionCreated, "创建权限", constants.AuditResourcePermission)
permissionUpdated := accessAction(constants.AuditActionPermissionUpdated, "更新权限", constants.AuditResourcePermission)
permissionDeleted := accessAction(constants.AuditActionPermissionDeleted, "删除权限", constants.AuditResourcePermission)
packageSeriesCreated := packageConfigAction(constants.AuditActionPackageSeriesCreated, "创建套餐系列", constants.AuditResourcePackageSeries, constants.AuditRiskNormal)
packageSeriesUpdated := packageConfigAction(constants.AuditActionPackageSeriesUpdated, "更新套餐系列", constants.AuditResourcePackageSeries, constants.AuditRiskNormal)
packageSeriesDeleted := packageConfigAction(constants.AuditActionPackageSeriesDeleted, "删除套餐系列", constants.AuditResourcePackageSeries, constants.AuditRiskHigh)
packageSeriesStatusUpdated := packageConfigAction(constants.AuditActionPackageSeriesStatusUpdated, "更新套餐系列状态", constants.AuditResourcePackageSeries, constants.AuditRiskNormal)
packageCreated := packageConfigAction(constants.AuditActionPackageCreated, "创建套餐商品", constants.AuditResourcePackage, constants.AuditRiskNormal)
packageUpdated := packageConfigAction(constants.AuditActionPackageUpdated, "更新套餐商品", constants.AuditResourcePackage, constants.AuditRiskNormal)
packageDeleted := packageConfigAction(constants.AuditActionPackageDeleted, "删除套餐商品", constants.AuditResourcePackage, constants.AuditRiskHigh)
packageStatusUpdated := packageConfigAction(constants.AuditActionPackageStatusUpdated, "更新套餐商品状态", constants.AuditResourcePackage, constants.AuditRiskNormal)
packageShelfStatusUpdated := packageConfigAction(constants.AuditActionPackageShelfStatusUpdated, "更新套餐上架状态", constants.AuditResourcePackage, constants.AuditRiskNormal)
shopPackageShelfStatusUpdated := packageConfigAction(constants.AuditActionShopPackageShelfStatusUpdated, "更新店铺套餐上架状态", constants.AuditResourceShopPackageAllocation, constants.AuditRiskNormal)
packageRetailPriceUpdated := packageConfigAction(constants.AuditActionPackageRetailPriceUpdated, "更新店铺套餐零售价", constants.AuditResourceShopPackageAllocation, constants.AuditRiskNormal)
shopSeriesGrantCreated := packageConfigAction(constants.AuditActionShopSeriesGrantCreated, "创建店铺套餐系列授权", constants.AuditResourceShopSeriesAllocation, constants.AuditRiskNormal)
shopSeriesGrantUpdated := packageConfigAction(constants.AuditActionShopSeriesGrantUpdated, "更新店铺套餐系列授权", constants.AuditResourceShopSeriesAllocation, constants.AuditRiskNormal)
shopSeriesGrantPackagesManaged := packageConfigAction(constants.AuditActionShopSeriesGrantPackagesManaged, "管理店铺系列套餐授权", constants.AuditResourceShopSeriesAllocation, constants.AuditRiskNormal)
shopSeriesGrantDeleted := packageConfigAction(constants.AuditActionShopSeriesGrantDeleted, "删除店铺套餐系列授权", constants.AuditResourceShopSeriesAllocation, constants.AuditRiskHigh)
shopPackageBatchAllocated := packageConfigAction(constants.AuditActionShopPackageBatchAllocated, "批量分配店铺套餐", constants.AuditResourcePackageConfigBatch, constants.AuditRiskNormal)
shopPackageAllocated := packageConfigAction(constants.AuditActionShopPackageAllocated, "分配店铺套餐", constants.AuditResourceShopPackageAllocation, constants.AuditRiskNormal)
shopPackageExpiryBaseUpdated := packageConfigAction(constants.AuditActionShopPackageExpiryBaseUpdated, "更新店铺套餐生效条件", constants.AuditResourceShopPackageAllocation, constants.AuditRiskNormal)
shopPackageBatchPricingUpdated := packageConfigAction(constants.AuditActionShopPackageBatchPricingUpdated, "批量更新店铺套餐成本价", constants.AuditResourcePackageConfigBatch, constants.AuditRiskNormal)
shopPackagePricingItemUpdated := packageConfigAction(constants.AuditActionShopPackagePricingItemUpdated, "更新店铺套餐成本价", constants.AuditResourceShopPackageAllocation, constants.AuditRiskNormal)
packageUsageActivated := packageUsageAction(constants.AuditActionPackageUsageActivated, "激活套餐权益")
packageUsageExpired := packageUsageAction(constants.AuditActionPackageUsageExpired, "套餐权益到期")
packageUsageTrafficDeducted := packageUsageAction(constants.AuditActionPackageUsageTrafficDeducted, "扣减套餐权益流量")
packageUsageTrafficReset := packageUsageAction(constants.AuditActionPackageUsageTrafficReset, "重置套餐权益流量")
packageUsageRefundInvalidated := packageUsageAction(constants.AuditActionPackageUsageRefundInvalidated, "退款失效套餐权益")
packageUsageAssetInvalidated := packageUsageAction(constants.AuditActionPackageUsageAssetInvalidated, "资产失效套餐权益")
orderCreated := orderAction(constants.AuditActionOrderCreated, "创建订单")
orderCancelled := orderAction(constants.AuditActionOrderCancelled, "取消订单")
orderWalletPaid := orderAction(constants.AuditActionOrderWalletPaid, "钱包支付订单")
orderExpiredClosed := orderAction(constants.AuditActionOrderExpiredClosed, "关闭过期订单")
orderOnlinePaid := orderAction(constants.AuditActionOrderOnlinePaid, "第三方支付订单")
orderOnlinePaid.AllowedOrigins = append(orderOnlinePaid.AllowedOrigins, ActionOrigin{Actor: constants.AuditActorExternalSystem, Source: constants.AuditSourceCallback})
agentWalletOrderDebited := agentWalletOrderAction(constants.AuditActionAgentWalletOrderDebited, "代理主钱包订单扣款")
agentWalletOrderReserved := agentWalletOrderAction(constants.AuditActionAgentWalletOrderReserved, "代理主钱包订单资金预占")
agentWalletOrderReleased := agentWalletOrderAction(constants.AuditActionAgentWalletOrderReleased, "释放代理主钱包订单预占")
agentWalletOrderCompleted := agentWalletOrderAction(constants.AuditActionAgentWalletOrderCompleted, "完成代理主钱包订单预占扣款")
agentWalletBalanceAdjusted := agentWalletAction(constants.AuditActionAgentWalletBalanceAdjusted, "人工调整代理主钱包余额")
agentWalletCreditChanged := agentWalletAction(constants.AuditActionAgentWalletCreditChanged, "调整代理主钱包信用额度")
paymentCreated := paymentAction(constants.AuditActionPaymentCreated, "创建支付记录", false)
paymentConfirmed := paymentAction(constants.AuditActionPaymentConfirmed, "确认支付成功", true)
paymentFailed := paymentAction(constants.AuditActionPaymentFailed, "关闭失败支付记录", false)
agentRechargeCreated := rechargeAction(constants.AuditActionAgentRechargeCreated, "创建代理充值申请", constants.AuditResourceAgentRecharge)
agentRechargeCredited := rechargeAction(constants.AuditActionAgentRechargeCredited, "代理充值资金入账", constants.AuditResourceAgentRecharge)
agentRechargeClosed := rechargeAction(constants.AuditActionAgentRechargeClosed, "关闭代理充值申请", constants.AuditResourceAgentRecharge)
assetRechargeAutoPurchased := rechargeAction(constants.AuditActionAssetRechargeAutoPurchased, "充值后自动购包", constants.AuditResourceRechargeOrder)
refundCreated := refundAction(constants.AuditActionRefundCreated, "提交退款申请", false)
refundApproved := refundAction(constants.AuditActionRefundApproved, "通过退款审批", true)
refundRejected := refundAction(constants.AuditActionRefundRejected, "拒绝退款审批", true)
refundReturned := refundAction(constants.AuditActionRefundReturned, "退回退款申请", false)
refundResubmitted := refundAction(constants.AuditActionRefundResubmitted, "重新提交退款申请", false)
refundCommissionInvalidated := refundSystemAction(constants.AuditActionRefundCommissionInvalidated, "退款失效佣金")
refundAssetProcessed := refundSystemAction(constants.AuditActionRefundAssetProcessed, "完成退款资产后处理")
approvalRequested := approvalAction(constants.AuditActionApprovalRequested, "提交通用审批申请", []ActionOrigin{
{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI},
})
approvalSubmissionSynced := approvalAction(constants.AuditActionApprovalSubmissionSynced, "同步审批提交结果", []ActionOrigin{
{Actor: constants.AuditActorSystemTask, Source: constants.AuditSourceWorker},
{Actor: constants.AuditActorScheduledJob, Source: constants.AuditSourceScheduler},
})
approvalSubmissionRecovered := approvalAction(constants.AuditActionApprovalSubmissionRecovered, "恢复审批提交结果", []ActionOrigin{
{Actor: constants.AuditActorScheduledJob, Source: constants.AuditSourceScheduler},
{Actor: constants.AuditActorExternalSystem, Source: constants.AuditSourceCallback},
})
approvalDecisionSynced := approvalAction(constants.AuditActionApprovalDecisionSynced, "同步审批权威终态", []ActionOrigin{
{Actor: constants.AuditActorExternalSystem, Source: constants.AuditSourceCallback},
{Actor: constants.AuditActorScheduledJob, Source: constants.AuditSourceScheduler},
{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI},
})
approvalDecisionSynced.Risk = constants.AuditRiskHigh
commissionCalculated := ActionDefinition{
Code: constants.AuditActionCommissionCalculated, Name: "计算订单佣金",
Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceOrder, AllowedActor: constants.AuditActorSystemTask,
Source: constants.AuditSourceWorker, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
commissionCredited := ActionDefinition{
Code: constants.AuditActionCommissionCredited, Name: "佣金入账",
Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceCommissionRecord, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
AllowedOrigins: []ActionOrigin{
{Actor: constants.AuditActorSystemTask, Source: constants.AuditSourceWorker},
{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI},
},
}
commissionInvalidated := ActionDefinition{
Code: constants.AuditActionCommissionInvalidated, Name: "失效待审佣金",
Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceCommissionRecord, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
withdrawalRequested := commissionWithdrawalAction(constants.AuditActionCommissionWithdrawalRequested, "提交佣金提现申请")
withdrawalApproved := commissionWithdrawalAction(constants.AuditActionCommissionWithdrawalApproved, "通过佣金提现申请")
withdrawalRejected := commissionWithdrawalAction(constants.AuditActionCommissionWithdrawalRejected, "驳回佣金提现申请")
return &Registry{
actionsByOperation: map[string]ActionDefinition{
constants.AuditOperationSystemConfigUpdate: systemConfigUpdated,
constants.AuditOperationPaymentConfigCreate: paymentConfigCreated,
constants.AuditOperationPaymentConfigUpdate: paymentConfigUpdated,
constants.AuditOperationPaymentConfigDelete: paymentConfigDeleted,
constants.AuditOperationPaymentConfigActivate: paymentConfigActivated,
constants.AuditOperationPaymentConfigDeactivate: paymentConfigDeactivated,
constants.AuditOperationCarrierCreate: carrierCreated,
constants.AuditOperationCarrierUpdate: carrierUpdated,
constants.AuditOperationCarrierDelete: carrierDeleted,
constants.AuditOperationCarrierStatusUpdate: carrierStatusUpdated,
constants.AuditOperationWeComApplicationSave: wecomApplicationSaved,
constants.AuditOperationWeComDefaultCreatorSave: wecomDefaultCreatorSaved,
constants.AuditOperationWeComMembersSync: wecomMembersSynced,
constants.AuditOperationWeComApprovalSceneSave: wecomApprovalSceneSaved,
constants.AuditOperationOutboxReplay: outboxReplayed,
constants.AuditOperationOutboxReleaseExpiredLease: outboxExpiredLeaseReleased,
},
@@ -155,11 +388,109 @@ func NewRegistry() *Registry {
constants.AuditActionPersonalCustomerPhoneBound: personalPhoneBound,
constants.AuditActionPersonalCustomerPhoneChanged: personalPhoneChanged,
constants.AuditActionPersonalCustomerWechatIdentityUpdated: personalWechatIdentityUpdated,
constants.AuditActionPersonalCustomerAssetBound: personalAssetBound,
constants.AuditActionPersonalCustomerAssetUnbound: personalAssetUnbound,
constants.AuditActionPersonalCustomerAssetBindingMigrated: personalAssetBindingMigrated,
constants.AuditActionIotCardCreated: iotCardCreated,
constants.AuditActionIotCardDeleted: iotCardDeleted,
constants.AuditActionIotCardBatchDeleted: iotCardBatchDeleted,
constants.AuditActionIotCardAllocationBatch: iotCardAllocationBatch,
constants.AuditActionIotCardAllocated: iotCardAllocated,
constants.AuditActionIotCardRecallBatch: iotCardRecallBatch,
constants.AuditActionIotCardRecalled: iotCardRecalled,
constants.AuditActionIotCardSeriesBindingBatch: iotCardSeriesBindingBatch,
constants.AuditActionIotCardSeriesBound: iotCardSeriesBound,
constants.AuditActionIotCardSpeedTierSet: iotCardSpeedTierSet,
constants.AuditActionIotCardRealnamePolicyBatchUpdated: iotCardRealnamePolicyBatchUpdated,
constants.AuditActionIotCardRealnamePolicyUpdated: iotCardRealnamePolicyUpdated,
constants.AuditActionIotCardRealnameStatusUpdated: iotCardRealnameStatusUpdated,
constants.AuditActionIotCardRealnameCallbackSynced: iotCardRealnameCallbackSynced,
constants.AuditActionIotCardManualRefreshed: iotCardManualRefreshed,
constants.AuditActionIotCardPersonalRefreshed: iotCardPersonalRefreshed,
constants.AuditActionIotCardManualStopped: iotCardManualStopped,
constants.AuditActionIotCardManualStarted: iotCardManualStarted,
constants.AuditActionIotCardAutoStopped: iotCardAutoStopped,
constants.AuditActionIotCardAutoStarted: iotCardAutoStarted,
constants.AuditActionIotCardOpenAPIStarted: iotCardOpenAPIStarted,
constants.AuditActionIotCardAutoStopReasonUpdated: iotCardAutoStopReasonUpdated,
constants.AuditActionDeviceCreated: deviceCreated,
constants.AuditActionDeviceDeleted: deviceDeleted,
constants.AuditActionDeviceAllocationBatch: deviceAllocationBatch,
constants.AuditActionDeviceAllocated: deviceAllocated,
constants.AuditActionDeviceRecallBatch: deviceRecallBatch,
constants.AuditActionDeviceRecalled: deviceRecalled,
constants.AuditActionDeviceSeriesBindingBatch: deviceSeriesBindingBatch,
constants.AuditActionDeviceSeriesBound: deviceSeriesBound,
constants.AuditActionDeviceRealnamePolicyBatchUpdated: deviceRealnamePolicyBatchUpdated,
constants.AuditActionDeviceRealnamePolicyUpdated: deviceRealnamePolicyUpdated,
constants.AuditActionDeviceStopped: deviceStopped,
constants.AuditActionDeviceStarted: deviceStarted,
constants.AuditActionDeviceWiFiSet: deviceWiFiSet,
constants.AuditActionDeviceSwitchModeSet: deviceSwitchModeSet,
constants.AuditActionDeviceRebooted: deviceRebooted,
constants.AuditActionDeviceReset: deviceReset,
constants.AuditActionDeviceCardBound: deviceCardBound,
constants.AuditActionDeviceCardUnbound: deviceCardUnbound,
constants.AuditActionDeviceCurrentCardSwitched: deviceCurrentCardSwitched,
constants.AuditActionCardExchangeCreated: cardExchangeCreated,
constants.AuditActionCardExchangeShippingInfoSubmitted: cardExchangeShippingInfoSubmitted,
constants.AuditActionCardExchangeShipped: cardExchangeShipped,
constants.AuditActionCardExchangeCompleted: cardExchangeCompleted,
constants.AuditActionCardExchangeCancelled: cardExchangeCancelled,
constants.AuditActionCardExchangeRenewed: cardExchangeRenewed,
constants.AuditActionDeviceExchangeCreated: deviceExchangeCreated,
constants.AuditActionDeviceExchangeShippingInfoSubmitted: deviceExchangeShippingInfoSubmitted,
constants.AuditActionDeviceExchangeShipped: deviceExchangeShipped,
constants.AuditActionDeviceExchangeCompleted: deviceExchangeCompleted,
constants.AuditActionDeviceExchangeCancelled: deviceExchangeCancelled,
constants.AuditActionDeviceExchangeRenewed: deviceExchangeRenewed,
constants.AuditActionSystemConfigUpdated: systemConfigUpdated,
constants.AuditActionPaymentConfigCreated: paymentConfigCreated,
constants.AuditActionPaymentConfigUpdated: paymentConfigUpdated,
constants.AuditActionPaymentConfigDeleted: paymentConfigDeleted,
constants.AuditActionPaymentConfigActivated: paymentConfigActivated,
constants.AuditActionPaymentConfigDeactivated: paymentConfigDeactivated,
constants.AuditActionCarrierCreated: carrierCreated,
constants.AuditActionCarrierUpdated: carrierUpdated,
constants.AuditActionCarrierDeleted: carrierDeleted,
constants.AuditActionCarrierStatusUpdated: carrierStatusUpdated,
constants.AuditActionWeComApplicationSaved: wecomApplicationSaved,
constants.AuditActionWeComDefaultCreatorSaved: wecomDefaultCreatorSaved,
constants.AuditActionWeComMembersSynced: wecomMembersSynced,
constants.AuditActionWeComApprovalSceneSaved: wecomApprovalSceneSaved,
constants.AuditActionOutboxReplayed: outboxReplayed,
constants.AuditActionOutboxExpiredLeaseReleased: outboxExpiredLeaseReleased,
constants.AuditActionDeviceBatchAllocationCompleted: deviceBatchCompleted,
constants.AuditActionDeviceBatchAllocationItem: deviceBatchItem,
constants.AuditActionIotCardImportTaskCreated: iotCardImportTaskCreated,
constants.AuditActionIotCardImportTaskCompleted: iotCardImportTaskCompleted,
constants.AuditActionDeviceImportTaskCreated: deviceImportTaskCreated,
constants.AuditActionDeviceImportTaskCompleted: deviceImportTaskCompleted,
constants.AuditActionAssetPackageBatchOrderTaskCreated: assetPackageBatchOrderTaskCreated,
constants.AuditActionAssetPackageBatchOrderTaskCompleted: assetPackageBatchOrderTaskCompleted,
constants.AuditActionOrderPackageInvalidateTaskCreated: orderPackageInvalidateTaskCreated,
constants.AuditActionOrderPackageInvalidateTaskCompleted: orderPackageInvalidateTaskCompleted,
constants.AuditActionOrderPackageInvalidateItem: orderPackageInvalidateItem,
constants.AuditActionExportTaskCreated: exportTaskCreated,
constants.AuditActionExportTaskCancelled: exportTaskCancelled,
constants.AuditActionNotificationDelivered: notificationDelivered,
constants.AuditActionNotificationRead: notificationRead,
constants.AuditActionNotificationReadAll: notificationReadAll,
constants.AuditActionNotificationCleanup: notificationCleanup,
constants.AuditActionNotificationCleanupItem: notificationCleanupItem,
constants.AuditActionPollingConfigCreated: pollingConfigCreated,
constants.AuditActionPollingConfigUpdated: pollingConfigUpdated,
constants.AuditActionPollingConfigDeleted: pollingConfigDeleted,
constants.AuditActionPollingConfigStatusUpdated: pollingConfigStatusUpdated,
constants.AuditActionPollingConcurrencyUpdated: pollingConcurrencyUpdated,
constants.AuditActionPollingConcurrencyReset: pollingConcurrencyReset,
constants.AuditActionPollingAlertRuleCreated: pollingAlertRuleCreated,
constants.AuditActionPollingAlertRuleUpdated: pollingAlertRuleUpdated,
constants.AuditActionPollingAlertRuleDeleted: pollingAlertRuleDeleted,
constants.AuditActionPollingManualTriggerSingle: pollingManualTriggerSingle,
constants.AuditActionPollingManualTriggerBatch: pollingManualTriggerBatch,
constants.AuditActionPollingManualTriggerByCondition: pollingManualTriggerByCondition,
constants.AuditActionPollingManualCancelled: pollingManualCancelled,
constants.AuditActionWeComCredentialsRead: wecomCredentialsRead,
constants.AuditActionRoleCreated: roleCreated,
constants.AuditActionRoleUpdated: roleUpdated,
@@ -172,6 +503,67 @@ func NewRegistry() *Registry {
constants.AuditActionPermissionCreated: permissionCreated,
constants.AuditActionPermissionUpdated: permissionUpdated,
constants.AuditActionPermissionDeleted: permissionDeleted,
constants.AuditActionPackageSeriesCreated: packageSeriesCreated,
constants.AuditActionPackageSeriesUpdated: packageSeriesUpdated,
constants.AuditActionPackageSeriesDeleted: packageSeriesDeleted,
constants.AuditActionPackageSeriesStatusUpdated: packageSeriesStatusUpdated,
constants.AuditActionPackageCreated: packageCreated,
constants.AuditActionPackageUpdated: packageUpdated,
constants.AuditActionPackageDeleted: packageDeleted,
constants.AuditActionPackageStatusUpdated: packageStatusUpdated,
constants.AuditActionPackageShelfStatusUpdated: packageShelfStatusUpdated,
constants.AuditActionShopPackageShelfStatusUpdated: shopPackageShelfStatusUpdated,
constants.AuditActionPackageRetailPriceUpdated: packageRetailPriceUpdated,
constants.AuditActionShopSeriesGrantCreated: shopSeriesGrantCreated,
constants.AuditActionShopSeriesGrantUpdated: shopSeriesGrantUpdated,
constants.AuditActionShopSeriesGrantPackagesManaged: shopSeriesGrantPackagesManaged,
constants.AuditActionShopSeriesGrantDeleted: shopSeriesGrantDeleted,
constants.AuditActionShopPackageBatchAllocated: shopPackageBatchAllocated,
constants.AuditActionShopPackageAllocated: shopPackageAllocated,
constants.AuditActionShopPackageExpiryBaseUpdated: shopPackageExpiryBaseUpdated,
constants.AuditActionShopPackageBatchPricingUpdated: shopPackageBatchPricingUpdated,
constants.AuditActionShopPackagePricingItemUpdated: shopPackagePricingItemUpdated,
constants.AuditActionPackageUsageActivated: packageUsageActivated,
constants.AuditActionPackageUsageExpired: packageUsageExpired,
constants.AuditActionPackageUsageTrafficDeducted: packageUsageTrafficDeducted,
constants.AuditActionPackageUsageTrafficReset: packageUsageTrafficReset,
constants.AuditActionPackageUsageRefundInvalidated: packageUsageRefundInvalidated,
constants.AuditActionPackageUsageAssetInvalidated: packageUsageAssetInvalidated,
constants.AuditActionOrderCreated: orderCreated,
constants.AuditActionOrderCancelled: orderCancelled,
constants.AuditActionOrderWalletPaid: orderWalletPaid,
constants.AuditActionOrderExpiredClosed: orderExpiredClosed,
constants.AuditActionOrderOnlinePaid: orderOnlinePaid,
constants.AuditActionAgentWalletOrderDebited: agentWalletOrderDebited,
constants.AuditActionAgentWalletOrderReserved: agentWalletOrderReserved,
constants.AuditActionAgentWalletOrderReleased: agentWalletOrderReleased,
constants.AuditActionAgentWalletOrderCompleted: agentWalletOrderCompleted,
constants.AuditActionAgentWalletBalanceAdjusted: agentWalletBalanceAdjusted,
constants.AuditActionAgentWalletCreditChanged: agentWalletCreditChanged,
constants.AuditActionPaymentCreated: paymentCreated,
constants.AuditActionPaymentConfirmed: paymentConfirmed,
constants.AuditActionPaymentFailed: paymentFailed,
constants.AuditActionAgentRechargeCreated: agentRechargeCreated,
constants.AuditActionAgentRechargeCredited: agentRechargeCredited,
constants.AuditActionAgentRechargeClosed: agentRechargeClosed,
constants.AuditActionAssetRechargeAutoPurchased: assetRechargeAutoPurchased,
constants.AuditActionRefundCreated: refundCreated,
constants.AuditActionRefundApproved: refundApproved,
constants.AuditActionRefundRejected: refundRejected,
constants.AuditActionRefundReturned: refundReturned,
constants.AuditActionRefundResubmitted: refundResubmitted,
constants.AuditActionRefundCommissionInvalidated: refundCommissionInvalidated,
constants.AuditActionRefundAssetProcessed: refundAssetProcessed,
constants.AuditActionApprovalRequested: approvalRequested,
constants.AuditActionApprovalSubmissionSynced: approvalSubmissionSynced,
constants.AuditActionApprovalSubmissionRecovered: approvalSubmissionRecovered,
constants.AuditActionApprovalDecisionSynced: approvalDecisionSynced,
constants.AuditActionCommissionCalculated: commissionCalculated,
constants.AuditActionCommissionCredited: commissionCredited,
constants.AuditActionCommissionInvalidated: commissionInvalidated,
constants.AuditActionCommissionWithdrawalRequested: withdrawalRequested,
constants.AuditActionCommissionWithdrawalApproved: withdrawalApproved,
constants.AuditActionCommissionWithdrawalRejected: withdrawalRejected,
},
resources: map[string]ResourceDefinition{
constants.AuditResourceAccount: {
@@ -190,6 +582,18 @@ func NewRegistry() *Registry {
Type: constants.AuditResourceSystemConfig, Name: "受控系统配置",
IdentityFields: []string{"config_key", "module"},
},
constants.AuditResourcePaymentConfig: {
Type: constants.AuditResourcePaymentConfig, Name: "支付连接配置",
IdentityFields: []string{"id", "name", "provider_type", "is_active", "credentials_configured"},
},
constants.AuditResourceCarrier: {
Type: constants.AuditResourceCarrier, Name: "运营商配置",
IdentityFields: []string{"id", "carrier_code", "carrier_name", "carrier_type", "status"},
},
constants.AuditResourceWeComApprovalScene: {
Type: constants.AuditResourceWeComApprovalScene, Name: "企业微信审批场景配置",
IdentityFields: []string{"id", "business_type", "application_id", "template_id", "template_name", "status"},
},
constants.AuditResourceOutboxEvent: {
Type: constants.AuditResourceOutboxEvent, Name: "Outbox 事件",
IdentityFields: []string{
@@ -197,13 +601,68 @@ func NewRegistry() *Registry {
"resource_type", "resource_id", "business_key",
},
},
constants.AuditResourceIntegrationLog: {
Type: constants.AuditResourceIntegrationLog, Name: "外部集成日志",
IdentityFields: []string{
"integration_id", "provider", "direction", "operation", "result", "external_id",
"resource_type", "resource_id", "resource_key", "correlation_id",
},
},
constants.AuditResourceDeviceBatchTask: {
Type: constants.AuditResourceDeviceBatchTask, Name: "设备批量分配任务",
IdentityFields: []string{"task_no", "operation_type"},
},
constants.AuditResourceIotCardImportTask: {
Type: constants.AuditResourceIotCardImportTask, Name: "IoT 卡导入任务",
IdentityFields: []string{"id", "task_no", "file_name", "carrier_id", "carrier_name", "batch_no", "card_category", "realname_policy"},
},
constants.AuditResourceDeviceImportTask: {
Type: constants.AuditResourceDeviceImportTask, Name: "设备导入任务",
IdentityFields: []string{"id", "task_no", "file_name", "operation_type", "target_id", "batch_no", "realname_policy"},
},
constants.AuditResourceAssetPackageBatchOrderTask: {
Type: constants.AuditResourceAssetPackageBatchOrderTask, Name: "资产套餐批量订购任务",
IdentityFields: []string{"id", "task_no", "file_name", "package_id", "package_code", "package_name", "payment_method"},
},
constants.AuditResourceOrderPackageInvalidateTask: {
Type: constants.AuditResourceOrderPackageInvalidateTask, Name: "订单套餐批量失效任务",
IdentityFields: []string{"id", "task_no", "file_name"},
},
constants.AuditResourceExportTask: {
Type: constants.AuditResourceExportTask, Name: "业务导出任务",
IdentityFields: []string{"id", "task_no", "scene", "format", "creator_user_id", "creator_user_type", "creator_shop_id", "creator_enterprise_id", "scope_shop_ids"},
},
constants.AuditResourceNotification: {
Type: constants.AuditResourceNotification, Name: "站内通知",
IdentityFields: []string{"id", "event_id", "recipient_kind", "recipient_id", "category", "type", "severity", "ref_type", "ref_id", "ref_key"},
},
constants.AuditResourceNotificationReadBatch: {
Type: constants.AuditResourceNotificationReadBatch, Name: "通知批量已读",
IdentityFields: []string{"recipient_kind", "recipient_id", "category", "updated_count"},
},
constants.AuditResourceNotificationCleanupBatch: {
Type: constants.AuditResourceNotificationCleanupBatch, Name: "通知清理批次",
IdentityFields: []string{"category", "cutoff", "deleted_count", "first_id", "last_id"},
},
constants.AuditResourcePollingConfig: {
Type: constants.AuditResourcePollingConfig, Name: "轮询配置",
IdentityFields: []string{"id", "config_name", "card_condition", "card_category", "carrier_id", "priority", "status"},
},
constants.AuditResourcePollingConcurrencyConfig: {
Type: constants.AuditResourcePollingConcurrencyConfig, Name: "轮询并发配置",
IdentityFields: []string{"id", "task_type", "max_concurrency"},
},
constants.AuditResourcePollingAlertRule: {
Type: constants.AuditResourcePollingAlertRule, Name: "轮询告警规则",
IdentityFields: []string{"id", "rule_name", "task_type", "metric_type", "operator", "threshold", "alert_level", "status"},
},
constants.AuditResourcePollingManualTrigger: {
Type: constants.AuditResourcePollingManualTrigger, Name: "手动轮询任务",
IdentityFields: []string{"id", "task_type", "trigger_type", "total_count", "status", "triggered_by"},
},
constants.AuditResourceDevice: {
Type: constants.AuditResourceDevice, Name: "设备",
IdentityFields: []string{"id", "virtual_no", "imei", "sn", "generation"},
IdentityFields: []string{"id", "virtual_no", "imei", "sn", "device_name", "device_model", "device_type", "manufacturer", "shop_id", "series_id", "generation"},
},
constants.AuditResourceIotCard: {
Type: constants.AuditResourceIotCard, Name: "IoT卡",
@@ -215,11 +674,11 @@ func NewRegistry() *Registry {
},
constants.AuditResourceOrder: {
Type: constants.AuditResourceOrder, Name: "订单",
IdentityFields: []string{"id", "order_no", "buyer_type", "buyer_id", "asset_identifier", "total_amount", "payment_method", "payment_status"},
IdentityFields: []string{"id", "order_no", "order_type", "buyer_type", "buyer_id", "iot_card_id", "device_id", "asset_identifier", "total_amount", "actual_paid_amount", "payment_method", "payment_status", "purchase_role", "source", "operator_account_id", "operator_account_type", "operator_account_name", "seller_shop_id", "expires_at"},
},
constants.AuditResourceRefund: {
Type: constants.AuditResourceRefund, Name: "退款单",
IdentityFields: []string{"id", "refund_no", "order_id", "order_no", "asset_identifier", "shop_id", "requested_refund_amount", "status"},
IdentityFields: []string{"id", "refund_no", "order_id", "order_no", "order_type", "package_usage_id", "asset_identifier", "shop_id", "requested_refund_amount", "actual_received_amount", "refund_reason", "approved_refund_amount", "approval_instance_id", "status", "commission_deducted", "asset_reset"},
},
constants.AuditResourceEnterprise: {
Type: constants.AuditResourceEnterprise, Name: "企业",
@@ -233,21 +692,81 @@ func NewRegistry() *Registry {
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.AuditResourcePackageSeries: {
Type: constants.AuditResourcePackageSeries, Name: "套餐系列",
IdentityFields: []string{"id", "series_code", "series_name", "status", "enable_one_time_commission"},
},
constants.AuditResourcePackage: {
Type: constants.AuditResourcePackage, Name: "套餐商品",
IdentityFields: []string{"id", "package_code", "package_name", "series_id", "package_type", "duration_months", "duration_days", "price_config_status", "is_gift", "status", "shelf_status"},
},
constants.AuditResourceShopSeriesAllocation: {
Type: constants.AuditResourceShopSeriesAllocation, Name: "店铺套餐系列授权",
IdentityFields: []string{"id", "shop_id", "series_id", "allocator_shop_id", "status"},
},
constants.AuditResourceShopPackageAllocation: {
Type: constants.AuditResourceShopPackageAllocation, Name: "店铺套餐授权",
IdentityFields: []string{"id", "shop_id", "package_id", "allocator_shop_id", "series_allocation_id", "status", "shelf_status", "retail_price_config_status"},
},
constants.AuditResourceShopPackagePriceHistory: {
Type: constants.AuditResourceShopPackagePriceHistory, Name: "店铺套餐价格历史",
IdentityFields: []string{"id", "allocation_id", "changed_by", "effective_from"},
},
constants.AuditResourcePackageConfigBatch: {
Type: constants.AuditResourcePackageConfigBatch, Name: "套餐配置批次",
IdentityFields: []string{"batch_key", "operation", "shop_id", "series_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"},
IdentityFields: []string{"id", "exchange_no", "flow_type", "old_asset_type", "old_asset_id", "old_asset_identifier", "new_asset_type", "new_asset_id", "new_asset_identifier", "shop_id", "status"},
},
constants.AuditResourceAgentRecharge: {
Type: constants.AuditResourceAgentRecharge, Name: "代理充值单",
IdentityFields: []string{"id", "recharge_no", "shop_id", "agent_wallet_id", "approval_instance_id", "status"},
IdentityFields: []string{"id", "recharge_no", "user_id", "shop_id", "agent_wallet_id", "amount", "payment_method", "payment_channel", "payment_transaction_id", "approval_instance_id", "status"},
},
constants.AuditResourceRechargeOrder: {
Type: constants.AuditResourceRechargeOrder, Name: "资产充值单",
IdentityFields: []string{"id", "recharge_order_no", "user_id", "asset_wallet_id", "resource_type", "resource_id", "amount", "status"},
},
constants.AuditResourceAssetWallet: {
Type: constants.AuditResourceAssetWallet, Name: "资产钱包",
IdentityFields: []string{"id", "resource_type", "resource_id", "currency"},
IdentityFields: []string{"id", "resource_type", "resource_id", "currency", "shop_id_tag", "enterprise_id_tag"},
},
constants.AuditResourceAssetWalletTransaction: {
Type: constants.AuditResourceAssetWalletTransaction, Name: "资产钱包流水",
IdentityFields: []string{"id", "asset_wallet_id", "resource_type", "resource_id", "transaction_type", "reference_type", "reference_no", "status"},
},
constants.AuditResourceAgentWallet: {
Type: constants.AuditResourceAgentWallet, Name: "代理主钱包",
IdentityFields: []string{"id", "shop_id", "wallet_type", "currency", "status", "credit_enabled", "credit_limit"},
},
constants.AuditResourceAgentWalletTransaction: {
Type: constants.AuditResourceAgentWalletTransaction, Name: "代理主钱包流水",
IdentityFields: []string{"id", "agent_wallet_id", "shop_id", "transaction_type", "transaction_subtype", "reference_type", "reference_id", "status"},
},
constants.AuditResourceAgentWalletReservation: {
Type: constants.AuditResourceAgentWalletReservation, Name: "代理主钱包预占",
IdentityFields: []string{"id", "agent_wallet_id", "shop_id", "amount", "status", "reference_type", "reference_id"},
},
constants.AuditResourcePayment: {
Type: constants.AuditResourcePayment, Name: "支付记录",
IdentityFields: []string{"id", "payment_no", "order_id", "order_type", "payment_method", "amount", "status", "third_party_trade_no", "payment_config_id"},
},
constants.AuditResourcePackageUsage: {
Type: constants.AuditResourcePackageUsage, Name: "套餐权益",
IdentityFields: []string{"id", "order_id", "order_no", "refund_id", "refund_no", "package_id", "package_name", "usage_type", "iot_card_id", "device_id", "data_limit_mb", "data_usage_mb", "activated_at", "expires_at", "status", "pending_realname_activation", "last_reset_at", "next_reset_at", "generation"},
},
constants.AuditResourceApprovalInstance: {
Type: constants.AuditResourceApprovalInstance, Name: "审批实例",
IdentityFields: []string{"id", "business_type", "business_id", "provider", "external_ref", "status"},
IdentityFields: []string{"id", "business_type", "business_id", "submitter_account_id", "provider", "external_ref", "correlation_id", "status"},
},
constants.AuditResourceCommissionRecord: {
Type: constants.AuditResourceCommissionRecord, Name: "佣金记录",
IdentityFields: []string{"id", "shop_id", "order_id", "iot_card_id", "device_id", "commission_source", "amount", "status", "released_at"},
},
constants.AuditResourceCommissionWithdrawal: {
Type: constants.AuditResourceCommissionWithdrawal, Name: "佣金提现单",
IdentityFields: []string{"id", "withdrawal_no", "shop_id", "applicant_id", "amount", "fee", "fee_rate", "actual_amount", "withdrawal_method", "payment_type", "status", "processor_id", "processed_at", "paid_at"},
},
constants.AuditResourceWeComApplication: {
Type: constants.AuditResourceWeComApplication, Name: "企业微信应用配置",
@@ -277,6 +796,22 @@ func NewRegistry() *Registry {
Type: constants.AuditResourcePersonalCustomerOpenID, Name: "个人客户微信主体",
IdentityFields: []string{"id", "customer_id", "app_id", "open_id", "union_id", "app_type"},
},
constants.AuditResourcePersonalCustomerDevice: {
Type: constants.AuditResourcePersonalCustomerDevice, Name: "个人客户设备号绑定",
IdentityFields: []string{"id", "customer_id", "virtual_no", "bind_at", "last_used_at", "status"},
},
constants.AuditResourcePersonalCustomerICCID: {
Type: constants.AuditResourcePersonalCustomerICCID, Name: "个人客户 ICCID 绑定",
IdentityFields: []string{"id", "customer_id", "iccid", "iccid_19", "bind_at", "last_used_at", "status"},
},
constants.AuditResourceIotCardBatch: {
Type: constants.AuditResourceIotCardBatch, Name: "IoT 卡批量操作",
IdentityFields: []string{"request_id", "card_count"},
},
constants.AuditResourceDeviceBatch: {
Type: constants.AuditResourceDeviceBatch, Name: "设备批量操作",
IdentityFields: []string{"request_id", "correlation_id", "device_count", "operation_type"},
},
},
}
}
@@ -352,6 +887,233 @@ func personalAction(code, name string, subjectFields []string) ActionDefinition
}
}
func customerAssetAdminAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourcePersonalCustomer, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
}
}
func packageConfigAction(code, name, primaryResource, risk string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryConfiguration, Risk: risk,
PrimaryResource: primaryResource, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func packageUsageAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourcePackageUsage, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
AllowedOrigins: []ActionOrigin{
{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI},
{Actor: constants.AuditActorSystemTask, Source: constants.AuditSourceWorker},
{Actor: constants.AuditActorScheduledJob, Source: constants.AuditSourceScheduler},
},
}
}
func orderAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourceOrder, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult, constants.AuditSubjectDetail},
AllowedOrigins: []ActionOrigin{
{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI},
{Actor: constants.AuditActorPersonalCustomer, Source: constants.AuditSourcePersonalAPI},
{Actor: constants.AuditActorOpenAPI, Source: constants.AuditSourceOpenAPI},
{Actor: constants.AuditActorSystemTask, Source: constants.AuditSourceWorker},
{Actor: constants.AuditActorScheduledJob, Source: constants.AuditSourceScheduler},
},
}
}
func agentWalletOrderAction(code, name string) ActionDefinition {
action := orderAction(code, name)
action.Risk = constants.AuditRiskHigh
return action
}
func agentWalletAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceAgentWallet, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
}
}
func paymentAction(code, name string, confirmation bool) ActionDefinition {
origins := []ActionOrigin{
{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI},
{Actor: constants.AuditActorPersonalCustomer, Source: constants.AuditSourcePersonalAPI},
{Actor: constants.AuditActorSystemTask, Source: constants.AuditSourceWorker},
{Actor: constants.AuditActorScheduledJob, Source: constants.AuditSourceScheduler},
}
if confirmation {
origins = append(origins, ActionOrigin{Actor: constants.AuditActorExternalSystem, Source: constants.AuditSourceCallback})
}
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourcePayment, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
AllowedOrigins: origins,
}
}
func rechargeAction(code, name, primaryResource string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskHigh,
PrimaryResource: primaryResource, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult, constants.AuditSubjectDetail},
AllowedOrigins: []ActionOrigin{
{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI},
{Actor: constants.AuditActorPersonalCustomer, Source: constants.AuditSourcePersonalAPI},
{Actor: constants.AuditActorExternalSystem, Source: constants.AuditSourceCallback},
{Actor: constants.AuditActorSystemTask, Source: constants.AuditSourceWorker},
{Actor: constants.AuditActorScheduledJob, Source: constants.AuditSourceScheduler},
},
}
}
func refundAction(code, name string, allowWorker bool) ActionDefinition {
origins := []ActionOrigin{{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI}}
if allowWorker {
origins = append(origins, ActionOrigin{Actor: constants.AuditActorSystemTask, Source: constants.AuditSourceWorker})
}
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceRefund, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
AllowedOrigins: origins,
}
}
func refundSystemAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceRefund, AllowedActor: constants.AuditActorSystemTask,
Source: constants.AuditSourceWorker, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
}
}
func approvalAction(code, name string, origins []ActionOrigin) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourceApprovalInstance, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly}, AllowedOrigins: origins,
}
}
func commissionWithdrawalAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskHigh,
PrimaryResource: constants.AuditResourceCommissionWithdrawal,
AllowedActor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI,
RequireTransaction: true, DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult, constants.AuditSubjectDetail},
SubjectFields: []string{"amount", "fee", "actual_amount", "withdrawal_method", "payment_type", "status"},
}
}
func iotCardAction(code, name, actorKind, source string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourceIotCard, AllowedActor: actorKind, Source: source,
RequireTransaction: true, DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
}
}
func deviceAction(code, name, actorKind, source string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourceDevice, AllowedActor: actorKind, Source: source,
RequireTransaction: true, DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
}
}
func deviceMultiOriginAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourceDevice, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
AllowedOrigins: []ActionOrigin{
{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI},
{Actor: constants.AuditActorSystemTask, Source: constants.AuditSourceWorker},
},
}
}
func deviceExternalAction(code, name string, allowOpenAPI bool) ActionDefinition {
action := deviceAction(code, name, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
action.AllowedOrigins = []ActionOrigin{{Actor: constants.AuditActorPersonalCustomer, Source: constants.AuditSourcePersonalAPI}}
if allowOpenAPI {
action.AllowedOrigins = append(action.AllowedOrigins, ActionOrigin{Actor: constants.AuditActorOpenAPI, Source: constants.AuditSourceOpenAPI})
}
return action
}
func cardExchangeAction(code, name, risk string, personal bool) ActionDefinition {
action := ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: risk,
PrimaryResource: constants.AuditResourceExchangeOrder, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectResult,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult},
}
if personal {
action.AllowedOrigins = []ActionOrigin{{Actor: constants.AuditActorPersonalCustomer, Source: constants.AuditSourcePersonalAPI}}
}
return action
}
func deviceMultiOriginBatchAction(code, name string) ActionDefinition {
action := deviceMultiOriginAction(code, name)
action.PrimaryResource = constants.AuditResourceDeviceBatch
action.DefaultVisibility = constants.AuditSubjectInternalOnly
action.AllowedVisibility = []string{constants.AuditSubjectInternalOnly}
return action
}
func deviceAccountBatchAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourceDeviceBatch, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func iotCardBatchAction(code, name string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskNormal,
PrimaryResource: constants.AuditResourceIotCardBatch, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func accountLifecycleAction(code, name, risk string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryIdentity, Risk: risk,
@@ -372,6 +1134,34 @@ func deviceBatchAction(code, name, primaryResource string) ActionDefinition {
}
}
func taskAction(code, name, primaryResource, actor, source string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskNormal,
PrimaryResource: primaryResource, AllowedActor: actor, Source: source, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func notificationAction(code, name, primaryResource, actor, source string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskLow,
PrimaryResource: primaryResource, AllowedActor: actor, Source: source, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func pollingAction(code, name, primaryResource, risk string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: risk,
PrimaryResource: primaryResource, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, 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,
@@ -382,6 +1172,16 @@ func outboxRecoveryAction(code, name string) ActionDefinition {
}
}
func connectionConfigAction(code, name, resourceType, risk string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryConfiguration, Risk: risk,
PrimaryResource: resourceType, 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 {

View File

@@ -0,0 +1,91 @@
package audit
import (
"context"
"crypto/sha256"
"fmt"
"strconv"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/pkg/constants"
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
)
// TaskInput 描述导入、批购、失效或导出任务的安全审计事实。
type TaskInput struct {
EventID string
ActionCode string
Summary string
TaskID uint
TaskNo string
DisplayName string
Actor ActorInput
Source string
ScopeType string
ScopeID string
ScopeName string
Result string
ErrorCode string
ErrorSummary string
CorrelationID string
ParentEventID string
BatchTotal int
SuccessCount int
FailCount int
IdentitySnapshot map[string]any
BeforeData map[string]any
AfterData map[string]any
Metadata map[string]any
}
// WriteTask 将任务状态与批量统计写入对应的注册任务资源。
func (w *Writer) WriteTask(ctx context.Context, tx *gorm.DB, input TaskInput) error {
if w == nil || w.registry == nil {
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "任务统一审计 Writer 未正确配置")
}
action, ok := w.registry.Action(input.ActionCode)
if !ok {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "任务审计动作未注册")
}
if input.TaskNo == "" {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "任务审计缺少稳定任务编号")
}
var taskID *string
if input.TaskID != 0 {
value := strconv.FormatUint(uint64(input.TaskID), 10)
taskID = &value
}
displayName := input.DisplayName
if displayName == "" {
displayName = input.TaskNo
}
return w.Append(ctx, tx, AppendInput{
EventID: input.EventID, ActionCode: input.ActionCode, Summary: input.Summary,
Actor: input.Actor, Source: input.Source,
ScopeType: input.ScopeType, ScopeID: input.ScopeID, ScopeName: input.ScopeName,
Result: input.Result, ErrorCode: input.ErrorCode, ErrorSummary: input.ErrorSummary,
CorrelationID: input.CorrelationID, ParentEventID: input.ParentEventID,
BatchTotal: input.BatchTotal, SuccessCount: input.SuccessCount, FailCount: input.FailCount,
Metadata: input.Metadata,
Resources: []ResourceInput{{
Type: action.PrimaryResource, ID: taskID, Key: input.TaskNo, DisplayName: displayName,
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleBatchTask,
IdentitySnapshot: input.IdentitySnapshot, BeforeData: input.BeforeData, AfterData: input.AfterData,
SubjectVisibility: constants.AuditSubjectInternalOnly,
}},
})
}
// TaskEventID 返回可重试任务阶段的稳定审计事件 ID。
func TaskEventID(resourceType string, taskID uint, phase string) string {
if taskID == 0 || phase == "" {
return ""
}
value := fmt.Sprintf("task:%s:%d:%s", resourceType, taskID, phase)
if len(value) <= 64 {
return value
}
digest := sha256.Sum256([]byte(value))
return fmt.Sprintf("task:%x", digest[:16])
}

View File

@@ -0,0 +1,293 @@
package audit
import (
"context"
"strconv"
"strings"
"gorm.io/gorm"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// WriteAgentWalletBalanceAdjustment 将人工余额调整写入统一 Audit Event。
func (w *Writer) WriteAgentWalletBalanceAdjustment(ctx context.Context, tx *gorm.DB, event walletapp.CreditedEvent) error {
if event.WalletID == 0 || event.ReferenceType != constants.ReferenceTypeManualAdjustment ||
event.ReferenceID == 0 || event.TransactionType != constants.AgentTransactionTypeAdjustment ||
event.Amount <= 0 || strings.TrimSpace(event.Remark) == "" {
return errors.New(errors.CodeInvalidParam, "代理主钱包人工调整审计事实不完整")
}
var wallet model.AgentWallet
if err := tx.WithContext(ctx).Unscoped().First(&wallet, event.WalletID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询人工调整代理主钱包审计快照失败")
}
var transaction model.AgentWalletTransaction
if err := tx.WithContext(ctx).Unscoped().Where(
"agent_wallet_id = ? AND reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
event.WalletID, event.ReferenceType, event.ReferenceID, event.TransactionType, constants.TransactionStatusSuccess,
).First(&transaction).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包人工调整流水失败")
}
walletResource := agentWalletAuditResource(&wallet, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleWalletTarget)
walletResource.BeforeData = map[string]any{"balance": transaction.BalanceBefore, "frozen_balance": wallet.FrozenBalance}
walletResource.AfterData = map[string]any{"balance": transaction.BalanceAfter, "frozen_balance": wallet.FrozenBalance}
walletResource.SubjectVisibility = constants.AuditSubjectResult
walletResource.SubjectSummary = "代理主钱包余额已人工调整"
transactionResource := agentWalletTransactionResource(&transaction)
transactionResource.Role = constants.AuditResourceRoleWalletTransaction
return w.Append(ctx, tx, AppendInput{
ActionCode: constants.AuditActionAgentWalletBalanceAdjusted, Summary: "人工调整代理主钱包余额",
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
CorrelationID: event.CorrelationID,
Metadata: map[string]any{
"amount": event.Amount, "reason": strings.TrimSpace(event.Remark),
"reference_type": event.ReferenceType, "reference_id": event.ReferenceID,
},
Resources: []ResourceInput{walletResource, transactionResource},
})
}
// WriteAgentWalletCreditChange 将实际信用额度变化写入统一 Audit Event。
func (w *Writer) WriteAgentWalletCreditChange(ctx context.Context, tx *gorm.DB, change walletapp.CreditChangeAudit) error {
if change.Wallet == nil || change.Wallet.ID == 0 || change.Wallet.ShopID == 0 || change.Wallet.WalletType != constants.AgentWalletTypeMain {
return errors.New(errors.CodeInvalidParam, "代理主钱包信用额度审计事实不完整")
}
walletResource := agentWalletAuditResource(change.Wallet, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleWalletTarget)
walletResource.BeforeData = change.BeforeData
walletResource.AfterData = change.AfterData
walletResource.SubjectVisibility = constants.AuditSubjectResult
var shop model.Shop
if err := tx.WithContext(ctx).Unscoped().First(&shop, change.Wallet.ShopID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询信用额度关联店铺审计快照失败")
}
shopResource := ShopResource(&shop, constants.AuditResourceRelationReference, constants.AuditResourceRoleWalletShop)
result := change.Result
if result == "" {
result = constants.AuditResultSuccess
}
walletResource.SubjectSummary = "代理主钱包信用额度已更新"
if result != constants.AuditResultSuccess {
walletResource.SubjectSummary = "代理主钱包信用额度更新未完成"
}
return w.Append(ctx, tx, AppendInput{
ActionCode: constants.AuditActionAgentWalletCreditChanged, Summary: "调整代理主钱包信用额度",
ScopeType: constants.AuditScopePlatform, Result: result,
ErrorCode: change.ErrorCode, ErrorSummary: change.ErrorSummary,
Resources: []ResourceInput{walletResource, shopResource},
})
}
// WriteAgentWalletDebit 将代理主钱包订单扣款写入统一 Audit Event。
func (w *Writer) WriteAgentWalletDebit(ctx context.Context, tx *gorm.DB, event walletapp.DebitedEvent) error {
if event.WalletID == 0 || event.ReferenceType != constants.ReferenceTypeOrder || event.ReferenceID == 0 || event.Amount <= 0 {
return errors.New(errors.CodeInvalidParam, "代理主钱包订单扣款审计事实不完整")
}
completed, err := completedReservationExists(ctx, tx, event.ReferenceID)
if err != nil || completed {
return err
}
order, wallet, transaction, err := loadAgentWalletDebitFacts(ctx, tx, event)
if err != nil {
return err
}
resources := []ResourceInput{
walletOrderResource(order, "代理主钱包订单扣款"),
agentWalletResource(wallet, transaction.BalanceBefore, wallet.FrozenBalance, transaction.BalanceAfter, wallet.FrozenBalance),
agentWalletTransactionResource(transaction),
}
return w.Append(ctx, tx, AppendInput{
ActionCode: constants.AuditActionAgentWalletOrderDebited, Summary: "代理主钱包完成订单扣款",
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
CorrelationID: event.CorrelationID, Metadata: map[string]any{"amount": event.Amount}, Resources: resources,
})
}
// WriteAgentWalletReservation 将代理主钱包订单预占状态变化写入统一 Audit Event。
func (w *Writer) WriteAgentWalletReservation(ctx context.Context, tx *gorm.DB, event walletapp.ReservationEvent) error {
actionCode, summary, err := reservationAuditAction(event.Status)
if err != nil {
return err
}
if event.ReservationID == 0 || event.WalletID == 0 || event.ReferenceType != constants.ReferenceTypeOrder || event.ReferenceID == 0 || event.Amount <= 0 {
return errors.New(errors.CodeInvalidParam, "代理主钱包订单预占审计事实不完整")
}
order, wallet, reservation, transaction, err := loadAgentWalletReservationFacts(ctx, tx, event)
if err != nil {
return err
}
beforeBalance, beforeFrozen, afterBalance, afterFrozen := reservationWalletState(wallet, transaction, event)
resources := []ResourceInput{
walletOrderResource(order, summary),
agentWalletResource(wallet, beforeBalance, beforeFrozen, afterBalance, afterFrozen),
agentWalletReservationResource(reservation, event.Status),
}
if transaction != nil {
resources = append(resources, agentWalletTransactionResource(transaction))
}
return w.Append(ctx, tx, AppendInput{
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
Result: constants.AuditResultSuccess, CorrelationID: event.CorrelationID,
Metadata: map[string]any{"amount": event.Amount}, Resources: resources,
})
}
func completedReservationExists(ctx context.Context, tx *gorm.DB, orderID uint) (bool, error) {
var count int64
err := tx.WithContext(ctx).Model(&model.AgentWalletReservation{}).
Where("reference_type = ? AND reference_id = ? AND status = ?", constants.ReferenceTypeOrder, orderID, constants.AgentWalletReservationStatusCompleted).
Count(&count).Error
if err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询订单钱包预占终态失败")
}
return count > 0, nil
}
func loadAgentWalletDebitFacts(ctx context.Context, tx *gorm.DB, event walletapp.DebitedEvent) (*model.Order, *model.AgentWallet, *model.AgentWalletTransaction, error) {
var order model.Order
if err := tx.WithContext(ctx).Unscoped().First(&order, event.ReferenceID).Error; err != nil {
return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询钱包扣款订单审计快照失败")
}
var wallet model.AgentWallet
if err := tx.WithContext(ctx).Unscoped().First(&wallet, event.WalletID).Error; err != nil {
return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包审计快照失败")
}
transaction, err := loadOrderDebitTransaction(ctx, tx, event.WalletID, event.ReferenceID)
if err != nil {
return nil, nil, nil, err
}
return &order, &wallet, transaction, nil
}
func loadAgentWalletReservationFacts(ctx context.Context, tx *gorm.DB, event walletapp.ReservationEvent) (*model.Order, *model.AgentWallet, *model.AgentWalletReservation, *model.AgentWalletTransaction, error) {
var order model.Order
if err := tx.WithContext(ctx).Unscoped().First(&order, event.ReferenceID).Error; err != nil {
return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询钱包预占订单审计快照失败")
}
var wallet model.AgentWallet
if err := tx.WithContext(ctx).Unscoped().First(&wallet, event.WalletID).Error; err != nil {
return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包审计快照失败")
}
var reservation model.AgentWalletReservation
if err := tx.WithContext(ctx).First(&reservation, event.ReservationID).Error; err != nil {
return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包预占审计快照失败")
}
var transaction *model.AgentWalletTransaction
if event.Status == constants.AgentWalletReservationStatusCompleted {
loaded, err := loadOrderDebitTransaction(ctx, tx, event.WalletID, event.ReferenceID)
if err != nil {
return nil, nil, nil, nil, err
}
transaction = loaded
}
return &order, &wallet, &reservation, transaction, nil
}
func loadOrderDebitTransaction(ctx context.Context, tx *gorm.DB, walletID, orderID uint) (*model.AgentWalletTransaction, error) {
var transaction model.AgentWalletTransaction
err := tx.WithContext(ctx).Unscoped().Where(
"agent_wallet_id = ? AND reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
walletID, constants.ReferenceTypeOrder, orderID, constants.AgentTransactionTypeDeduct, constants.TransactionStatusSuccess,
).First(&transaction).Error
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包订单扣款流水失败")
}
return &transaction, nil
}
func walletOrderResource(order *model.Order, summary string) ResourceInput {
resource := OrderResource(order, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleOrderTarget)
resource.SubjectVisibility = constants.AuditSubjectResult
resource.SubjectSummary = summary
return resource
}
func agentWalletResource(wallet *model.AgentWallet, beforeBalance, beforeFrozen, afterBalance, afterFrozen int64) ResourceInput {
id := strconv.FormatUint(uint64(wallet.ID), 10)
return ResourceInput{
Type: constants.AuditResourceAgentWallet, ID: &id, Key: id, DisplayName: "代理主钱包 " + id,
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleOrderWallet,
IdentitySnapshot: map[string]any{
"id": wallet.ID, "shop_id": wallet.ShopID, "wallet_type": wallet.WalletType,
"currency": wallet.Currency, "status": wallet.Status,
},
BeforeData: map[string]any{"balance": beforeBalance, "frozen_balance": beforeFrozen},
AfterData: map[string]any{"balance": afterBalance, "frozen_balance": afterFrozen},
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "代理主钱包订单资金已更新",
}
}
func agentWalletAuditResource(wallet *model.AgentWallet, relation, role string) ResourceInput {
id := strconv.FormatUint(uint64(wallet.ID), 10)
return ResourceInput{
Type: constants.AuditResourceAgentWallet, ID: &id, Key: id, DisplayName: "代理主钱包 " + id,
Relation: relation, Role: role,
IdentitySnapshot: map[string]any{
"id": wallet.ID, "shop_id": wallet.ShopID, "wallet_type": wallet.WalletType,
"currency": wallet.Currency, "status": wallet.Status,
"credit_enabled": wallet.CreditEnabled, "credit_limit": wallet.CreditLimit,
},
}
}
func agentWalletReservationResource(reservation *model.AgentWalletReservation, status int) ResourceInput {
id := strconv.FormatUint(uint64(reservation.ID), 10)
return ResourceInput{
Type: constants.AuditResourceAgentWalletReservation, ID: &id, Key: reservation.ReferenceType + ":" + strconv.FormatUint(uint64(reservation.ReferenceID), 10),
DisplayName: "订单钱包预占 " + id, Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleOrderWalletReservation,
IdentitySnapshot: map[string]any{
"id": reservation.ID, "agent_wallet_id": reservation.AgentWalletID, "shop_id": reservation.ShopID,
"amount": reservation.Amount, "status": status, "reference_type": reservation.ReferenceType, "reference_id": reservation.ReferenceID,
},
BeforeData: map[string]any{"status": reservationStatusBefore(status)}, AfterData: map[string]any{"status": status},
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "订单钱包预占状态已更新",
}
}
func agentWalletTransactionResource(transaction *model.AgentWalletTransaction) ResourceInput {
id := strconv.FormatUint(uint64(transaction.ID), 10)
return ResourceInput{
Type: constants.AuditResourceAgentWalletTransaction, ID: &id, Key: id, DisplayName: "代理钱包流水 " + id,
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleOrderWalletTransaction,
IdentitySnapshot: map[string]any{
"id": transaction.ID, "agent_wallet_id": transaction.AgentWalletID, "shop_id": transaction.ShopID,
"transaction_type": transaction.TransactionType, "transaction_subtype": transaction.TransactionSubtype,
"reference_type": transaction.ReferenceType, "reference_id": transaction.ReferenceID, "status": transaction.Status,
},
AfterData: map[string]any{"amount": transaction.Amount, "balance_before": transaction.BalanceBefore, "balance_after": transaction.BalanceAfter},
}
}
func reservationAuditAction(status int) (string, string, error) {
switch status {
case constants.AgentWalletReservationStatusFrozen:
return constants.AuditActionAgentWalletOrderReserved, "代理主钱包已预占订单资金", nil
case constants.AgentWalletReservationStatusReleased:
return constants.AuditActionAgentWalletOrderReleased, "代理主钱包已释放订单预占", nil
case constants.AgentWalletReservationStatusCompleted:
return constants.AuditActionAgentWalletOrderCompleted, "代理主钱包已完成订单预占扣款", nil
default:
return "", "", errors.New(errors.CodeInvalidParam, "代理主钱包预占状态不受支持")
}
}
func reservationWalletState(wallet *model.AgentWallet, transaction *model.AgentWalletTransaction, event walletapp.ReservationEvent) (int64, int64, int64, int64) {
afterBalance, afterFrozen := wallet.Balance, wallet.FrozenBalance
switch event.Status {
case constants.AgentWalletReservationStatusFrozen:
return afterBalance, afterFrozen - event.Amount, afterBalance, afterFrozen
case constants.AgentWalletReservationStatusReleased:
return afterBalance, afterFrozen + event.Amount, afterBalance, afterFrozen
default:
return transaction.BalanceBefore, afterFrozen + event.Amount, transaction.BalanceAfter, afterFrozen
}
}
func reservationStatusBefore(status int) any {
if status == constants.AgentWalletReservationStatusFrozen {
return nil
}
return constants.AgentWalletReservationStatusFrozen
}

View File

@@ -167,7 +167,7 @@ func (w *Writer) WriteAccessChange(ctx context.Context, tx *gorm.DB, change acce
}
func accessResources(change accessauditapp.ChangeAudit, primaryResource string) ([]ResourceInput, error) {
resources := make([]ResourceInput, 0, 2+len(change.Accounts)+len(change.Cards)+len(change.CardAuthorizations)+len(change.Devices)+len(change.DeviceBindings)+len(change.DeviceAuthorizations)+len(change.PersonalPhones)+len(change.PersonalOpenIDs)+len(change.Roles)+len(change.Permissions))
resources := make([]ResourceInput, 0, 2+len(change.Accounts)+len(change.Cards)+len(change.CardAuthorizations)+len(change.Devices)+len(change.DeviceBindings)+len(change.DeviceAuthorizations)+len(change.PersonalPhones)+len(change.PersonalOpenIDs)+len(change.PersonalDevices)+len(change.PersonalICCIDs)+len(change.Roles)+len(change.Permissions))
switch primaryResource {
case constants.AuditResourceAccount:
if change.Account == nil || (change.Account.ID == 0 && change.Account.Username == "") {
@@ -392,6 +392,46 @@ func accessResources(change accessauditapp.ChangeAudit, primaryResource string)
SubjectVisibility: constants.AuditSubjectInternalOnly, SortOrder: index + 1,
})
}
for index, item := range change.PersonalDevices {
if item.Binding == nil || item.Binding.ID == 0 {
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "个人客户设备绑定审计资源不完整")
}
relation := item.Relation
if relation == "" {
relation = constants.AuditResourceRelationAffected
}
role := item.Role
if role == "" {
role = constants.AuditResourceRolePersonalCustomerAssetBinding
}
resources = append(resources, ResourceInput{
Type: constants.AuditResourcePersonalCustomerDevice, ID: optionalResourceID(item.Binding.ID),
Key: strconv.FormatUint(uint64(item.Binding.ID), 10), DisplayName: item.Binding.VirtualNo,
Relation: relation, Role: role, IdentitySnapshot: personalCustomerDeviceIdentity(item.Binding),
BeforeData: item.BeforeData, AfterData: item.AfterData,
SubjectVisibility: constants.AuditSubjectInternalOnly, SortOrder: index + 1,
})
}
for index, item := range change.PersonalICCIDs {
if item.Binding == nil || item.Binding.ID == 0 {
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "个人客户 ICCID 绑定审计资源不完整")
}
relation := item.Relation
if relation == "" {
relation = constants.AuditResourceRelationAffected
}
role := item.Role
if role == "" {
role = constants.AuditResourceRolePersonalCustomerAssetBinding
}
resources = append(resources, ResourceInput{
Type: constants.AuditResourcePersonalCustomerICCID, ID: optionalResourceID(item.Binding.ID),
Key: strconv.FormatUint(uint64(item.Binding.ID), 10), DisplayName: item.Binding.ICCID,
Relation: relation, Role: role, IdentitySnapshot: personalCustomerICCIDIdentity(item.Binding),
BeforeData: item.BeforeData, AfterData: item.AfterData,
SubjectVisibility: constants.AuditSubjectInternalOnly, SortOrder: index + 1,
})
}
if primaryResource == constants.AuditResourceRole {
if change.Role == nil || (change.Role.ID == 0 && change.Role.RoleName == "") {
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "角色审计资源不完整")
@@ -482,6 +522,16 @@ func iotCardIdentity(card *model.IotCard) map[string]any {
}
}
// IotCardIdentitySnapshot 返回统一 Registry 允许的 IoT 卡身份快照。
func IotCardIdentitySnapshot(card *model.IotCard) map[string]any {
return iotCardIdentity(card)
}
// IotCardResourceKey 返回 IoT 卡审计使用的稳定资源 Key。
func IotCardResourceKey(card *model.IotCard) string {
return iotCardResourceKey(card)
}
func deviceResourceKey(device *model.Device) string {
if device.ID != 0 {
return strconv.FormatUint(uint64(device.ID), 10)
@@ -492,10 +542,22 @@ func deviceResourceKey(device *model.Device) string {
func deviceIdentity(device *model.Device) map[string]any {
return map[string]any{
"id": device.ID, "virtual_no": device.VirtualNo, "imei": device.IMEI,
"sn": device.SN, "generation": device.Generation,
"sn": device.SN, "device_name": device.DeviceName, "device_model": device.DeviceModel,
"device_type": device.DeviceType, "manufacturer": device.Manufacturer,
"shop_id": device.ShopID, "series_id": device.SeriesID, "generation": device.Generation,
}
}
// DeviceIdentitySnapshot 返回统一 Registry 允许的设备身份快照。
func DeviceIdentitySnapshot(device *model.Device) map[string]any {
return deviceIdentity(device)
}
// DeviceResourceKey 返回设备审计使用的稳定资源 Key。
func DeviceResourceKey(device *model.Device) string {
return deviceResourceKey(device)
}
func deviceSimBindingIdentity(binding *model.DeviceSimBinding) map[string]any {
return map[string]any{
"id": binding.ID, "device_id": binding.DeviceID, "slot_position": binding.SlotPosition,
@@ -541,6 +603,21 @@ func personalCustomerOpenIDIdentity(openID *model.PersonalCustomerOpenID) map[st
}
}
func personalCustomerDeviceIdentity(binding *model.PersonalCustomerDevice) map[string]any {
return map[string]any{
"id": binding.ID, "customer_id": binding.CustomerID, "virtual_no": binding.VirtualNo,
"bind_at": binding.BindAt, "last_used_at": binding.LastUsedAt, "status": binding.Status,
}
}
func personalCustomerICCIDIdentity(binding *model.PersonalCustomerICCID) map[string]any {
return map[string]any{
"id": binding.ID, "customer_id": binding.CustomerID, "iccid": binding.ICCID,
"iccid_19": binding.ICCID19, "bind_at": binding.BindAt,
"last_used_at": binding.LastUsedAt, "status": binding.Status,
}
}
func roleResource(role *model.Role, beforeData, afterData map[string]any) ResourceInput {
return ResourceInput{
Type: constants.AuditResourceRole, ID: optionalResourceID(role.ID), Key: roleResourceKey(role), DisplayName: role.RoleName,
@@ -706,6 +783,14 @@ func (w *Writer) WriteConfigChange(ctx context.Context, tx *gorm.DB, change syst
if result == "" {
result = constants.AuditResultSuccess
}
displayName := change.DisplayName
if displayName == "" {
displayName = change.ConfigKey
}
identity := change.Identity
if identity == nil {
identity = map[string]any{"config_key": change.ConfigKey, "module": change.Module}
}
return w.Append(ctx, tx, AppendInput{
ActionCode: action.Code, Summary: change.Description,
Actor: ActorInput{
@@ -719,9 +804,9 @@ func (w *Writer) WriteConfigChange(ctx context.Context, tx *gorm.DB, change syst
ErrorCode: change.ErrorCode, ErrorSummary: change.ErrorSummary,
RequestID: change.RequestID, CorrelationID: change.CorrelationID,
Resources: []ResourceInput{{
Type: action.PrimaryResource, Key: change.ConfigKey, DisplayName: change.ConfigKey,
Type: action.PrimaryResource, ID: change.ResourceID, Key: change.ConfigKey, DisplayName: displayName,
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleConfig,
IdentitySnapshot: map[string]any{"config_key": change.ConfigKey, "module": change.Module},
IdentitySnapshot: identity,
BeforeData: change.BeforeData, AfterData: change.AfterData,
SubjectVisibility: action.DefaultVisibility,
}},
@@ -795,7 +880,7 @@ func (w *Writer) Append(ctx context.Context, tx *gorm.DB, input AppendInput) err
if !ok {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "审计动作未注册")
}
if input.Actor.Kind != action.AllowedActor || input.Actor.ID == "" || input.Source != action.Source {
if !actionAllowsOrigin(action, input.Actor.Kind, input.Source) || input.Actor.ID == "" {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "审计操作者或入口不符合动作注册规则")
}
if !validResult(input.Result) || len(input.Resources) == 0 {
@@ -853,6 +938,18 @@ func (w *Writer) Append(ctx context.Context, tx *gorm.DB, input AppendInput) err
return nil
}
func actionAllowsOrigin(action ActionDefinition, actor, source string) bool {
if action.AllowedActor == actor && action.Source == source {
return true
}
for _, origin := range action.AllowedOrigins {
if origin.Actor == actor && origin.Source == source {
return true
}
}
return false
}
func fillFromContext(ctx context.Context, input AppendInput) AppendInput {
value := auditcontext.From(ctx)
if input.Actor.Kind == "" {

View File

@@ -15,6 +15,7 @@ import (
cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
@@ -180,7 +181,7 @@ func (c *RealnameChangedConsumer) Consume(ctx context.Context, envelope outbox.D
if c == nil || c.db == nil {
return errors.New(errors.CodeInternalError, "卡实名事件消费者未配置")
}
ctx = cardapp.SuppressSeriesTriggerContext(ctx)
ctx = cardapp.SuppressSeriesTriggerContext(auditcontext.With(ctx, auditcontext.Context{ParentEventID: envelope.EventID}))
if envelope.EventType != constants.OutboxEventTypeCardRealnameChanged || envelope.PayloadVersion != constants.CardRealnameChangedPayloadVersionV1 {
return errors.New(errors.CodeInvalidParam, "卡实名事件类型或版本不受支持")
}
@@ -237,7 +238,7 @@ func (c *TrafficIncrementedConsumer) Consume(ctx context.Context, envelope outbo
if c == nil || c.db == nil || c.redis == nil || c.deductor == nil {
return errors.New(errors.CodeInternalError, "卡流量事件消费者未配置")
}
ctx = cardapp.SuppressSeriesTriggerContext(ctx)
ctx = cardapp.SuppressSeriesTriggerContext(auditcontext.With(ctx, auditcontext.Context{ParentEventID: envelope.EventID}))
if envelope.EventType != constants.OutboxEventTypeCardTrafficIncremented || envelope.PayloadVersion != constants.CardTrafficIncrementedPayloadVersionV1 {
return errors.New(errors.CodeInvalidParam, "卡流量事件类型或版本不受支持")
}

View File

@@ -2,28 +2,39 @@ package notification
import (
"context"
"fmt"
"strconv"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// CleanupService 按通知类别的保留期限分批删除过期通知事实。
type CleanupService struct {
db *gorm.DB
logger *zap.Logger
now func() time.Time
db *gorm.DB
logger *zap.Logger
auditWriter *audit.Writer
now func() time.Time
}
// NewCleanupService 创建通知保留清理服务。
func NewCleanupService(db *gorm.DB, logger *zap.Logger) *CleanupService {
func NewCleanupService(db *gorm.DB, logger *zap.Logger, auditWriters ...*audit.Writer) *CleanupService {
if logger == nil {
logger = zap.NewNop()
}
return &CleanupService{db: db, logger: logger, now: time.Now}
service := &CleanupService{db: db, logger: logger, now: time.Now}
if len(auditWriters) > 0 {
service.auditWriter = auditWriters[0]
}
return service
}
// Run 按类别、创建时间和稳定主键执行有界分批清理。
@@ -49,24 +60,74 @@ func (s *CleanupService) Run(ctx context.Context) error {
}
func (s *CleanupService) cleanupCategory(ctx context.Context, category string, cutoff time.Time) (int64, error) {
if s.auditWriter == nil {
return 0, errors.New(errors.CodeInvalidStatus, "通知清理统一审计接缝未配置")
}
var total int64
for batch := 0; batch < constants.NotificationCleanupMaxBatches; batch++ {
result := s.db.WithContext(ctx).Exec(`WITH candidates AS (
SELECT id FROM tb_notification
WHERE category = ? AND created_at < ?
ORDER BY created_at ASC, id ASC
LIMIT ?
)
DELETE FROM tb_notification AS notification
USING candidates
WHERE notification.id = candidates.id`, category, cutoff, constants.NotificationCleanupBatchSize)
if result.Error != nil {
return total, errors.Wrap(errors.CodeDatabaseError, result.Error, "清理站内通知失败")
var deleted []*model.Notification
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
Where("category = ? AND created_at < ?", category, cutoff).
Order("created_at ASC, id ASC").Limit(constants.NotificationCleanupBatchSize).Find(&deleted).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询待清理站内通知失败")
}
if len(deleted) == 0 {
return nil
}
ids := make([]uint, 0, len(deleted))
for _, notification := range deleted {
ids = append(ids, notification.ID)
}
result := tx.WithContext(ctx).Where("id IN ?", ids).Delete(&model.Notification{})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "清理站内通知失败")
}
if result.RowsAffected != int64(len(deleted)) {
return errors.New(errors.CodeInvalidStatus, "通知清理数量发生并发变化")
}
return s.appendCleanupAudit(ctx, tx, category, cutoff, deleted)
}); err != nil {
return total, err
}
total += result.RowsAffected
if result.RowsAffected < constants.NotificationCleanupBatchSize {
total += int64(len(deleted))
if len(deleted) < constants.NotificationCleanupBatchSize {
break
}
}
return total, nil
}
func (s *CleanupService) appendCleanupAudit(ctx context.Context, tx *gorm.DB, category string, cutoff time.Time, notifications []*model.Notification) error {
firstID, lastID := notifications[0].ID, notifications[len(notifications)-1].ID
key := fmt.Sprintf("%s:%s:%d:%d", category, cutoff.UTC().Format(time.RFC3339), firstID, lastID)
rootID := "evt_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte("notification-cleanup:"+key)).String()
children := make([]audit.AppendInput, 0, len(notifications))
for _, notification := range notifications {
children = append(children, audit.AppendInput{
EventID: audit.TaskEventID(constants.AuditResourceNotification, notification.ID, "cleanup"),
ActionCode: constants.AuditActionNotificationCleanupItem, Summary: "清理单条过期通知",
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
Resources: []audit.ResourceInput{audit.NotificationResource(notification,
constants.AuditResourceRelationPrimary, constants.AuditResourceRoleNotificationTarget,
map[string]any{"exists": true}, map[string]any{"deleted": true})},
})
}
return s.auditWriter.AppendBatch(ctx, tx, audit.BatchInput{
Root: audit.AppendInput{
EventID: rootID, ActionCode: constants.AuditActionNotificationCleanup, Summary: "清理过期通知",
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
BatchTotal: len(notifications), SuccessCount: len(notifications),
Resources: []audit.ResourceInput{{
Type: constants.AuditResourceNotificationCleanupBatch, Key: rootID, DisplayName: "通知清理批次",
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleBatchTask,
IdentitySnapshot: map[string]any{
"category": category, "cutoff": cutoff.UTC(), "deleted_count": len(notifications),
"first_id": firstID, "last_id": lastID,
}, SubjectVisibility: constants.AuditSubjectInternalOnly,
}},
Metadata: map[string]any{"first_id": strconv.FormatUint(uint64(firstID), 10), "last_id": strconv.FormatUint(uint64(lastID), 10)},
},
Children: children,
})
}

View File

@@ -20,6 +20,12 @@ func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
// DB 返回通知 Repository 使用的数据库连接。
func (r *Repository) DB() *gorm.DB { return r.db }
// WithTx 返回绑定指定事务的通知 Repository。
func (r *Repository) WithTx(tx *gorm.DB) *Repository { return &Repository{db: tx} }
// CreateIdempotent 以事件、接收人类型和接收人 ID 唯一键幂等写入通知。
func (r *Repository) CreateIdempotent(ctx context.Context, notification *model.Notification) (bool, error) {
result := r.db.WithContext(ctx).Clauses(clause.OnConflict{

View File

@@ -20,16 +20,17 @@ import (
type AgentRechargePaymentConsumer struct {
db *gorm.DB
posting *walletapp.PostingService
audit agentrecharge.RechargeAuditWriter
}
// NewAgentRechargePaymentConsumer 创建代理在线充值入账消费者。
func NewAgentRechargePaymentConsumer(db *gorm.DB, posting *walletapp.PostingService) *AgentRechargePaymentConsumer {
return &AgentRechargePaymentConsumer{db: db, posting: posting}
func NewAgentRechargePaymentConsumer(db *gorm.DB, posting *walletapp.PostingService, audit agentrecharge.RechargeAuditWriter) *AgentRechargePaymentConsumer {
return &AgentRechargePaymentConsumer{db: db, posting: posting, audit: audit}
}
// Consume 校验支付与充值权威事实后,在独立事务中完成唯一入账和充值终态。
func (c *AgentRechargePaymentConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
if c == nil || c.db == nil || c.posting == nil {
if c == nil || c.db == nil || c.posting == nil || c.audit == nil {
return errors.New(errors.CodeInternalError, "代理在线充值入账消费者未配置")
}
if envelope.EventType != constants.OutboxEventTypeAgentRechargePaymentConfirmed ||
@@ -45,23 +46,24 @@ func (c *AgentRechargePaymentConsumer) Consume(ctx context.Context, envelope out
return errors.New(errors.CodeInvalidParam, "代理充值支付确认事件载荷不完整")
}
return c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
recharge, _, err := lockCreditingFacts(ctx, tx, event)
recharge, payment, err := lockCreditingFacts(ctx, tx, event)
if err != nil {
return err
}
if recharge.Status != constants.RechargeStatusPaid && recharge.Status != constants.RechargeStatusCompleted {
return errors.New(errors.CodeInvalidStatus, "代理在线充值当前状态不可入账")
}
if _, err := c.posting.PostInTx(ctx, tx, walletapp.PostingCommand{
posting, err := c.posting.PostInTx(ctx, tx, walletapp.PostingCommand{
ShopID: recharge.ShopID, WalletID: recharge.AgentWalletID, Amount: recharge.Amount,
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: recharge.ID,
TransactionType: constants.AgentTransactionTypeRecharge,
UserID: recharge.UserID, Creator: recharge.UserID, Remark: "代理在线扫码充值",
RequestID: envelope.RequestID, CorrelationID: envelope.CorrelationID,
}); err != nil {
})
if err != nil {
return err
}
if recharge.Status == constants.RechargeStatusCompleted {
if posting.AlreadyApplied && recharge.Status == constants.RechargeStatusCompleted {
return nil
}
completedAt := time.Now().UTC()
@@ -74,7 +76,25 @@ func (c *AgentRechargePaymentConsumer) Consume(ctx context.Context, envelope out
if update.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "代理在线充值状态已变化")
}
return nil
var wallet model.AgentWallet
if err := tx.WithContext(ctx).First(&wallet, recharge.AgentWalletID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值钱包审计快照失败")
}
var transaction model.AgentWalletTransaction
if err := tx.WithContext(ctx).Where("reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
constants.ReferenceTypeTopup, recharge.ID, constants.AgentTransactionTypeRecharge, constants.TransactionStatusSuccess).
First(&transaction).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值入账流水审计快照失败")
}
after := *recharge
after.Status = constants.RechargeStatusCompleted
after.CompletedAt = &completedAt
return c.audit.WriteAgentRecharge(ctx, tx, agentrecharge.RechargeAudit{
ActionCode: constants.AuditActionAgentRechargeCredited, Summary: "代理在线充值已入账",
Record: &after, Payment: payment, Wallet: &wallet, Transaction: &transaction,
BeforeData: map[string]any{"status": recharge.Status},
AfterData: map[string]any{"status": after.Status, "completed_at": completedAt},
})
})
}

View File

@@ -14,19 +14,25 @@ import (
"gorm.io/gorm"
)
// CreditEventWriter 将代理主钱包正向入账事实写入公共 Outbox
// BalanceAdjustmentAuditWriter 在人工调整事务内追加统一 Audit Event
type BalanceAdjustmentAuditWriter interface {
WriteAgentWalletBalanceAdjustment(context.Context, *gorm.DB, walletapp.CreditedEvent) error
}
// CreditEventWriter 将代理主钱包正向入账事实写入公共 Outbox并审计人工调整。
type CreditEventWriter struct {
outbox *outbox.Repository
audit BalanceAdjustmentAuditWriter
}
// NewCreditEventWriter 创建代理主钱包入账 Outbox Writer。
func NewCreditEventWriter(repository *outbox.Repository) *CreditEventWriter {
return &CreditEventWriter{outbox: repository}
func NewCreditEventWriter(repository *outbox.Repository, auditWriter BalanceAdjustmentAuditWriter) *CreditEventWriter {
return &CreditEventWriter{outbox: repository, audit: auditWriter}
}
// Append 在调用方业务事务中追加代理主钱包入账事件。
// Append 在调用方业务事务中追加代理主钱包入账事件及必要审计
func (w *CreditEventWriter) Append(ctx context.Context, tx *gorm.DB, event walletapp.CreditedEvent) error {
if w == nil || w.outbox == nil {
if w == nil || w.outbox == nil || w.audit == nil {
return errors.New(errors.CodeInternalError, "代理主钱包入账 Outbox Writer 未配置")
}
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
@@ -36,9 +42,15 @@ func (w *CreditEventWriter) Append(ctx context.Context, tx *gorm.DB, event walle
ResourceType: event.ReferenceType, ResourceID: strconv.FormatUint(uint64(event.ReferenceID), 10),
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event,
})
if err != nil || event.ReferenceType != constants.ReferenceTypeTopup || event.TransactionType != constants.AgentTransactionTypeRecharge {
if err != nil {
return err
}
if event.ReferenceType == constants.ReferenceTypeManualAdjustment && event.TransactionType == constants.AgentTransactionTypeAdjustment {
return w.audit.WriteAgentWalletBalanceAdjustment(ctx, tx, event)
}
if event.ReferenceType != constants.ReferenceTypeTopup || event.TransactionType != constants.AgentTransactionTypeRecharge {
return nil
}
rechargeID := strconv.FormatUint(uint64(event.ReferenceID), 10)
var shop model.Shop
if err := tx.WithContext(ctx).Select("shop_name").Where("id = ?", event.ShopID).Take(&shop).Error; err != nil {

View File

@@ -6,26 +6,28 @@ import (
"strconv"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"gorm.io/gorm"
)
// DebitEventWriter 将代理主钱包扣款事实写入公共 Outbox。
// DebitEventWriter 将代理主钱包扣款事实写入公共 Outbox 和统一审计
type DebitEventWriter struct {
outbox *outbox.Repository
audit *audit.Writer
}
// NewDebitEventWriter 创建代理主钱包扣款 Outbox Writer。
func NewDebitEventWriter(repository *outbox.Repository) *DebitEventWriter {
return &DebitEventWriter{outbox: repository}
// NewDebitEventWriter 创建代理主钱包扣款事件 Writer。
func NewDebitEventWriter(repository *outbox.Repository, auditWriter *audit.Writer) *DebitEventWriter {
return &DebitEventWriter{outbox: repository, audit: auditWriter}
}
// Append 在调用方业务事务中追加代理主钱包扣款事件。
// Append 在调用方业务事务中追加代理主钱包扣款 Outbox 与审计事件。
func (w *DebitEventWriter) Append(ctx context.Context, tx *gorm.DB, event walletapp.DebitedEvent) error {
if w == nil || w.outbox == nil {
return errors.New(errors.CodeInternalError, "代理主钱包扣款 Outbox Writer 未配置")
if w == nil || w.outbox == nil || w.audit == nil {
return errors.New(errors.CodeInternalError, "代理主钱包扣款事件 Writer 未配置")
}
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
EventID: event.EventID, EventType: constants.OutboxEventTypeAgentMainWalletDebited,
@@ -34,5 +36,8 @@ func (w *DebitEventWriter) Append(ctx context.Context, tx *gorm.DB, event wallet
ResourceType: event.ReferenceType, ResourceID: strconv.FormatUint(uint64(event.ReferenceID), 10),
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event,
})
return err
if err != nil {
return err
}
return w.audit.WriteAgentWalletDebit(ctx, tx, event)
}

View File

@@ -5,26 +5,28 @@ import (
"strconv"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"gorm.io/gorm"
)
// ReservationEventWriter 将代理主钱包预占状态写入公共 Outbox。
// ReservationEventWriter 将代理主钱包预占状态写入公共 Outbox 和统一审计
type ReservationEventWriter struct {
outbox *outbox.Repository
audit *audit.Writer
}
// NewReservationEventWriter 创建代理主钱包预占 Outbox Writer。
func NewReservationEventWriter(repository *outbox.Repository) *ReservationEventWriter {
return &ReservationEventWriter{outbox: repository}
// NewReservationEventWriter 创建代理主钱包预占事件 Writer。
func NewReservationEventWriter(repository *outbox.Repository, auditWriter *audit.Writer) *ReservationEventWriter {
return &ReservationEventWriter{outbox: repository, audit: auditWriter}
}
// Append 在调用方事务中追加预占状态事件。
// Append 在调用方事务中追加预占状态 Outbox 与审计事件。
func (w *ReservationEventWriter) Append(ctx context.Context, tx *gorm.DB, event walletapp.ReservationEvent) error {
if w == nil || w.outbox == nil {
return errors.New(errors.CodeInternalError, "代理主钱包预占 Outbox Writer 未配置")
if w == nil || w.outbox == nil || w.audit == nil {
return errors.New(errors.CodeInternalError, "代理主钱包预占事件 Writer 未配置")
}
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
EventID: event.EventID, EventType: constants.OutboxEventTypeAgentMainWalletReservationChanged,
@@ -33,5 +35,8 @@ func (w *ReservationEventWriter) Append(ctx context.Context, tx *gorm.DB, event
ResourceType: event.ReferenceType, ResourceID: strconv.FormatUint(uint64(event.ReferenceID), 10),
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event,
})
return err
if err != nil {
return err
}
return w.audit.WriteAgentWalletReservation(ctx, tx, event)
}

View File

@@ -11,6 +11,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
@@ -35,13 +36,18 @@ type ApprovalRecoveryRecord struct {
// ApprovalContextRepository 管理企微提交的领取、终态和结果未知状态。
type ApprovalContextRepository struct {
db *gorm.DB
now func() time.Time
db *gorm.DB
audit approvalapp.AuditWriter
now func() time.Time
}
// NewApprovalContextRepository 创建企微审批渠道上下文 Repository。
func NewApprovalContextRepository(db *gorm.DB) *ApprovalContextRepository {
return &ApprovalContextRepository{db: db, now: time.Now}
func NewApprovalContextRepository(db *gorm.DB, audits ...approvalapp.AuditWriter) *ApprovalContextRepository {
var audit approvalapp.AuditWriter
if len(audits) > 0 {
audit = audits[0]
}
return &ApprovalContextRepository{db: db, audit: audit, now: time.Now}
}
// ClaimSubmission 将待提交上下文原子置为请求处理中,阻止并发或重投重复提单。
@@ -68,7 +74,7 @@ func (r *ApprovalContextRepository) ClaimSubmission(ctx context.Context, instanc
// PromoteStaleSendingToUnknown 将超出租约的提交中记录保守转为结果未知,禁止直接重新提交。
func (r *ApprovalContextRepository) PromoteStaleSendingToUnknown(ctx context.Context, cutoff time.Time) error {
if r == nil || r.db == nil || cutoff.IsZero() {
if r == nil || r.db == nil || r.audit == nil || cutoff.IsZero() {
return errors.New(errors.CodeInvalidParam, "企业微信审批恢复参数无效")
}
now := r.now().UTC()
@@ -97,13 +103,38 @@ func (r *ApprovalContextRepository) PromoteStaleSendingToUnknown(ctx context.Con
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "标记企业微信审批提交结果未知失败")
}
if err := tx.Model(&model.ApprovalInstance{}).
instanceResult := tx.Model(&model.ApprovalInstance{}).
Where("id IN ? AND status = ?", instanceIDs, constants.ApprovalStatusSubmitting).
Updates(map[string]any{
"status": constants.ApprovalStatusSubmissionUnknown, "status_changed_at": now,
"version": gorm.Expr("version + 1"), "updated_at": now,
}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "同步通用审批提交结果未知状态失败")
})
if instanceResult.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, instanceResult.Error, "同步通用审批提交结果未知状态失败")
}
if instanceResult.RowsAffected != result.RowsAffected {
return errors.New(errors.CodeConflict, "通用审批提交结果未知状态已变化")
}
for _, channelContext := range stale {
instance, err := loadApprovalAuditInstance(ctx, tx, channelContext.ApprovalInstanceID)
if err != nil {
return err
}
beforeStatus := constants.ApprovalStatusSubmitting
afterStatus := constants.ApprovalStatusSubmissionUnknown
if err := r.audit.WriteApproval(ctx, tx, approvalapp.AuditChange{
EventID: approvalSubmissionAuditEventID(instance.ID, afterStatus),
ActionCode: constants.AuditActionApprovalSubmissionSynced, Summary: "审批提交处理中断,结果转为未知",
InstanceID: instance.ID, BusinessType: instance.BusinessType, BusinessID: instance.BusinessID,
SubmitterAccountID: instance.SubmitterAccountID, SubmitterSnapshot: instance.SubmitterSnapshot,
Provider: instance.Provider, BeforeExternalRef: instance.ExternalRef, AfterExternalRef: instance.ExternalRef,
CorrelationID: instance.CorrelationID, BeforeStatus: &beforeStatus, AfterStatus: &afterStatus,
ActorKind: constants.AuditActorScheduledJob, ActorID: constants.ApprovalAuditActorRecoveryJob,
Source: constants.AuditSourceScheduler, Result: constants.AuditResultUnknown,
ErrorSummary: "企业微信审批提交处理中断,已进入结果未知恢复",
}); err != nil {
return err
}
}
return nil
})
@@ -160,7 +191,7 @@ func (r *ApprovalContextRepository) ListPendingSync(ctx context.Context, cutoff
}
// FindSuccessfulSubmissionSPNo 从已成功的安全 Integration Log 摘要恢复本地未保存的审批单号。
func (r *ApprovalContextRepository) FindSuccessfulSubmissionSPNo(ctx context.Context, instanceID uint) (string, error) {
func (r *ApprovalContextRepository) FindSuccessfulSubmissionSPNo(ctx context.Context, instanceID uint) (string, string, error) {
resourceID := strconv.FormatUint(uint64(instanceID), 10)
var log model.IntegrationLog
err := r.db.WithContext(ctx).
@@ -169,18 +200,18 @@ func (r *ApprovalContextRepository) FindSuccessfulSubmissionSPNo(ctx context.Con
constants.IntegrationDirectionOutbound, resourceID, constants.IntegrationResultSuccess).
Order("id DESC").First(&log).Error
if err == gorm.ErrRecordNotFound {
return "", nil
return "", "", nil
}
if err != nil {
return "", errors.Wrap(errors.CodeDatabaseError, err, "查询企业微信审批提交日志失败")
return "", "", errors.Wrap(errors.CodeDatabaseError, err, "查询企业微信审批提交日志失败")
}
var summary struct {
SPNo string `json:"sp_no"`
}
if sonic.Unmarshal(log.ResponseSummary, &summary) != nil {
return "", nil
return "", "", nil
}
return strings.TrimSpace(summary.SPNo), nil
return strings.TrimSpace(summary.SPNo), log.IntegrationID, nil
}
// ExistingSPNos 批量过滤已经关联到本地审批实例的企微审批单号。
@@ -231,13 +262,24 @@ func (r *ApprovalContextRepository) FindUniqueUnknownByFingerprint(
}
// RecoverSubmitted 将唯一确认的企微审批单号原子关联回结果未知实例。
func (r *ApprovalContextRepository) RecoverSubmitted(ctx context.Context, instanceID uint, spNo string) (bool, error) {
func (r *ApprovalContextRepository) RecoverSubmitted(
ctx context.Context,
instanceID uint,
spNo string,
integrationIDs []string,
actorKind string,
actorID string,
source string,
) (bool, error) {
spNo = strings.TrimSpace(spNo)
if instanceID == 0 || spNo == "" {
return false, errors.New(errors.CodeInvalidParam, "企业微信审批恢复关联参数无效")
}
now := r.now().UTC()
recovered := false
if r.audit == nil {
return false, errors.New(errors.CodeServiceUnavailable, "通用审批审计 Writer 未配置")
}
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
contextResult := tx.Model(&model.WeComApprovalContext{}).
Where("approval_instance_id = ? AND submission_status = ? AND sp_no = ''", instanceID, constants.WeComSubmissionStatusUnknown).
@@ -263,6 +305,24 @@ func (r *ApprovalContextRepository) RecoverSubmitted(ctx context.Context, instan
if instanceResult.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "通用审批恢复状态已变化")
}
instance, err := loadApprovalAuditInstance(ctx, tx, instanceID)
if err != nil {
return err
}
beforeStatus := constants.ApprovalStatusSubmissionUnknown
afterStatus := constants.ApprovalStatusPending
if err := r.audit.WriteApproval(ctx, tx, approvalapp.AuditChange{
EventID: "approval:" + strconv.FormatUint(uint64(instanceID), 10) + ":audit:submission_recovered",
ActionCode: constants.AuditActionApprovalSubmissionRecovered, Summary: "恢复结果未知的审批提交",
InstanceID: instance.ID, BusinessType: instance.BusinessType, BusinessID: instance.BusinessID,
SubmitterAccountID: instance.SubmitterAccountID, SubmitterSnapshot: instance.SubmitterSnapshot,
Provider: instance.Provider, BeforeExternalRef: "", AfterExternalRef: spNo,
CorrelationID: instance.CorrelationID, BeforeStatus: &beforeStatus, AfterStatus: &afterStatus,
ActorKind: actorKind, ActorID: actorID, Source: source, Result: constants.AuditResultSuccess,
IntegrationIDs: integrationIDs,
}); err != nil {
return err
}
recovered = true
return nil
})
@@ -335,7 +395,10 @@ func (r *ApprovalContextRepository) ReleaseForRetry(ctx context.Context, instanc
}
// MarkSubmitted 原子保存企微 sp_no并把通用审批实例置为审批中。
func (r *ApprovalContextRepository) MarkSubmitted(ctx context.Context, instanceID uint, spNo string) error {
func (r *ApprovalContextRepository) MarkSubmitted(ctx context.Context, instanceID uint, spNo string, integrationID string) error {
if r.audit == nil {
return errors.New(errors.CodeServiceUnavailable, "通用审批审计 Writer 未配置")
}
now := r.now().UTC()
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
contextResult := tx.Model(&model.WeComApprovalContext{}).
@@ -359,37 +422,90 @@ func (r *ApprovalContextRepository) MarkSubmitted(ctx context.Context, instanceI
if instanceResult.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "通用审批提交状态已变化")
}
return nil
instance, err := loadApprovalAuditInstance(ctx, tx, instanceID)
if err != nil {
return err
}
beforeStatus := constants.ApprovalStatusSubmitting
afterStatus := constants.ApprovalStatusPending
return r.audit.WriteApproval(ctx, tx, approvalapp.AuditChange{
EventID: approvalSubmissionAuditEventID(instanceID, afterStatus),
ActionCode: constants.AuditActionApprovalSubmissionSynced, Summary: "企业微信审批提交成功",
InstanceID: instance.ID, BusinessType: instance.BusinessType, BusinessID: instance.BusinessID,
SubmitterAccountID: instance.SubmitterAccountID, SubmitterSnapshot: instance.SubmitterSnapshot,
Provider: instance.Provider, BeforeExternalRef: "", AfterExternalRef: spNo,
CorrelationID: instance.CorrelationID, BeforeStatus: &beforeStatus, AfterStatus: &afterStatus,
ActorKind: constants.AuditActorSystemTask, ActorID: constants.ApprovalAuditActorSubmissionWorker,
Source: constants.AuditSourceWorker, Result: constants.AuditResultSuccess, IntegrationIDs: []string{integrationID},
})
})
}
// MarkFailed 将企微明确拒绝的提交记录为提交失败。
func (r *ApprovalContextRepository) MarkFailed(ctx context.Context, instanceID uint, message string) error {
return r.markSubmissionState(ctx, instanceID, constants.WeComSubmissionStatusFailed, constants.ApprovalStatusSubmissionFailed, message)
func (r *ApprovalContextRepository) MarkFailed(ctx context.Context, instanceID uint, message string, integrationID string) error {
return r.markSubmissionState(ctx, instanceID, constants.WeComSubmissionStatusFailed, constants.ApprovalStatusSubmissionFailed, message, constants.AuditResultFailed, integrationID)
}
// MarkUnknown 将请求已发出但无法确认结果的提交记录为结果未知。
func (r *ApprovalContextRepository) MarkUnknown(ctx context.Context, instanceID uint, message string) error {
return r.markSubmissionState(ctx, instanceID, constants.WeComSubmissionStatusUnknown, constants.ApprovalStatusSubmissionUnknown, message)
func (r *ApprovalContextRepository) MarkUnknown(ctx context.Context, instanceID uint, message string, integrationID string) error {
return r.markSubmissionState(ctx, instanceID, constants.WeComSubmissionStatusUnknown, constants.ApprovalStatusSubmissionUnknown, message, constants.AuditResultUnknown, integrationID)
}
// markSubmissionState 在同一事务中同步企微渠道状态与通用审批状态,避免两侧事实分裂。
func (r *ApprovalContextRepository) markSubmissionState(ctx context.Context, instanceID uint, channelStatus, approvalStatus int, message string) error {
func (r *ApprovalContextRepository) markSubmissionState(ctx context.Context, instanceID uint, channelStatus, approvalStatus int, message, auditResult, integrationID string) error {
if r.audit == nil {
return errors.New(errors.CodeServiceUnavailable, "通用审批审计 Writer 未配置")
}
now := r.now().UTC()
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.WeComApprovalContext{}).
contextResult := tx.Model(&model.WeComApprovalContext{}).
Where("approval_instance_id = ? AND submission_status = ?", instanceID, constants.WeComSubmissionStatusSending).
Updates(map[string]any{"submission_status": channelStatus, "last_error": message, "updated_at": now}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新企业微信审批提交结果失败")
Updates(map[string]any{"submission_status": channelStatus, "last_error": message, "updated_at": now})
if contextResult.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, contextResult.Error, "更新企业微信审批提交结果失败")
}
if err := tx.Model(&model.ApprovalInstance{}).
if contextResult.RowsAffected == 0 {
return nil
}
instanceResult := tx.Model(&model.ApprovalInstance{}).
Where("id = ? AND status = ?", instanceID, constants.ApprovalStatusSubmitting).
Updates(map[string]any{
"status": approvalStatus, "status_changed_at": now,
"version": gorm.Expr("version + 1"), "updated_at": now,
}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新通用审批提交结果失败")
})
if instanceResult.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, instanceResult.Error, "更新通用审批提交结果失败")
}
return nil
if instanceResult.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "通用审批提交结果状态已变化")
}
instance, err := loadApprovalAuditInstance(ctx, tx, instanceID)
if err != nil {
return err
}
beforeStatus := constants.ApprovalStatusSubmitting
return r.audit.WriteApproval(ctx, tx, approvalapp.AuditChange{
EventID: approvalSubmissionAuditEventID(instanceID, approvalStatus),
ActionCode: constants.AuditActionApprovalSubmissionSynced, Summary: "同步企业微信审批提交结果",
InstanceID: instance.ID, BusinessType: instance.BusinessType, BusinessID: instance.BusinessID,
SubmitterAccountID: instance.SubmitterAccountID, SubmitterSnapshot: instance.SubmitterSnapshot,
Provider: instance.Provider, BeforeExternalRef: "", AfterExternalRef: instance.ExternalRef,
CorrelationID: instance.CorrelationID, BeforeStatus: &beforeStatus, AfterStatus: &approvalStatus,
ActorKind: constants.AuditActorSystemTask, ActorID: constants.ApprovalAuditActorSubmissionWorker,
Source: constants.AuditSourceWorker, Result: auditResult, ErrorSummary: message,
IntegrationIDs: []string{integrationID},
})
})
}
func loadApprovalAuditInstance(ctx context.Context, tx *gorm.DB, instanceID uint) (*model.ApprovalInstance, error) {
var instance model.ApprovalInstance
if err := tx.WithContext(ctx).First(&instance, instanceID).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询通用审批审计快照失败")
}
return &instance, nil
}
func approvalSubmissionAuditEventID(instanceID uint, status int) string {
return "approval:" + strconv.FormatUint(uint64(instanceID), 10) + ":audit:submission:" + strconv.Itoa(status)
}

View File

@@ -19,9 +19,10 @@ import (
// ApprovalDetail 是企微审批详情的权威状态和安全原始 JSON 快照。
type ApprovalDetail struct {
SPNo string
SPStatus int
Snapshot []byte
SPNo string
SPStatus int
Snapshot []byte
IntegrationID string
}
// ApprovalDetailClient 获取企业微信审批申请详情。
@@ -93,7 +94,7 @@ func (c *ApprovalDetailClient) Get(ctx context.Context, applicationID uint, spNo
ProviderCode: strconv.FormatInt(errCode, 10), ProviderMessage: errMsg,
ResponseSummary: map[string]any{"sp_no": spNo, "sp_status": status}, DurationMS: c.now().Sub(startedAt).Milliseconds(),
})
return ApprovalDetail{SPNo: spNo, SPStatus: status, Snapshot: snapshot}, err
return ApprovalDetail{SPNo: spNo, SPStatus: status, Snapshot: snapshot, IntegrationID: attempt.IntegrationID}, err
}
// newRequest 组装按字符串审批单号查询权威详情的企微请求。

View File

@@ -71,6 +71,7 @@ func (h *ApprovalDetailTaskHandler) Handle(ctx context.Context, task *asynq.Task
for _, decision := range decisions {
if _, err := h.decisions.Execute(ctx, approvalapp.SyncDecisionCommand{
InstanceID: record.Instance.ID, Decision: decision, DecisionSnapshot: detail.Snapshot, Source: payload.Source,
IntegrationIDs: []string{payload.IntegrationID, detail.IntegrationID},
}); err != nil {
return err
}
@@ -100,7 +101,10 @@ func (h *ApprovalDetailTaskHandler) recoverUnknownCallback(ctx context.Context,
if err != nil || candidate == nil {
return nil, err
}
recovered, err := h.contexts.RecoverSubmitted(ctx, candidate.InstanceID, payload.SPNo)
recovered, err := h.contexts.RecoverSubmitted(
ctx, candidate.InstanceID, payload.SPNo,
[]string{payload.IntegrationID, detail.IntegrationID}, constants.AuditActorExternalSystem, constants.ApprovalAuditActorWeCom, constants.AuditSourceCallback,
)
if err != nil || !recovered {
return nil, err
}

View File

@@ -30,8 +30,9 @@ type ApprovalInfoQuery struct {
// ApprovalInfoPage 是企业微信批量审批单号接口的一页结果。
type ApprovalInfoPage struct {
SPNos []string
NextCursor string
SPNos []string
NextCursor string
IntegrationID string
}
// ApprovalInfoClient 按提交时间窗批量获取企业微信审批单号。
@@ -116,7 +117,7 @@ func (c *ApprovalInfoClient) List(ctx context.Context, input ApprovalInfoQuery)
},
DurationMS: c.now().Sub(startedAt).Milliseconds(),
})
return ApprovalInfoPage{SPNos: spNos, NextCursor: strings.TrimSpace(result.NewNextCursor)}, err
return ApprovalInfoPage{SPNos: spNos, NextCursor: strings.TrimSpace(result.NewNextCursor), IntegrationID: attempt.IntegrationID}, err
}
func validateApprovalInfoQuery(input ApprovalInfoQuery) error {

View File

@@ -67,20 +67,26 @@ func (h *ApprovalRecoveryTaskHandler) recoverUnknown(ctx context.Context, now ti
if !claimed {
continue
}
spNo, err := h.contexts.FindSuccessfulSubmissionSPNo(ctx, record.InstanceID)
spNo, submissionIntegrationID, err := h.contexts.FindSuccessfulSubmissionSPNo(ctx, record.InstanceID)
if err != nil {
return err
}
integrationIDs := []string{submissionIntegrationID}
if spNo == "" {
spNo, err = h.findUniqueSPNo(ctx, record, now)
var recoveryIntegrationIDs []string
spNo, recoveryIntegrationIDs, err = h.findUniqueSPNo(ctx, record, now)
if err != nil {
return err
}
integrationIDs = append(integrationIDs, recoveryIntegrationIDs...)
}
if spNo == "" {
continue
}
recovered, err := h.contexts.RecoverSubmitted(ctx, record.InstanceID, spNo)
recovered, err := h.contexts.RecoverSubmitted(
ctx, record.InstanceID, spNo, integrationIDs,
constants.AuditActorScheduledJob, constants.ApprovalAuditActorRecoveryJob, constants.AuditSourceScheduler,
)
if err != nil {
return err
}
@@ -94,19 +100,20 @@ func (h *ApprovalRecoveryTaskHandler) recoverUnknown(ctx context.Context, now ti
}
// findUniqueSPNo 使用提交时间附近的固定窄窗口分页查询,并只接受唯一未关联候选。
func (h *ApprovalRecoveryTaskHandler) findUniqueSPNo(ctx context.Context, record ApprovalRecoveryRecord, now time.Time) (string, error) {
func (h *ApprovalRecoveryTaskHandler) findUniqueSPNo(ctx context.Context, record ApprovalRecoveryRecord, now time.Time) (string, []string, error) {
startTime := record.SubmissionAttemptedAt.Add(-constants.WeComApprovalRecoveryWindow)
endTime := record.SubmissionAttemptedAt.Add(constants.WeComApprovalRecoveryWindow)
if endTime.After(now) {
endTime = now
}
if !startTime.Before(endTime) {
return "", nil
return "", nil, nil
}
cursor := ""
seenCursors := make(map[string]struct{})
seenCandidates := make(map[string]struct{})
unbound := make([]string, 0, 2)
integrationIDs := make([]string, 0, 2)
for {
page, err := h.infos.List(ctx, ApprovalInfoQuery{
ApplicationID: record.ApplicationID, StartTime: startTime, EndTime: endTime,
@@ -114,11 +121,12 @@ func (h *ApprovalRecoveryTaskHandler) findUniqueSPNo(ctx context.Context, record
Cursor: cursor, Size: constants.WeComApprovalInfoMaxPageSize,
})
if err != nil {
return "", err
return "", nil, err
}
integrationIDs = append(integrationIDs, page.IntegrationID)
existing, err := h.contexts.ExistingSPNos(ctx, record.ApplicationID, page.SPNos)
if err != nil {
return "", err
return "", nil, err
}
for _, candidate := range page.SPNos {
if _, seen := seenCandidates[candidate]; seen {
@@ -128,7 +136,7 @@ func (h *ApprovalRecoveryTaskHandler) findUniqueSPNo(ctx context.Context, record
if _, exists := existing[candidate]; !exists {
unbound = append(unbound, candidate)
if len(unbound) > 1 {
return "", nil
return "", integrationIDs, nil
}
}
}
@@ -137,15 +145,15 @@ func (h *ApprovalRecoveryTaskHandler) findUniqueSPNo(ctx context.Context, record
break
}
if _, exists := seenCursors[next]; exists {
return "", errors.New(errors.CodeServiceUnavailable, "企业微信批量审批单号分页游标重复")
return "", nil, errors.New(errors.CodeServiceUnavailable, "企业微信批量审批单号分页游标重复")
}
seenCursors[next] = struct{}{}
cursor = next
}
if len(unbound) != 1 {
return "", nil
return "", integrationIDs, nil
}
return unbound[0], nil
return unbound[0], integrationIDs, nil
}
func (h *ApprovalRecoveryTaskHandler) enqueueDetailSync(ctx context.Context, applicationID uint, spNo string) error {

View File

@@ -35,10 +35,11 @@ type ApprovalSubmitRequest struct {
// ApprovalSubmitResult 描述企微是否明确创建审批单。
type ApprovalSubmitResult struct {
Outcome string
SPNo string
Message string
SafeToRetry bool
Outcome string
SPNo string
Message string
SafeToRetry bool
IntegrationID string
}
// ApprovalSubmissionClient 调用企微 applyevent 并记录每次真实外呼。
@@ -98,9 +99,9 @@ func (c *ApprovalSubmissionClient) Submit(ctx context.Context, input ApprovalSub
RecoveryStrategy: "按申请时间窗批量获取审批单号并核对详情,确认不存在后才允许受控重提",
})
if completeErr != nil {
return ApprovalSubmitResult{Outcome: submissionOutcomeUnknown, Message: message}, completeErr
return ApprovalSubmitResult{Outcome: submissionOutcomeUnknown, Message: message, IntegrationID: attempt.IntegrationID}, completeErr
}
return ApprovalSubmitResult{Outcome: submissionOutcomeUnknown, Message: message}, nil
return ApprovalSubmitResult{Outcome: submissionOutcomeUnknown, Message: message, IntegrationID: attempt.IntegrationID}, nil
}
defer response.Body.Close()
responseBody, readErr := io.ReadAll(io.LimitReader(response.Body, constants.WeComMaxResponseBodyBytes+1))
@@ -113,9 +114,9 @@ func (c *ApprovalSubmissionClient) Submit(ctx context.Context, input ApprovalSub
RecoveryStrategy: "按申请时间窗批量获取审批单号并核对详情,确认不存在后才允许受控重提",
})
if completeErr != nil {
return ApprovalSubmitResult{Outcome: submissionOutcomeUnknown, Message: message}, completeErr
return ApprovalSubmitResult{Outcome: submissionOutcomeUnknown, Message: message, IntegrationID: attempt.IntegrationID}, completeErr
}
return ApprovalSubmitResult{Outcome: submissionOutcomeUnknown, Message: message}, nil
return ApprovalSubmitResult{Outcome: submissionOutcomeUnknown, Message: message, IntegrationID: attempt.IntegrationID}, nil
}
var result struct {
ErrCode int64 `json:"errcode"`
@@ -141,9 +142,9 @@ func (c *ApprovalSubmissionClient) Submit(ctx context.Context, input ApprovalSub
DurationMS: c.now().Sub(startedAt).Milliseconds(),
})
if completeErr != nil {
return ApprovalSubmitResult{Outcome: submissionOutcomeFailed, Message: message}, completeErr
return ApprovalSubmitResult{Outcome: submissionOutcomeFailed, Message: message, IntegrationID: attempt.IntegrationID}, completeErr
}
return ApprovalSubmitResult{Outcome: submissionOutcomeFailed, Message: message}, nil
return ApprovalSubmitResult{Outcome: submissionOutcomeFailed, Message: message, IntegrationID: attempt.IntegrationID}, nil
}
_, err = c.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
Result: constants.IntegrationResultSuccess, HTTPStatus: response.StatusCode,
@@ -151,7 +152,7 @@ func (c *ApprovalSubmissionClient) Submit(ctx context.Context, input ApprovalSub
ResponseSummary: map[string]any{"success": true, "sp_no": strings.TrimSpace(result.SPNo)},
DurationMS: c.now().Sub(startedAt).Milliseconds(), StateChanged: true,
})
return ApprovalSubmitResult{Outcome: submissionOutcomeSuccess, SPNo: strings.TrimSpace(result.SPNo)}, err
return ApprovalSubmitResult{Outcome: submissionOutcomeSuccess, SPNo: strings.TrimSpace(result.SPNo), IntegrationID: attempt.IntegrationID}, err
}
// newSubmitRequest 只组装本次企微审批请求,调用前不会产生外部副作用。

View File

@@ -59,7 +59,7 @@ func (c *ApprovalSubmissionConsumer) Consume(ctx context.Context, envelope outbo
}
return err
}
if markErr := c.repository.MarkFailed(ctx, event.InstanceID, err.Error()); markErr != nil {
if markErr := c.repository.MarkFailed(ctx, event.InstanceID, err.Error(), ""); markErr != nil {
return markErr
}
return nil
@@ -71,11 +71,11 @@ func (c *ApprovalSubmissionConsumer) Consume(ctx context.Context, envelope outbo
})
switch result.Outcome {
case submissionOutcomeSuccess:
if err := c.repository.MarkSubmitted(ctx, event.InstanceID, result.SPNo); err != nil {
if err := c.repository.MarkSubmitted(ctx, event.InstanceID, result.SPNo, result.IntegrationID); err != nil {
return err
}
case submissionOutcomeUnknown:
if err := c.repository.MarkUnknown(ctx, event.InstanceID, result.Message); err != nil {
if err := c.repository.MarkUnknown(ctx, event.InstanceID, result.Message, result.IntegrationID); err != nil {
return err
}
case submissionOutcomeFailed:
@@ -88,7 +88,7 @@ func (c *ApprovalSubmissionConsumer) Consume(ctx context.Context, envelope outbo
}
return errors.New(errors.CodeServiceUnavailable, result.Message)
}
if err := c.repository.MarkFailed(ctx, event.InstanceID, result.Message); err != nil {
if err := c.repository.MarkFailed(ctx, event.InstanceID, result.Message, result.IntegrationID); err != nil {
return err
}
default:

View File

@@ -24,28 +24,25 @@ func NewMemberRepository(db *gorm.DB) *MemberRepository {
}
// ReplaceVisible 原子替换指定应用当前可见成员,历史不可见成员仅标记为不可见。
func (r *MemberRepository) ReplaceVisible(ctx context.Context, applicationID uint, members []model.WeComMember, syncedAt time.Time) error {
if r == nil || r.db == nil {
func (r *MemberRepository) ReplaceVisible(ctx context.Context, tx *gorm.DB, applicationID uint, members []model.WeComMember, syncedAt time.Time) error {
if r == nil || tx == nil {
return errors.New(errors.CodeDatabaseError, "企业微信成员存储未配置")
}
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.WeComMember{}).Where("application_id = ?", applicationID).
Updates(map[string]any{"visible": false, "updated_at": syncedAt}).Error; err != nil {
return err
}
if len(members) == 0 {
return nil
}
return tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "application_id"}, {Name: "userid"}},
DoUpdates: clause.Assignments(map[string]any{
"corp_id": gorm.Expr("EXCLUDED.corp_id"), "name": gorm.Expr("EXCLUDED.name"),
"department_ids": gorm.Expr("EXCLUDED.department_ids"), "visible": true,
"synced_at": gorm.Expr("EXCLUDED.synced_at"), "updated_at": syncedAt,
}),
}).CreateInBatches(&members, constants.WeComMemberSyncBatchSize).Error
})
if err != nil {
if err := tx.WithContext(ctx).Model(&model.WeComMember{}).Where("application_id = ?", applicationID).
Updates(map[string]any{"visible": false, "updated_at": syncedAt}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "同步企业微信可见成员失败")
}
if len(members) == 0 {
return nil
}
if err := tx.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "application_id"}, {Name: "userid"}},
DoUpdates: clause.Assignments(map[string]any{
"corp_id": gorm.Expr("EXCLUDED.corp_id"), "name": gorm.Expr("EXCLUDED.name"),
"department_ids": gorm.Expr("EXCLUDED.department_ids"), "visible": true,
"synced_at": gorm.Expr("EXCLUDED.synced_at"), "updated_at": syncedAt,
}),
}).CreateInBatches(&members, constants.WeComMemberSyncBatchSize).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "同步企业微信可见成员失败")
}
return nil