收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
189
internal/infrastructure/audit/approval.go
Normal file
189
internal/infrastructure/audit/approval.go
Normal 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
|
||||
}
|
||||
46
internal/infrastructure/audit/commission.go
Normal file
46
internal/infrastructure/audit/commission.go
Normal 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
|
||||
}
|
||||
62
internal/infrastructure/audit/failure.go
Normal file
62
internal/infrastructure/audit/failure.go
Normal 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,
|
||||
)
|
||||
}
|
||||
24
internal/infrastructure/audit/notification.go
Normal file
24
internal/infrastructure/audit/notification.go
Normal 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,
|
||||
}
|
||||
}
|
||||
218
internal/infrastructure/audit/package.go
Normal file
218
internal/infrastructure/audit/package.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
84
internal/infrastructure/audit/payment.go
Normal file
84
internal/infrastructure/audit/payment.go
Normal 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,
|
||||
})
|
||||
}
|
||||
73
internal/infrastructure/audit/polling.go
Normal file
73
internal/infrastructure/audit/polling.go
Normal 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,
|
||||
})
|
||||
}
|
||||
187
internal/infrastructure/audit/recharge.go
Normal file
187
internal/infrastructure/audit/recharge.go
Normal 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
|
||||
}
|
||||
128
internal/infrastructure/audit/refund.go
Normal file
128
internal/infrastructure/audit/refund.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
91
internal/infrastructure/audit/task.go
Normal file
91
internal/infrastructure/audit/task.go
Normal 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])
|
||||
}
|
||||
293
internal/infrastructure/audit/wallet.go
Normal file
293
internal/infrastructure/audit/wallet.go
Normal 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
|
||||
}
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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, "卡流量事件类型或版本不受支持")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 组装按字符串审批单号查询权威详情的企微请求。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 只组装本次企微审批请求,调用前不会产生外部副作用。
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user