全局审计完成
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。