feat(questions): make brand questions the primary monitoring & template axis
- add /questions/combination-fill endpoint with AI-driven, IP-region-aware matrix fill - extract ip2region resolver from ops/app into shared/ipregion for cross-service use - thread question_id filter through dashboard composite, citation summary, and collect-now - switch template wizard from keyword inputs to brand-question selection (primary + supplemental) - pass brand_question and supplemental_questions through assist/title/outline prompts - add AiWaitingModal + 45s client timeout and request_timeout error mapping for long AI flows Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -356,6 +356,7 @@ func (s *MonitoringService) DashboardComposite(
|
||||
ctx context.Context,
|
||||
brandID int64,
|
||||
keywordID *int64,
|
||||
questionID *int64,
|
||||
days int,
|
||||
businessDate string,
|
||||
aiPlatformID *string,
|
||||
@@ -388,6 +389,10 @@ func (s *MonitoringService) DashboardComposite(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configuredQuestions, err = filterConfiguredQuestionsByQuestionID(configuredQuestions, questionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platforms := defaultMonitoringPlatformMetadata()
|
||||
selectedPlatformID := normalizedOptionalMonitoringPlatformPointer(aiPlatformID)
|
||||
@@ -396,11 +401,12 @@ func (s *MonitoringService) DashboardComposite(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
derivedMetrics, err := s.loadDerivedParseMetrics(ctx, actor.TenantID, brand.ID, brand.Name, startDate, endDate, selectedPlatformID)
|
||||
questionIDs := configuredQuestionIDs(configuredQuestions)
|
||||
derivedMetrics, err := s.loadDerivedParseMetrics(ctx, actor.TenantID, brand.ID, questionIDs, brand.Name, startDate, endDate, selectedPlatformID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
overview, _, err := s.loadOverview(ctx, actor.TenantID, brand.ID, startDate, endDate, quota.CollectionMode, selectedPlatformID, derivedMetrics)
|
||||
overview, _, err := s.loadOverview(ctx, actor.TenantID, brand.ID, questionIDs, startDate, endDate, quota.CollectionMode, selectedPlatformID, derivedMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -410,7 +416,7 @@ func (s *MonitoringService) DashboardComposite(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platformBreakdown, err := s.loadPlatformBreakdown(ctx, actor.TenantID, brand.ID, endDate, quota.CollectionMode, filteredPlatforms, accessStates, derivedMetrics)
|
||||
platformBreakdown, err := s.loadPlatformBreakdown(ctx, actor.TenantID, brand.ID, questionIDs, endDate, quota.CollectionMode, filteredPlatforms, accessStates, derivedMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -440,6 +446,7 @@ func (s *MonitoringService) CitationSummary(
|
||||
days int,
|
||||
brandID int64,
|
||||
keywordID *int64,
|
||||
questionID *int64,
|
||||
businessDate string,
|
||||
aiPlatformID *string,
|
||||
) (*MonitoringCitationSummaryResponse, error) {
|
||||
@@ -461,6 +468,10 @@ func (s *MonitoringService) CitationSummary(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configuredQuestions, err = filterConfiguredQuestionsByQuestionID(configuredQuestions, questionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
questionIDs = configuredQuestionIDs(configuredQuestions)
|
||||
}
|
||||
|
||||
@@ -641,6 +652,7 @@ func (s *MonitoringService) CollectNow(
|
||||
ctx context.Context,
|
||||
brandID int64,
|
||||
keywordID *int64,
|
||||
questionID *int64,
|
||||
options MonitoringCollectNowOptions,
|
||||
) (*MonitoringCollectNowResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
@@ -666,13 +678,24 @@ func (s *MonitoringService) CollectNow(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configuredQuestions, err = filterConfiguredQuestionsByQuestionID(configuredQuestions, questionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
limitApplied := false
|
||||
if len(configuredQuestions) > monitoringCollectNowQuestionLimit {
|
||||
if questionID == nil && len(configuredQuestions) > monitoringCollectNowQuestionLimit {
|
||||
configuredQuestions = configuredQuestions[:monitoringCollectNowQuestionLimit]
|
||||
limitApplied = true
|
||||
}
|
||||
if len(configuredQuestions) == 0 {
|
||||
message := "当前品牌下暂无品牌库问题,未创建采集任务"
|
||||
if keywordID != nil {
|
||||
message = "当前问题集下暂无品牌库问题,未创建采集任务"
|
||||
}
|
||||
if questionID != nil {
|
||||
message = "当前问题不可用,未创建采集任务"
|
||||
}
|
||||
return &MonitoringCollectNowResponse{
|
||||
CollectionMode: quota.CollectionMode,
|
||||
RefreshedTaskCount: 0,
|
||||
@@ -680,7 +703,7 @@ func (s *MonitoringService) CollectNow(
|
||||
LeasedTaskCount: 0,
|
||||
CompletedTaskCount: 0,
|
||||
HasEffectiveSnapshot: true,
|
||||
Message: "当前关键词下暂无品牌库问题,未创建采集任务",
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -800,7 +823,7 @@ func (s *MonitoringService) CollectNow(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.syncMonitoringQuestionSnapshots(ctx, tx, actor.TenantID, brand.ID, configuredQuestions, keywordID == nil); err != nil {
|
||||
if err := s.syncMonitoringQuestionSnapshots(ctx, tx, actor.TenantID, brand.ID, configuredQuestions, keywordID == nil && questionID == nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1487,6 +1510,21 @@ func configuredQuestionIDs(questions []monitoringConfiguredQuestion) []int64 {
|
||||
return ids
|
||||
}
|
||||
|
||||
func filterConfiguredQuestionsByQuestionID(questions []monitoringConfiguredQuestion, questionID *int64) ([]monitoringConfiguredQuestion, error) {
|
||||
if questionID == nil {
|
||||
return questions, nil
|
||||
}
|
||||
if *questionID <= 0 {
|
||||
return nil, response.ErrBadRequest(40031, "invalid_question_id", "question_id must be a positive number")
|
||||
}
|
||||
for _, question := range questions {
|
||||
if question.ID == *questionID {
|
||||
return []monitoringConfiguredQuestion{question}, nil
|
||||
}
|
||||
}
|
||||
return nil, response.ErrBadRequest(40041, "invalid_question", "question does not belong to the selected brand or question set")
|
||||
}
|
||||
|
||||
func (s *MonitoringService) syncMonitoringQuestionSnapshots(ctx context.Context, tx pgx.Tx, tenantID, brandID int64, questions []monitoringConfiguredQuestion, pruneMissing bool) error {
|
||||
if pruneMissing {
|
||||
if len(questions) == 0 {
|
||||
@@ -2195,11 +2233,15 @@ func (stats monitoringDerivedRateStats) positiveMentionRate() *float64 {
|
||||
func (s *MonitoringService) loadDerivedParseMetrics(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questionIDs []int64,
|
||||
brandName string,
|
||||
startDate, endDate time.Time,
|
||||
aiPlatformID *string,
|
||||
) (monitoringDerivedMetrics, error) {
|
||||
metrics := newMonitoringDerivedMetrics()
|
||||
if questionIDs != nil && len(questionIDs) == 0 {
|
||||
return metrics, nil
|
||||
}
|
||||
platformQueryIDs := monitoringPlatformQueryIDs(aiPlatformID)
|
||||
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
@@ -2221,8 +2263,9 @@ func (s *MonitoringService) loadDerivedParseMetrics(
|
||||
AND r.collector_type = $3
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
AND ($6::text[] IS NULL OR r.ai_platform_id = ANY($6))
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableStringArray(platformQueryIDs))
|
||||
AND ($6::bigint[] IS NULL OR r.question_id = ANY($6))
|
||||
AND ($7::text[] IS NULL OR r.ai_platform_id = ANY($7))
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableInt64Array(questionIDs), nullableStringArray(platformQueryIDs))
|
||||
if err != nil {
|
||||
return metrics, response.ErrInternal(50041, "query_failed", "failed to load monitoring derived parse metrics")
|
||||
}
|
||||
@@ -2318,11 +2361,15 @@ func decodeMonitoringMatchedBrandTerms(raw []byte) []string {
|
||||
func (s *MonitoringService) loadOverview(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questionIDs []int64,
|
||||
startDate, endDate time.Time,
|
||||
collectionMode string,
|
||||
aiPlatformID *string,
|
||||
derived monitoringDerivedMetrics,
|
||||
) (MonitoringOverview, time.Time, error) {
|
||||
if questionIDs != nil {
|
||||
return s.loadOverviewFromRuns(ctx, tenantID, brandID, questionIDs, startDate, endDate, collectionMode, aiPlatformID, derived)
|
||||
}
|
||||
if platformID := normalizedOptionalMonitoringPlatformID(aiPlatformID); platformID != "" {
|
||||
return s.loadOverviewForPlatform(ctx, tenantID, brandID, startDate, endDate, collectionMode, platformID, derived)
|
||||
}
|
||||
@@ -2364,6 +2411,56 @@ func (s *MonitoringService) loadOverview(
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOverviewFromRuns(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questionIDs []int64,
|
||||
startDate, endDate time.Time,
|
||||
collectionMode string,
|
||||
aiPlatformID *string,
|
||||
derived monitoringDerivedMetrics,
|
||||
) (MonitoringOverview, time.Time, error) {
|
||||
overview := MonitoringOverview{
|
||||
CollectionMode: collectionMode,
|
||||
ConfidenceLevel: "low",
|
||||
}
|
||||
latestDate := endDate
|
||||
if len(questionIDs) == 0 {
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
platformQueryIDs := monitoringPlatformQueryIDs(aiPlatformID)
|
||||
current, found, err := s.loadOverviewRunAggregateForDate(ctx, tenantID, brandID, questionIDs, endDate, platformQueryIDs)
|
||||
if err != nil {
|
||||
return overview, latestDate, err
|
||||
}
|
||||
if !found {
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
applyOverviewSnapshot(&overview, current, derived)
|
||||
if current.ActualSampleCount <= 0 {
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
latestDate = current.Date
|
||||
previous, foundPrevious, err := s.loadLatestOverviewRunAggregate(ctx, tenantID, brandID, questionIDs, startDate, current.Date.AddDate(0, 0, -1), platformQueryIDs)
|
||||
if err != nil {
|
||||
return overview, latestDate, err
|
||||
}
|
||||
if !foundPrevious {
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
applyDerivedRatesToOverviewSnapshot(&previous, derived)
|
||||
overview.PrevSnapshotMentionRate = floatPointer(previous.MentionRate)
|
||||
overview.PrevSnapshotTop1MentionRate = floatPointer(previous.Top1MentionRate)
|
||||
overview.PrevSnapshotFirstRecommendRate = floatPointer(previous.FirstRecommendRate)
|
||||
overview.PrevSnapshotPositiveMentionRate = floatPointer(previous.PositiveMentionRate)
|
||||
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOverviewForPlatform(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
@@ -2434,6 +2531,140 @@ type monitoringOverviewSnapshot struct {
|
||||
CitationRate sql.NullFloat64
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOverviewRunAggregateForDate(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questionIDs []int64,
|
||||
businessDate time.Time,
|
||||
platformIDs []string,
|
||||
) (monitoringOverviewSnapshot, bool, error) {
|
||||
return s.loadOverviewRunAggregate(ctx, tenantID, brandID, questionIDs, businessDate, businessDate, platformIDs, false)
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadLatestOverviewRunAggregate(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questionIDs []int64,
|
||||
startDate, endDate time.Time,
|
||||
platformIDs []string,
|
||||
) (monitoringOverviewSnapshot, bool, error) {
|
||||
if endDate.Before(startDate) {
|
||||
return monitoringOverviewSnapshot{}, false, nil
|
||||
}
|
||||
return s.loadOverviewRunAggregate(ctx, tenantID, brandID, questionIDs, startDate, endDate, platformIDs, true)
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOverviewRunAggregate(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questionIDs []int64,
|
||||
startDate, endDate time.Time,
|
||||
platformIDs []string,
|
||||
requireSamples bool,
|
||||
) (monitoringOverviewSnapshot, bool, error) {
|
||||
var item monitoringOverviewSnapshot
|
||||
if len(questionIDs) == 0 {
|
||||
return item, false, nil
|
||||
}
|
||||
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
SELECT
|
||||
r.business_date,
|
||||
COUNT(*)::bigint AS actual_sample_count,
|
||||
COUNT(*) FILTER (WHERE p.brand_mentioned IS TRUE)::bigint AS mentioned_count,
|
||||
COUNT(*) FILTER (WHERE p.brand_mention_position = 'top1')::bigint AS top1_mentioned_count,
|
||||
COUNT(*) FILTER (WHERE p.first_recommended IS TRUE)::bigint AS first_recommended_count,
|
||||
COUNT(*) FILTER (WHERE p.sentiment_label = 'positive')::bigint AS positive_mentioned_count,
|
||||
MAX(r.completed_at) AS last_sampled_at
|
||||
FROM question_monitor_runs r
|
||||
LEFT JOIN question_monitor_parse_results p
|
||||
ON p.tenant_id = r.tenant_id AND p.run_id = r.id
|
||||
WHERE r.tenant_id = $1
|
||||
AND r.brand_id = $2
|
||||
AND r.collector_type = $3
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
AND r.question_id = ANY($6)
|
||||
AND ($7::text[] IS NULL OR r.ai_platform_id = ANY($7))
|
||||
GROUP BY r.business_date
|
||||
ORDER BY r.business_date DESC
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), questionIDs, nullableStringArray(platformIDs))
|
||||
if err != nil {
|
||||
return item, false, response.ErrInternal(50041, "query_failed", "failed to load monitoring overview")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var mentionedCount int64
|
||||
var top1MentionedCount int64
|
||||
var firstRecommendedCount int64
|
||||
var positiveMentionedCount int64
|
||||
if scanErr := rows.Scan(
|
||||
&item.Date,
|
||||
&item.ActualSampleCount,
|
||||
&mentionedCount,
|
||||
&top1MentionedCount,
|
||||
&firstRecommendedCount,
|
||||
&positiveMentionedCount,
|
||||
&item.SnapshotUpdatedAt,
|
||||
); scanErr != nil {
|
||||
return monitoringOverviewSnapshot{}, false, response.ErrInternal(50041, "scan_failed", "failed to parse monitoring overview")
|
||||
}
|
||||
if requireSamples && item.ActualSampleCount <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
plannedSampleCount, planErr := s.loadOverviewRunPlannedCount(ctx, tenantID, brandID, questionIDs, item.Date, platformIDs)
|
||||
if planErr != nil {
|
||||
return monitoringOverviewSnapshot{}, false, planErr
|
||||
}
|
||||
item.DesiredSampleCount = plannedSampleCount
|
||||
item.PlannedSampleCount = plannedSampleCount
|
||||
item.CoverageRate = pointerFloat64ToNull(divideAsPointer(item.ActualSampleCount, plannedSampleCount))
|
||||
item.ConfidenceLevel = deriveMonitoringConfidenceLevel(item.ActualSampleCount, plannedSampleCount, floatPointer(item.CoverageRate))
|
||||
item.MentionRate = pointerFloat64ToNull(divideAsPointer(mentionedCount, item.ActualSampleCount))
|
||||
item.Top1MentionRate = pointerFloat64ToNull(divideAsPointer(top1MentionedCount, item.ActualSampleCount))
|
||||
item.FirstRecommendRate = pointerFloat64ToNull(divideAsPointer(firstRecommendedCount, item.ActualSampleCount))
|
||||
item.PositiveMentionRate = pointerFloat64ToNull(divideAsPointer(positiveMentionedCount, item.ActualSampleCount))
|
||||
return item, true, nil
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return monitoringOverviewSnapshot{}, false, response.ErrInternal(50041, "scan_failed", "failed to iterate monitoring overview")
|
||||
}
|
||||
|
||||
return monitoringOverviewSnapshot{}, false, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOverviewRunPlannedCount(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questionIDs []int64,
|
||||
businessDate time.Time,
|
||||
platformIDs []string,
|
||||
) (int64, error) {
|
||||
var plannedSampleCount int64
|
||||
if err := s.monitoringPool.QueryRow(ctx, `
|
||||
SELECT COUNT(*)::bigint
|
||||
FROM monitoring_collect_tasks
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND collector_type = $3
|
||||
AND business_date = $4::date
|
||||
AND question_id = ANY($5)
|
||||
AND ($6::text[] IS NULL OR ai_platform_id = ANY($6))
|
||||
AND NOT (status = 'skipped' AND skip_reason = ANY($7::text[]))
|
||||
`, tenantID, brandID, monitoringCollectorType, businessDate.Format("2006-01-02"), questionIDs, nullableStringArray(platformIDs), monitoringProjectionExcludedSkipReasons).Scan(&plannedSampleCount); err != nil {
|
||||
return 0, response.ErrInternal(50041, "query_failed", "failed to load monitoring overview")
|
||||
}
|
||||
if plannedSampleCount > 0 {
|
||||
return plannedSampleCount, nil
|
||||
}
|
||||
if platformIDs != nil {
|
||||
return int64(len(questionIDs) * len(platformIDs)), nil
|
||||
}
|
||||
return int64(len(questionIDs) * len(defaultMonitoringPlatforms)), nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOverviewSnapshotForDate(ctx context.Context, tenantID, brandID int64, collectionMode string, businessDate time.Time) (monitoringOverviewSnapshot, bool, error) {
|
||||
var item monitoringOverviewSnapshot
|
||||
err := s.monitoringPool.QueryRow(ctx, `
|
||||
@@ -2765,7 +2996,11 @@ func (s *MonitoringService) monitoringClientBelongsToWorkspace(ctx context.Conte
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadPlatformBreakdown(ctx context.Context, tenantID, brandID int64, businessDate time.Time, collectionMode string, platforms []monitoringPlatformMetadata, accessStates map[string]monitoringAccessState, derived monitoringDerivedMetrics) ([]MonitoringPlatformDaily, error) {
|
||||
func (s *MonitoringService) loadPlatformBreakdown(ctx context.Context, tenantID, brandID int64, questionIDs []int64, businessDate time.Time, collectionMode string, platforms []monitoringPlatformMetadata, accessStates map[string]monitoringAccessState, derived monitoringDerivedMetrics) ([]MonitoringPlatformDaily, error) {
|
||||
if questionIDs != nil {
|
||||
return s.loadPlatformBreakdownFromRuns(ctx, tenantID, brandID, questionIDs, businessDate, platforms, accessStates, derived)
|
||||
}
|
||||
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
SELECT
|
||||
ai_platform_id,
|
||||
@@ -2854,6 +3089,102 @@ func (s *MonitoringService) loadPlatformBreakdown(ctx context.Context, tenantID,
|
||||
return breakdown, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadPlatformBreakdownFromRuns(ctx context.Context, tenantID, brandID int64, questionIDs []int64, businessDate time.Time, platforms []monitoringPlatformMetadata, accessStates map[string]monitoringAccessState, derived monitoringDerivedMetrics) ([]MonitoringPlatformDaily, error) {
|
||||
if len(questionIDs) == 0 {
|
||||
return emptyPlatformBreakdown(platforms, accessStates), nil
|
||||
}
|
||||
|
||||
dateKey := businessDate.Format("2006-01-02")
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
SELECT
|
||||
r.ai_platform_id,
|
||||
COUNT(*)::bigint AS actual_sample_count,
|
||||
COUNT(*) FILTER (WHERE p.brand_mentioned IS TRUE)::bigint AS mentioned_count,
|
||||
COUNT(*) FILTER (WHERE p.brand_mention_position = 'top1')::bigint AS top1_mentioned_count,
|
||||
MAX(r.completed_at) AS last_sampled_at
|
||||
FROM question_monitor_runs r
|
||||
LEFT JOIN question_monitor_parse_results p
|
||||
ON p.tenant_id = r.tenant_id AND p.run_id = r.id
|
||||
WHERE r.tenant_id = $1
|
||||
AND r.brand_id = $2
|
||||
AND r.collector_type = $3
|
||||
AND r.business_date = $4::date
|
||||
AND r.status = 'succeeded'
|
||||
AND r.question_id = ANY($5)
|
||||
GROUP BY r.ai_platform_id
|
||||
`, tenantID, brandID, monitoringCollectorType, dateKey, questionIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load platform breakdown")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type row struct {
|
||||
MentionRate sql.NullFloat64
|
||||
Top1MentionRate sql.NullFloat64
|
||||
ActualSampleCount int64
|
||||
LastSampledAt sql.NullTime
|
||||
}
|
||||
|
||||
items := make(map[string]row)
|
||||
for rows.Next() {
|
||||
var platformID string
|
||||
var mentionedCount int64
|
||||
var top1MentionedCount int64
|
||||
var item row
|
||||
if scanErr := rows.Scan(
|
||||
&platformID,
|
||||
&item.ActualSampleCount,
|
||||
&mentionedCount,
|
||||
&top1MentionedCount,
|
||||
&item.LastSampledAt,
|
||||
); scanErr != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse platform breakdown")
|
||||
}
|
||||
platformID = normalizeMonitoringPlatformID(platformID)
|
||||
if platformID == "" {
|
||||
continue
|
||||
}
|
||||
item.MentionRate = pointerFloat64ToNull(divideAsPointer(mentionedCount, item.ActualSampleCount))
|
||||
item.Top1MentionRate = pointerFloat64ToNull(divideAsPointer(top1MentionedCount, item.ActualSampleCount))
|
||||
items[platformID] = item
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to iterate platform breakdown")
|
||||
}
|
||||
|
||||
breakdown := make([]MonitoringPlatformDaily, 0, len(platforms))
|
||||
for _, platform := range platforms {
|
||||
item := items[platform.ID]
|
||||
if platformStats, ok := derived.ByDatePlatform[dateKey][platform.ID]; ok && platformStats.hasSamples() {
|
||||
item.MentionRate = pointerFloat64ToNull(platformStats.mentionRate())
|
||||
item.Top1MentionRate = pointerFloat64ToNull(platformStats.top1MentionRate())
|
||||
}
|
||||
breakdown = append(breakdown, MonitoringPlatformDaily{
|
||||
AIPlatformID: platform.ID,
|
||||
PlatformName: platform.Name,
|
||||
MentionRate: floatPointer(item.MentionRate),
|
||||
Top1MentionRate: floatPointer(item.Top1MentionRate),
|
||||
PlatformSampleStatus: deriveMonitoringPlatformSampleStatus(item.ActualSampleCount, item.ActualSampleCount, accessStates[platform.ID].AccessStatus),
|
||||
ActualSampleCount: item.ActualSampleCount,
|
||||
LastSampledAt: formatNullTime(item.LastSampledAt),
|
||||
})
|
||||
}
|
||||
|
||||
return breakdown, nil
|
||||
}
|
||||
|
||||
func emptyPlatformBreakdown(platforms []monitoringPlatformMetadata, accessStates map[string]monitoringAccessState) []MonitoringPlatformDaily {
|
||||
breakdown := make([]MonitoringPlatformDaily, 0, len(platforms))
|
||||
for _, platform := range platforms {
|
||||
breakdown = append(breakdown, MonitoringPlatformDaily{
|
||||
AIPlatformID: platform.ID,
|
||||
PlatformName: platform.Name,
|
||||
PlatformSampleStatus: derivePlatformSampleStatus("", accessStates[platform.ID]),
|
||||
})
|
||||
}
|
||||
return breakdown
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadHotQuestions(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
|
||||
Reference in New Issue
Block a user