Files
break c64f3d8b80
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s
全局审计完成
2026-08-07 11:02:52 +08:00

457 lines
25 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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:"资源内部稳定IDtype和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
}