162 lines
4.5 KiB
Go
162 lines
4.5 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"dashboard-cdc-worker/internal/dashboard"
|
|
)
|
|
|
|
const userReconcileSource = "likei.user_base_info"
|
|
|
|
type userReconcileRow struct {
|
|
user dashboard.UserCountry
|
|
}
|
|
|
|
func (r *MySQLRepository) StartUserReconciler(ctx context.Context, logger *slog.Logger,
|
|
projector *dashboard.Projector) {
|
|
if !r.cfg.User.Enabled || r.cfg.User.Interval <= 0 {
|
|
return
|
|
}
|
|
run := func() {
|
|
location, err := time.LoadLocation(r.cfg.Dashboard.StorageTimezone)
|
|
if err != nil {
|
|
location = time.UTC
|
|
}
|
|
now := time.Now().In(location).Add(-r.cfg.User.CurrentDayLag)
|
|
end := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), 0, location)
|
|
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location).
|
|
AddDate(0, 0, -r.cfg.User.LookbackDays+1)
|
|
|
|
events, err := r.BuildUserReconcileEvents(ctx, start, end, r.cfg.User.BatchLimit)
|
|
if err != nil {
|
|
if r.metrics != nil {
|
|
r.metrics.RecordError(err)
|
|
}
|
|
logger.Error("dashboard user reconcile failed", "error", err)
|
|
return
|
|
}
|
|
if len(events) == 0 {
|
|
logger.Info("dashboard user reconcile skipped, no events", "since", start, "until", end)
|
|
return
|
|
}
|
|
if err := projector.ApplyEvents(ctx, events); err != nil {
|
|
if r.metrics != nil {
|
|
r.metrics.RecordError(err)
|
|
}
|
|
logger.Error("dashboard user reconcile apply failed", "events", len(events), "error", err)
|
|
return
|
|
}
|
|
logger.Info("dashboard user reconcile applied", "events", len(events), "since", start, "until", end)
|
|
}
|
|
go func() {
|
|
run()
|
|
ticker := time.NewTicker(r.cfg.User.Interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
run()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (r *MySQLRepository) BuildUserReconcileEvents(ctx context.Context, since, until time.Time,
|
|
limit int) ([]dashboard.Event, error) {
|
|
rows, err := r.loadUserReconcileRows(ctx, since, until, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events := make([]dashboard.Event, 0, len(rows))
|
|
for _, row := range rows {
|
|
user := row.user
|
|
displayCreateTime := user.CreatedAt.Add(r.cfg.User.CreateTimeDisplayOffset)
|
|
contribution := dashboard.Contribution{
|
|
SourceSchema: r.cfg.MySQL.Schema,
|
|
SourceTable: "user_base_info",
|
|
SourcePK: fmt.Sprint(user.UserID),
|
|
SourceAction: "insert",
|
|
DeltaSign: 1,
|
|
SysOrigin: user.SysOrigin,
|
|
UserID: user.UserID,
|
|
Country: user.Country,
|
|
EventTime: displayCreateTime,
|
|
UserCreatedAt: user.CreatedAt,
|
|
CountryNewUser: 1,
|
|
}
|
|
events = append(events, dashboard.Event{
|
|
EventKey: userReconcileEventKey(user.UserID),
|
|
Source: userReconcileSource,
|
|
User: &user,
|
|
Contributions: []dashboard.Contribution{contribution},
|
|
})
|
|
}
|
|
return events, nil
|
|
}
|
|
|
|
func (r *MySQLRepository) loadUserReconcileRows(ctx context.Context, since, until time.Time,
|
|
limit int) ([]userReconcileRow, error) {
|
|
args := []interface{}{
|
|
r.cfg.Dashboard.UnknownCountryCode,
|
|
r.cfg.Dashboard.UnknownCountryCode,
|
|
since,
|
|
until,
|
|
}
|
|
originSQL := ""
|
|
if len(r.cfg.Dashboard.SysOrigins) > 0 {
|
|
originSQL = " AND origin_sys IN " + inPlaceholders(len(r.cfg.Dashboard.SysOrigins))
|
|
for _, origin := range r.cfg.Dashboard.SysOrigins {
|
|
args = append(args, origin)
|
|
}
|
|
}
|
|
limitSQL := ""
|
|
if limit > 0 {
|
|
limitSQL = " LIMIT ?"
|
|
args = append(args, limit)
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, origin_sys,
|
|
IFNULL(NULLIF(country_code, ''), ?),
|
|
IFNULL(NULLIF(country_name, ''), IFNULL(NULLIF(country_code, ''), ?)),
|
|
IFNULL(is_del, 0), create_time, update_time
|
|
FROM user_base_info
|
|
WHERE create_time >= ? AND create_time < ?
|
|
AND IFNULL(is_del, 0) = 0`+originSQL+`
|
|
ORDER BY create_time ASC`+limitSQL, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
result := make([]userReconcileRow, 0)
|
|
for rows.Next() {
|
|
var user dashboard.UserCountry
|
|
var createdAt, updatedAt sql.NullTime
|
|
if err := rows.Scan(&user.UserID, &user.SysOrigin, &user.Country.Code, &user.Country.Name,
|
|
&user.IsDeleted, &createdAt, &updatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if createdAt.Valid {
|
|
user.CreatedAt = createdAt.Time
|
|
}
|
|
if updatedAt.Valid {
|
|
user.UpdatedAt = updatedAt.Time
|
|
}
|
|
if user.UserID == 0 || user.SysOrigin == "" || user.CreatedAt.IsZero() {
|
|
continue
|
|
}
|
|
result = append(result, userReconcileRow{user: user})
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func userReconcileEventKey(userID int64) string {
|
|
return fmt.Sprintf("reconcile:%s:%d:created:v2-display-time", userReconcileSource, userID)
|
|
}
|