All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s
457 lines
25 KiB
Go
457 lines
25 KiB
Go
// Package integration 提供 Integration Log 只读调查投影。
|
||
package integration
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"time"
|
||
|
||
"github.com/bytedance/sonic"
|
||
"gorm.io/datatypes"
|
||
"gorm.io/gorm"
|
||
|
||
"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"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
"github.com/break/junhong_cmp_fiber/pkg/sanitizer"
|
||
pkgvalidator "github.com/break/junhong_cmp_fiber/pkg/validator"
|
||
)
|
||
|
||
// ListFilter 定义 Integration Log 组合筛选。
|
||
type ListFilter struct {
|
||
CreatedFrom, CreatedTo *time.Time
|
||
IntegrationID, Provider, Direction string
|
||
Operation, Result, ResultCategory string
|
||
ExternalID, ResourceType, ResourceID string
|
||
ResourceKey, TriggerSource, TriggerScene string
|
||
TriggerSeries, ProviderCode string
|
||
RequestID, CorrelationID string
|
||
StateChanged *bool
|
||
HTTPStatus *int
|
||
Page, PageSize int
|
||
}
|
||
|
||
// ListPage 是按创建时间和主键稳定倒序的分页结果。
|
||
type ListPage struct {
|
||
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" 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" description:"Resource Registry注册类型"`
|
||
ID *string `json:"id" description:"资源内部稳定ID;type和id均有值时可跳转资源时间线"`
|
||
Key *string `json:"key" description:"资源业务稳定Key"`
|
||
}
|
||
|
||
// Detail 是按稳定 integration_id 返回的结构化详情。
|
||
type Detail struct {
|
||
Identity IdentityView `json:"identity" 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" description:"在线查询留存边界"`
|
||
}
|
||
|
||
// AttemptView 是显式 trigger_series 下的单次技术尝试。
|
||
type AttemptView struct {
|
||
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" 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" 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" 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" 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" 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" 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" 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 列表和详情读取。
|
||
type Query struct{ db *gorm.DB }
|
||
|
||
// New 创建 Integration Log 调查 Query。
|
||
func New(db *gorm.DB) *Query { return &Query{db: db} }
|
||
|
||
// List 查询受时间范围约束的 Integration Log 列表。
|
||
func (q *Query) List(ctx context.Context, filter ListFilter) (*ListPage, error) {
|
||
if err := q.authorize(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
filter, retention, err := q.normalizeOnlineFilter(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !validFilter(filter) {
|
||
return nil, errors.New(errors.CodeInvalidParam)
|
||
}
|
||
filter.Page, filter.PageSize = normalizePage(filter.Page, filter.PageSize)
|
||
query := applyFilters(q.db.WithContext(ctx).Model(&model.IntegrationLog{}), filter)
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计外部交互日志失败")
|
||
}
|
||
rows, err := q.loadListPage(ctx, query, filter.Page, filter.PageSize)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
items := make([]ListItem, len(rows))
|
||
for i, row := range rows {
|
||
items[i] = projectListItem(row)
|
||
}
|
||
return &ListPage{Total: total, Page: filter.Page, PageSize: filter.PageSize, Items: items, Retention: retention}, nil
|
||
}
|
||
|
||
// loadListPage 先分页主键,再批量读取列表字段,避免加载正文摘要 JSON。
|
||
func (q *Query) loadListPage(ctx context.Context, query *gorm.DB, page, pageSize int) ([]model.IntegrationLog, error) {
|
||
ids := make([]uint, 0, pageSize)
|
||
if err := query.Select("id").Order("created_at DESC, id DESC").
|
||
Offset((page-1)*pageSize).Limit(pageSize).Pluck("id", &ids).Error; err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询外部交互日志分页ID失败")
|
||
}
|
||
rows := make([]model.IntegrationLog, 0, len(ids))
|
||
if len(ids) == 0 {
|
||
return rows, nil
|
||
}
|
||
if err := q.db.WithContext(ctx).Select(
|
||
"id", "integration_id", "provider", "direction", "operation",
|
||
"resource_type", "resource_id", "resource_key", "result", "duration_ms",
|
||
"state_changed", "request_id", "correlation_id", "created_at",
|
||
).Where("id IN ?", ids).Order("created_at DESC, id DESC").Find(&rows).Error; err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量投影外部交互日志失败")
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// Get 使用稳定 integration_id 查询结构化详情。
|
||
func (q *Query) Get(ctx context.Context, integrationID string) (*DetailResponse, error) {
|
||
if err := q.authorize(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if integrationID == "" {
|
||
return nil, errors.New(errors.CodeInvalidParam)
|
||
}
|
||
retention, err := retentionquery.Load(ctx, q.db, retentionquery.SourceIntegration)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var row model.IntegrationLog
|
||
if err := q.db.WithContext(ctx).Where("integration_id = ? AND created_at >= ?", integrationID, retention.OnlineFrom.UTC()).First(&row).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "外部交互日志不存在")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询外部交互日志详情失败")
|
||
}
|
||
requestSummary, err := decodeObject(row.RequestSummary)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
responseSummary, err := decodeObject(row.ResponseSummary)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
metadata, err := decodeObject(row.Metadata)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
attempts, attemptSequenceReliable, err := q.loadAttempts(ctx, row, retention.OnlineFrom)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
providerMessage, providerMessageFidelity := safeProviderMessage(row.ProviderMessage)
|
||
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: 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,
|
||
},
|
||
}, Retention: retention}, nil
|
||
}
|
||
|
||
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, 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, 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, reliable, nil
|
||
}
|
||
|
||
func (q *Query) authorize(ctx context.Context) error {
|
||
if q == nil || q.db == nil {
|
||
return errors.New(errors.CodeServiceUnavailable, "外部交互调查能力未配置")
|
||
}
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (q *Query) normalizeOnlineFilter(ctx context.Context, filter ListFilter) (ListFilter, retentionquery.Info, error) {
|
||
retention, err := retentionquery.Load(ctx, q.db, retentionquery.SourceIntegration)
|
||
if err != nil {
|
||
return filter, retention, err
|
||
}
|
||
filter.CreatedFrom, filter.CreatedTo, err = retentionquery.NormalizeRange(retention, filter.CreatedFrom, filter.CreatedTo, constants.IntegrationQueryMaxRange)
|
||
return filter, retention, err
|
||
}
|
||
|
||
func validFilter(filter ListFilter) bool {
|
||
if filter.CreatedFrom == nil || filter.CreatedTo == nil || !filter.CreatedFrom.Before(*filter.CreatedTo) || filter.CreatedTo.Sub(*filter.CreatedFrom) > constants.IntegrationQueryMaxRange {
|
||
return false
|
||
}
|
||
if filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize {
|
||
return false
|
||
}
|
||
if filter.Direction != "" && filter.Direction != constants.IntegrationDirectionInbound && filter.Direction != constants.IntegrationDirectionOutbound {
|
||
return false
|
||
}
|
||
if filter.Result != "" && constants.IntegrationResultName(filter.Result) == "" {
|
||
return false
|
||
}
|
||
if filter.ResultCategory != "" && len(categoryResults(filter.ResultCategory)) == 0 {
|
||
return false
|
||
}
|
||
return filter.HTTPStatus == nil || (*filter.HTTPStatus >= 100 && *filter.HTTPStatus <= 599)
|
||
}
|
||
|
||
func applyFilters(query *gorm.DB, filter ListFilter) *gorm.DB {
|
||
query = query.Where("created_at >= ? AND created_at < ?", filter.CreatedFrom.UTC(), filter.CreatedTo.UTC())
|
||
for column, value := range map[string]string{
|
||
"integration_id": filter.IntegrationID, "provider": filter.Provider, "direction": filter.Direction,
|
||
"operation": filter.Operation, "result": filter.Result, "external_id": filter.ExternalID,
|
||
"resource_type": filter.ResourceType, "resource_id": filter.ResourceID,
|
||
"trigger_source": filter.TriggerSource, "trigger_scene": filter.TriggerScene, "trigger_series": filter.TriggerSeries,
|
||
"provider_code": filter.ProviderCode, "request_id": filter.RequestID, "correlation_id": filter.CorrelationID,
|
||
} {
|
||
if value != "" {
|
||
query = query.Where(column+" = ?", value)
|
||
}
|
||
}
|
||
if filter.ResourceKey != "" {
|
||
query = query.Where("resource_key IN ?", compatibleResourceKeys(filter.ResourceType, filter.ResourceKey))
|
||
}
|
||
if filter.ResultCategory != "" {
|
||
query = query.Where("result IN ?", categoryResults(filter.ResultCategory))
|
||
}
|
||
if filter.StateChanged != nil {
|
||
query = query.Where("state_changed = ?", *filter.StateChanged)
|
||
}
|
||
if filter.HTTPStatus != nil {
|
||
query = query.Where("http_status = ?", *filter.HTTPStatus)
|
||
}
|
||
return query
|
||
}
|
||
|
||
func compatibleResourceKeys(resourceType, resourceKey string) []string {
|
||
keys := []string{resourceKey}
|
||
if resourceType != constants.AssetTypeIotCard || !pkgvalidator.ValidateICCIDWithoutCarrier(resourceKey).Valid {
|
||
return keys
|
||
}
|
||
sum := sha256.Sum256([]byte(resourceKey))
|
||
return append(keys, "iccid-sha256:"+hex.EncodeToString(sum[:])[:32])
|
||
}
|
||
|
||
func categoryResults(category string) []string {
|
||
switch category {
|
||
case constants.IntegrationResultCategoryProcessing:
|
||
return []string{constants.IntegrationResultPending}
|
||
case constants.IntegrationResultCategorySucceeded:
|
||
return []string{constants.IntegrationResultSuccess}
|
||
case constants.IntegrationResultCategoryIndeterminate:
|
||
return []string{constants.IntegrationResultUnknown}
|
||
case constants.IntegrationResultCategoryFailed:
|
||
return []string{constants.IntegrationResultFailed, constants.IntegrationResultNotFound, constants.IntegrationResultInvalidPayload, constants.IntegrationResultConflict}
|
||
case constants.IntegrationResultCategoryNotSent:
|
||
return []string{constants.IntegrationResultIgnored, constants.IntegrationResultMerged, constants.IntegrationResultRateLimited, constants.IntegrationResultCompleted, constants.IntegrationResultCancelled}
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func projectListItem(row model.IntegrationLog) ListItem {
|
||
return ListItem{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), Resource: resourceView(row), Result: row.Result, ResultName: constants.IntegrationResultName(row.Result), ResultCategory: constants.IntegrationResultCategory(row.Result), DurationMS: row.DurationMS, StateChanged: row.StateChanged, RequestID: row.RequestID, CorrelationID: row.CorrelationID, CreatedAt: row.CreatedAt}
|
||
}
|
||
|
||
func resourceView(row model.IntegrationLog) ResourceView {
|
||
return ResourceView{Type: row.ResourceType, ID: row.ResourceID, Key: row.ResourceKey}
|
||
}
|
||
|
||
func decodeObject(value datatypes.JSON) (map[string]any, error) {
|
||
result := map[string]any{}
|
||
if len(value) == 0 {
|
||
return result, nil
|
||
}
|
||
if err := sonic.Unmarshal(value, &result); err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "解析外部交互结构化摘要失败")
|
||
}
|
||
sanitizer.RemoveForbiddenFields(result)
|
||
return result, nil
|
||
}
|
||
|
||
func safeProviderMessage(value *string) (*string, string) {
|
||
if value == nil || *value == "" {
|
||
return nil, "missing"
|
||
}
|
||
if len(*value) >= len("外部文本摘要") && (*value)[:len("外部文本摘要")] == "外部文本摘要" {
|
||
return value, "historical_summary"
|
||
}
|
||
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
|
||
}
|
||
if pageSize < 1 {
|
||
pageSize = constants.DefaultPageSize
|
||
}
|
||
return page, pageSize
|
||
}
|