feat(成就): 多了 9 个好拿的档和「挑战成功」一条线,比赛里做的题也算,这学期八成学生多拿到成就
- 补档:做对 20 道、第一道中等题、交满 30 次、5 天交过题、一次就对 5 道、错 3 次后做对、一天做对 5 道; 新线「挑战成功」1 次 / 5 次 - 比赛里的提交也计入成就(比赛其实是单班测验,2612 班整班在比赛里做题,原来一个成就都没有); 参加过几场比赛改成判题时就更新,以前只在后台重扫时刷新 - 奖杯收藏家从 15 个提到 20 个 - 0044 迁移:按新口径从提交记录重数指标,给达标的人补发(已经拿到的不收回) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1fdee94812
commit
cccf7a37be
7 files changed
+4356
-18
No files matched your search
@@ -0,0 +1,184 @@
|
||||
-- 成就惠及更多学生(2026-11-03)。已经拿到的一个不收回。
|
||||
--
|
||||
-- 这学期 375 个交过题的学生,成就数中位数 5 个、三分之一只有 0–2 个。三件事:
|
||||
--
|
||||
-- 1. 比赛里的提交也算成就(判题结算同步改了,见 services/achievements.ts 的 solvedCounts)。
|
||||
-- 这里的比赛其实是单班测验,2612 班整个班都在比赛里做题,人均做对 17 道却一个成就也没有;
|
||||
-- 「参加过几场比赛」以前判题时也不更新,只在后台重扫时刷新,所以连「初入赛场」都没拿到。
|
||||
-- 2. 几条线补上低一档(10 道和 50 道之间、一次就对 1 次和 20 次之间……),外加一条新线
|
||||
-- 「挑战成功」。括号里是这学期学生已经达到的比例。
|
||||
-- 3. 档多了,「奖杯收藏家」从 15 个提到 20 个,不然人人都有。
|
||||
--
|
||||
-- 然后从提交记录把含比赛的指标重数一遍,按新配置给所有达标的人补发(backfilled,不显示日期)。
|
||||
-- 编译错误次数和按天的连续不重数:前者上线前的历史本来就没有,后者已经下架。
|
||||
|
||||
INSERT INTO "achievement" ("name", "description", "icon", "rarity", "hidden", "metric", "operator", "threshold", "visible", "unlock_count", "order", "create_time") VALUES
|
||||
('稳扎稳打', '通过 20 道题目', 'noto:orange-book', 'bronze', false, 'accepted_count', 'gte', 20, true, 0, 2, now()), -- 44%
|
||||
('初露锋芒', '通过 1 道中等难度题目', 'noto:sunrise-over-mountains', 'bronze', false, 'mid_ac_count', 'gte', 1, true, 0, 6, now()), -- 21%
|
||||
('迎难而上', '挑战成功 1 次', 'noto:puzzle-piece', 'bronze', false, 'challenge_count', 'gte', 1, true, 0, 10, now()), -- 60%
|
||||
('精益求精', '挑战成功 5 次', 'noto:gem-stone', 'silver', false, 'challenge_count', 'gte', 5, true, 0, 10, now()), -- 12%
|
||||
('勤学苦练', '累计提交 30 次', 'noto:pencil', 'bronze', false, 'submission_count', 'gte', 30, true, 0, 11, now()), -- 48%
|
||||
('渐成习惯', '累计 5 天有提交记录', 'noto:spiral-calendar', 'bronze', false, 'active_days', 'gte', 5, true, 0, 15, now()), -- 59%
|
||||
('手感火热', '累计 5 次首次提交即通过', 'noto:glowing-star', 'bronze', true, 'first_try_ac_count', 'gte', 5, true, 0, 25, now()), -- 67%
|
||||
('再试一次', '同一道题失败 3 次后终于通过', 'noto:counterclockwise-arrows-button', 'bronze', true, 'max_wa_before_ac', 'gte', 3, true, 0, 32, now()), -- 54%
|
||||
('一鼓作气', '一天之内通过 5 道题', 'noto:dashing-away', 'bronze', true, 'max_ac_in_one_day', 'gte', 5, true, 0, 34, now()); -- 57%
|
||||
--> statement-breakpoint
|
||||
UPDATE "achievement" SET "threshold" = 20, "description" = '解锁 20 个成就(不含白金)'
|
||||
WHERE "metric" = 'achievement_unlocked_count' AND "threshold" = 15;
|
||||
--> statement-breakpoint
|
||||
-- 交过题、还没有 user_stat 的人先建一行(2612 班大多是这样:比赛提交以前根本不进结算)
|
||||
INSERT INTO "user_stat" ("user_id", "metrics", "update_time")
|
||||
SELECT DISTINCT s."user_id", '{}'::jsonb, now()
|
||||
FROM "submission" s
|
||||
JOIN "user" u ON u."id" = s."user_id"
|
||||
ON CONFLICT ("user_id") DO NOTHING;
|
||||
--> statement-breakpoint
|
||||
-- 按判题结算的口径从提交记录重数(services/achievements.ts):含比赛;在判的(6/7)不算,
|
||||
-- 它们判完时结算会自己加;做对 = ACCEPTED(0) / AST_CHECK_FAILED(10);日历按东八区。
|
||||
-- 「新做对」= 这道题第一次做对的那条提交。
|
||||
UPDATE "user_stat" AS target
|
||||
SET "metrics" = target."metrics" || fresh.metrics, "update_time" = now()
|
||||
FROM (
|
||||
WITH subs AS (
|
||||
SELECT s."id", s."user_id", s."problem_id", s."contest_id", s."result", s."language", s."code", s."create_time",
|
||||
(s."create_time" AT TIME ZONE 'Asia/Shanghai') AS local, p."difficulty",
|
||||
s."result" IN (0, 10) AS ac,
|
||||
-- 这道题在这条之前交过几次(同一时刻的不算「之前」,和结算里的 create_time < 一致)
|
||||
rank() OVER (PARTITION BY s."user_id", s."problem_id" ORDER BY s."create_time") - 1 AS before
|
||||
FROM "submission" s
|
||||
JOIN "user" u ON u."id" = s."user_id"
|
||||
JOIN "problem" p ON p."id" = s."problem_id"
|
||||
WHERE s."result" NOT IN (6, 7)
|
||||
),
|
||||
totals AS (
|
||||
SELECT "user_id",
|
||||
count(DISTINCT "problem_id") FILTER (WHERE ac)::int AS accepted,
|
||||
count(DISTINCT "problem_id") FILTER (WHERE ac AND "difficulty" = 'Mid')::int AS mid,
|
||||
count(DISTINCT "problem_id") FILTER (WHERE ac AND "difficulty" = 'High')::int AS hard,
|
||||
count(*)::int AS submissions,
|
||||
count(DISTINCT "contest_id")::int AS contests,
|
||||
count(*) FILTER (WHERE extract(hour FROM local) < 5)::int AS midnight,
|
||||
count(*) FILTER (WHERE extract(hour FROM local) >= 5 AND extract(hour FROM local) < 7)::int AS early,
|
||||
jsonb_agg(DISTINCT "language") AS languages,
|
||||
jsonb_agg(DISTINCT to_char(local, 'YYYY-MM-DD')) AS dates
|
||||
FROM subs GROUP BY "user_id"
|
||||
),
|
||||
first_try AS (
|
||||
SELECT "user_id", count(*) FILTER (WHERE ac)::int AS n
|
||||
FROM (SELECT DISTINCT ON ("user_id", "problem_id") "user_id", ac FROM subs ORDER BY "user_id", "problem_id", "create_time", "id") f
|
||||
GROUP BY "user_id"
|
||||
),
|
||||
code_lines AS (
|
||||
SELECT "user_id", max((
|
||||
SELECT count(*) FROM regexp_split_to_table("code", E'\n') AS line WHERE btrim(line, E' \t\r') <> ''
|
||||
))::int AS n
|
||||
FROM subs WHERE ac GROUP BY "user_id"
|
||||
),
|
||||
first_ac AS (
|
||||
SELECT DISTINCT ON ("user_id", "problem_id") "user_id", "problem_id", local, before
|
||||
FROM subs WHERE ac ORDER BY "user_id", "problem_id", "create_time", "id"
|
||||
),
|
||||
wa_before AS (
|
||||
SELECT "user_id", max(before)::int AS n FROM first_ac GROUP BY "user_id"
|
||||
),
|
||||
per_day AS (
|
||||
SELECT "user_id", jsonb_object_agg(day, n) AS days, max(n)::int AS best
|
||||
FROM (SELECT "user_id", to_char(local, 'YYYY-MM-DD') AS day, count(*)::int AS n FROM first_ac GROUP BY 1, 2) d
|
||||
GROUP BY "user_id"
|
||||
),
|
||||
weeks AS (
|
||||
SELECT DISTINCT "user_id", date_trunc('week', local)::date AS w FROM first_ac
|
||||
),
|
||||
runs AS (
|
||||
SELECT "user_id", count(*)::int AS len, max(w) AS last_w
|
||||
FROM (SELECT "user_id", w, w - (row_number() OVER (PARTITION BY "user_id" ORDER BY w))::int * 7 AS g FROM weeks) i
|
||||
GROUP BY "user_id", g
|
||||
),
|
||||
streak AS (
|
||||
SELECT "user_id", max(len) AS best, (array_agg(len ORDER BY last_w DESC))[1] AS current, max(last_w) AS last_w
|
||||
FROM runs GROUP BY "user_id"
|
||||
),
|
||||
challenges AS (
|
||||
SELECT "user_id", count(*)::int AS n FROM "challenge_success" GROUP BY "user_id"
|
||||
)
|
||||
SELECT t."user_id",
|
||||
jsonb_build_object(
|
||||
'accepted_count', t.accepted,
|
||||
'mid_ac_count', t.mid,
|
||||
'hard_ac_count', t.hard,
|
||||
'first_try_ac_count', coalesce(ft.n, 0),
|
||||
'submission_count', t.submissions,
|
||||
'contest_joined', t.contests,
|
||||
'midnight_submissions', t.midnight,
|
||||
'early_bird_submissions', t.early,
|
||||
'_languages', t.languages,
|
||||
'languages_used', jsonb_array_length(t.languages),
|
||||
'_active_dates', t.dates,
|
||||
'active_days', jsonb_array_length(t.dates),
|
||||
'max_code_lines', coalesce(cl.n, 0),
|
||||
'max_wa_before_ac', coalesce(wb.n, 0),
|
||||
'_ac_per_day', coalesce(pd.days, '{}'::jsonb),
|
||||
'max_ac_in_one_day', coalesce(pd.best, 0),
|
||||
'challenge_count', coalesce(ch.n, 0)
|
||||
) || CASE WHEN st."user_id" IS NULL THEN '{}'::jsonb ELSE jsonb_build_object(
|
||||
'max_ac_week_streak', st.best,
|
||||
'_current_ac_week_streak', st.current,
|
||||
'_last_ac_week', to_char(st.last_w, 'YYYY-MM-DD')
|
||||
) END AS metrics
|
||||
FROM totals t
|
||||
LEFT JOIN first_try ft ON ft."user_id" = t."user_id"
|
||||
LEFT JOIN code_lines cl ON cl."user_id" = t."user_id"
|
||||
LEFT JOIN wa_before wb ON wb."user_id" = t."user_id"
|
||||
LEFT JOIN per_day pd ON pd."user_id" = t."user_id"
|
||||
LEFT JOIN streak st ON st."user_id" = t."user_id"
|
||||
LEFT JOIN challenges ch ON ch."user_id" = t."user_id"
|
||||
) fresh
|
||||
WHERE target."user_id" = fresh."user_id";
|
||||
--> statement-breakpoint
|
||||
-- 挑战成功了、却一条提交都没有的(不会有,保险)也给个数
|
||||
UPDATE "user_stat" AS target
|
||||
SET "metrics" = jsonb_set(target."metrics", '{challenge_count}', to_jsonb(c.n))
|
||||
FROM (SELECT "user_id", count(*)::int AS n FROM "challenge_success" GROUP BY "user_id") c
|
||||
WHERE target."user_id" = c."user_id" AND (target."metrics" -> 'challenge_count') IS DISTINCT FROM to_jsonb(c.n);
|
||||
--> statement-breakpoint
|
||||
-- 补发:上架的、非元成就,达标没发的都发(口径同 rescanAchievement:backfilled、待通知)
|
||||
INSERT INTO "user_achievement" ("user_id", "achievement_id", "unlock_time", "backfilled", "notified")
|
||||
SELECT st."user_id", a."id", now(), true, false
|
||||
FROM "user_stat" st
|
||||
JOIN "achievement" a ON a."visible" AND a."metric" <> 'achievement_unlocked_count'
|
||||
WHERE jsonb_typeof(st."metrics" -> a."metric") = 'number'
|
||||
AND CASE WHEN a."operator" = 'gte' THEN (st."metrics" ->> a."metric")::numeric >= a."threshold"
|
||||
ELSE (st."metrics" ->> a."metric")::numeric <= a."threshold" END
|
||||
ON CONFLICT ("user_id", "achievement_id") DO NOTHING;
|
||||
--> statement-breakpoint
|
||||
-- 已解锁数(上架的非白金)重数,口径同 refreshUnlockedCount
|
||||
UPDATE "user_stat" AS target
|
||||
SET "metrics" = jsonb_set(target."metrics", '{achievement_unlocked_count}', to_jsonb(fresh.value))
|
||||
FROM (
|
||||
SELECT s."id", coalesce(c.value, 0) AS value
|
||||
FROM "user_stat" s
|
||||
LEFT JOIN (
|
||||
SELECT ua."user_id", count(*)::int AS value
|
||||
FROM "user_achievement" ua
|
||||
JOIN "achievement" a ON a."id" = ua."achievement_id"
|
||||
WHERE a."rarity" <> 'platinum' AND a."visible"
|
||||
GROUP BY ua."user_id"
|
||||
) c ON c."user_id" = s."user_id"
|
||||
) fresh
|
||||
WHERE target."id" = fresh."id"
|
||||
AND (target."metrics" -> 'achievement_unlocked_count') IS DISTINCT FROM to_jsonb(fresh.value);
|
||||
--> statement-breakpoint
|
||||
-- 第二轮:元成就(奖杯收藏家)
|
||||
INSERT INTO "user_achievement" ("user_id", "achievement_id", "unlock_time", "backfilled", "notified")
|
||||
SELECT st."user_id", a."id", now(), true, false
|
||||
FROM "user_stat" st
|
||||
JOIN "achievement" a ON a."visible" AND a."metric" = 'achievement_unlocked_count'
|
||||
WHERE jsonb_typeof(st."metrics" -> a."metric") = 'number'
|
||||
AND CASE WHEN a."operator" = 'gte' THEN (st."metrics" ->> a."metric")::numeric >= a."threshold"
|
||||
ELSE (st."metrics" ->> a."metric")::numeric <= a."threshold" END
|
||||
ON CONFLICT ("user_id", "achievement_id") DO NOTHING;
|
||||
--> statement-breakpoint
|
||||
UPDATE "achievement" AS a
|
||||
SET "unlock_count" = coalesce(c.n, 0)
|
||||
FROM (SELECT "achievement_id", count(*)::int AS n FROM "user_achievement" GROUP BY "achievement_id") c
|
||||
WHERE c."achievement_id" = a."id" AND a."unlock_count" IS DISTINCT FROM c.n;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -309,6 +309,13 @@
|
||||
"when": 1793693911394,
|
||||
"tag": "0043_mascot_event",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 44,
|
||||
"version": "7",
|
||||
"when": 1793695621303,
|
||||
"tag": "0044_achievement_more_tiers",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17,22 +17,22 @@ export const ACHIEVEMENT_METRICS: AchievementMetric[] = [
|
||||
{
|
||||
key: 'accepted_count',
|
||||
name: '做对的题数',
|
||||
helpText: '去重后通过的题目数量(不含比赛)',
|
||||
helpText: '去重后通过的题目数量(含比赛题)',
|
||||
},
|
||||
{
|
||||
key: 'mid_ac_count',
|
||||
name: '做对的中等题',
|
||||
helpText: '去重后通过的中等难度题目数(不含比赛)',
|
||||
helpText: '去重后通过的中等难度题目数(含比赛题)',
|
||||
},
|
||||
{
|
||||
key: 'hard_ac_count',
|
||||
name: '做对的困难题',
|
||||
helpText: '去重后通过的困难题目数(不含比赛)',
|
||||
helpText: '去重后通过的困难题目数(含比赛题)',
|
||||
},
|
||||
{
|
||||
key: 'submission_count',
|
||||
name: '提交次数',
|
||||
helpText: '提交次数(不含比赛)',
|
||||
helpText: '提交次数(含比赛)',
|
||||
},
|
||||
{ key: 'active_days', name: '有提交的天数', helpText: '有过提交的累计天数' },
|
||||
{
|
||||
@@ -45,13 +45,13 @@ export const ACHIEVEMENT_METRICS: AchievementMetric[] = [
|
||||
{
|
||||
key: 'max_ac_week_streak',
|
||||
name: '连续有新做对的周数',
|
||||
helpText: '连续几个自然周(周一起)每周都至少新通过一道题(不含比赛)',
|
||||
helpText: '连续几个自然周(周一起)每周都至少新通过一道题(含比赛题)',
|
||||
},
|
||||
{ key: 'languages_used', name: '用过几种语言', helpText: '用过多少种编程语言' },
|
||||
{
|
||||
key: 'contest_joined',
|
||||
name: '参加比赛',
|
||||
helpText: '参加过的比赛数量(本指标是比赛维度,不受比赛提交不计入的限制)',
|
||||
helpText: '交过题的比赛数量',
|
||||
},
|
||||
{ key: 'badge_count', name: '专题奖章', helpText: '获得的专题奖章数量' },
|
||||
{
|
||||
@@ -59,6 +59,11 @@ export const ACHIEVEMENT_METRICS: AchievementMetric[] = [
|
||||
name: '做完的专题',
|
||||
helpText: '完成的专题数量',
|
||||
},
|
||||
{
|
||||
key: 'challenge_count',
|
||||
name: '挑战成功',
|
||||
helpText: '做对之后的挑战,做到了几条(每道题的每条挑战算一次)',
|
||||
},
|
||||
{
|
||||
key: 'first_try_ac_count',
|
||||
name: '一次就对的题',
|
||||
@@ -92,7 +97,7 @@ export const ACHIEVEMENT_METRICS: AchievementMetric[] = [
|
||||
{
|
||||
key: 'max_code_lines',
|
||||
name: '最长的代码行数',
|
||||
helpText: '做对的提交里最长的代码有多少行(空行不算,不含比赛)',
|
||||
helpText: '做对的提交里最长的代码有多少行(空行不算,含比赛)',
|
||||
},
|
||||
{
|
||||
key: 'achievement_unlocked_count',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, count, countDistinct, eq, inArray, isNotNull, isNull, lt, ne, notInArray, sql } from 'drizzle-orm'
|
||||
import { and, count, countDistinct, eq, inArray, isNotNull, lt, ne, notInArray, sql } from 'drizzle-orm'
|
||||
|
||||
import { db, schema, type DbOrTx } from '../db'
|
||||
import { publishAchievementNotification } from '../events'
|
||||
@@ -82,12 +82,17 @@ const ACCEPTED_RESULTS = sql.join([sql`${JudgeStatus.ACCEPTED}`, sql`${JudgeStat
|
||||
|
||||
/**
|
||||
* 「做对几道 / 中等几道 / 困难几道 / 一次就对几道」按定义从提交表现数,不再逐次 +1。
|
||||
* 参加过几场比赛、挑战成功几次也在这里一起数。
|
||||
*
|
||||
* 原来是读一遍这道题之前的提交、判断「是不是第一次做对」再 +1,两处漏:同一个人同一道题
|
||||
* 两条提交挨着判完,两边都看见对方已经做对(都不加)或都还没做对(加两次);重判一条
|
||||
* 做对的提交又会再加一次。2026-10 的库里 372 人的做对题数比实际多 1–4 道。
|
||||
* 现数是幂等的,怎么并发、怎么重判都是同一个答案。走 submission_public_metrics_idx,
|
||||
* 一个人的提交也就几百条。
|
||||
*
|
||||
* **比赛里的提交也算**(2026-11-03 起):这里的比赛其实是单班测验,有的班整学期都在
|
||||
* 比赛里做题,不算的话一个成就都拿不到。比赛题是单独的题目行,不会和题库里同一道题重复数。
|
||||
* 个人主页 / 排名的做对题数(user_profile.accepted_number)仍然不含比赛,两边对不上是有意的。
|
||||
*/
|
||||
async function solvedCounts(tx: DbOrTx, userId: number) {
|
||||
const [row] = await tx.execute<{
|
||||
@@ -95,13 +100,15 @@ async function solvedCounts(tx: DbOrTx, userId: number) {
|
||||
mid: number
|
||||
hard: number
|
||||
first_try: number
|
||||
contests: number
|
||||
challenges: number
|
||||
}>(
|
||||
sql`
|
||||
with mine as (
|
||||
select s.problem_id, s.result, s.create_time, s.id, p.difficulty
|
||||
select s.problem_id, s.contest_id, s.result, s.create_time, s.id, p.difficulty
|
||||
from ${schema.submission} s
|
||||
join ${schema.problem} p on p.id = s.problem_id
|
||||
where s.user_id = ${userId} and s.contest_id is null
|
||||
where s.user_id = ${userId}
|
||||
)
|
||||
select
|
||||
count(distinct problem_id) filter (where result in (${ACCEPTED_RESULTS}))::int as accepted,
|
||||
@@ -111,11 +118,13 @@ async function solvedCounts(tx: DbOrTx, userId: number) {
|
||||
select count(*)::int from (
|
||||
select distinct on (problem_id) result from mine order by problem_id, create_time, id
|
||||
) first where result in (${ACCEPTED_RESULTS})
|
||||
) as first_try
|
||||
) as first_try,
|
||||
count(distinct contest_id)::int as contests,
|
||||
(select count(*)::int from ${schema.challengeSuccess} where user_id = ${userId}) as challenges
|
||||
from mine
|
||||
`,
|
||||
)
|
||||
return row ?? { accepted: 0, mid: 0, hard: 0, first_try: 0 }
|
||||
return row ?? { accepted: 0, mid: 0, hard: 0, first_try: 0, contests: 0, challenges: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +139,7 @@ export async function updateAchievementsForSubmission(submissionId: string, coun
|
||||
.innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id))
|
||||
.where(eq(schema.submission.id, submissionId))
|
||||
.limit(1)
|
||||
if (!row || row.submission.contestId !== null) return []
|
||||
if (!row) return []
|
||||
|
||||
const rejudged = countedAs !== null
|
||||
const accepted = isAccepted(row.submission.result)
|
||||
@@ -157,6 +166,8 @@ export async function updateAchievementsForSubmission(submissionId: string, coun
|
||||
value.mid_ac_count = solved.mid
|
||||
value.hard_ac_count = solved.hard
|
||||
value.first_try_ac_count = solved.first_try
|
||||
value.contest_joined = solved.contests
|
||||
value.challenge_count = solved.challenges
|
||||
if (!rejudged) value.submission_count = numberMetric(value, 'submission_count') + 1
|
||||
if (firstAc) {
|
||||
// 只数这条之前的:重判一条老提交时,它后面的提交不算「做对之前错的」
|
||||
@@ -167,7 +178,6 @@ export async function updateAchievementsForSubmission(submissionId: string, coun
|
||||
and(
|
||||
eq(schema.submission.userId, row.submission.userId),
|
||||
eq(schema.submission.problemId, row.submission.problemId),
|
||||
isNull(schema.submission.contestId),
|
||||
lt(schema.submission.createTime, row.submission.createTime),
|
||||
),
|
||||
)
|
||||
@@ -301,7 +311,7 @@ export async function rescanAchievement(achievementId: number) {
|
||||
const metric = findMetric(achievement.metric)
|
||||
if (!metric) return { scanned: 0, unlocked: 0 }
|
||||
|
||||
// contest_joined 不由判题结算维护,扫之前先把它刷新一遍,否则永远读到旧值(或没有值)
|
||||
// contest_joined 判题结算从 2026-11-03 起才维护,那之后没再交过题的人还是旧值,扫之前刷新一遍
|
||||
if (achievement.metric === 'contest_joined') await refreshContestJoinedForAll()
|
||||
// max_ac_week_streak 是后加的指标,判题结算只从上线那天起累计,存量用户要从提交记录补算
|
||||
if (achievement.metric === 'max_ac_week_streak') await refreshAcWeekStreakForAll()
|
||||
@@ -482,7 +492,7 @@ async function refreshContestJoinedForAll() {
|
||||
* `_last_ac_week` / `_current_ac_week_streak` 一起写回 user_stat(jsonb 浅合并,
|
||||
* 其余指标不动,写法同 refreshContestJoinedForAll)。
|
||||
*
|
||||
* 「新通过」= 这道题(不含比赛)第一次通过的那次,和判题结算里 firstAc 同一个口径;
|
||||
* 「新通过」= 这道题(含比赛题)第一次通过的那次,和判题结算里 firstAc 同一个口径;
|
||||
* 周按东八区自然周(周一起),SQL 里用 localTime 取墙上时间再 date_trunc。
|
||||
*/
|
||||
async function refreshAcWeekStreakForAll() {
|
||||
@@ -495,7 +505,7 @@ async function refreshAcWeekStreakForAll() {
|
||||
// 只算还在的用户:库里有提交的 user_id 已经对不上任何用户(submission.user_id 上没有
|
||||
// 外键,是历史遗留),直接写 user_stat 会撞 user_stat 的外键,整次重算失败
|
||||
.innerJoin(schema.user, eq(schema.user.id, schema.submission.userId))
|
||||
.where(and(isNull(schema.submission.contestId), inArray(schema.submission.result, [JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED])))
|
||||
.where(inArray(schema.submission.result, [JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]))
|
||||
.groupBy(schema.submission.userId, schema.submission.problemId)
|
||||
.as('first_accepted')
|
||||
const week = sql<string>`to_char(date_trunc('week', ${localTime(firstAccepted.firstAt)}), 'YYYY-MM-DD')`
|
||||
|
||||
@@ -34,6 +34,12 @@ const METRICS: Record<string, MetricInfo> = {
|
||||
unit: '道',
|
||||
remain: (r) => `再做对 ${r} 道困难题`,
|
||||
},
|
||||
challenge_count: {
|
||||
group: '做题',
|
||||
label: '挑战成功',
|
||||
unit: '次',
|
||||
remain: (r) => `再挑战成功 ${r} 次`,
|
||||
},
|
||||
max_ac_week_streak: {
|
||||
group: '坚持',
|
||||
label: '连着几周有做对',
|
||||
|
||||
@@ -507,7 +507,7 @@ export const problemListSortSchema = z.enum(['', 'create_time', '-submission_num
|
||||
* 离哪个成就最近;没做对的只给一个数(「有 n 道差一点」),不列出来。
|
||||
*/
|
||||
export const problemProgressSchema = z.object({
|
||||
/** 做对的题数,和成就的「AC 题目数」同一个数(不含比赛),这样「再做对 n 道」加得上 */
|
||||
/** 做对的题数,和成就的「AC 题目数」同一个数(含比赛题,2026-11-03 起),这样「再做对 n 道」加得上 */
|
||||
solved: z.number().int(),
|
||||
/** 本周(东八区周一起)新做对几道 */
|
||||
weekSolved: z.number().int(),
|
||||
|
||||
Reference in new issue
Block a user