This commit is contained in:
@@ -40,104 +40,104 @@ type EventFilter struct {
|
||||
|
||||
// EventPage 是平台全局事件稳定分页结果。
|
||||
type EventPage struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Items []EventView `json:"items"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Total int64 `json:"total" description:"符合条件的事件总数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
Items []EventView `json:"items" description:"审计事件列表,按发生时间和主键稳定倒序"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// EventDetail 是单个审计事件及在线留存边界。
|
||||
type EventDetail struct {
|
||||
EventView
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// EventView 是不暴露 GORM Model 的审计事件投影。
|
||||
type EventView struct {
|
||||
EventID string `json:"event_id"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
Category string `json:"category"`
|
||||
ActionCode string `json:"action_code"`
|
||||
ActionName string `json:"action_name"`
|
||||
Summary string `json:"summary"`
|
||||
ActorKind string `json:"actor_kind"`
|
||||
ActorID string `json:"actor_id"`
|
||||
ActorName string `json:"actor_name"`
|
||||
ActorShopID *uint `json:"actor_shop_id,omitempty"`
|
||||
ActorShopName string `json:"actor_shop_name"`
|
||||
ActorEnterpriseID *uint `json:"actor_enterprise_id,omitempty"`
|
||||
ActorEnterpriseName string `json:"actor_enterprise_name"`
|
||||
Source string `json:"source"`
|
||||
RequestPath string `json:"request_path"`
|
||||
RequestMethod string `json:"request_method"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
ScopeType string `json:"scope_type"`
|
||||
ScopeID string `json:"scope_id"`
|
||||
ScopeName string `json:"scope_name"`
|
||||
Result string `json:"result"`
|
||||
RiskLevel string `json:"risk_level"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
ErrorSummary string `json:"error_summary"`
|
||||
RequestID string `json:"request_id"`
|
||||
CorrelationID string `json:"correlation_id"`
|
||||
ParentEventID string `json:"parent_event_id"`
|
||||
BatchTotal int `json:"batch_total"`
|
||||
SuccessCount int `json:"success_count"`
|
||||
FailCount int `json:"fail_count"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
ContentHash string `json:"content_hash"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Resources []ResourceView `json:"resources"`
|
||||
InvestigationRefs InvestigationRefs `json:"investigation_refs"`
|
||||
EventID string `json:"event_id" description:"稳定审计事件ID,可传给事件详情接口"`
|
||||
OccurredAt time.Time `json:"occurred_at" description:"业务事实发生时间"`
|
||||
Category string `json:"category" enum:"configuration,reliability,asset,security,identity,business" description:"动作类别稳定编码"`
|
||||
ActionCode string `json:"action_code" description:"稳定动作编码;筛选和跳转必须使用该值"`
|
||||
ActionName string `json:"action_name" description:"action_code对应的中文展示名称"`
|
||||
Summary string `json:"summary" description:"事件中文摘要"`
|
||||
ActorKind string `json:"actor_kind" enum:"account,personal_customer,openapi,system_task,scheduled_job,external_system" description:"操作者类型稳定编码"`
|
||||
ActorID string `json:"actor_id" description:"操作者稳定ID,与actor_kind共同定位操作者时间线"`
|
||||
ActorName string `json:"actor_name" description:"事件发生时的操作者名称快照"`
|
||||
ActorShopID *uint `json:"actor_shop_id,omitempty" description:"操作者所属店铺ID快照"`
|
||||
ActorShopName string `json:"actor_shop_name" description:"操作者所属店铺名称快照"`
|
||||
ActorEnterpriseID *uint `json:"actor_enterprise_id,omitempty" description:"操作者所属企业ID快照"`
|
||||
ActorEnterpriseName string `json:"actor_enterprise_name" description:"操作者所属企业名称快照"`
|
||||
Source string `json:"source" enum:"admin_api,personal_api,openapi,worker,scheduler,callback" description:"操作入口来源稳定编码"`
|
||||
RequestPath string `json:"request_path" description:"触发操作的HTTP路径;非HTTP入口可为空"`
|
||||
RequestMethod string `json:"request_method" description:"触发操作的HTTP方法;非HTTP入口可为空"`
|
||||
IPAddress string `json:"ip_address" description:"触发请求的IP地址;非HTTP入口可为空"`
|
||||
UserAgent string `json:"user_agent" description:"触发请求的User-Agent;非HTTP入口可为空"`
|
||||
ScopeType string `json:"scope_type" enum:"platform,shop,personal_customer" description:"业务范围类型稳定编码"`
|
||||
ScopeID string `json:"scope_id" description:"业务范围稳定ID,与scope_type共同使用"`
|
||||
ScopeName string `json:"scope_name" description:"业务范围名称快照"`
|
||||
Result string `json:"result" enum:"success,failed,denied,partial,unknown" description:"事件结果稳定编码"`
|
||||
RiskLevel string `json:"risk_level" enum:"low,normal,high,critical" description:"风险等级稳定编码"`
|
||||
ErrorCode string `json:"error_code" description:"失败或拒绝时的稳定错误码"`
|
||||
ErrorSummary string `json:"error_summary" description:"已脱敏的失败原因摘要"`
|
||||
RequestID string `json:"request_id" description:"HTTP请求关联ID,可传给请求时间线接口"`
|
||||
CorrelationID string `json:"correlation_id" description:"跨请求业务链路ID,可传给关联时间线接口"`
|
||||
ParentEventID string `json:"parent_event_id" description:"批量或异步链路的父审计事件ID"`
|
||||
BatchTotal int `json:"batch_total" description:"批次声明处理总数,非批次为0"`
|
||||
SuccessCount int `json:"success_count" description:"批次成功数,非批次为0"`
|
||||
FailCount int `json:"fail_count" description:"批次失败数,非批次为0"`
|
||||
Metadata map[string]any `json:"metadata" description:"已脱敏的动作扩展元数据,字段由action_code定义"`
|
||||
ContentHash string `json:"content_hash" description:"事件不可变内容摘要"`
|
||||
CreatedAt time.Time `json:"created_at" description:"审计记录写入时间"`
|
||||
Resources []ResourceView `json:"resources" description:"事件涉及的全部资源及各自前后快照"`
|
||||
InvestigationRefs InvestigationRefs `json:"investigation_refs" description:"跨审计视角的稳定跳转参数集合"`
|
||||
}
|
||||
|
||||
// InvestigationRefs 是平台调查视角间唯一允许使用的稳定跳转引用。
|
||||
type InvestigationRefs struct {
|
||||
EventID *string `json:"event_id"`
|
||||
ActorRef *ActorRef `json:"actor_ref"`
|
||||
ResourceRefs []InvestigationResourceRef `json:"resource_refs"`
|
||||
RequestID *string `json:"request_id"`
|
||||
CorrelationID *string `json:"correlation_id"`
|
||||
IntegrationRefs []IntegrationRef `json:"integration_refs"`
|
||||
EventID *string `json:"event_id" description:"传给GET /audit/events/{event_id}"`
|
||||
ActorRef *ActorRef `json:"actor_ref" description:"kind/id传给GET /audit/actors/{kind}/{id}/events"`
|
||||
ResourceRefs []InvestigationResourceRef `json:"resource_refs" description:"resource_type/resource_id传给GET /audit/resources/{resource_type}/{resource_id}/timeline;resource_id为空时不可跳转"`
|
||||
RequestID *string `json:"request_id" description:"传给GET /audit/requests/{request_id}/timeline"`
|
||||
CorrelationID *string `json:"correlation_id" description:"传给GET /audit/correlations/{correlation_id}/timeline"`
|
||||
IntegrationRefs []IntegrationRef `json:"integration_refs" description:"integration_id传给GET /audit/integrations/{integration_id}"`
|
||||
}
|
||||
|
||||
// ActorRef 是操作者时间线的稳定引用。
|
||||
type ActorRef struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind" enum:"account,personal_customer,openapi,system_task,scheduled_job,external_system" description:"操作者类型"`
|
||||
ID string `json:"id" description:"操作者稳定ID"`
|
||||
}
|
||||
|
||||
// InvestigationResourceRef 是通用资源时间线的稳定引用。
|
||||
type InvestigationResourceRef struct {
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID *string `json:"resource_id"`
|
||||
ResourceKey string `json:"resource_key"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ResourceType string `json:"resource_type" description:"Resource Registry注册类型"`
|
||||
ResourceID *string `json:"resource_id" description:"资源内部稳定ID;为空时不展示平台资源时间线入口"`
|
||||
ResourceKey string `json:"resource_key" description:"资源业务稳定Key,用于展示或精确搜索"`
|
||||
DisplayName string `json:"display_name" description:"事件发生时的资源展示名称"`
|
||||
}
|
||||
|
||||
// IntegrationRef 是 Integration 详情的稳定引用。
|
||||
type IntegrationRef struct {
|
||||
IntegrationID string `json:"integration_id"`
|
||||
IntegrationID string `json:"integration_id" description:"稳定外部集成记录ID"`
|
||||
}
|
||||
|
||||
// ResourceView 是事件发生时独立资源身份与变化的只读投影。
|
||||
type ResourceView struct {
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID *string `json:"resource_id,omitempty"`
|
||||
ResourceKey string `json:"resource_key"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Relation string `json:"relation"`
|
||||
Role string `json:"role"`
|
||||
IdentitySnapshot map[string]any `json:"identity_snapshot"`
|
||||
BeforeData map[string]any `json:"before_data"`
|
||||
AfterData map[string]any `json:"after_data"`
|
||||
SubjectVisibility string `json:"subject_visibility"`
|
||||
SubjectSummary string `json:"subject_summary"`
|
||||
SubjectData map[string]any `json:"subject_data"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ResourceType string `json:"resource_type" description:"Resource Registry注册类型"`
|
||||
ResourceID *string `json:"resource_id,omitempty" description:"资源内部稳定ID"`
|
||||
ResourceKey string `json:"resource_key" description:"资源业务稳定Key"`
|
||||
DisplayName string `json:"display_name" description:"事件发生时的资源展示名称"`
|
||||
Relation string `json:"relation" enum:"primary,affected,reference" description:"资源关系 (primary:主要资源, affected:受影响资源, reference:引用资源)"`
|
||||
Role string `json:"role" description:"Resource Registry定义的资源业务角色编码"`
|
||||
IdentitySnapshot map[string]any `json:"identity_snapshot" description:"事件发生时的资源身份快照"`
|
||||
BeforeData map[string]any `json:"before_data" description:"该资源变更前的完整平台审计数据"`
|
||||
AfterData map[string]any `json:"after_data" description:"该资源变更后的完整平台审计数据"`
|
||||
SubjectVisibility string `json:"subject_visibility" enum:"internal_only,subject_result,subject_detail" description:"主体可见性 (internal_only:仅平台, subject_result:主体可见结论, subject_detail:主体可见安全详情)"`
|
||||
SubjectSummary string `json:"subject_summary" description:"允许代理或企业查看的安全摘要"`
|
||||
SubjectData map[string]any `json:"subject_data" description:"写入时生成的主体安全字段,不等同于before_data或after_data"`
|
||||
SortOrder int `json:"sort_order" description:"资源在事件内的稳定展示顺序"`
|
||||
CreatedAt time.Time `json:"created_at" description:"资源关联记录写入时间"`
|
||||
}
|
||||
|
||||
// Query 提供平台统一审计事件列表与详情读取。
|
||||
|
||||
@@ -40,45 +40,45 @@ type FinanceFilter struct {
|
||||
|
||||
// FinanceTimelinePage 是资金多源投影的稳定分页结果。
|
||||
type FinanceTimelinePage struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Items []FinanceTimelineNode `json:"items"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Total int64 `json:"total" description:"关联资金事实总数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
Items []FinanceTimelineNode `json:"items" description:"按发生时间稳定倒序的资金事实节点"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// FinanceTimelineNode 是明确事实来源和金额权威的资金时间线节点。
|
||||
type FinanceTimelineNode struct {
|
||||
RecordSource string `json:"record_source"`
|
||||
NodeID string `json:"node_id"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
Result string `json:"result"`
|
||||
ResultName string `json:"result_name"`
|
||||
Amount *int64 `json:"amount"`
|
||||
BalanceBefore *int64 `json:"balance_before"`
|
||||
BalanceAfter *int64 `json:"balance_after"`
|
||||
Currency string `json:"currency"`
|
||||
ShopID *uint `json:"shop_id"`
|
||||
Wallet *FinanceWalletRef `json:"wallet"`
|
||||
AmountAuthority FinanceAmountAuthority `json:"amount_authority"`
|
||||
Facts map[string]any `json:"facts"`
|
||||
InvestigationRefs InvestigationRefs `json:"investigation_refs"`
|
||||
RecordSource string `json:"record_source" enum:"audit_event,domain_ledger_ref,agent_wallet_transaction,asset_wallet_transaction,agent_wallet_reservation,order,payment,refund,agent_recharge,recharge_order,commission_record,commission_withdrawal,approval_instance" description:"资金事实来源稳定编码"`
|
||||
NodeID string `json:"node_id" description:"该事实来源内的稳定节点ID"`
|
||||
OccurredAt time.Time `json:"occurred_at" description:"资金事实发生时间"`
|
||||
Code string `json:"code" description:"来源内稳定业务动作或状态编码"`
|
||||
Title string `json:"title" description:"code对应的中文展示名称"`
|
||||
Result string `json:"result" description:"来源内原始结果或状态编码"`
|
||||
ResultName string `json:"result_name" description:"result对应的中文展示名称"`
|
||||
Amount *int64 `json:"amount" description:"本节点金额,单位分;为空表示该节点不承载金额"`
|
||||
BalanceBefore *int64 `json:"balance_before" description:"变更前余额,单位分"`
|
||||
BalanceAfter *int64 `json:"balance_after" description:"变更后余额,单位分"`
|
||||
Currency string `json:"currency" description:"币种编码,人民币为CNY"`
|
||||
ShopID *uint `json:"shop_id" description:"关联店铺ID"`
|
||||
Wallet *FinanceWalletRef `json:"wallet" description:"关联钱包稳定引用"`
|
||||
AmountAuthority FinanceAmountAuthority `json:"amount_authority" description:"金额是否权威及权威字段来源"`
|
||||
Facts map[string]any `json:"facts" description:"该事实来源的安全结构化业务字段"`
|
||||
InvestigationRefs InvestigationRefs `json:"investigation_refs" description:"可继续跳转的稳定调查引用"`
|
||||
}
|
||||
|
||||
// FinanceWalletRef 是资金节点关联的钱包稳定引用。
|
||||
type FinanceWalletRef struct {
|
||||
ResourceType string `json:"resource_type"`
|
||||
WalletID uint `json:"wallet_id"`
|
||||
ResourceType string `json:"resource_type" enum:"agent_wallet,asset_wallet" description:"钱包资源类型"`
|
||||
WalletID uint `json:"wallet_id" description:"钱包内部稳定ID,可作为finance/timeline的wallet_id"`
|
||||
}
|
||||
|
||||
// FinanceAmountAuthority 说明当前金额是否为业务权威及其字段来源。
|
||||
type FinanceAmountAuthority struct {
|
||||
Authoritative bool `json:"authoritative"`
|
||||
Table string `json:"table"`
|
||||
Field string `json:"field"`
|
||||
ConflictRule string `json:"conflict_rule"`
|
||||
Authoritative bool `json:"authoritative" description:"当前amount或余额是否来自业务权威表"`
|
||||
Table string `json:"table" description:"权威金额所在业务表;非权威节点可为空"`
|
||||
Field string `json:"field" description:"权威金额所在字段;非权威节点可为空"`
|
||||
ConflictRule string `json:"conflict_rule" description:"多来源冲突时的取值规则说明"`
|
||||
}
|
||||
|
||||
type financeRefs struct {
|
||||
|
||||
@@ -8,12 +8,15 @@ import (
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
|
||||
auditinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
retentionquery "github.com/break/junhong_cmp_fiber/internal/query/retention"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
var timelineRegistry = auditinfra.NewRegistry()
|
||||
|
||||
// ResourceSearchFilter 定义注册资源的精确标识搜索。
|
||||
type ResourceSearchFilter struct {
|
||||
ResourceType string
|
||||
@@ -25,21 +28,21 @@ type ResourceSearchFilter struct {
|
||||
|
||||
// ResourceSearchPage 是资源候选稳定分页结果。
|
||||
type ResourceSearchPage struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Items []ResourceCandidate `json:"items"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Total int64 `json:"total" description:"符合精确标识的资源总数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
Items []ResourceCandidate `json:"items" description:"当前资源或历史快照解析出的候选资源"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// ResourceCandidate 是当前业务表或历史事件快照解析出的稳定资源候选。
|
||||
type ResourceCandidate struct {
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
ResourceKey string `json:"resource_key"`
|
||||
DisplayName string `json:"display_name"`
|
||||
IdentitySnapshot map[string]any `json:"identity_snapshot"`
|
||||
Historical bool `json:"historical"`
|
||||
ResourceType string `json:"resource_type" enum:"iot_card,device,shop,order,refund" description:"资源类型稳定编码"`
|
||||
ResourceID string `json:"resource_id" description:"资源内部稳定ID,可传给通用资源时间线接口"`
|
||||
ResourceKey string `json:"resource_key" description:"资源业务稳定Key"`
|
||||
DisplayName string `json:"display_name" description:"资源展示名称"`
|
||||
IdentitySnapshot map[string]any `json:"identity_snapshot" description:"当前业务表或历史事件保存的资源身份快照"`
|
||||
Historical bool `json:"historical" description:"是否仅由历史事件快照解析,true不代表资源当前仍存在"`
|
||||
}
|
||||
|
||||
// ResourceTimelineFilter 定义通用资源时间线筛选。
|
||||
@@ -95,16 +98,8 @@ func (q *Query) ResourceTimeline(ctx context.Context, filter ResourceTimelineFil
|
||||
}
|
||||
|
||||
func timelineResourceType(resourceType string) bool {
|
||||
switch resourceType {
|
||||
case constants.AuditResourceAccount, constants.AuditResourceShop, constants.AuditResourceEnterprise,
|
||||
constants.AuditResourceIotCard, constants.AuditResourceDevice, constants.AuditResourceDeviceSIMBinding,
|
||||
constants.AuditResourceAssetAllocationRecord, constants.AuditResourceExchangeOrder, constants.AuditResourceOrder,
|
||||
constants.AuditResourceRefund, constants.AuditResourceAgentRecharge, constants.AuditResourceAssetWallet,
|
||||
constants.AuditResourceApprovalInstance:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
_, ok := timelineRegistry.Resource(resourceType)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (q *Query) searchCurrent(ctx context.Context, filter ResourceSearchFilter) ([]ResourceCandidate, int64, error) {
|
||||
|
||||
@@ -26,44 +26,44 @@ type RiskFilter struct {
|
||||
|
||||
// RiskOverview 是风险信号、固定维度与时间趋势的只读聚合。
|
||||
type RiskOverview struct {
|
||||
Total int64 `json:"total"`
|
||||
Bucket string `json:"bucket"`
|
||||
Signals []RiskNamedCount `json:"signals"`
|
||||
Risks []RiskNamedCount `json:"risks"`
|
||||
Results []RiskNamedCount `json:"results"`
|
||||
Actions []RiskNamedCount `json:"actions"`
|
||||
Sources []RiskNamedCount `json:"sources"`
|
||||
Trend []RiskTrendPoint `json:"trend"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Total int64 `json:"total" description:"固定风险集合内的事件总数"`
|
||||
Bucket string `json:"bucket" enum:"hour,day" description:"服务端选择的趋势时间粒度"`
|
||||
Signals []RiskNamedCount `json:"signals" description:"固定信号分布,code为high_risk、finance、security、failed、denied、partial或unknown,name为中文展示名"`
|
||||
Risks []RiskNamedCount `json:"risks" description:"风险等级分布,code为low、normal、high或critical,name为中文展示名"`
|
||||
Results []RiskNamedCount `json:"results" description:"结果分布,code为success、failed、denied、partial或unknown,name为中文展示名"`
|
||||
Actions []RiskNamedCount `json:"actions" description:"动作分布,code为稳定action_code,name为中文action_name"`
|
||||
Sources []RiskNamedCount `json:"sources" description:"来源分布,code为admin_api、personal_api、openapi、worker、scheduler或callback,name为中文展示名"`
|
||||
Trend []RiskTrendPoint `json:"trend" description:"固定风险信号的时间趋势"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// RiskNamedCount 是风险聚合维度的稳定编码、中文名称和数量。
|
||||
type RiskNamedCount struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Count int64 `json:"count"`
|
||||
Code string `json:"code" description:"当前聚合维度的稳定编码,具体枚举域由所属数组字段说明"`
|
||||
Name string `json:"name" description:"code对应的中文展示名称"`
|
||||
Count int64 `json:"count" description:"该编码的事件数量"`
|
||||
}
|
||||
|
||||
// RiskTrendPoint 是固定时间桶内的风险信号趋势。
|
||||
type RiskTrendPoint struct {
|
||||
BucketAt time.Time `json:"bucket_at"`
|
||||
Total int64 `json:"total"`
|
||||
HighRisk int64 `json:"high_risk"`
|
||||
Finance int64 `json:"finance"`
|
||||
Security int64 `json:"security"`
|
||||
Failed int64 `json:"failed"`
|
||||
Denied int64 `json:"denied"`
|
||||
Partial int64 `json:"partial"`
|
||||
Unknown int64 `json:"unknown"`
|
||||
BucketAt time.Time `json:"bucket_at" description:"时间桶起点"`
|
||||
Total int64 `json:"total" description:"桶内固定风险集合事件数"`
|
||||
HighRisk int64 `json:"high_risk" description:"桶内high或critical风险事件数"`
|
||||
Finance int64 `json:"finance" description:"桶内资金类风险事件数"`
|
||||
Security int64 `json:"security" description:"桶内安全类风险事件数"`
|
||||
Failed int64 `json:"failed" description:"桶内failed事件数"`
|
||||
Denied int64 `json:"denied" description:"桶内denied事件数"`
|
||||
Partial int64 `json:"partial" description:"桶内partial事件数"`
|
||||
Unknown int64 `json:"unknown" description:"桶内unknown事件数"`
|
||||
}
|
||||
|
||||
// RiskEventPage 是风险事件的稳定分页结果。
|
||||
type RiskEventPage struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Items []EventView `json:"items"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Total int64 `json:"total" description:"符合条件的风险事件总数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
Items []EventView `json:"items" description:"风险事件及稳定调查引用"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// RiskOverview 查询指定时间范围内的固定风险调查总览。
|
||||
|
||||
@@ -27,31 +27,31 @@ type SubjectActivityFilter struct {
|
||||
|
||||
// SubjectActivityPage 是不包含平台调查字段的代理资源活动分页结果。
|
||||
type SubjectActivityPage struct {
|
||||
Resource SubjectResourceSummary `json:"resource"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Items []SubjectActivity `json:"items"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Resource SubjectResourceSummary `json:"resource" description:"已完成授权校验的目标资源"`
|
||||
Total int64 `json:"total" description:"主体可见活动总数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
Items []SubjectActivity `json:"items" description:"不包含平台内部调查字段的安全活动列表"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// SubjectActivity 是写入时已生成的主体安全活动投影。
|
||||
type SubjectActivity struct {
|
||||
ActionCode string `json:"action_code"`
|
||||
ActionName string `json:"action_name"`
|
||||
SubjectSummary string `json:"subject_summary"`
|
||||
SubjectData map[string]any `json:"subject_data"`
|
||||
Result string `json:"result"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
RelatedResources []SubjectResourceSummary `json:"related_resources"`
|
||||
ActionCode string `json:"action_code" description:"稳定动作编码"`
|
||||
ActionName string `json:"action_name" description:"action_code对应的中文展示名称"`
|
||||
SubjectSummary string `json:"subject_summary" description:"写入时生成的主体安全摘要"`
|
||||
SubjectData map[string]any `json:"subject_data" description:"写入时生成的主体安全业务字段,不包含平台before/after或内部原因"`
|
||||
Result string `json:"result" enum:"success,failed,denied,partial,unknown" description:"活动结果稳定编码"`
|
||||
OccurredAt time.Time `json:"occurred_at" description:"业务事实发生时间"`
|
||||
RelatedResources []SubjectResourceSummary `json:"related_resources" description:"当前主体授权范围内的相关资源摘要"`
|
||||
}
|
||||
|
||||
// SubjectResourceSummary 是主体活动允许公开的资源摘要。
|
||||
type SubjectResourceSummary struct {
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
ResourceKey string `json:"resource_key"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ResourceType string `json:"resource_type" description:"资源类型稳定编码"`
|
||||
ResourceID string `json:"resource_id" description:"资源内部稳定ID;主体前端不据此调用平台审计接口"`
|
||||
ResourceKey string `json:"resource_key" description:"资源业务稳定Key"`
|
||||
DisplayName string `json:"display_name" description:"资源安全展示名称"`
|
||||
}
|
||||
|
||||
type subjectTarget struct {
|
||||
@@ -60,14 +60,15 @@ type subjectTarget struct {
|
||||
}
|
||||
|
||||
type subjectActivityRow struct {
|
||||
ID uint
|
||||
ActionCode string
|
||||
ActionName string
|
||||
Result string
|
||||
OccurredAt time.Time
|
||||
SubjectSummary string
|
||||
SubjectData datatypes.JSON
|
||||
TargetResourceID uint
|
||||
ID uint
|
||||
ActionCode string
|
||||
ActionName string
|
||||
Result string
|
||||
OccurredAt time.Time
|
||||
SubjectVisibility string
|
||||
SubjectSummary string
|
||||
SubjectData datatypes.JSON
|
||||
TargetResourceID uint
|
||||
}
|
||||
|
||||
type subjectResourceAuthorizer func(context.Context, []model.AuditEventResource) (map[string]bool, error)
|
||||
@@ -78,9 +79,12 @@ func (q *Query) AgentResourceActivities(ctx context.Context, filter SubjectActiv
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter.Identifier == "" || !agentActivityResourceType(filter.ResourceType) || filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize {
|
||||
if filter.Identifier == "" || filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if !agentActivityResourceType(filter.ResourceType) {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
filter.Page, filter.PageSize = normalizePage(filter.Page, filter.PageSize)
|
||||
retention, err := retentionquery.Load(ctx, q.db, retentionquery.SourceAudit)
|
||||
if err != nil {
|
||||
@@ -108,9 +112,12 @@ func (q *Query) EnterpriseResourceActivities(ctx context.Context, filter Subject
|
||||
if enterpriseID == 0 {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
if filter.Identifier == "" || !enterpriseActivityResourceType(filter.ResourceType) || filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize {
|
||||
if filter.Identifier == "" || filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if !enterpriseActivityResourceType(filter.ResourceType) {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
filter.Page, filter.PageSize = normalizePage(filter.Page, filter.PageSize)
|
||||
retention, err := retentionquery.Load(ctx, q.db, retentionquery.SourceAudit)
|
||||
if err != nil {
|
||||
@@ -142,13 +149,13 @@ func (q *Query) subjectActivitiesForTarget(ctx context.Context, filter SubjectAc
|
||||
}
|
||||
|
||||
rows := make([]subjectActivityRow, 0, filter.PageSize)
|
||||
if err := base.Select("tb_audit_event.id, action_code, action_name, result, occurred_at, target.subject_summary, target.subject_data, target.id AS target_resource_id").
|
||||
if err := base.Select("tb_audit_event.id, action_code, action_name, result, occurred_at, target.subject_visibility, target.subject_summary, target.subject_data, target.id AS target_resource_id").
|
||||
Joins("JOIN tb_audit_event_resource AS target ON target.audit_event_id = tb_audit_event.id AND target.resource_type = ? AND target.resource_id = ?", filter.ResourceType, target.id).
|
||||
Where("target.subject_visibility IN ?", []string{constants.AuditSubjectResult, constants.AuditSubjectDetail}).
|
||||
Order("occurred_at DESC, tb_audit_event.id DESC").Offset((filter.Page - 1) * filter.PageSize).Limit(filter.PageSize).Scan(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理资源活动失败")
|
||||
}
|
||||
items, err := q.projectSubjectActivities(ctx, rows, authorize)
|
||||
items, err := q.projectSubjectActivities(ctx, rows, target, authorize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -265,24 +272,28 @@ func newSubjectTarget(resourceType string, id uint, key, name string) subjectTar
|
||||
return subjectTarget{summary: SubjectResourceSummary{ResourceType: resourceType, ResourceID: resourceID, ResourceKey: key, DisplayName: name}, id: resourceID}
|
||||
}
|
||||
|
||||
func (q *Query) projectSubjectActivities(ctx context.Context, rows []subjectActivityRow, authorize subjectResourceAuthorizer) ([]SubjectActivity, error) {
|
||||
func (q *Query) projectSubjectActivities(ctx context.Context, rows []subjectActivityRow, target subjectTarget, authorize subjectResourceAuthorizer) ([]SubjectActivity, error) {
|
||||
items := make([]SubjectActivity, 0, len(rows))
|
||||
if len(rows) == 0 {
|
||||
return items, nil
|
||||
}
|
||||
eventIDs := make([]uint, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
eventIDs = append(eventIDs, row.ID)
|
||||
}
|
||||
var resources []model.AuditEventResource
|
||||
if err := q.db.WithContext(ctx).Where("audit_event_id IN ? AND subject_visibility IN ?", eventIDs, []string{constants.AuditSubjectResult, constants.AuditSubjectDetail}).
|
||||
Order("audit_event_id ASC, sort_order ASC, id ASC").Find(&resources).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询主体可见关联资源失败")
|
||||
resources := make([]model.AuditEventResource, 0)
|
||||
if len(eventIDs) > 0 {
|
||||
if err := q.db.WithContext(ctx).Where("audit_event_id IN ? AND subject_visibility IN ?", eventIDs, []string{constants.AuditSubjectResult, constants.AuditSubjectDetail}).
|
||||
Order("audit_event_id ASC, sort_order ASC, id ASC").Find(&resources).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询主体可见关联资源失败")
|
||||
}
|
||||
}
|
||||
allowed, err := authorize(ctx, resources)
|
||||
targetResourceID := target.id
|
||||
authorizationResources := append(resources, model.AuditEventResource{ResourceType: target.summary.ResourceType, ResourceID: &targetResourceID})
|
||||
allowed, err := authorize(ctx, authorizationResources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !allowed[resourceAccessKey(target.summary.ResourceType, target.id)] {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
related := make(map[uint][]SubjectResourceSummary, len(rows))
|
||||
for _, resource := range resources {
|
||||
if resource.ResourceID == nil || !allowed[resourceAccessKey(resource.ResourceType, *resource.ResourceID)] {
|
||||
@@ -294,9 +305,12 @@ func (q *Query) projectSubjectActivities(ctx context.Context, rows []subjectActi
|
||||
})
|
||||
}
|
||||
for _, row := range rows {
|
||||
data, err := decodeObject(row.SubjectData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
data := map[string]any{}
|
||||
if row.SubjectVisibility == constants.AuditSubjectDetail {
|
||||
data, err = decodeObject(row.SubjectData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
items = append(items, SubjectActivity{ActionCode: row.ActionCode, ActionName: row.ActionName,
|
||||
SubjectSummary: row.SubjectSummary, SubjectData: data, Result: row.Result,
|
||||
|
||||
@@ -15,39 +15,39 @@ import (
|
||||
|
||||
// LinkTimeline 是 request 或 correlation 的跨事实只读时间线。
|
||||
type LinkTimeline struct {
|
||||
RequestID *string `json:"request_id"`
|
||||
CorrelationID *string `json:"correlation_id"`
|
||||
AccessLogLookupRequestID *string `json:"access_log_lookup_request_id"`
|
||||
Nodes []LinkTimelineNode `json:"nodes"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
RequestID *string `json:"request_id" description:"本次按请求查询的稳定ID"`
|
||||
CorrelationID *string `json:"correlation_id" description:"本次按业务关联查询的稳定ID"`
|
||||
AccessLogLookupRequestID *string `json:"access_log_lookup_request_id" description:"可复制到Access Log检索的request_id;本接口自身不扫描Access Log"`
|
||||
Nodes []LinkTimelineNode `json:"nodes" description:"跨事实来源按发生时间稳定排序的节点"`
|
||||
Retention retentionquery.Info `json:"retention" description:"Audit与Integration共同在线留存边界"`
|
||||
}
|
||||
|
||||
// LinkTimelineNode 是保留各事实源权威边界的时间线节点。
|
||||
type LinkTimelineNode struct {
|
||||
RecordSource string `json:"record_source"`
|
||||
NodeID string `json:"node_id"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
Result string `json:"result"`
|
||||
ResultName string `json:"result_name"`
|
||||
Summary string `json:"summary"`
|
||||
ReferenceOnly bool `json:"reference_only"`
|
||||
RequestID *string `json:"request_id"`
|
||||
CorrelationID *string `json:"correlation_id"`
|
||||
ParentEventID *string `json:"parent_event_id"`
|
||||
Resources []InvestigationResourceRef `json:"resources"`
|
||||
InvestigationRefs InvestigationRefs `json:"investigation_refs"`
|
||||
Fidelity LinkageFidelity `json:"fidelity"`
|
||||
RecordSource string `json:"record_source" enum:"audit_event,integration_log,outbox_event,asynq_task,domain_ledger_ref" description:"事实来源 (audit_event:审计事件, integration_log:外部交互, outbox_event:可靠事件引用, asynq_task:异步任务引用, domain_ledger_ref:业务账本引用)"`
|
||||
NodeID string `json:"node_id" description:"该事实来源内的稳定节点ID"`
|
||||
OccurredAt time.Time `json:"occurred_at" description:"节点发生时间"`
|
||||
Code string `json:"code" description:"来源内稳定动作、操作或事件编码"`
|
||||
Title string `json:"title" description:"code对应的中文展示名称"`
|
||||
Result string `json:"result" description:"来源内原始结果稳定编码"`
|
||||
ResultName string `json:"result_name" description:"result对应的中文展示名称"`
|
||||
Summary string `json:"summary" description:"已脱敏节点摘要"`
|
||||
ReferenceOnly bool `json:"reference_only" description:"true表示仅保存其他事实的引用,不代表该来源独立完成业务状态变更"`
|
||||
RequestID *string `json:"request_id" description:"HTTP请求关联ID"`
|
||||
CorrelationID *string `json:"correlation_id" description:"跨请求业务链路ID"`
|
||||
ParentEventID *string `json:"parent_event_id" description:"父审计事件ID"`
|
||||
Resources []InvestigationResourceRef `json:"resources" description:"节点可稳定定位的资源引用"`
|
||||
InvestigationRefs InvestigationRefs `json:"investigation_refs" description:"可继续跳转的稳定调查引用"`
|
||||
Fidelity LinkageFidelity `json:"fidelity" description:"历史字段完整度和可关联能力"`
|
||||
}
|
||||
|
||||
// LinkageFidelity 明确节点已有的稳定关联能力,不补猜历史缺失字段。
|
||||
type LinkageFidelity struct {
|
||||
RequestAvailable bool `json:"request_available"`
|
||||
CorrelationAvailable bool `json:"correlation_available"`
|
||||
ParentEventAvailable bool `json:"parent_event_available"`
|
||||
DirectAuditLinkAvailable bool `json:"direct_audit_link_available"`
|
||||
StableResourceAvailable bool `json:"stable_resource_available"`
|
||||
RequestAvailable bool `json:"request_available" description:"是否有稳定request_id"`
|
||||
CorrelationAvailable bool `json:"correlation_available" description:"是否有稳定correlation_id"`
|
||||
ParentEventAvailable bool `json:"parent_event_available" description:"是否有稳定parent_event_id"`
|
||||
DirectAuditLinkAvailable bool `json:"direct_audit_link_available" description:"是否可直接跳转审计事件详情"`
|
||||
StableResourceAvailable bool `json:"stable_resource_available" description:"是否至少有一个含resource_id的稳定资源引用"`
|
||||
}
|
||||
|
||||
// RequestTimeline 按精确 request ID 组合已持久化事实,不扫描 Access Log。
|
||||
|
||||
@@ -36,134 +36,137 @@ type ListFilter struct {
|
||||
|
||||
// ListPage 是按创建时间和主键稳定倒序的分页结果。
|
||||
type ListPage struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Items []ListItem `json:"items"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Total int64 `json:"total" description:"符合条件的外部交互总数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
Items []ListItem `json:"items" description:"按创建时间和主键稳定倒序的外部交互"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// ListItem 是 Integration Log 列表投影。
|
||||
type ListItem struct {
|
||||
IntegrationID string `json:"integration_id"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
Direction string `json:"direction"`
|
||||
DirectionName string `json:"direction_name"`
|
||||
Operation string `json:"operation"`
|
||||
OperationName string `json:"operation_name"`
|
||||
Resource ResourceView `json:"resource"`
|
||||
Result string `json:"result"`
|
||||
ResultName string `json:"result_name"`
|
||||
ResultCategory string `json:"result_category"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
StateChanged bool `json:"state_changed"`
|
||||
RequestID *string `json:"request_id"`
|
||||
CorrelationID *string `json:"correlation_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
IntegrationID string `json:"integration_id" description:"稳定外部集成记录ID,可传给详情接口"`
|
||||
Provider string `json:"provider" enum:"ctcc,cmcc,cucc,wechat_pay,alipay,fuiou,wecom,gateway" description:"外部服务提供方稳定编码"`
|
||||
ProviderName string `json:"provider_name" description:"provider对应的中文展示名称"`
|
||||
Direction string `json:"direction" enum:"inbound,outbound" description:"交互方向稳定编码"`
|
||||
DirectionName string `json:"direction_name" description:"direction对应的中文展示名称"`
|
||||
Operation string `json:"operation" enum:"realname_callback,realname_removal_callback,payment_precreate,payment_query,payment_callback,get_access_token,list_visible_members,list_visible_departments,get_template_detail,upload_approval_attachment,submit_approval,approval_callback,get_approval_detail,get_approval_info,query_realname_status,query_flow,query_card_status,query_device_info,set_speed_tier,stop_card,start_card,set_device_wifi,set_device_switch_mode,switch_device_card,reboot_device,reset_device" description:"外部操作稳定编码,可直接用于列表筛选"`
|
||||
OperationName string `json:"operation_name" description:"operation对应的中文展示名称"`
|
||||
Resource ResourceView `json:"resource" description:"外部交互直接关联的本地主要资源"`
|
||||
Result string `json:"result" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"外部交互原始结果稳定编码"`
|
||||
ResultName string `json:"result_name" description:"result对应的中文展示名称"`
|
||||
ResultCategory string `json:"result_category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"由result派生的固定结果类别"`
|
||||
DurationMS int64 `json:"duration_ms" description:"交互耗时,单位毫秒"`
|
||||
StateChanged bool `json:"state_changed" description:"本次交互是否改变本地业务状态"`
|
||||
RequestID *string `json:"request_id" description:"来源HTTP请求ID,可跳转请求时间线"`
|
||||
CorrelationID *string `json:"correlation_id" description:"跨步骤业务链路ID,可跳转关联时间线"`
|
||||
CreatedAt time.Time `json:"created_at" description:"外部交互记录创建时间"`
|
||||
}
|
||||
|
||||
// ResourceView 是外部交互直接主资源投影。
|
||||
type ResourceView struct {
|
||||
Type *string `json:"type"`
|
||||
ID *string `json:"id"`
|
||||
Key *string `json:"key"`
|
||||
Type *string `json:"type" description:"Resource Registry注册类型"`
|
||||
ID *string `json:"id" description:"资源内部稳定ID;type和id均有值时可跳转资源时间线"`
|
||||
Key *string `json:"key" description:"资源业务稳定Key"`
|
||||
}
|
||||
|
||||
// Detail 是按稳定 integration_id 返回的结构化详情。
|
||||
type Detail struct {
|
||||
Identity IdentityView `json:"identity"`
|
||||
Resource ResourceView `json:"resource"`
|
||||
Trigger TriggerView `json:"trigger"`
|
||||
Result ResultView `json:"result"`
|
||||
Content ContentView `json:"content"`
|
||||
Linkage LinkageView `json:"linkage"`
|
||||
Timestamps TimestampView `json:"timestamps"`
|
||||
Attempts []AttemptView `json:"attempts"`
|
||||
Fidelity FidelityView `json:"fidelity"`
|
||||
Identity IdentityView `json:"identity" description:"提供方、方向、操作和外部标识"`
|
||||
Resource ResourceView `json:"resource" description:"本地主要资源引用"`
|
||||
Trigger TriggerView `json:"trigger" description:"触发来源、场景和显式尝试序列"`
|
||||
Result ResultView `json:"result" description:"原始结果、派生类别和本地状态变化"`
|
||||
Content ContentView `json:"content" description:"已脱敏的结构化请求、响应和元数据摘要"`
|
||||
Linkage LinkageView `json:"linkage" description:"可跳转请求、关联和审计视角的稳定字段"`
|
||||
Timestamps TimestampView `json:"timestamps" description:"调度、开始、创建和更新时间"`
|
||||
Attempts []AttemptView `json:"attempts" description:"同一trigger_series下按attempt排序的技术尝试"`
|
||||
Fidelity FidelityView `json:"fidelity" description:"历史记录字段完整度,false时禁止前端猜测关联"`
|
||||
}
|
||||
|
||||
// DetailResponse 是外部交互详情及在线留存边界。
|
||||
type DetailResponse struct {
|
||||
Detail
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// AttemptView 是显式 trigger_series 下的单次技术尝试。
|
||||
type AttemptView struct {
|
||||
IntegrationID string `json:"integration_id"`
|
||||
Attempt int `json:"attempt"`
|
||||
Sent bool `json:"sent"`
|
||||
Result string `json:"result"`
|
||||
ResultName string `json:"result_name"`
|
||||
ResultCategory string `json:"result_category"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
StateChanged bool `json:"state_changed"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
IntegrationID string `json:"integration_id" description:"本次尝试的稳定外部集成记录ID"`
|
||||
Attempt int `json:"attempt" description:"同一显式序列内从1开始的尝试序号"`
|
||||
Operation string `json:"operation" enum:"realname_callback,realname_removal_callback,payment_precreate,payment_query,payment_callback,get_access_token,list_visible_members,list_visible_departments,get_template_detail,upload_approval_attachment,submit_approval,approval_callback,get_approval_detail,get_approval_info,query_realname_status,query_flow,query_card_status,query_device_info,set_speed_tier,stop_card,start_card,set_device_wifi,set_device_switch_mode,switch_device_card,reboot_device,reset_device" description:"外部操作稳定编码"`
|
||||
OperationName string `json:"operation_name" description:"operation对应的中文展示名称"`
|
||||
Sent bool `json:"sent" description:"是否实际向外部系统发送请求"`
|
||||
Result string `json:"result" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"本次尝试原始结果"`
|
||||
ResultName string `json:"result_name" description:"result对应的中文展示名称"`
|
||||
ResultCategory string `json:"result_category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"本次尝试的派生结果类别"`
|
||||
DurationMS int64 `json:"duration_ms" description:"本次尝试耗时,单位毫秒"`
|
||||
StateChanged bool `json:"state_changed" description:"本次尝试是否改变本地业务状态"`
|
||||
CreatedAt time.Time `json:"created_at" description:"本次尝试记录创建时间"`
|
||||
}
|
||||
|
||||
// FidelityView 明确历史记录可关联能力,不推断缺失字段。
|
||||
type FidelityView struct {
|
||||
TriggerSeriesAvailable bool `json:"trigger_series_available"`
|
||||
CorrelationAvailable bool `json:"correlation_available"`
|
||||
ResourceIDAvailable bool `json:"resource_id_available"`
|
||||
ProviderMessageFidelity string `json:"provider_message_fidelity"`
|
||||
TriggerSeriesAvailable bool `json:"trigger_series_available" description:"是否存在显式trigger_series"`
|
||||
AttemptSequenceReliable bool `json:"attempt_sequence_reliable" description:"attempt是否连续且operation一致"`
|
||||
CorrelationAvailable bool `json:"correlation_available" description:"是否存在稳定correlation_id"`
|
||||
ResourceIDAvailable bool `json:"resource_id_available" description:"是否存在稳定本地resource.id"`
|
||||
ProviderMessageFidelity string `json:"provider_message_fidelity" description:"外部消息保真等级;受限时只展示已脱敏摘要"`
|
||||
}
|
||||
|
||||
// IdentityView 是外部交互身份分组。
|
||||
type IdentityView struct {
|
||||
IntegrationID string `json:"integration_id"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
Direction string `json:"direction"`
|
||||
DirectionName string `json:"direction_name"`
|
||||
Operation string `json:"operation"`
|
||||
OperationName string `json:"operation_name"`
|
||||
ExternalID *string `json:"external_id"`
|
||||
IntegrationID string `json:"integration_id" description:"稳定外部集成记录ID"`
|
||||
Provider string `json:"provider" enum:"ctcc,cmcc,cucc,wechat_pay,alipay,fuiou,wecom,gateway" description:"外部服务提供方稳定编码"`
|
||||
ProviderName string `json:"provider_name" description:"provider对应的中文展示名称"`
|
||||
Direction string `json:"direction" enum:"inbound,outbound" description:"交互方向稳定编码"`
|
||||
DirectionName string `json:"direction_name" description:"direction对应的中文展示名称"`
|
||||
Operation string `json:"operation" enum:"realname_callback,realname_removal_callback,payment_precreate,payment_query,payment_callback,get_access_token,list_visible_members,list_visible_departments,get_template_detail,upload_approval_attachment,submit_approval,approval_callback,get_approval_detail,get_approval_info,query_realname_status,query_flow,query_card_status,query_device_info,set_speed_tier,stop_card,start_card,set_device_wifi,set_device_switch_mode,switch_device_card,reboot_device,reset_device" description:"外部操作稳定编码"`
|
||||
OperationName string `json:"operation_name" description:"operation对应的中文展示名称"`
|
||||
ExternalID *string `json:"external_id" description:"外部系统业务或请求标识"`
|
||||
}
|
||||
|
||||
// TriggerView 是外部交互触发分组。
|
||||
type TriggerView struct {
|
||||
Source *string `json:"source"`
|
||||
Scene *string `json:"scene"`
|
||||
Series *string `json:"series"`
|
||||
Attempt int `json:"attempt"`
|
||||
Source *string `json:"source" description:"触发来源稳定编码"`
|
||||
Scene *string `json:"scene" description:"触发业务场景"`
|
||||
Series *string `json:"series" description:"显式技术尝试序列ID;为空时禁止按时间或资源猜测重试关系"`
|
||||
Attempt int `json:"attempt" description:"显式序列内的尝试序号"`
|
||||
}
|
||||
|
||||
// ResultView 是外部交互结果分组。
|
||||
type ResultView struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
HTTPStatus *int `json:"http_status"`
|
||||
ProviderCode *string `json:"provider_code"`
|
||||
ProviderMessage *string `json:"provider_message"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
StateChanged bool `json:"state_changed"`
|
||||
RecoveryStrategy *string `json:"recovery_strategy"`
|
||||
Code string `json:"code" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"原始结果稳定编码"`
|
||||
Name string `json:"name" description:"code对应的中文展示名称"`
|
||||
Category string `json:"category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"由code派生的固定结果类别"`
|
||||
HTTPStatus *int `json:"http_status" description:"外部HTTP响应状态码"`
|
||||
ProviderCode *string `json:"provider_code" description:"外部服务稳定结果码"`
|
||||
ProviderMessage *string `json:"provider_message" description:"已脱敏的外部结果摘要"`
|
||||
DurationMS int64 `json:"duration_ms" description:"交互耗时,单位毫秒"`
|
||||
StateChanged bool `json:"state_changed" description:"是否改变本地业务状态"`
|
||||
RecoveryStrategy *string `json:"recovery_strategy" description:"已脱敏的既有恢复策略说明;本接口不执行恢复"`
|
||||
}
|
||||
|
||||
// ContentView 是已持久化安全摘要分组。
|
||||
type ContentView struct {
|
||||
RequestSummary map[string]any `json:"request_summary"`
|
||||
ResponseSummary map[string]any `json:"response_summary"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
ContentHash string `json:"content_hash"`
|
||||
RequestSummary map[string]any `json:"request_summary" description:"按白名单重新清理的请求摘要"`
|
||||
ResponseSummary map[string]any `json:"response_summary" description:"按白名单重新清理的响应摘要"`
|
||||
Metadata map[string]any `json:"metadata" description:"按白名单重新清理的扩展元数据"`
|
||||
ContentHash string `json:"content_hash" description:"持久化内容摘要"`
|
||||
}
|
||||
|
||||
// LinkageView 是外部交互关联分组。
|
||||
type LinkageView struct {
|
||||
RequestID *string `json:"request_id"`
|
||||
CorrelationID *string `json:"correlation_id"`
|
||||
AuditEventID *uint `json:"audit_event_id"`
|
||||
RequestID *string `json:"request_id" description:"传给GET /audit/requests/{request_id}/timeline"`
|
||||
CorrelationID *string `json:"correlation_id" description:"传给GET /audit/correlations/{correlation_id}/timeline"`
|
||||
AuditEventID *uint `json:"audit_event_id" description:"内部审计事件数据库引用;前端优先使用调查接口返回的稳定event_id"`
|
||||
}
|
||||
|
||||
// TimestampView 是外部交互时间分组。
|
||||
type TimestampView struct {
|
||||
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||
StartedAt *time.Time `json:"started_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" description:"计划发送时间"`
|
||||
StartedAt *time.Time `json:"started_at" description:"实际开始时间"`
|
||||
CreatedAt time.Time `json:"created_at" description:"记录创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"记录最后更新时间"`
|
||||
}
|
||||
|
||||
// Query 提供平台 Integration Log 列表和详情读取。
|
||||
@@ -253,7 +256,7 @@ func (q *Query) Get(ctx context.Context, integrationID string) (*DetailResponse,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attempts, err := q.loadAttempts(ctx, row, retention.OnlineFrom)
|
||||
attempts, attemptSequenceReliable, err := q.loadAttempts(ctx, row, retention.OnlineFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -261,13 +264,14 @@ func (q *Query) Get(ctx context.Context, integrationID string) (*DetailResponse,
|
||||
return &DetailResponse{Detail: Detail{
|
||||
Identity: IdentityView{IntegrationID: row.IntegrationID, Provider: row.Provider, ProviderName: constants.IntegrationProviderName(row.Provider), Direction: row.Direction, DirectionName: constants.IntegrationDirectionName(row.Direction), Operation: row.Operation, OperationName: constants.IntegrationOperationName(row.Operation), ExternalID: row.ExternalID},
|
||||
Resource: resourceView(row), Trigger: TriggerView{Source: row.TriggerSource, Scene: row.TriggerScene, Series: row.TriggerSeries, Attempt: row.Attempt},
|
||||
Result: ResultView{Code: row.Result, Name: constants.IntegrationResultName(row.Result), Category: constants.IntegrationResultCategory(row.Result), HTTPStatus: row.HTTPStatus, ProviderCode: row.ProviderCode, ProviderMessage: providerMessage, DurationMS: row.DurationMS, StateChanged: row.StateChanged, RecoveryStrategy: row.RecoveryStrategy},
|
||||
Result: ResultView{Code: row.Result, Name: constants.IntegrationResultName(row.Result), Category: constants.IntegrationResultCategory(row.Result), HTTPStatus: row.HTTPStatus, ProviderCode: row.ProviderCode, ProviderMessage: providerMessage, DurationMS: row.DurationMS, StateChanged: row.StateChanged, RecoveryStrategy: sanitizedTextPointer(row.RecoveryStrategy)},
|
||||
Content: ContentView{RequestSummary: requestSummary, ResponseSummary: responseSummary, Metadata: metadata, ContentHash: row.ContentHash},
|
||||
Linkage: LinkageView{RequestID: row.RequestID, CorrelationID: row.CorrelationID, AuditEventID: row.AuditEventID},
|
||||
Timestamps: TimestampView{ScheduledAt: row.ScheduledAt, StartedAt: row.StartedAt, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt},
|
||||
Attempts: attempts,
|
||||
Fidelity: FidelityView{
|
||||
TriggerSeriesAvailable: row.TriggerSeries != nil && *row.TriggerSeries != "",
|
||||
AttemptSequenceReliable: attemptSequenceReliable,
|
||||
CorrelationAvailable: row.CorrelationID != nil && *row.CorrelationID != "",
|
||||
ResourceIDAvailable: row.ResourceID != nil && *row.ResourceID != "",
|
||||
ProviderMessageFidelity: providerMessageFidelity,
|
||||
@@ -275,25 +279,31 @@ func (q *Query) Get(ctx context.Context, integrationID string) (*DetailResponse,
|
||||
}, Retention: retention}, nil
|
||||
}
|
||||
|
||||
func (q *Query) loadAttempts(ctx context.Context, current model.IntegrationLog, onlineFrom time.Time) ([]AttemptView, error) {
|
||||
func (q *Query) loadAttempts(ctx context.Context, current model.IntegrationLog, onlineFrom time.Time) ([]AttemptView, bool, error) {
|
||||
rows := []model.IntegrationLog{current}
|
||||
if current.TriggerSeries != nil && *current.TriggerSeries != "" {
|
||||
if err := q.db.WithContext(ctx).Where("trigger_series = ? AND created_at >= ?", *current.TriggerSeries, onlineFrom.UTC()).
|
||||
Order("attempt ASC, created_at ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询外部交互尝试序列失败")
|
||||
return nil, false, errors.Wrap(errors.CodeDatabaseError, err, "查询外部交互尝试序列失败")
|
||||
}
|
||||
}
|
||||
items := make([]AttemptView, len(rows))
|
||||
reliable := current.TriggerSeries != nil && *current.TriggerSeries != ""
|
||||
expectedAttempt := 1
|
||||
for index, row := range rows {
|
||||
category := constants.IntegrationResultCategory(row.Result)
|
||||
items[index] = AttemptView{
|
||||
IntegrationID: row.IntegrationID, Attempt: row.Attempt,
|
||||
IntegrationID: row.IntegrationID, Attempt: row.Attempt, Operation: row.Operation, OperationName: constants.IntegrationOperationName(row.Operation),
|
||||
Sent: category != constants.IntegrationResultCategoryNotSent,
|
||||
Result: row.Result, ResultName: constants.IntegrationResultName(row.Result), ResultCategory: category,
|
||||
DurationMS: row.DurationMS, StateChanged: row.StateChanged, CreatedAt: row.CreatedAt,
|
||||
}
|
||||
if row.Operation != current.Operation || row.Attempt != expectedAttempt {
|
||||
reliable = false
|
||||
}
|
||||
expectedAttempt = row.Attempt + 1
|
||||
}
|
||||
return items, nil
|
||||
return items, reliable, nil
|
||||
}
|
||||
|
||||
func (q *Query) authorize(ctx context.Context) error {
|
||||
@@ -418,12 +428,23 @@ func safeProviderMessage(value *string) (*string, string) {
|
||||
}
|
||||
if len(*value) >= len(constants.IntegrationSafeMessagePrefix) && (*value)[:len(constants.IntegrationSafeMessagePrefix)] == constants.IntegrationSafeMessagePrefix {
|
||||
message := (*value)[len(constants.IntegrationSafeMessagePrefix):]
|
||||
if sanitized := sanitizer.SanitizeText(message); sanitized != message {
|
||||
return &sanitized, "redacted"
|
||||
}
|
||||
return &message, "readable"
|
||||
}
|
||||
summary := sanitizer.TextSummary(*value)
|
||||
return &summary, "historical_redacted"
|
||||
}
|
||||
|
||||
func sanitizedTextPointer(value *string) *string {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
sanitized := sanitizer.SanitizeText(*value)
|
||||
return &sanitized
|
||||
}
|
||||
|
||||
func normalizePage(page, pageSize int) (int, int) {
|
||||
if page < 1 {
|
||||
page = constants.DefaultPage
|
||||
|
||||
@@ -20,44 +20,44 @@ type OverviewFilter struct {
|
||||
|
||||
// Overview 是外部交互固定维度聚合结果。
|
||||
type Overview struct {
|
||||
Total int64 `json:"total"`
|
||||
AnomalyCount int64 `json:"anomaly_count"`
|
||||
UnknownCount int64 `json:"unknown_count"`
|
||||
StalePendingCount int64 `json:"stale_pending_count"`
|
||||
StateChangedCount int64 `json:"state_changed_count"`
|
||||
AverageDurationMS float64 `json:"average_duration_ms"`
|
||||
P95DurationMS float64 `json:"p95_duration_ms"`
|
||||
Results []ResultCount `json:"results"`
|
||||
Providers []NamedCount `json:"providers"`
|
||||
Directions []NamedCount `json:"directions"`
|
||||
Trend []TrendPoint `json:"trend"`
|
||||
Retention retentionquery.Info `json:"retention"`
|
||||
Total int64 `json:"total" description:"外部交互总数"`
|
||||
AnomalyCount int64 `json:"anomaly_count" description:"failed或indeterminate类别的异常交互数"`
|
||||
UnknownCount int64 `json:"unknown_count" description:"原始结果为unknown的交互数"`
|
||||
StalePendingCount int64 `json:"stale_pending_count" description:"超过既定阈值仍为pending的交互数"`
|
||||
StateChangedCount int64 `json:"state_changed_count" description:"改变本地业务状态的交互数"`
|
||||
AverageDurationMS float64 `json:"average_duration_ms" description:"平均交互耗时,单位毫秒"`
|
||||
P95DurationMS float64 `json:"p95_duration_ms" description:"P95交互耗时,单位毫秒"`
|
||||
Results []ResultCount `json:"results" description:"原始结果分布,code/name/category分别为稳定编码、中文名和派生类别"`
|
||||
Providers []NamedCount `json:"providers" description:"提供方分布,code为provider枚举,name为中文展示名"`
|
||||
Directions []NamedCount `json:"directions" description:"方向分布,code为inbound或outbound,name为中文展示名"`
|
||||
Trend []TrendPoint `json:"trend" description:"五类派生结果的时间趋势"`
|
||||
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
|
||||
}
|
||||
|
||||
// ResultCount 是原始结果及其派生类别计数。
|
||||
type ResultCount struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Count int64 `json:"count"`
|
||||
Code string `json:"code" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"外部交互原始结果稳定编码"`
|
||||
Name string `json:"name" description:"code对应的中文展示名称"`
|
||||
Category string `json:"category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"由code派生的固定结果类别"`
|
||||
Count int64 `json:"count" description:"该原始结果的交互数量"`
|
||||
}
|
||||
|
||||
// NamedCount 是稳定编码、中文名称和数量。
|
||||
type NamedCount struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Count int64 `json:"count"`
|
||||
Code string `json:"code" description:"当前聚合维度的稳定编码,枚举域由所属数组字段说明"`
|
||||
Name string `json:"name" description:"code对应的中文展示名称"`
|
||||
Count int64 `json:"count" description:"该编码的交互数量"`
|
||||
}
|
||||
|
||||
// TrendPoint 是固定时间桶内的结果类别趋势。
|
||||
type TrendPoint struct {
|
||||
BucketAt time.Time `json:"bucket_at"`
|
||||
Total int64 `json:"total"`
|
||||
Succeeded int64 `json:"succeeded"`
|
||||
Processing int64 `json:"processing"`
|
||||
Indeterminate int64 `json:"indeterminate"`
|
||||
Failed int64 `json:"failed"`
|
||||
NotSent int64 `json:"not_sent"`
|
||||
BucketAt time.Time `json:"bucket_at" description:"时间桶起点"`
|
||||
Total int64 `json:"total" description:"桶内外部交互总数"`
|
||||
Succeeded int64 `json:"succeeded" description:"桶内succeeded类别数量"`
|
||||
Processing int64 `json:"processing" description:"桶内processing类别数量"`
|
||||
Indeterminate int64 `json:"indeterminate" description:"桶内indeterminate类别数量"`
|
||||
Failed int64 `json:"failed" description:"桶内failed类别数量"`
|
||||
NotSent int64 `json:"not_sent" description:"桶内not_sent类别数量"`
|
||||
}
|
||||
|
||||
// Overview 查询指定时间范围的固定维度外部交互总览。
|
||||
|
||||
Reference in New Issue
Block a user