全局审计完成
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s

This commit is contained in:
2026-08-07 11:02:52 +08:00
parent 88cc5e96ec
commit c64f3d8b80
94 changed files with 8641 additions and 6714 deletions

View File

@@ -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}/timelineresource_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 提供平台统一审计事件列表与详情读取。

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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或unknownname为中文展示名"`
Risks []RiskNamedCount `json:"risks" description:"风险等级分布code为low、normal、high或criticalname为中文展示名"`
Results []RiskNamedCount `json:"results" description:"结果分布code为success、failed、denied、partial或unknownname为中文展示名"`
Actions []RiskNamedCount `json:"actions" description:"动作分布code为稳定action_codename为中文action_name"`
Sources []RiskNamedCount `json:"sources" description:"来源分布code为admin_api、personal_api、openapi、worker、scheduler或callbackname为中文展示名"`
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 查询指定时间范围内的固定风险调查总览。

View File

@@ -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,

View File

@@ -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。

View File

@@ -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:"资源内部稳定IDtype和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

View File

@@ -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或outboundname为中文展示名"`
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 查询指定时间范围的固定维度外部交互总览。