This commit is contained in:
@@ -19,6 +19,11 @@ func (w *Writer) AppendBatch(ctx context.Context, tx *gorm.DB, input BatchInput)
|
||||
if input.Root.EventID == "" {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidParam, "批次根事件缺少稳定事件ID")
|
||||
}
|
||||
if input.Root.BatchTotal < 0 || input.Root.SuccessCount < 0 || input.Root.FailCount < 0 ||
|
||||
input.Root.SuccessCount+input.Root.FailCount > input.Root.BatchTotal ||
|
||||
len(input.Children) < input.Root.SuccessCount || len(input.Children) > input.Root.SuccessCount+input.Root.FailCount {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidParam, "批次根子事件计数不一致")
|
||||
}
|
||||
if err := w.Append(ctx, tx, input.Root); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ func NewRegistry() *Registry {
|
||||
iotCardCreated := iotCardAction(constants.AuditActionIotCardCreated, "创建 IoT 卡", constants.AuditActorSystemTask, constants.AuditSourceWorker)
|
||||
iotCardDeleted := iotCardAction(constants.AuditActionIotCardDeleted, "删除 IoT 卡", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
|
||||
iotCardDeleted.Risk = constants.AuditRiskHigh
|
||||
iotCardDeactivated := iotCardAction(constants.AuditActionIotCardDeactivated, "停用 IoT 卡资产", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
|
||||
iotCardDeactivated.Risk = constants.AuditRiskHigh
|
||||
iotCardPollingStatusUpdated := iotCardAction(constants.AuditActionIotCardPollingStatusUpdated, "更新 IoT 卡轮询开关", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
|
||||
iotCardPollingStatusBatchUpdated := iotCardBatchAction(constants.AuditActionIotCardPollingStatusBatchUpdated, "批量更新 IoT 卡轮询开关")
|
||||
iotCardBatchDeleted := ActionDefinition{
|
||||
Code: constants.AuditActionIotCardBatchDeleted, Name: "批量删除 IoT 卡",
|
||||
Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskHigh,
|
||||
@@ -117,6 +121,9 @@ func NewRegistry() *Registry {
|
||||
deviceCreated := deviceAction(constants.AuditActionDeviceCreated, "导入创建设备", constants.AuditActorSystemTask, constants.AuditSourceWorker)
|
||||
deviceDeleted := deviceAction(constants.AuditActionDeviceDeleted, "删除设备", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
|
||||
deviceDeleted.Risk = constants.AuditRiskHigh
|
||||
deviceDeactivated := deviceAction(constants.AuditActionDeviceDeactivated, "停用设备资产", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
|
||||
deviceDeactivated.Risk = constants.AuditRiskHigh
|
||||
devicePollingStatusUpdated := deviceAction(constants.AuditActionDevicePollingStatusUpdated, "更新设备轮询开关", constants.AuditActorAccount, constants.AuditSourceAdminAPI)
|
||||
deviceAllocationBatch := deviceMultiOriginBatchAction(constants.AuditActionDeviceAllocationBatch, "批量分配设备")
|
||||
deviceAllocated := deviceMultiOriginAction(constants.AuditActionDeviceAllocated, "分配设备")
|
||||
deviceRecallBatch := deviceMultiOriginBatchAction(constants.AuditActionDeviceRecallBatch, "批量回收设备")
|
||||
@@ -180,17 +187,6 @@ func NewRegistry() *Registry {
|
||||
constants.AuditActionOutboxExpiredLeaseReleased,
|
||||
"人工释放 Outbox 过期租约",
|
||||
)
|
||||
deviceBatchCompleted := deviceBatchAction(
|
||||
constants.AuditActionDeviceBatchAllocationCompleted,
|
||||
"完成设备批量分配",
|
||||
constants.AuditResourceDeviceBatchTask,
|
||||
)
|
||||
deviceBatchItem := deviceBatchAction(
|
||||
constants.AuditActionDeviceBatchAllocationItem,
|
||||
"处理设备批量分配项",
|
||||
constants.AuditResourceDevice,
|
||||
)
|
||||
deviceBatchItem.AllowedVisibility = []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult}
|
||||
iotCardImportTaskCreated := taskAction(constants.AuditActionIotCardImportTaskCreated, "创建 IoT 卡导入任务", constants.AuditResourceIotCardImportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
|
||||
iotCardImportTaskCompleted := taskAction(constants.AuditActionIotCardImportTaskCompleted, "完成 IoT 卡导入任务", constants.AuditResourceIotCardImportTask, constants.AuditActorSystemTask, constants.AuditSourceWorker)
|
||||
deviceImportTaskCreated := taskAction(constants.AuditActionDeviceImportTaskCreated, "创建设备导入任务", constants.AuditResourceDeviceImportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
|
||||
@@ -274,6 +270,8 @@ func NewRegistry() *Registry {
|
||||
packageUsageTrafficReset := packageUsageAction(constants.AuditActionPackageUsageTrafficReset, "重置套餐权益流量")
|
||||
packageUsageRefundInvalidated := packageUsageAction(constants.AuditActionPackageUsageRefundInvalidated, "退款失效套餐权益")
|
||||
packageUsageAssetInvalidated := packageUsageAction(constants.AuditActionPackageUsageAssetInvalidated, "资产失效套餐权益")
|
||||
packageUsageExpiresAtUpdated := packageUsageAction(constants.AuditActionPackageUsageExpiresAtUpdated, "调整套餐权益过期时间")
|
||||
packageUsageTrafficAdjusted := packageUsageAction(constants.AuditActionPackageUsageTrafficAdjusted, "调整套餐权益已用量")
|
||||
orderCreated := orderAction(constants.AuditActionOrderCreated, "创建订单")
|
||||
orderCancelled := orderAction(constants.AuditActionOrderCancelled, "取消订单")
|
||||
orderWalletPaid := orderAction(constants.AuditActionOrderWalletPaid, "钱包支付订单")
|
||||
@@ -405,6 +403,9 @@ func NewRegistry() *Registry {
|
||||
constants.AuditActionPersonalCustomerAssetBindingMigrated: personalAssetBindingMigrated,
|
||||
constants.AuditActionIotCardCreated: iotCardCreated,
|
||||
constants.AuditActionIotCardDeleted: iotCardDeleted,
|
||||
constants.AuditActionIotCardDeactivated: iotCardDeactivated,
|
||||
constants.AuditActionIotCardPollingStatusUpdated: iotCardPollingStatusUpdated,
|
||||
constants.AuditActionIotCardPollingStatusBatchUpdated: iotCardPollingStatusBatchUpdated,
|
||||
constants.AuditActionIotCardBatchDeleted: iotCardBatchDeleted,
|
||||
constants.AuditActionIotCardAllocationBatch: iotCardAllocationBatch,
|
||||
constants.AuditActionIotCardAllocated: iotCardAllocated,
|
||||
@@ -430,6 +431,8 @@ func NewRegistry() *Registry {
|
||||
constants.AuditActionIotCardAutoStopReasonUpdated: iotCardAutoStopReasonUpdated,
|
||||
constants.AuditActionDeviceCreated: deviceCreated,
|
||||
constants.AuditActionDeviceDeleted: deviceDeleted,
|
||||
constants.AuditActionDeviceDeactivated: deviceDeactivated,
|
||||
constants.AuditActionDevicePollingStatusUpdated: devicePollingStatusUpdated,
|
||||
constants.AuditActionDeviceAllocationBatch: deviceAllocationBatch,
|
||||
constants.AuditActionDeviceAllocated: deviceAllocated,
|
||||
constants.AuditActionDeviceRecallBatch: deviceRecallBatch,
|
||||
@@ -476,8 +479,6 @@ func NewRegistry() *Registry {
|
||||
constants.AuditActionWeComApprovalSceneSaved: wecomApprovalSceneSaved,
|
||||
constants.AuditActionOutboxReplayed: outboxReplayed,
|
||||
constants.AuditActionOutboxExpiredLeaseReleased: outboxExpiredLeaseReleased,
|
||||
constants.AuditActionDeviceBatchAllocationCompleted: deviceBatchCompleted,
|
||||
constants.AuditActionDeviceBatchAllocationItem: deviceBatchItem,
|
||||
constants.AuditActionIotCardImportTaskCreated: iotCardImportTaskCreated,
|
||||
constants.AuditActionIotCardImportTaskCompleted: iotCardImportTaskCompleted,
|
||||
constants.AuditActionDeviceImportTaskCreated: deviceImportTaskCreated,
|
||||
@@ -546,6 +547,8 @@ func NewRegistry() *Registry {
|
||||
constants.AuditActionPackageUsageTrafficReset: packageUsageTrafficReset,
|
||||
constants.AuditActionPackageUsageRefundInvalidated: packageUsageRefundInvalidated,
|
||||
constants.AuditActionPackageUsageAssetInvalidated: packageUsageAssetInvalidated,
|
||||
constants.AuditActionPackageUsageExpiresAtUpdated: packageUsageExpiresAtUpdated,
|
||||
constants.AuditActionPackageUsageTrafficAdjusted: packageUsageTrafficAdjusted,
|
||||
constants.AuditActionOrderCreated: orderCreated,
|
||||
constants.AuditActionOrderCancelled: orderCancelled,
|
||||
constants.AuditActionOrderWalletPaid: orderWalletPaid,
|
||||
|
||||
@@ -900,14 +900,14 @@ func (w *Writer) Append(ctx context.Context, tx *gorm.DB, input AppendInput) err
|
||||
}
|
||||
event := model.AuditEvent{
|
||||
OccurredAt: occurredAt, Category: action.Category, ActionCode: action.Code, ActionName: action.Name,
|
||||
Summary: input.Summary, ActorKind: input.Actor.Kind, ActorID: input.Actor.ID, ActorName: input.Actor.Name,
|
||||
ActorShopID: input.Actor.ShopID, ActorShopName: input.Actor.ShopName,
|
||||
ActorEnterpriseID: input.Actor.EnterpriseID, ActorEnterpriseName: input.Actor.EnterpriseName,
|
||||
Source: input.Source, RequestPath: input.RequestPath, RequestMethod: input.RequestMethod,
|
||||
IPAddress: input.IPAddress, UserAgent: input.UserAgent,
|
||||
ScopeType: input.ScopeType, ScopeID: input.ScopeID, ScopeName: input.ScopeName,
|
||||
Summary: sanitizer.SanitizeText(input.Summary), ActorKind: input.Actor.Kind, ActorID: input.Actor.ID, ActorName: sanitizer.SanitizeText(input.Actor.Name),
|
||||
ActorShopID: input.Actor.ShopID, ActorShopName: sanitizer.SanitizeText(input.Actor.ShopName),
|
||||
ActorEnterpriseID: input.Actor.EnterpriseID, ActorEnterpriseName: sanitizer.SanitizeText(input.Actor.EnterpriseName),
|
||||
Source: input.Source, RequestPath: sanitizer.SanitizeText(input.RequestPath), RequestMethod: input.RequestMethod,
|
||||
IPAddress: input.IPAddress, UserAgent: sanitizer.SanitizeText(input.UserAgent),
|
||||
ScopeType: input.ScopeType, ScopeID: input.ScopeID, ScopeName: sanitizer.SanitizeText(input.ScopeName),
|
||||
Result: input.Result, RiskLevel: action.Risk, RequestID: input.RequestID,
|
||||
ErrorCode: input.ErrorCode, ErrorSummary: input.ErrorSummary,
|
||||
ErrorCode: input.ErrorCode, ErrorSummary: sanitizer.SanitizeText(input.ErrorSummary),
|
||||
CorrelationID: input.CorrelationID, ParentEventID: input.ParentEventID, Metadata: metadata,
|
||||
BatchTotal: input.BatchTotal, SuccessCount: input.SuccessCount, FailCount: input.FailCount,
|
||||
}
|
||||
@@ -1036,10 +1036,10 @@ func (w *Writer) buildResources(inputs []ResourceInput, action ActionDefinition)
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, model.AuditEventResource{
|
||||
ResourceType: input.Type, ResourceID: input.ID, ResourceKey: input.Key, DisplayName: input.DisplayName,
|
||||
ResourceType: input.Type, ResourceID: input.ID, ResourceKey: sanitizer.SanitizeText(input.Key), DisplayName: sanitizer.SanitizeText(input.DisplayName),
|
||||
Relation: input.Relation, Role: input.Role, IdentitySnapshot: identity,
|
||||
BeforeData: before, AfterData: after, SubjectVisibility: visibility,
|
||||
SubjectSummary: input.SubjectSummary, SubjectData: subjectData, SortOrder: input.SortOrder,
|
||||
SubjectSummary: sanitizer.SanitizeText(input.SubjectSummary), SubjectData: subjectData, SortOrder: input.SortOrder,
|
||||
})
|
||||
}
|
||||
if primaryCount != 1 {
|
||||
|
||||
@@ -37,7 +37,7 @@ func (l *SeriesAttemptLogger) Record(ctx context.Context, payload cardapp.Series
|
||||
return apperrors.New(apperrors.CodeInternalError, "卡观测 Integration Log 未配置")
|
||||
}
|
||||
resourceID := payload.ResourceID
|
||||
source, scene, seriesID := payload.Source, payload.Scene, payload.SeriesID
|
||||
source, scene, seriesID := payload.Source, payload.Scene, payload.SeriesID+":"+payload.SyncType
|
||||
requestID, correlationID := optionalText(payload.RequestID), optionalText(payload.CorrelationID)
|
||||
attempt, err := l.repository.Start(ctx, integrationlog.Attempt{
|
||||
IntegrationID: unsentIntegrationID(payload), Provider: constants.IntegrationProviderGateway,
|
||||
@@ -59,12 +59,12 @@ func (l *SeriesAttemptLogger) Record(ctx context.Context, payload cardapp.Series
|
||||
// RecordMerged 记录同场景重复触发被合并,不创建第二组三任务。
|
||||
func (l *SeriesAttemptLogger) RecordMerged(ctx context.Context, request cardapp.SeriesRequest, seriesID string) error {
|
||||
resourceID := request.ResourceID
|
||||
source, scene := request.Source, request.Scene
|
||||
source, scene, technicalSeriesID := request.Source, request.Scene, seriesID+":"+request.SyncType
|
||||
now := time.Now().UTC()
|
||||
_, err := l.repository.Start(ctx, integrationlog.Attempt{
|
||||
Provider: constants.IntegrationProviderGateway, Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: operationForSyncType(request.SyncType), ResourceType: request.ResourceType,
|
||||
ResourceID: &resourceID, TriggerSource: &source, TriggerScene: &scene, TriggerSeries: &seriesID,
|
||||
ResourceID: &resourceID, TriggerSource: &source, TriggerScene: &scene, TriggerSeries: &technicalSeriesID,
|
||||
ScheduledAt: &now, Attempt: 1, InitialResult: constants.IntegrationResultMerged,
|
||||
RequestID: optionalText(request.RequestID), CorrelationID: optionalText(request.CorrelationID),
|
||||
Metadata: map[string]any{"reason": "同场景未结束序列已存在", "sync_type": request.SyncType},
|
||||
@@ -215,7 +215,7 @@ func (r *SeriesRunner) runDeviceInfo(ctx context.Context, payload cardapp.Series
|
||||
}
|
||||
|
||||
func (r *SeriesRunner) startDeviceAttempt(ctx context.Context, payload cardapp.SeriesTaskPayload, device *model.Device) (*model.IntegrationLog, error) {
|
||||
resourceID, source, scene, seriesID := payload.ResourceID, payload.Source, payload.Scene, payload.SeriesID
|
||||
resourceID, source, scene, seriesID := payload.ResourceID, payload.Source, payload.Scene, payload.SeriesID+":"+payload.SyncType
|
||||
resourceKey := "device:" + strconv.FormatUint(uint64(device.ID), 10)
|
||||
return r.integration.Start(ctx, integrationlog.Attempt{
|
||||
IntegrationID: gatewayIntegrationID(payload), Provider: constants.IntegrationProviderGateway,
|
||||
@@ -525,7 +525,7 @@ func parseGatewayTime(raw gateway.FlexString) *time.Time {
|
||||
}
|
||||
|
||||
func (r *SeriesRunner) startAttempt(ctx context.Context, payload cardapp.SeriesTaskPayload, card *model.IotCard) (*model.IntegrationLog, error) {
|
||||
resourceID, source, scene, seriesID := payload.ResourceID, payload.Source, payload.Scene, payload.SeriesID
|
||||
resourceID, source, scene, seriesID := payload.ResourceID, payload.Source, payload.Scene, payload.SeriesID+":"+payload.SyncType
|
||||
resourceKey := "card:" + formatCardID(card.ID)
|
||||
return r.integration.Start(ctx, integrationlog.Attempt{
|
||||
IntegrationID: gatewayIntegrationID(payload), Provider: constants.IntegrationProviderGateway,
|
||||
|
||||
@@ -107,15 +107,9 @@ func (r *Repository) Start(ctx context.Context, input Attempt) (*model.Integrati
|
||||
if input.IntegrationID == "" {
|
||||
input.IntegrationID = uuid.NewString()
|
||||
}
|
||||
autoAttempt := input.Attempt <= 0 && input.TriggerSeries != nil
|
||||
if input.Attempt <= 0 {
|
||||
input.Attempt = 1
|
||||
if input.TriggerSeries != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&model.IntegrationLog{}).
|
||||
Select("COALESCE(MAX(attempt), 0) + 1").
|
||||
Where("trigger_series = ?", *input.TriggerSeries).Scan(&input.Attempt).Error; err != nil {
|
||||
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "计算 Integration Log 尝试序号失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
if input.StartedAt == nil {
|
||||
startedAt := r.now().UTC()
|
||||
@@ -128,16 +122,35 @@ func (r *Repository) Start(ctx context.Context, input Attempt) (*model.Integrati
|
||||
resourceType := optionalString(input.ResourceType)
|
||||
log := &model.IntegrationLog{
|
||||
IntegrationID: input.IntegrationID, Provider: input.Provider, Direction: input.Direction,
|
||||
Operation: input.Operation, ExternalID: input.ExternalID, ResourceType: resourceType,
|
||||
ResourceID: input.ResourceID, ResourceKey: input.ResourceKey, TriggerSource: input.TriggerSource,
|
||||
TriggerScene: input.TriggerScene, TriggerSeries: input.TriggerSeries, ScheduledAt: input.ScheduledAt,
|
||||
Operation: input.Operation, ExternalID: sanitizedOptionalText(input.ExternalID), ResourceType: resourceType,
|
||||
ResourceID: input.ResourceID, ResourceKey: sanitizedOptionalText(input.ResourceKey), TriggerSource: input.TriggerSource,
|
||||
TriggerScene: sanitizedOptionalText(input.TriggerScene), TriggerSeries: input.TriggerSeries, ScheduledAt: input.ScheduledAt,
|
||||
StartedAt: input.StartedAt, Attempt: input.Attempt, Result: result,
|
||||
RequestSummary: requestSummary, Metadata: metadata, RequestID: input.RequestID,
|
||||
CorrelationID: input.CorrelationID, AuditEventID: input.AuditEventID,
|
||||
RecoveryStrategy: input.RecoveryStrategy,
|
||||
RecoveryStrategy: sanitizedOptionalText(input.RecoveryStrategy),
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Create(log).Error; err != nil {
|
||||
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "写入 Integration Log 失败")
|
||||
createAttempt := func(tx *gorm.DB) error {
|
||||
if !autoAttempt {
|
||||
return tx.Create(log).Error
|
||||
}
|
||||
if err := tx.Exec("SELECT pg_advisory_xact_lock(hashtext(?))", *input.TriggerSeries).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.IntegrationLog{}).Select("COALESCE(MAX(attempt), 0) + 1").
|
||||
Where("trigger_series = ?", *input.TriggerSeries).Scan(&log.Attempt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(log).Error
|
||||
}
|
||||
var createErr error
|
||||
if autoAttempt {
|
||||
createErr = r.db.WithContext(ctx).Transaction(createAttempt)
|
||||
} else {
|
||||
createErr = createAttempt(r.db.WithContext(ctx))
|
||||
}
|
||||
if createErr != nil {
|
||||
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, createErr, "写入 Integration Log 失败")
|
||||
}
|
||||
return log, nil
|
||||
}
|
||||
@@ -156,7 +169,7 @@ func (r *Repository) Complete(ctx context.Context, integrationID string, complet
|
||||
if completion.Result == constants.IntegrationResultUnknown && strings.TrimSpace(completion.RecoveryStrategy) == "" {
|
||||
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "结果未知必须记录明确恢复策略")
|
||||
}
|
||||
safeProviderMessage := strings.TrimSpace(completion.SafeProviderMessage)
|
||||
safeProviderMessage := sanitizer.SanitizeText(strings.TrimSpace(completion.SafeProviderMessage))
|
||||
if safeProviderMessage != "" && utf8.RuneCountInString(constants.IntegrationSafeMessagePrefix+safeProviderMessage) > constants.IntegrationProviderMessageMaxLength {
|
||||
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 安全结果摘要过长")
|
||||
}
|
||||
@@ -187,10 +200,10 @@ func (r *Repository) Complete(ctx context.Context, integrationID string, complet
|
||||
updates["resource_id"] = completion.ResourceID
|
||||
}
|
||||
if completion.ResourceKey != nil {
|
||||
updates["resource_key"] = completion.ResourceKey
|
||||
updates["resource_key"] = sanitizedOptionalText(completion.ResourceKey)
|
||||
}
|
||||
if completion.RecoveryStrategy != "" {
|
||||
updates["recovery_strategy"] = completion.RecoveryStrategy
|
||||
updates["recovery_strategy"] = sanitizer.SanitizeText(completion.RecoveryStrategy)
|
||||
}
|
||||
result := r.db.WithContext(ctx).Model(&model.IntegrationLog{}).
|
||||
Where("integration_id = ? AND result = ?", integrationID, constants.IntegrationResultPending).
|
||||
@@ -237,8 +250,8 @@ func (r *Repository) RecordInbound(ctx context.Context, input InboundAttempt) (*
|
||||
log := &model.IntegrationLog{
|
||||
IntegrationID: input.IntegrationID, IdempotencyKey: &input.IdempotencyKey,
|
||||
Provider: input.Provider, Direction: constants.IntegrationDirectionInbound, Operation: input.Operation,
|
||||
ExternalID: optionalString(input.ExternalID), ResourceType: optionalString(input.ResourceType),
|
||||
ResourceID: input.ResourceID, ResourceKey: input.ResourceKey, StartedAt: &now, Attempt: 1,
|
||||
ExternalID: sanitizedOptionalText(optionalString(input.ExternalID)), ResourceType: optionalString(input.ResourceType),
|
||||
ResourceID: input.ResourceID, ResourceKey: sanitizedOptionalText(input.ResourceKey), StartedAt: &now, Attempt: 1,
|
||||
TriggerSeries: &triggerSeries,
|
||||
Result: constants.IntegrationResultPending, RequestSummary: summary,
|
||||
ContentHash: hex.EncodeToString(hash[:]), RequestID: input.RequestID, CorrelationID: input.CorrelationID,
|
||||
@@ -380,3 +393,11 @@ func optionalString(value string) *string {
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func sanitizedOptionalText(value *string) *string {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
sanitized := sanitizer.SanitizeText(*value)
|
||||
return &sanitized
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user