package mysql import ( "context" "database/sql" "strings" "time" "hyapp/pkg/activitymq" "hyapp/pkg/appcode" "hyapp/pkg/xerr" ) const ( activityParticipantScopeSummary = "summary" activityParticipantScopeDay = "day" activityParticipantScopeDayRegion = "day_region" activityParticipantScopeGift = "gift" activityParticipantScopeGiftRegion = "gift_region" activityParticipantScopeTask = "task" activityParticipantScopeTaskRegion = "task_region" activityParticipantSummaryDay = "1970-01-01" activityDimensionLimit = 100 ) const activityTemplateGiftDayUpsert = ` INSERT INTO stat_activity_template_gift_day ( app_code, template_id, template_code, published_version, stat_day, region_id, gift_id, participant_count, gift_event_count, gift_count, coin_amount, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?) ON DUPLICATE KEY UPDATE template_code = VALUES(template_code), participant_count = participant_count + VALUES(participant_count), gift_event_count = gift_event_count + 1, gift_count = gift_count + VALUES(gift_count), coin_amount = coin_amount + VALUES(coin_amount), updated_at_ms = VALUES(updated_at_ms)` // ActivityTemplateFactEvent aliases the owner MQ contract so replay jobs and // online consumers cannot accidentally evolve two independent payload shapes. type ActivityTemplateFactEvent = activitymq.ActivityTemplateFactMessage type ActivityTemplateDataQuery struct { AppCode string TemplateID string PublishedVersion int64 RegionID int64 StartMS int64 EndMS int64 } type ActivityTemplateOverview struct { TemplateID string `json:"template_id"` TemplateCode string `json:"template_code"` PublishedVersion int64 `json:"published_version"` Participants int64 `json:"participants"` VisitCount int64 `json:"visit_count"` GiftUsers int64 `json:"gift_users"` TaskUsers int64 `json:"task_users"` GiftEventCount int64 `json:"gift_event_count"` GiftCount int64 `json:"gift_count"` CoinAmount int64 `json:"coin_amount"` TaskProgress int64 `json:"task_progress"` TaskCompleted int64 `json:"task_completed"` TaskClaimed int64 `json:"task_claimed"` } type ActivityTemplateTrendPoint struct { StatDay string `json:"stat_day"` Participants int64 `json:"participants"` VisitCount int64 `json:"visit_count"` GiftUsers int64 `json:"gift_users"` TaskUsers int64 `json:"task_users"` GiftEventCount int64 `json:"gift_event_count"` GiftCount int64 `json:"gift_count"` CoinAmount int64 `json:"coin_amount"` TaskProgress int64 `json:"task_progress"` TaskCompleted int64 `json:"task_completed"` TaskClaimed int64 `json:"task_claimed"` } type ActivityTemplateGiftStat struct { GiftID string `json:"gift_id"` GiftUsers int64 `json:"gift_users"` GiftEventCount int64 `json:"gift_event_count"` GiftCount int64 `json:"gift_count"` CoinAmount int64 `json:"coin_amount"` } type ActivityTemplateTaskStat struct { TaskKey string `json:"task_key"` TaskType string `json:"task_type"` TaskUsers int64 `json:"task_users"` TaskProgress int64 `json:"task_progress"` TaskCompleted int64 `json:"task_completed"` TaskClaimed int64 `json:"task_claimed"` } type ActivityTemplateData struct { Overview ActivityTemplateOverview `json:"overview"` Trend []ActivityTemplateTrendPoint `json:"trend"` Gifts []ActivityTemplateGiftStat `json:"gifts"` Tasks []ActivityTemplateTaskStat `json:"tasks"` Granularity string `json:"granularity"` Timezone string `json:"timezone"` StartMS int64 `json:"start_ms"` EndMS int64 `json:"end_ms"` RegionID int64 `json:"region_id"` DimensionLimit int `json:"dimension_limit"` GiftSort string `json:"gift_sort"` TaskSort string `json:"task_sort"` ServerTimeMS int64 `json:"server_time_ms"` } func activityTemplateStatSchemaStatements() []string { return []string{ `CREATE TABLE IF NOT EXISTS stat_activity_template_summary ( app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, template_code VARCHAR(64) NOT NULL, published_version BIGINT NOT NULL, participant_count BIGINT NOT NULL DEFAULT 0, visit_event_count BIGINT NOT NULL DEFAULT 0, gift_event_count BIGINT NOT NULL DEFAULT 0, gift_count BIGINT NOT NULL DEFAULT 0, coin_amount BIGINT NOT NULL DEFAULT 0, progress_value BIGINT NOT NULL DEFAULT 0, completed_count BIGINT NOT NULL DEFAULT 0, claimed_count BIGINT NOT NULL DEFAULT 0, first_occurred_at_ms BIGINT NOT NULL DEFAULT 0, last_occurred_at_ms BIGINT NOT NULL DEFAULT 0, updated_at_ms BIGINT NOT NULL, PRIMARY KEY (app_code, template_id, published_version), KEY idx_stat_activity_template_summary_code (app_code, template_code, published_version) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, `CREATE TABLE IF NOT EXISTS stat_activity_template_day ( app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, template_code VARCHAR(64) NOT NULL, published_version BIGINT NOT NULL, stat_day DATE NOT NULL, region_id BIGINT NOT NULL DEFAULT 0, participant_count BIGINT NOT NULL DEFAULT 0, visit_event_count BIGINT NOT NULL DEFAULT 0, gift_event_count BIGINT NOT NULL DEFAULT 0, gift_count BIGINT NOT NULL DEFAULT 0, coin_amount BIGINT NOT NULL DEFAULT 0, progress_value BIGINT NOT NULL DEFAULT 0, completed_count BIGINT NOT NULL DEFAULT 0, claimed_count BIGINT NOT NULL DEFAULT 0, updated_at_ms BIGINT NOT NULL, PRIMARY KEY (app_code, template_id, published_version, stat_day, region_id), KEY idx_stat_activity_template_day_code (app_code, template_code, published_version, stat_day), KEY idx_stat_activity_template_day_region (app_code, template_id, published_version, region_id, stat_day) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, `CREATE TABLE IF NOT EXISTS stat_activity_template_gift_day ( app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, template_code VARCHAR(64) NOT NULL, published_version BIGINT NOT NULL, stat_day DATE NOT NULL, region_id BIGINT NOT NULL DEFAULT 0, gift_id VARCHAR(96) NOT NULL, participant_count BIGINT NOT NULL DEFAULT 0, gift_event_count BIGINT NOT NULL DEFAULT 0, gift_count BIGINT NOT NULL DEFAULT 0, coin_amount BIGINT NOT NULL DEFAULT 0, updated_at_ms BIGINT NOT NULL, PRIMARY KEY (app_code, template_id, published_version, stat_day, region_id, gift_id), KEY idx_stat_activity_template_gift_rank (app_code, template_id, published_version, stat_day, coin_amount, gift_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, `CREATE TABLE IF NOT EXISTS stat_activity_template_task_day ( app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, template_code VARCHAR(64) NOT NULL, published_version BIGINT NOT NULL, stat_day DATE NOT NULL, region_id BIGINT NOT NULL DEFAULT 0, task_key VARCHAR(96) NOT NULL, task_type VARCHAR(64) NOT NULL, participant_count BIGINT NOT NULL DEFAULT 0, progress_value BIGINT NOT NULL DEFAULT 0, completed_count BIGINT NOT NULL DEFAULT 0, claimed_count BIGINT NOT NULL DEFAULT 0, updated_at_ms BIGINT NOT NULL, PRIMARY KEY (app_code, template_id, published_version, stat_day, region_id, task_key), KEY idx_stat_activity_template_task_rank (app_code, template_id, published_version, stat_day, completed_count, task_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, `CREATE TABLE IF NOT EXISTS stat_activity_template_participants ( app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, published_version BIGINT NOT NULL, scope_type VARCHAR(20) NOT NULL, stat_day DATE NOT NULL, region_id BIGINT NOT NULL DEFAULT 0, dimension_key VARCHAR(128) NOT NULL DEFAULT '', user_id BIGINT NOT NULL, first_participated_at_ms BIGINT NOT NULL, PRIMARY KEY (app_code, template_id, published_version, scope_type, stat_day, region_id, dimension_key, user_id), KEY idx_stat_activity_participant_dimension (app_code, template_id, published_version, scope_type, dimension_key, stat_day, user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, } } func (r *Repository) migrateActivityTemplateStats(ctx context.Context) error { for _, statement := range activityTemplateStatSchemaStatements() { if _, err := r.db.ExecContext(ctx, statement); err != nil { return err } } if err := r.ensureActivityTemplateColumn(ctx, "stat_activity_template_summary", "visit_event_count", "visit_event_count BIGINT NOT NULL DEFAULT 0 AFTER participant_count"); err != nil { return err } if err := r.ensureActivityTemplateColumn(ctx, "stat_activity_template_day", "visit_event_count", "visit_event_count BIGINT NOT NULL DEFAULT 0 AFTER participant_count"); err != nil { return err } return nil } func (r *Repository) ensureActivityTemplateColumn(ctx context.Context, tableName string, columnName string, columnDDL string) error { var count int if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`, tableName, columnName, ).Scan(&count); err != nil { return err } if count > 0 { return nil } // MySQL does not support ADD COLUMN IF NOT EXISTS across the versions used by // production and local Compose. These identifiers are fixed owner-schema constants; // the metadata check keeps repeated startup migrations idempotent without accepting SQL input. _, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` ADD COLUMN `+columnDDL) return err } // ConsumeActivityTemplateFact projects one immutable fact and all participant // uniqueness scopes in one transaction. Event consumption is written first, so // RocketMQ redelivery cannot inflate either counters or unique participants. func (r *Repository) ConsumeActivityTemplateFact(ctx context.Context, event ActivityTemplateFactEvent) error { app := appcode.Normalize(event.AppCode) event.AppCode = app event.EventID = strings.TrimSpace(event.EventID) event.EventType = strings.TrimSpace(event.EventType) event.TemplateID = strings.TrimSpace(event.TemplateID) event.TemplateCode = strings.TrimSpace(event.TemplateCode) event.GiftID = strings.TrimSpace(event.GiftID) event.TaskKey = strings.TrimSpace(event.TaskKey) event.TaskType = strings.TrimSpace(event.TaskType) if _, err := activitymq.EncodeActivityTemplateFactMessage(event); err != nil { return xerr.New(xerr.InvalidArgument, err.Error()) } ctx = appcode.WithContext(ctx, app) return r.withEvent(ctx, SourceActivity, event.EventID, event.EventType, func(tx *sql.Tx, nowMS int64) error { return r.applyActivityTemplateFact(ctx, tx, event, nowMS) }) } func (r *Repository) applyActivityTemplateFact(ctx context.Context, tx *sql.Tx, event ActivityTemplateFactEvent, nowMS int64) error { giftEventCount := int64(0) if event.EventType == activitymq.EventTypeGiftScored { giftEventCount = 1 } // summary uniqueness has no time or region dimension; it remains the exact // lifetime participant count even when one user appears in many daily rows. summaryParticipant, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeSummary, activityParticipantSummaryDay, 0, "") if err != nil { return err } if _, err := tx.ExecContext(ctx, ` INSERT INTO stat_activity_template_summary ( app_code, template_id, template_code, published_version, participant_count, visit_event_count, gift_event_count, gift_count, coin_amount, progress_value, completed_count, claimed_count, first_occurred_at_ms, last_occurred_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE template_code = VALUES(template_code), participant_count = participant_count + VALUES(participant_count), visit_event_count = visit_event_count + VALUES(visit_event_count), gift_event_count = gift_event_count + VALUES(gift_event_count), gift_count = gift_count + VALUES(gift_count), coin_amount = coin_amount + VALUES(coin_amount), progress_value = progress_value + VALUES(progress_value), completed_count = completed_count + VALUES(completed_count), claimed_count = claimed_count + VALUES(claimed_count), first_occurred_at_ms = IF(first_occurred_at_ms = 0, VALUES(first_occurred_at_ms), LEAST(first_occurred_at_ms, VALUES(first_occurred_at_ms))), last_occurred_at_ms = GREATEST(last_occurred_at_ms, VALUES(last_occurred_at_ms)), updated_at_ms = VALUES(updated_at_ms) `, event.AppCode, event.TemplateID, event.TemplateCode, event.PublishedVersion, summaryParticipant, event.VisitCountDelta, giftEventCount, event.GiftCountDelta, event.CoinAmountDelta, event.ProgressDelta, event.CompletedDelta, event.ClaimedDelta, event.OccurredAtMS, event.OccurredAtMS, nowMS); err != nil { return err } // A global day scope supports exact cross-region query counts; the regional // scope independently increments the corresponding regional aggregate row. if _, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeDay, event.StatDay, 0, ""); err != nil { return err } dayParticipant, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeDayRegion, event.StatDay, event.RegionID, "") if err != nil { return err } if _, err := tx.ExecContext(ctx, ` INSERT INTO stat_activity_template_day ( app_code, template_id, template_code, published_version, stat_day, region_id, participant_count, visit_event_count, gift_event_count, gift_count, coin_amount, progress_value, completed_count, claimed_count, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE template_code = VALUES(template_code), participant_count = participant_count + VALUES(participant_count), visit_event_count = visit_event_count + VALUES(visit_event_count), gift_event_count = gift_event_count + VALUES(gift_event_count), gift_count = gift_count + VALUES(gift_count), coin_amount = coin_amount + VALUES(coin_amount), progress_value = progress_value + VALUES(progress_value), completed_count = completed_count + VALUES(completed_count), claimed_count = claimed_count + VALUES(claimed_count), updated_at_ms = VALUES(updated_at_ms) `, event.AppCode, event.TemplateID, event.TemplateCode, event.PublishedVersion, event.StatDay, event.RegionID, dayParticipant, event.VisitCountDelta, giftEventCount, event.GiftCountDelta, event.CoinAmountDelta, event.ProgressDelta, event.CompletedDelta, event.ClaimedDelta, nowMS); err != nil { return err } if event.EventType == activitymq.EventTypeGiftScored && event.GiftID != "" { if _, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeGift, event.StatDay, 0, event.GiftID); err != nil { return err } giftParticipant, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeGiftRegion, event.StatDay, event.RegionID, event.GiftID) if err != nil { return err } if _, err := tx.ExecContext(ctx, activityTemplateGiftDayUpsert, event.AppCode, event.TemplateID, event.TemplateCode, event.PublishedVersion, event.StatDay, event.RegionID, event.GiftID, giftParticipant, event.GiftCountDelta, event.CoinAmountDelta, nowMS); err != nil { return err } } if (event.EventType == activitymq.EventTypeTaskProgressed || event.EventType == activitymq.EventTypeTaskClaimed) && event.TaskKey != "" { if _, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeTask, event.StatDay, 0, event.TaskKey); err != nil { return err } taskParticipant, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeTaskRegion, event.StatDay, event.RegionID, event.TaskKey) if err != nil { return err } if _, err := tx.ExecContext(ctx, ` INSERT INTO stat_activity_template_task_day ( app_code, template_id, template_code, published_version, stat_day, region_id, task_key, task_type, participant_count, progress_value, completed_count, claimed_count, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE template_code = VALUES(template_code), task_type = VALUES(task_type), participant_count = participant_count + VALUES(participant_count), progress_value = progress_value + VALUES(progress_value), completed_count = completed_count + VALUES(completed_count), claimed_count = claimed_count + VALUES(claimed_count), updated_at_ms = VALUES(updated_at_ms) `, event.AppCode, event.TemplateID, event.TemplateCode, event.PublishedVersion, event.StatDay, event.RegionID, event.TaskKey, event.TaskType, taskParticipant, event.ProgressDelta, event.CompletedDelta, event.ClaimedDelta, nowMS); err != nil { return err } } return nil } func (r *Repository) insertActivityTemplateParticipant( ctx context.Context, tx *sql.Tx, event ActivityTemplateFactEvent, scopeType string, statDay string, regionID int64, dimensionKey string, ) (int64, error) { return insertUnique(ctx, tx, ` INSERT IGNORE INTO stat_activity_template_participants ( app_code, template_id, published_version, scope_type, stat_day, region_id, dimension_key, user_id, first_participated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, event.AppCode, event.TemplateID, event.PublishedVersion, scopeType, statDay, regionID, dimensionKey, event.UserID, event.OccurredAtMS) } // QueryActivityTemplateData reads only statistics-owned aggregate/uniqueness // projections. start_ms/end_ms use the service-wide UTC [start,end) contract. // Both boundaries must be UTC 00:00 because a day aggregate cannot truthfully // answer a partial-day range; rejecting it prevents silently over-reporting data. func (r *Repository) QueryActivityTemplateData(ctx context.Context, query ActivityTemplateDataQuery) (ActivityTemplateData, error) { query.AppCode = appcode.Normalize(query.AppCode) query.TemplateID = strings.TrimSpace(query.TemplateID) if r == nil || r.db == nil { return ActivityTemplateData{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") } if query.AppCode == "" || query.TemplateID == "" || query.PublishedVersion <= 0 || query.RegionID < 0 || query.StartMS <= 0 || query.EndMS <= query.StartMS { return ActivityTemplateData{}, xerr.New(xerr.InvalidArgument, "app_code, template_id, published_version and a valid time range are required") } if !isUTCActivityDayBoundary(query.StartMS) || !isUTCActivityDayBoundary(query.EndMS) { return ActivityTemplateData{}, xerr.New(xerr.InvalidArgument, "start_ms and end_ms must be UTC 00:00 day boundaries") } startDay := time.UnixMilli(query.StartMS).UTC().Format("2006-01-02") endDay := time.UnixMilli(query.EndMS - 1).UTC().Format("2006-01-02") result := ActivityTemplateData{ Overview: ActivityTemplateOverview{TemplateID: query.TemplateID, PublishedVersion: query.PublishedVersion}, Trend: make([]ActivityTemplateTrendPoint, 0), Gifts: make([]ActivityTemplateGiftStat, 0), Tasks: make([]ActivityTemplateTaskStat, 0), Granularity: "day", Timezone: "UTC", StartMS: query.StartMS, EndMS: query.EndMS, RegionID: query.RegionID, DimensionLimit: activityDimensionLimit, GiftSort: "coin_amount_desc,gift_count_desc,gift_id_asc", TaskSort: "task_completed_desc,task_claimed_desc,task_key_asc", ServerTimeMS: time.Now().UTC().UnixMilli(), } if err := r.db.QueryRowContext(ctx, ` SELECT COALESCE(MAX(template_code), ''), COALESCE(SUM(visit_event_count), 0), COALESCE(SUM(gift_event_count), 0), COALESCE(SUM(gift_count), 0), COALESCE(SUM(coin_amount), 0), COALESCE(SUM(progress_value), 0), COALESCE(SUM(completed_count), 0), COALESCE(SUM(claimed_count), 0) FROM stat_activity_template_day WHERE app_code = ? AND template_id = ? AND published_version = ? AND stat_day BETWEEN ? AND ? AND (? = 0 OR region_id = ?) `, query.AppCode, query.TemplateID, query.PublishedVersion, startDay, endDay, query.RegionID, query.RegionID).Scan( &result.Overview.TemplateCode, &result.Overview.VisitCount, &result.Overview.GiftEventCount, &result.Overview.GiftCount, &result.Overview.CoinAmount, &result.Overview.TaskProgress, &result.Overview.TaskCompleted, &result.Overview.TaskClaimed, ); err != nil { return ActivityTemplateData{}, err } dayScope, giftScope, taskScope, participantRegionID := activityParticipantQueryScopes(query.RegionID) if err := r.db.QueryRowContext(ctx, ` SELECT COUNT(DISTINCT CASE WHEN scope_type = ? THEN user_id END), COUNT(DISTINCT CASE WHEN scope_type = ? THEN user_id END), COUNT(DISTINCT CASE WHEN scope_type = ? THEN user_id END) FROM stat_activity_template_participants WHERE app_code = ? AND template_id = ? AND published_version = ? AND scope_type IN (?, ?, ?) AND region_id = ? AND stat_day BETWEEN ? AND ? `, dayScope, giftScope, taskScope, query.AppCode, query.TemplateID, query.PublishedVersion, dayScope, giftScope, taskScope, participantRegionID, startDay, endDay, ).Scan(&result.Overview.Participants, &result.Overview.GiftUsers, &result.Overview.TaskUsers); err != nil { return ActivityTemplateData{}, err } days, err := r.queryActivityTemplateDays(ctx, query, startDay, endDay) if err != nil { return ActivityTemplateData{}, err } result.Trend = days gifts, err := r.queryActivityTemplateGifts(ctx, query, startDay, endDay) if err != nil { return ActivityTemplateData{}, err } result.Gifts = gifts tasks, err := r.queryActivityTemplateTasks(ctx, query, startDay, endDay) if err != nil { return ActivityTemplateData{}, err } result.Tasks = tasks return result, nil } func isUTCActivityDayBoundary(ms int64) bool { if ms <= 0 { return false } timestamp := time.UnixMilli(ms).UTC() return timestamp.Hour() == 0 && timestamp.Minute() == 0 && timestamp.Second() == 0 && timestamp.Nanosecond() == 0 } func activityParticipantQueryScopes(regionID int64) (dayScope, giftScope, taskScope string, participantRegionID int64) { if regionID > 0 { return activityParticipantScopeDayRegion, activityParticipantScopeGiftRegion, activityParticipantScopeTaskRegion, regionID } return activityParticipantScopeDay, activityParticipantScopeGift, activityParticipantScopeTask, 0 } func (r *Repository) queryActivityTemplateDays(ctx context.Context, query ActivityTemplateDataQuery, startDay, endDay string) ([]ActivityTemplateTrendPoint, error) { dayScope, giftScope, taskScope, participantRegionID := activityParticipantQueryScopes(query.RegionID) rows, err := r.db.QueryContext(ctx, ` SELECT d.stat_day, COALESCE((SELECT COUNT(*) FROM stat_activity_template_participants p WHERE p.app_code = d.app_code AND p.template_id = d.template_id AND p.published_version = d.published_version AND p.scope_type = ? AND p.region_id = ? AND p.stat_day = d.stat_day), 0), COALESCE((SELECT COUNT(DISTINCT p.user_id) FROM stat_activity_template_participants p WHERE p.app_code = d.app_code AND p.template_id = d.template_id AND p.published_version = d.published_version AND p.scope_type = ? AND p.region_id = ? AND p.stat_day = d.stat_day), 0), COALESCE((SELECT COUNT(DISTINCT p.user_id) FROM stat_activity_template_participants p WHERE p.app_code = d.app_code AND p.template_id = d.template_id AND p.published_version = d.published_version AND p.scope_type = ? AND p.region_id = ? AND p.stat_day = d.stat_day), 0), SUM(d.visit_event_count), SUM(d.gift_event_count), SUM(d.gift_count), SUM(d.coin_amount), SUM(d.progress_value), SUM(d.completed_count), SUM(d.claimed_count) FROM stat_activity_template_day d WHERE d.app_code = ? AND d.template_id = ? AND d.published_version = ? AND d.stat_day BETWEEN ? AND ? AND (? = 0 OR d.region_id = ?) GROUP BY d.app_code, d.template_id, d.published_version, d.stat_day ORDER BY d.stat_day ASC `, dayScope, participantRegionID, giftScope, participantRegionID, taskScope, participantRegionID, query.AppCode, query.TemplateID, query.PublishedVersion, startDay, endDay, query.RegionID, query.RegionID) if err != nil { return nil, err } defer rows.Close() out := make([]ActivityTemplateTrendPoint, 0) for rows.Next() { var item ActivityTemplateTrendPoint var statDay time.Time if err := rows.Scan(&statDay, &item.Participants, &item.GiftUsers, &item.TaskUsers, &item.VisitCount, &item.GiftEventCount, &item.GiftCount, &item.CoinAmount, &item.TaskProgress, &item.TaskCompleted, &item.TaskClaimed); err != nil { return nil, err } item.StatDay = statDay.UTC().Format("2006-01-02") out = append(out, item) } return out, rows.Err() } func (r *Repository) queryActivityTemplateGifts(ctx context.Context, query ActivityTemplateDataQuery, startDay, endDay string) ([]ActivityTemplateGiftStat, error) { _, giftScope, _, participantRegionID := activityParticipantQueryScopes(query.RegionID) rows, err := r.db.QueryContext(ctx, ` SELECT g.gift_id, COALESCE((SELECT COUNT(DISTINCT p.user_id) FROM stat_activity_template_participants p WHERE p.app_code = g.app_code AND p.template_id = g.template_id AND p.published_version = g.published_version AND p.scope_type = ? AND p.region_id = ? AND p.dimension_key = g.gift_id AND p.stat_day BETWEEN ? AND ?), 0), SUM(g.gift_event_count), SUM(g.gift_count), SUM(g.coin_amount) FROM stat_activity_template_gift_day g WHERE g.app_code = ? AND g.template_id = ? AND g.published_version = ? AND g.stat_day BETWEEN ? AND ? AND (? = 0 OR g.region_id = ?) GROUP BY g.app_code, g.template_id, g.published_version, g.gift_id ORDER BY SUM(g.coin_amount) DESC, SUM(g.gift_count) DESC, g.gift_id ASC LIMIT ? `, giftScope, participantRegionID, startDay, endDay, query.AppCode, query.TemplateID, query.PublishedVersion, startDay, endDay, query.RegionID, query.RegionID, activityDimensionLimit) if err != nil { return nil, err } defer rows.Close() out := make([]ActivityTemplateGiftStat, 0) for rows.Next() { var item ActivityTemplateGiftStat if err := rows.Scan(&item.GiftID, &item.GiftUsers, &item.GiftEventCount, &item.GiftCount, &item.CoinAmount); err != nil { return nil, err } out = append(out, item) } return out, rows.Err() } func (r *Repository) queryActivityTemplateTasks(ctx context.Context, query ActivityTemplateDataQuery, startDay, endDay string) ([]ActivityTemplateTaskStat, error) { _, _, taskScope, participantRegionID := activityParticipantQueryScopes(query.RegionID) rows, err := r.db.QueryContext(ctx, ` SELECT t.task_key, MAX(t.task_type), COALESCE((SELECT COUNT(DISTINCT p.user_id) FROM stat_activity_template_participants p WHERE p.app_code = t.app_code AND p.template_id = t.template_id AND p.published_version = t.published_version AND p.scope_type = ? AND p.region_id = ? AND p.dimension_key = t.task_key AND p.stat_day BETWEEN ? AND ?), 0), SUM(t.progress_value), SUM(t.completed_count), SUM(t.claimed_count) FROM stat_activity_template_task_day t WHERE t.app_code = ? AND t.template_id = ? AND t.published_version = ? AND t.stat_day BETWEEN ? AND ? AND (? = 0 OR t.region_id = ?) GROUP BY t.app_code, t.template_id, t.published_version, t.task_key ORDER BY SUM(t.completed_count) DESC, SUM(t.claimed_count) DESC, t.task_key ASC LIMIT ? `, taskScope, participantRegionID, startDay, endDay, query.AppCode, query.TemplateID, query.PublishedVersion, startDay, endDay, query.RegionID, query.RegionID, activityDimensionLimit) if err != nil { return nil, err } defer rows.Close() out := make([]ActivityTemplateTaskStat, 0) for rows.Next() { var item ActivityTemplateTaskStat if err := rows.Scan(&item.TaskKey, &item.TaskType, &item.TaskUsers, &item.TaskProgress, &item.TaskCompleted, &item.TaskClaimed); err != nil { return nil, err } out = append(out, item) } return out, rows.Err() }