2026-07-15 09:43:33 +08:00

529 lines
28 KiB
Go

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()
}