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

@@ -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 查询指定时间范围的固定维度外部交互总览。