529 lines
28 KiB
Go
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()
|
|
}
|