Every month the top student in each of Starters, Movers, Flyers, KET and PET on ranking.flyer.us wins a bicycle. Part A is the lean version: a small monthly event run by hand from September 2026 with no development. Part B is the audit of today's leaderboard and the full design to build if the event is kept after the pilot.
Small monthly event first, system laterwebs-ranking, exam and flyer-go repositories, so the “what exists today” claims can be checked against file paths. Sections 5 onward are a design proposal. Anything not yet decided is in Open questions, not silently assumed.The decision is to start now with what exists, not to wait for a system. Nothing in Part A needs a developer. It needs a one-page rule text, a printable teacher letter, one CS owner, a Google Sheet and a budget line. The public board stays exactly as it is; FLYER adds the prize, the eligibility check and the announcement around it.
This is the text a parent reads. It is deliberately short. Anything not here is decided by FLYER and explained to the family concerned.
| Rule | Text |
|---|---|
| Prize | Every month, the student ranked first on the Monthly tab of ranking.flyer.us in each of Starters, Movers, Flyers, KET and PET receives one bicycle. Five bicycles a month. |
| When | Standings are taken at 23:59 on the last day of the month, Vietnam time. FLYER keeps a screenshot as the record. Tests submitted after that belong to the next month. |
| Who can win | A student using their own personal account. Class, centre, teacher and family-shared accounts cannot win. The student's age on the last day of the month must be inside the level's band: Starters 6 to 8, Movers 8 to 10, Flyers 10 to 12, KET 11 to 14, PET 13 to 16. |
| Teacher letter | Within 7 days of being contacted, the family sends a photo of the confirmation letter signed by the student's current English teacher (school or centre), stating the student's name, date of birth, school and class, that they are in the age band, and that they sat the tests themselves. FLYER may call the teacher. |
| Reserves | If the first-ranked student cannot be reached, does not send the letter in time, or is not eligible, the prize passes to rank 2, then rank 3. |
| Limits | One bicycle per student per month (if first in two levels, the higher level counts). One bicycle per student per calendar year. |
| Fair play | FLYER may set aside a result that shows signs of abuse, for example several accounts on one device, more than 20 full tests in a day, or a birthday changed during the month, and pass the prize to the next reserve. |
| Announcement | Winners are announced on the 12th of the following month on FLYER's Zalo and Facebook, with first name, school and province. Delivery within two weeks of the announcement. |
| Exclusions | FLYER staff, partner and school administrator accounts, and demonstration accounts are not eligible. |
One owner, about one working day per month. Times are Vietnam time.
| When | Task | Time |
|---|---|---|
| Last day, 23:50 | Open ranking.flyer.us, Monthly tab, each of the five levels. Screenshot the top 5 of each. Save to the Drive folder for the month. | 15 min |
| 1st, 08:00 to 09:00 | Screenshot again (the hourly refresh may have moved a late submission). The 08:00 screenshot is the record if the two differ. Fill the Sheet: level, rank 1 to 3, display name, score, profile id. | 30 min |
| 1st | In admin, open each of the 15 profiles: personal account or class account, birthday on file, school, parent phone. Note obvious class or centre names. Mark provisional winner and reserves. | 45 min |
| 1st to 2nd | Zalo or call the parent of each provisional winner (message A3.1). Send the letter template as PDF. Tell reserves they are reserves (message A3.2). | 1 h |
| 2nd to 8th | Receive letters. Check name and date of birth against the profile and the age band. Call the teacher on the number given (script A3.3). Log the call in the Sheet. | 2 h |
| 8th to 10th | Decide: winner confirmed, or passed to reserve with a reason. Product lead signs off in the Sheet. Ask the winner's family for address, bicycle size, and consent to publish first name, school and photo. | 45 min |
| 12th | Marketing posts the five winners (message A3.4). CS orders the bicycles. | 30 min |
| By the 26th | Delivery. Family sends a photo; posted with consent. Sheet row closed. | 30 min |
One tab per month, one row per level and rank (15 rows). Columns: level, rank, screenshot link, display name, profile id, account type, birthday on file, age at month end, in band (yes/no), parent name, parent phone, contacted on, letter received on, letter link, teacher name, teacher phone, teacher called on, call outcome, decision, decided by, address, bicycle size, consent, ordered on, delivered on, photo link, notes.
A printable one-page bilingual form is at roadmap.flyer.vn/ranking-prize-letter. CS exports it to PDF once and sends the same file to every family. A phone photo of the signed page is accepted. Fields: student name, date of birth, school, grade and class, level and month, three statements (age band, learning at this level, sat the tests personally), teacher name, role, school or centre, phone, signature and date, parent name, phone, signature and consent to publish.
| Risk | How the pilot handles it |
|---|---|
| Rank 1 is a class or centre account (as on the board today) | Not eligible under “own personal account”. The prize passes to the reserve. CS can usually tell from the display name; the teacher letter settles the rest. |
| The winner is a student who sat several hundred tests | Accepted for the pilot if they are a real child in the band; the fair-play clause covers extreme daily volumes. Two such winners at one level trigger the best-ten query for the next month. |
| No birthday on the profile (58% of August racers) | The letter carries the date of birth and the teacher confirms it. Ask the family to add it to the profile as part of the claim. |
| Same student first in two levels | Higher level counts; the lower level's prize passes to its reserve. |
| The board moved between 23:59 and the 08:00 screenshot | The 08:00 screenshot on the 1st is the record, because it contains every test submitted before midnight. Say so in the rules post. |
| Dispute from a family | Reply with the screenshot and the rule text. The rules say FLYER's decision is final. |
| Teacher unreachable | Two attempts on two days, then the school's office; if still nothing by the 10th, pass to the reserve and tell the family why. |
The five backlog items on the board are ordered for that moment and stay parked until then.
FLYER wants a recurring, visible reward on the public leaderboard: each month, the student with the top score in Starters, Movers, Flyers, KET and PET receives a bicycle. The request came with one condition: a winner must present a letter from their English teacher confirming they are within the Cambridge age range for the level they won.
The leaderboard already exists and is well used. It has Daily, Weekly, Monthly and Yearly tabs, a level selector, and student and school tables. What it does not have is any of the properties a physical prize needs: a formula that is written down and versioned, eligibility rules, a way to tell a real child from an anonymous or duplicate profile, an age on the profile, and any tooling to run a claim and verification process.
The risk of skipping this work is concrete. A bicycle is worth enough that families will optimise for it. Today a student can be ranked from an anonymous profile, one user can hold several profiles, staff and demo accounts are not excluded, and the score is a running sum that rewards volume over quality. The first month of prizes would produce disputes that the team cannot adjudicate, because the rule that produced the number is not in any repository.
| Metric | Definition | Type |
|---|---|---|
| Active racers per level | Distinct eligible students with at least one counted test in the month, per level | Primary |
| Tests per racer | Median counted tests per active racer in the month | Primary |
| Profiles with verified DOB | Share of active student profiles with a date of birth entered and locked | Primary, the lasting asset |
| Claim completion | Share of provisional winners who upload a valid letter within the claim window | Guardrail |
| Verification turnaround | Days from freeze to announcement, target 12 | Guardrail |
| Disputes | Complaints per cycle that require a rule interpretation | Guardrail, target zero after month 2 |
| Retention | Share of month N racers still active in month N+1 | The real test of whether a prize changes behaviour |
| Level | Active students | 1 test | 2 to 4 | 5 to 9 | 10 or more |
|---|---|---|---|---|---|
| Starters | 11,950 | 4,181 | 3,553 | 1,899 | 2,317 |
| Movers | 9,954 | 3,463 | 2,881 | 1,615 | 1,995 |
| Flyers | 9,325 | 3,484 | 2,635 | 1,392 | 1,814 |
| KET | 6,077 | 2,227 | 1,775 | 1,085 | 990 |
| PET | 4,265 | 1,752 | 1,344 | 673 | 496 |
Read from the source, not assumed. The live app is flyer/webs-ranking (Next.js, last commit 27/08/2026) served at ranking.flyer.us; the older ranking-web repository is dead since October 2024. The ranking API is GraphQL v2 in flyer/exam.
| Aspect | Current behaviour | Where |
|---|---|---|
| Time windows | Daily, Weekly, Monthly, Yearly. No all-time. Default tab is Daily. | RankFrequency enum, pages/index.tsx |
| Levels | A dropdown of test levels of type CLASSIC: Starters (default), Movers, Flyers, KET, PET, plus IOE, SET, TOEFL Primary and Junior. IELTS is its own type and does not appear. | TestLevelsWithFilter |
| Tables | Student table and School table. Search box switches to challenge code (digits), class code (CL…) or a name search. | TableRanking |
| Score shown | The grade column: a sum of per-test grades normalised to 10 points each, over the window. Not a best score and not an average. A student who takes 40 tests outranks a student who takes 10 perfect ones. | learner_test_level_ranks |
| Where the formula lives | A materialized view created by hand on the production database. It is declared in Go only as a column list. No CREATE VIEW exists in any repository, so the exact tie-break cannot be read from git. | ent/schema/views.go, module/mview |
| Refresh | Hourly via Temporal, Asia/Ho_Chi_Minh. A submitted test can take up to an hour to move the board. | module/mview/mview.go |
| Which attempts count | Full mock tests only (type_attempt = 'exam'), submitted, grade above zero. Practice, speaking, writing and placement attempts do not count. Teacher-assigned and self-served tests count the same. No sample or paid filter. | sibling views in flyer-go |
| Tie-break | By analogy with the versioned monthly recap view: score descending, then total execution time ascending. Unverified for the leaderboard view itself. | 0092_user_score_views.up.sql |
| Filters | Province, school, class code, challenge code, display name. No age, grade or birth-year filter exists on the API. | LearnerTestLevelRankFilter |
| Identity shown | Rank, avatar and frame, display name, province, school, score. Clicking any row opens a public profile modal with coins, total points, study time, completed tests, accuracy, achievements and full attempt history. | ModalInfoProfileRanking |
| Date of birth | Fetched in the ranking fragment and never rendered. On the profile it is nullable, unvalidated and never read by any ranking, recap or badge job. | learner_profiles.date_of_birth |
| Eligibility | None. Viewing needs no login. Anonymous profiles (no user id) rank. One user can hold several profiles and each ranks separately. No staff or test-account exclusion. Demo students have their attempt dates shifted forward hourly so sample classes look active, and they surface in Daily and Weekly rankings. | AuthProvider, IsAnonymous, update_attempt_dates_by_students |
| Rewards today | One badge, TOURNAMENT_STAR, granted every Monday 01:00 to every learner holding rank 1 in any level for the week. Wisdom-stone gift store and giveaway campaigns exist but are unrelated to rank. No prize system, no admin screen for prizes. | AchievementWeeklyRankSt |
| Known UI defect | Medal icons for the top three are keyed off the row index, not the server rank. With any filter applied, the first three rows of the filtered list get gold, silver and bronze even if their real rank is 5,000. | TableRanking/index.tsx |
| # | Gap | Why it matters for a bicycle |
|---|---|---|
| 1 | Formula not in version control | Any dispute ends with “the database says so”. The team cannot show a parent the rule, cannot test it on dev (dev and prod already drift on the sibling monthly view), and cannot change it with a review trail. |
| 2 | Volume beats quality | A running sum means the winner is whoever sits the most tests. In August 2026 the number one at KET submitted 849 attempts and the number one at Movers sat 602 distinct tests, roughly twenty a day. That is neither the behaviour FLYER wants nor a result Academic can stand behind. |
| 3 | No eligibility | Anonymous profiles, duplicate profiles, staff, school demo accounts and artificially refreshed sample students are all rankable. At least one of these will be in a top three in month one. |
| 4 | No age on record | The condition “right Cambridge age for the level” cannot be checked, because date of birth is optional and unvalidated, and nothing in the API can filter on it. |
| 5 | No claim or verification tooling | There is nowhere for a winner to upload a letter, nowhere for CS to approve it, and no record of who won, when, and on what evidence. |
| 6 | Hourly lag and index-based medals | Small on their own, but on the last evening of the month a student who sees themselves in first place at 23:30 and third at 00:30 will not accept “the view had not refreshed”. |
| Level | Attempts | Distinct tests |
|---|---|---|
| Starters | 822 | 125 |
| Movers | 680 | 602 |
| Flyers | 444 | 353 |
| KET | 849 | 549 |
| PET | 311 | 204 |
These are the rules as a parent should be able to read them on a public rules page. Everything in sections 9 to 14 exists to make these rules true. The numbers quoted come from the August 2026 read-replica baseline in section 2.
| Level | Students at 100 / 100 |
|---|---|
| Starters | 641 |
| Movers | 410 |
| Flyers | 372 |
| KET | 163 |
| PET | 61 |
Cambridge publishes the Young Learners suite (Pre A1 Starters, A1 Movers, A2 Flyers) for children aged roughly 6 to 12, with each paper pitched at a typical band inside that range. For A2 Key for Schools and B1 Preliminary for Schools it publishes no fixed age, only that the papers are written for school-aged learners. The bands below are FLYER's operational bands, aligned to that guidance and to Vietnamese school grades. Age is counted in completed years on the last day of the prize month.
| Level | Cambridge guidance | FLYER band (age at month end) | Typical VN grade | Born in, for a September 2026 race |
|---|---|---|---|---|
| Starters (Pre A1) | YLE, children 6 to 12; Starters typically 7 to 8 | 6 to 8 | Grade 1 to 3 | 2018 to 2020 |
| Movers (A1) | Typically 8 to 11 | 8 to 10 | Grade 3 to 5 | 2016 to 2018 |
| Flyers (A2) | Typically 9 to 12 | 10 to 12 | Grade 5 to 7 | 2014 to 2016 |
| KET (A2 Key for Schools) | School-aged; commonly lower secondary | 11 to 14 | Grade 6 to 9 | 2012 to 2015 |
| PET (B1 Preliminary for Schools) | School-aged; commonly upper secondary | 13 to 16 | Grade 8 to 11 | 2010 to 2013 |
The letter is the human check. The platform can compute an age from a date of birth, but it cannot know whether the date is true or whether the child sat the tests. A named English teacher who signs a short form does both, and gives CS one phone number to call.
| Field | Purpose |
|---|---|
| Student full name, date of birth, school, grade and class | Matched against the profile; a mismatch on date of birth is an automatic reject. |
| Level won, prize month | Ties the letter to one cycle so it cannot be reused. |
| Statement 1: the student is within the FLYER age band for this level | The condition the programme was built on. |
| Statement 2: the student is currently learning English at or around this level | Catches an eleven-year-old parked in Starters for the prize even if the age fits the overlap. |
| Statement 3: to the teacher's knowledge the student completed the tests personally | The integrity check; makes the teacher a witness, not just a clerk. |
| Teacher full name, role, school or centre, mobile number, signature, date; school stamp if available | Gives CS a person to call. The stamp is optional because centre teachers often cannot obtain one. |
| Parent or guardian name, phone, signature, and consent to publish the winner's first name, school and photo | Required by the winner wall and by the delivery. |
The template is a one-page PDF, bilingual, downloadable from the claim page. A photo of the signed page taken with a phone is accepted; the upload is limited to 10 MB and to JPG, PNG, HEIC and PDF.
One cycle per level per month, all five running on the same calendar. Steps 1 and 2 are the existing leaderboard with the new formula; steps 3 to 7 are new.
Replace the hand-made monthly ranking for the five prize levels with a view whose definition lives in a migration file, computes the best-ten score in section 5.2, and carries a version number that is printed on the board.
monthly_prize_ranks (level, month, learner profile, score, distinct test count, capped total time, time of final score, rank, formula version), added to the hourly refresh list next to learner_test_level_ranks.row_number), never a percentile bucket. The sibling monthly recap view uses ntile(100) on production and row_number() on dev; that drift must not be repeated here.championship_month and championship_level, hidden from the library and from search until its window opens, single attempt enforced server-side (a second start is refused, not silently ignored), and released into the normal library once the window closes. Academic delivers five such tests by the 20th of each month.WITH counted AS ( -- one row per (student, test) = best attempt that month
SELECT ua.learner_profile_id, ua.test_level_id, ua.check_point_id,
bool_or(cp.championship_month IS NOT NULL) AS is_championship,
max(round(10 * ua.grade / ua.max_grade, 2)) AS best_grade,
min(least(ua.execution_time, tl.execution_time)) AS best_time,
min(ua.submitted_at) AS first_at
FROM user_attempts ua
JOIN test_level_exam_settings tl ON tl.test_level_id = ua.test_level_id
JOIN check_points cp ON cp.id = ua.check_point_id
WHERE ua.type_attempt = 'exam' AND ua.submitted_at IS NOT NULL
AND ua.grade > 0 AND ua.max_grade > 0
AND (ua.submitted_at AT TIME ZONE 'Asia/Ho_Chi_Minh') >= date_trunc('month', now() AT TIME ZONE 'Asia/Ho_Chi_Minh')
GROUP BY 1,2,3
), ranked AS (
SELECT *, row_number() OVER (PARTITION BY learner_profile_id, test_level_id
ORDER BY best_grade DESC, first_at) AS k
FROM counted WHERE NOT is_championship
), best10 AS (
SELECT learner_profile_id, test_level_id,
sum(best_grade) FILTER (WHERE k <= 10) AS score10,
count(*) AS distinct_tests,
sum(best_time) FILTER (WHERE k <= 10) AS counted_time,
max(first_at) FILTER (WHERE k <= 10) AS final_score_at
FROM ranked GROUP BY 1,2
), champ AS ( -- the month's single-attempt championship test
SELECT learner_profile_id, test_level_id, best_grade * 10 AS champ_score, best_time AS champ_time
FROM counted WHERE is_championship
)
SELECT b.learner_profile_id, b.test_level_id,
b.score10 + coalesce(c.champ_score, 0) AS score,
b.score10, c.champ_score, b.distinct_tests, b.counted_time, c.champ_time, b.final_score_at,
row_number() OVER (PARTITION BY b.test_level_id
ORDER BY b.score10 + coalesce(c.champ_score, 0) DESC,
c.champ_time ASC NULLS LAST, b.distinct_tests DESC,
b.counted_time ASC, b.final_score_at ASC) AS rank,
1 AS formula_version
FROM best10 b LEFT JOIN champ c USING (learner_profile_id, test_level_id);
exam repository and produces the same rank on dev and production for the same fixture data.dob_locked_at). Changing it requires a support ticket with a teacher letter or birth certificate. This is what makes the age check meaningful.A nightly job (and again at freeze) writes an eligibility row per active student and level: eligible or not, with a list of reasons. Reasons are a fixed enum so the UI can explain them.
| Reason code | Meaning | Shown to the student as |
|---|---|---|
ANONYMOUS | Profile has no user account | Create an account to be eligible |
NO_PARENT_PHONE | No verified parent phone | Add a parent's phone number |
NO_DOB | No date of birth, or added after the 20th | Add your birthday before the 20th |
AGE_OUT_OF_BAND | Age at month end outside the level's band | This level is for ages 8 to 10; you can race in Flyers |
EXCLUDED | On the exclusion list (staff, demo, partner admin, integrity hold) | Not eligible for prizes |
SECONDARY_PROFILE | User has another profile marked primary | Only your main profile can win |
WON_THIS_YEAR | Already received a bicycle this calendar year | You already won in March, congratulations; next year again |
@flyer.us or @flyer.vn user, the configured demo student ids that the sample-class job shifts dates for, and profiles owned by partner admin accounts.A page on ranking.flyer.us reachable from the notification and from the board banner, for the logged-in provisional winner or reserve.
invited, draft, submitted, under_review, verified, rejected, expired, awarded, delivered. Each state change notifies the parent./prize/winners lists past winners by month and level: first name, school, province, score, delivery photo where consented./prize/rules renders section 5 and section 6 of this document, bilingual, with the current formula version.A list in admin-web of prize cycles, one row per month and level, with status (open, frozen, claiming, verifying, announced, closed), the frozen top ten, and the claim chain (winner, reserve 1, reserve 2 and who currently holds the prize).
A plain list, newest first, filterable by status and level. Each claim opens to a single screen: profile card, frozen standings row with the score breakdown (the ten counted tests), the uploaded letter, the teacher's phone, integrity signals (section 13), a call log, and Approve or Reject with the fixed reason list. Approving the winner sets the cycle to announced once all five levels are verified or on the 12th, whichever is first.
An exclusion list with add, reason, expiry and an audit log. A manual override to pass the prize to a reserve outside the automatic rules, with a mandatory note; overrides are listed on the cycle for later review.
If the student belongs to a class on FLYER Teacher or Teado.ai, the claim can name that teacher. The teacher receives an in-product request: “Confirm that Minh Anh is in your class, is 9 years old, and completed the tests personally.” One tap confirms; the claim records the teacher's account, which is stronger than a phone call. Paper remains for students without a FLYER teacher.
CSV of winners per cycle for finance (prize value, delivery cost) and a monthly report of claims by outcome and reason for the product review.
A bicycle attracts effort. Rules alone are not enough; the platform must surface signals so that CS reviews with evidence, and every decision must be reconstructible.
| Signal | Rule | Effect |
|---|---|---|
| Implausible speed | A counted test with grade 10 completed in under 25% of the level's allotted time | Flag on the claim; CS asks the teacher specifically about it |
| Multi-profile user | The user account owns more than one student profile with attempts this month | Only the primary profile is eligible; flag for review |
| Shared device or network | Several top-20 profiles at the same level share a device fingerprint or the same network across most attempts | Flag; does not auto-reject, because siblings and classrooms exist |
| Night-time bursts | More than five counted tests submitted between 00:00 and 05:00 in the month | Flag |
| Late birthday | Date of birth added within ten days of the freeze | Automatic NO_DOB for that month |
| Answer-pattern similarity | Two profiles with identical answer sequences on three or more tests | Flag both; integrity hold if confirmed |
exam repository)| Table | Key columns | Notes |
|---|---|---|
prize_cycles | id, month, test_level_id, status, formula_version, frozen_at, announced_at, closed_at | One row per level per month, created by the freeze job |
prize_standings | cycle_id, learner_profile_id, rank, score, distinct_tests, counted_time, final_score_at, eligible, reasons (jsonb), counted_tests (jsonb) | Frozen snapshot of the top 50 per level plus every eligible student's own row; immutable |
prize_eligibility | learner_profile_id, test_level_id, month, eligible, reasons (jsonb), computed_at | Nightly; drives the badge on the board |
prize_claims | id, cycle_id, learner_profile_id, position (winner / reserve1 / reserve2), status, letter_file, teacher_name, teacher_role, teacher_org, teacher_phone, parent_name, parent_phone, address, bike_size, consent_publish, reviewer_email, reject_reason, deadline_at, timestamps | One per invited student per cycle |
prize_claim_events | claim_id, actor, action, detail, created_at | Append-only audit log including call outcomes |
prize_exclusions | learner_profile_id or user_id, reason, expires_at, created_by | Seeded from staff domains and the demo student list |
prize_age_bands | test_level_id, min_age, max_age, effective_from | Academic-owned; history kept |
learner_profiles.dob_locked_at, learner_profiles.is_primary (one per user, default the oldest profile with attempts).date_of_birth: range check, and reject updates once locked unless the caller has the support role.monthly_prize_ranks alongside the existing views.prize_eligibility for the current month.prize_cycles and prize_standings, create claims for the winner and two reserves per level, send notifications.monthlyPrizeRanks(testLevelId, month, first, after, filterBy) returning score, distinct tests, rank, eligible and reasons; viewerPrizeStatus(testLevelId) for the checklist; prizeCycle(month, testLevelId) for the banner and winner wall.GET /v4/student/prize/claims/current, PUT /v4/student/prize/claims/{id}, POST /v4/student/prize/claims/{id}/letter (multipart), POST /v4/student/prize/claims/{id}/submit.webs-ranking: Monthly tab reads the new query for the five levels; eligibility badge and checklist; prize banner; pages /prize/claim/[month]/[level], /prize/winners, /prize/rules; fix the index-based medals.admin-web: /center/prizes/cycles, /center/prizes/claims, /center/prizes/exclusions, /center/prizes/age-bands, following the plain-list pattern of the wisdom-stone screens.| Phase | Contents | Outcome |
|---|---|---|
| Phase 1, first cycle | Requirement A (versioned formula, championship test as a single-attempt check point with a window, medals fix), Requirement B (date of birth prompt and lock, eligibility job, exclusion seed, board badges), Requirement C (claim page, letter upload, rules page, winner wall), Requirement D (cycles, claims queue, exclusions, age bands), freeze and notification jobs | The first month of prizes runs on rules a parent can read and a CS agent can enforce |
| Phase 2 | In-product teacher confirmation via FLYER Teacher and Teado.ai, integrity signals in the claim screen, answer-pattern similarity, reserve pre-submission, finance export | Verification takes hours instead of days and stops relying on phone calls |
| Phase 3 | Apply the versioned formula to Weekly and Yearly, school and province sub-prizes, weekly mini-prizes in wisdom stones | One formula family across every tab |
| Out of scope | Cash prizes, prizes for IELTS or TOEFL levels, changes to the Daily tab, changes to the wisdom-stone store |
| Item | Assumption | VND per month |
|---|---|---|
| Bicycles | 5 × 2,500,000 to 3,500,000 (child and junior sizes) | 12,500,000 to 17,500,000 |
| Delivery | 5 × 150,000 to 400,000, nationwide | 750,000 to 2,000,000 |
| CS time | 15 claims including reserves × 30 minutes | about 1 working day |
| Total | roughly 14,000,000 to 20,000,000 |
Bicycle sizes differ across the age bands: a six-year-old and a sixteen-year-old do not ride the same bicycle. The claim form asks for a size, and procurement should hold two models.
| Question | Why it affects the design | Decider |
|---|---|---|
| Confirm the age bands in section 6, in particular KET 11 to 14 and PET 13 to 16. | Cambridge publishes no fixed age for the for-Schools papers; these are FLYER's choice and they decide who can win. | Academic |
| Admit students younger than the band? | A seven-year-old winning Flyers is good marketing and not unfair to older students. Excluding them is simpler to explain. The default in this draft is to exclude. | Academic + Product |
| Ten tests, or a different cap? | Sets the effort floor. Ten is about four to five hours at Starters; if Academic thinks that is too much for six-year-olds, Starters could use eight. | Academic |
| Can Academic deliver five fresh, unpublished full tests every month? | The championship test only works if the content has never been seen. If capacity is the constraint, the window could reuse a test drawn at random from a sealed pool of twenty per level. | Academic + Content |
| Championship window: the whole last weekend, or Sunday only? | A longer window is fairer to families with weekend classes; a shorter one makes answer sharing harder. The draft uses Saturday 08:00 to Sunday 22:00. | Product + Academic |
| Should the versioned formula also replace Daily, Weekly and Yearly from day one? | Cleaner, but changes numbers students already watch and widens phase 1. The draft keeps it to the Monthly tab of the five prize levels. | Product |
| Is a parent phone number available on enough student profiles? | If most profiles lack one, requiring it in month one will disqualify most of the top ten. Needs a replica query. | Product + Data |
| Who signs for home-schooled students or students without an English teacher? | The draft accepts centre teachers and homeroom teachers for grades 1 to 3, but has no answer for a child with neither. | CS + Academic |
| Publish the full score breakdown of winners publicly? | Transparency reduces disputes but exposes which tests a child took. The draft shows the breakdown to the claimant and to admin only. | Product + Legal |
| Budget approval and procurement of two bicycle models. | Roughly 14 to 20 million VND a month before marketing. | Finance |
flyer/webs-ranking (src/pages/index.tsx, src/components/TableRanking, src/api/common.ts, src/providers/auth), flyer/exam (graph/schema/type_extended.graphql, graph/user/type_extended.resolvers.go, ent/schema/views.go, module/mview, module/learnerprofile) and flyer/flyer-go (db/migrations/base/0092_user_score_views.up.sql, service/student/db/queries/score.sql). The exact production definition of learner_test_level_ranks must be read from pg_matviews; it is in no repository.Mỗi tháng, học sinh đứng đầu mỗi cấp độ Starters, Movers, Flyers, KET và PET trên ranking.flyer.us nhận một chiếc xe đạp. Phần A là bản rút gọn: một sự kiện nhỏ hằng tháng, vận hành thủ công từ tháng 9/2026, không cần lập trình. Phần B là kết quả rà soát bảng xếp hạng hiện tại và thiết kế đầy đủ để xây nếu sự kiện được giữ lại sau giai đoạn thử.
Sự kiện nhỏ hằng tháng trước, hệ thống sauwebs-ranking, exam và flyer-go, nên các nhận định về hiện trạng đều kiểm chứng được theo đường dẫn tệp. Từ mục 5 trở đi là đề xuất thiết kế. Những điểm chưa quyết được liệt kê ở Câu hỏi mở, không mặc định ngầm.Quyết định là bắt đầu ngay với những gì đang có, không chờ hệ thống. Không phần nào của Phần A cần lập trình viên. Cần một thể lệ một trang, một mẫu giấy xác nhận in được, một người CS phụ trách, một Google Sheet và một dòng ngân sách. Bảng xếp hạng công khai giữ nguyên; FLYER thêm giải thưởng, bước kiểm tra điều kiện và bài công bố xung quanh nó.
Đây là nội dung phụ huynh đọc. Cố ý ngắn. Điều gì không có ở đây do FLYER quyết định và giải thích cho gia đình liên quan.
| Quy tắc | Nội dung |
|---|---|
| Giải thưởng | Mỗi tháng, học sinh xếp hạng nhất trên tab Tháng của ranking.flyer.us ở mỗi cấp độ Starters, Movers, Flyers, KET và PET nhận một chiếc xe đạp. Năm xe đạp mỗi tháng. |
| Thời điểm | Bảng xếp hạng chốt lúc 23:59 ngày cuối tháng, giờ Việt Nam. FLYER lưu ảnh chụp màn hình làm bằng chứng. Bài nộp sau thời điểm đó thuộc về tháng sau. |
| Ai được thắng | Học sinh dùng tài khoản cá nhân của chính mình. Tài khoản lớp, trung tâm, giáo viên và tài khoản dùng chung trong gia đình không được thắng. Tuổi của học sinh vào ngày cuối tháng phải nằm trong khung của cấp độ: Starters 6 đến 8, Movers 8 đến 10, Flyers 10 đến 12, KET 11 đến 14, PET 13 đến 16. |
| Giấy xác nhận của giáo viên | Trong 7 ngày kể từ khi được liên hệ, gia đình gửi ảnh chụp giấy xác nhận có chữ ký của giáo viên tiếng Anh hiện tại của học sinh (trường hoặc trung tâm), ghi họ tên, ngày sinh, trường, lớp, xác nhận học sinh trong khung tuổi và tự mình làm các bài thi. FLYER có thể gọi cho giáo viên. |
| Dự bị | Nếu học sinh hạng nhất không liên lạc được, không gửi giấy đúng hạn hoặc không đủ điều kiện, giải chuyển cho hạng 2, rồi hạng 3. |
| Giới hạn | Mỗi học sinh một xe đạp mỗi tháng (nếu đứng nhất ở hai cấp độ, tính cấp độ cao hơn). Mỗi học sinh một xe đạp mỗi năm dương lịch. |
| Công bằng | FLYER có thể không công nhận kết quả có dấu hiệu bất thường, ví dụ nhiều tài khoản trên một thiết bị, hơn 20 đề đầy đủ trong một ngày, hoặc ngày sinh thay đổi trong tháng, và chuyển giải cho dự bị kế tiếp. |
| Công bố | Người thắng được công bố ngày 12 tháng kế tiếp trên Zalo và Facebook của FLYER, với tên, trường và tỉnh thành. Giao xe trong hai tuần sau công bố. |
| Loại trừ | Nhân viên FLYER, tài khoản quản trị đối tác và nhà trường, tài khoản demo không được tham gia. |
Một người phụ trách, khoảng một ngày công mỗi tháng. Giờ Việt Nam.
| Khi nào | Việc | Thời gian |
|---|---|---|
| Ngày cuối tháng, 23:50 | Mở ranking.flyer.us, tab Tháng, lần lượt năm cấp độ. Chụp top 5 của mỗi cấp độ. Lưu vào thư mục Drive của tháng. | 15 phút |
| Ngày 1, 08:00 đến 09:00 | Chụp lại (lần làm mới mỗi giờ có thể đã cộng bài nộp muộn). Ảnh 08:00 là bằng chứng nếu hai ảnh khác nhau. Điền Sheet: cấp độ, hạng 1 đến 3, tên hiển thị, điểm, id hồ sơ. | 30 phút |
| Ngày 1 | Trong trang quản trị, mở 15 hồ sơ: tài khoản cá nhân hay tài khoản lớp, có ngày sinh không, trường, số phụ huynh. Ghi chú các tên rõ ràng là lớp hoặc trung tâm. Đánh dấu người thắng tạm thời và dự bị. | 45 phút |
| Ngày 1 đến 2 | Nhắn Zalo hoặc gọi phụ huynh của mỗi người thắng tạm thời (mẫu A3.1). Gửi mẫu giấy dạng PDF. Báo dự bị biết họ là dự bị (mẫu A3.2). | 1 giờ |
| Ngày 2 đến 8 | Nhận giấy. Đối chiếu tên, ngày sinh với hồ sơ và khung tuổi. Gọi giáo viên theo số đã ghi (kịch bản A3.3). Ghi cuộc gọi vào Sheet. | 2 giờ |
| Ngày 8 đến 10 | Quyết định: xác nhận người thắng, hoặc chuyển dự bị kèm lý do. Trưởng sản phẩm ký duyệt trong Sheet. Hỏi gia đình người thắng địa chỉ, cỡ xe, và đồng ý công bố tên, trường, ảnh. | 45 phút |
| Ngày 12 | Marketing đăng năm người thắng (mẫu A3.4). CS đặt xe. | 30 phút |
| Trước ngày 26 | Giao xe. Gia đình gửi ảnh; đăng khi được đồng ý. Đóng dòng trong Sheet. | 30 phút |
Mỗi tháng một tab, mỗi cấp độ và hạng một dòng (15 dòng). Các cột: cấp độ, hạng, link ảnh chụp, tên hiển thị, id hồ sơ, loại tài khoản, ngày sinh trong hồ sơ, tuổi cuối tháng, trong khung (có/không), tên phụ huynh, số phụ huynh, ngày liên hệ, ngày nhận giấy, link giấy, tên giáo viên, số giáo viên, ngày gọi, kết quả cuộc gọi, quyết định, người quyết, địa chỉ, cỡ xe, đồng ý công bố, ngày đặt xe, ngày giao, link ảnh, ghi chú.
Mẫu một trang song ngữ, in được, tại roadmap.flyer.vn/ranking-prize-letter. CS xuất PDF một lần và gửi cùng một tệp cho mọi gia đình. Chấp nhận ảnh chụp bằng điện thoại trang đã ký. Các trường: họ tên học sinh, ngày sinh, trường, khối và lớp, cấp độ và tháng, ba xác nhận (khung tuổi, đang học ở trình độ này, tự làm bài), họ tên giáo viên, vai trò, trường hoặc trung tâm, số điện thoại, chữ ký và ngày, họ tên phụ huynh, số điện thoại, chữ ký và đồng ý công bố.
| Rủi ro | Cách giai đoạn thử xử lý |
|---|---|
| Hạng 1 là tài khoản lớp hoặc trung tâm (như trên bảng hôm nay) | Không đủ điều kiện theo quy tắc “tài khoản cá nhân của chính mình”. Giải chuyển cho dự bị. CS thường nhận ra qua tên hiển thị; giấy xác nhận của giáo viên giải quyết phần còn lại. |
| Người thắng là học sinh làm vài trăm đề | Chấp nhận trong giai đoạn thử nếu là học sinh thật trong khung tuổi; điều khoản công bằng xử lý khối lượng cực đoan theo ngày. Hai người thắng như vậy ở một cấp độ sẽ kích hoạt truy vấn mười bài tốt nhất cho tháng sau. |
| Hồ sơ không có ngày sinh (58% học sinh đua trong tháng 8) | Giấy xác nhận ghi ngày sinh và giáo viên xác nhận. Đề nghị gia đình bổ sung vào hồ sơ như một phần của việc nhận giải. |
| Cùng một học sinh đứng nhất hai cấp độ | Tính cấp độ cao hơn; giải cấp độ thấp chuyển cho dự bị của cấp độ đó. |
| Bảng thay đổi giữa 23:59 và ảnh chụp 08:00 | Ảnh 08:00 ngày 1 là bằng chứng, vì đã gồm mọi bài nộp trước nửa đêm. Nói rõ điều này trong bài đăng thể lệ. |
| Gia đình khiếu nại | Trả lời bằng ảnh chụp và nội dung thể lệ. Thể lệ ghi quyết định của FLYER là cuối cùng. |
| Không liên lạc được giáo viên | Hai lần trong hai ngày, rồi văn phòng trường; nếu đến ngày 10 vẫn không được, chuyển dự bị và báo gia đình lý do. |
Năm item backlog trên board được sắp cho thời điểm đó và tạm gác lại cho tới lúc ấy.
FLYER muốn có một phần thưởng định kỳ, dễ thấy trên bảng xếp hạng công khai: mỗi tháng, học sinh có điểm cao nhất ở Starters, Movers, Flyers, KET và PET nhận một chiếc xe đạp. Yêu cầu đi kèm một điều kiện: người thắng phải nộp giấy xác nhận của giáo viên tiếng Anh, chứng nhận học sinh đúng độ tuổi chuẩn Cambridge của cấp độ đã thắng.
Bảng xếp hạng đã có và được dùng nhiều. Trang có các tab Ngày, Tuần, Tháng, Năm, bộ chọn cấp độ, bảng học sinh và bảng trường. Điều còn thiếu là mọi thuộc tính mà một giải thưởng hiện vật cần: một công thức được ghi thành văn và có phiên bản, quy tắc về điều kiện dự thi, cách phân biệt học sinh thật với hồ sơ ẩn danh hoặc hồ sơ trùng, ngày sinh trên hồ sơ, và công cụ để vận hành quy trình nhận giải và thẩm định.
Rủi ro nếu bỏ qua phần việc này là cụ thể. Một chiếc xe đạp đủ giá trị để các gia đình tối ưu vì nó. Hiện nay một học sinh có thể được xếp hạng từ hồ sơ ẩn danh, một người dùng có thể giữ nhiều hồ sơ, tài khoản nhân viên và tài khoản demo không bị loại, và điểm là tổng cộng dồn, thưởng cho số lượng hơn chất lượng. Tháng trao giải đầu tiên sẽ sinh ra tranh cãi mà đội ngũ không thể phân xử, vì quy tắc tạo ra con số đó không nằm trong kho mã nào.
| Chỉ số | Định nghĩa | Loại |
|---|---|---|
| Số học sinh đua mỗi cấp độ | Số học sinh đủ điều kiện có ít nhất một bài được tính trong tháng, theo từng cấp độ | Chính |
| Số bài mỗi học sinh | Trung vị số bài được tính của mỗi học sinh đang đua trong tháng | Chính |
| Hồ sơ có ngày sinh đã xác minh | Tỷ lệ hồ sơ học sinh đang hoạt động có ngày sinh đã nhập và đã khoá | Chính, tài sản lâu dài |
| Tỷ lệ hoàn tất nhận giải | Tỷ lệ người thắng tạm thời nộp giấy hợp lệ trong thời hạn | Ràng buộc |
| Thời gian thẩm định | Số ngày từ lúc chốt đến lúc công bố, mục tiêu 12 | Ràng buộc |
| Tranh chấp | Số khiếu nại mỗi chu kỳ cần diễn giải quy tắc | Ràng buộc, mục tiêu bằng không từ tháng thứ 2 |
| Giữ chân | Tỷ lệ học sinh đua ở tháng N vẫn hoạt động ở tháng N+1 | Phép thử thật sự về việc phần thưởng có thay đổi hành vi hay không |
| Cấp độ | Học sinh hoạt động | 1 bài | 2 đến 4 | 5 đến 9 | 10 trở lên |
|---|---|---|---|---|---|
| Starters | 11.950 | 4.181 | 3.553 | 1.899 | 2.317 |
| Movers | 9.954 | 3.463 | 2.881 | 1.615 | 1.995 |
| Flyers | 9.325 | 3.484 | 2.635 | 1.392 | 1.814 |
| KET | 6.077 | 2.227 | 1.775 | 1.085 | 990 |
| PET | 4.265 | 1.752 | 1.344 | 673 | 496 |
Đọc từ mã nguồn, không suy đoán. Ứng dụng đang chạy là flyer/webs-ranking (Next.js, commit gần nhất 27/08/2026) phục vụ tại ranking.flyer.us; kho ranking-web cũ đã ngừng từ tháng 10/2024. API xếp hạng là GraphQL v2 trong flyer/exam.
| Khía cạnh | Hành vi hiện tại | Vị trí |
|---|---|---|
| Khung thời gian | Ngày, Tuần, Tháng, Năm. Không có mọi thời điểm. Tab mặc định là Ngày. | enum RankFrequency, pages/index.tsx |
| Cấp độ | Danh sách thả xuống các cấp độ loại CLASSIC: Starters (mặc định), Movers, Flyers, KET, PET, cùng IOE, SET, TOEFL Primary và Junior. IELTS là loại riêng và không xuất hiện. | TestLevelsWithFilter |
| Bảng | Bảng học sinh và bảng trường. Ô tìm kiếm chuyển sang mã thử thách (chữ số), mã lớp (CL…) hoặc tìm theo tên. | TableRanking |
| Điểm hiển thị | Cột grade: tổng điểm từng bài quy về thang 10, cộng dồn trong khung thời gian. Không phải điểm cao nhất, cũng không phải trung bình. Học sinh làm 40 bài xếp trên học sinh làm 10 bài tuyệt đối. | learner_test_level_ranks |
| Công thức nằm ở đâu | Một materialized view tạo bằng tay trên cơ sở dữ liệu production. Trong mã Go chỉ khai báo danh sách cột. Không có CREATE VIEW trong bất kỳ kho nào, nên không đọc được quy tắc phân định đồng điểm từ git. | ent/schema/views.go, module/mview |
| Làm mới | Mỗi giờ qua Temporal, múi giờ Asia/Ho_Chi_Minh. Một bài vừa nộp có thể mất tới một giờ mới thay đổi bảng. | module/mview/mview.go |
| Bài nào được tính | Chỉ đề thi thử đầy đủ (type_attempt = 'exam'), đã nộp, điểm lớn hơn 0. Bài luyện tập, nói, viết và kiểm tra đầu vào không tính. Bài giáo viên giao và bài tự chọn tính như nhau. Không có bộ lọc mẫu hay trả phí. | các view cùng họ trong flyer-go |
| Phân định đồng điểm | Suy từ view tổng kết tháng có phiên bản: điểm giảm dần, rồi tổng thời gian làm bài tăng dần. Chưa kiểm chứng được cho chính view bảng xếp hạng. | 0092_user_score_views.up.sql |
| Bộ lọc | Tỉnh thành, trường, mã lớp, mã thử thách, tên hiển thị. API không có bộ lọc theo tuổi, khối lớp hay năm sinh. | LearnerTestLevelRankFilter |
| Danh tính hiển thị | Hạng, ảnh đại diện và khung, tên hiển thị, tỉnh thành, trường, điểm. Bấm vào bất kỳ dòng nào mở hồ sơ công khai với xu, tổng điểm, thời gian học, số bài hoàn thành, độ chính xác, thành tích và toàn bộ lịch sử làm bài. | ModalInfoProfileRanking |
| Ngày sinh | Được lấy trong fragment xếp hạng nhưng không bao giờ hiển thị. Trên hồ sơ, trường này cho phép rỗng, không kiểm tra, và không tác vụ xếp hạng, tổng kết hay huy hiệu nào đọc nó. | learner_profiles.date_of_birth |
| Điều kiện dự thi | Không có. Xem không cần đăng nhập. Hồ sơ ẩn danh (không có user id) vẫn được xếp hạng. Một người dùng có thể giữ nhiều hồ sơ, mỗi hồ sơ xếp hạng riêng. Không loại tài khoản nhân viên hay tài khoản thử nghiệm. Học sinh demo được dời ngày làm bài về phía trước mỗi giờ để lớp mẫu trông có hoạt động, và xuất hiện ở bảng Ngày, Tuần. | AuthProvider, IsAnonymous, update_attempt_dates_by_students |
| Phần thưởng hiện có | Một huy hiệu TOURNAMENT_STAR, cấp vào 01:00 thứ Hai hằng tuần cho mọi học sinh giữ hạng 1 ở bất kỳ cấp độ nào trong tuần. Cửa hàng đá tri thức và chiến dịch tặng quà tồn tại nhưng không liên quan đến thứ hạng. Không có hệ thống giải thưởng, không có màn quản trị giải thưởng. | AchievementWeeklyRankSt |
| Lỗi giao diện đã biết | Huy chương cho ba vị trí đầu gắn theo chỉ số dòng, không theo hạng từ máy chủ. Khi áp bất kỳ bộ lọc nào, ba dòng đầu của danh sách đã lọc nhận vàng, bạc, đồng dù hạng thật là 5.000. | TableRanking/index.tsx |
| # | Khoảng trống | Vì sao quan trọng với một chiếc xe đạp |
|---|---|---|
| 1 | Công thức không được quản lý phiên bản | Mọi tranh chấp kết thúc bằng “cơ sở dữ liệu nói vậy”. Đội ngũ không thể cho phụ huynh xem quy tắc, không thử được trên dev (dev và prod đã lệch nhau ở view tháng cùng họ), và không thể sửa có dấu vết duyệt. |
| 2 | Số lượng thắng chất lượng | Tổng cộng dồn nghĩa là người thắng là người làm nhiều bài nhất. Tháng 8/2026, học sinh đứng nhất KET nộp 849 lượt làm và học sinh đứng nhất Movers làm 602 bài khác nhau, khoảng hai mươi bài mỗi ngày. Đó vừa không phải hành vi FLYER muốn, vừa không phải kết quả Học thuật bảo vệ được. |
| 3 | Không có điều kiện dự thi | Hồ sơ ẩn danh, hồ sơ trùng, nhân viên, tài khoản demo của trường và học sinh mẫu bị dời ngày đều được xếp hạng. Ít nhất một trong số này sẽ lọt top 3 ngay tháng đầu. |
| 4 | Không có tuổi trong hồ sơ | Điều kiện “đúng độ tuổi Cambridge của cấp độ” không kiểm tra được, vì ngày sinh là tuỳ chọn, không kiểm tra, và API không lọc được theo trường này. |
| 5 | Không có công cụ nhận giải hay thẩm định | Người thắng không có chỗ nộp giấy, CS không có chỗ duyệt, và không có hồ sơ ghi ai thắng, khi nào, dựa trên bằng chứng gì. |
| 6 | Trễ một giờ và huy chương theo chỉ số dòng | Nhỏ nếu đứng riêng, nhưng vào tối cuối tháng, một học sinh thấy mình đứng nhất lúc 23:30 và đứng ba lúc 00:30 sẽ không chấp nhận lời giải thích “view chưa làm mới”. |
| Cấp độ | Lượt làm | Bài khác nhau |
|---|---|---|
| Starters | 822 | 125 |
| Movers | 680 | 602 |
| Flyers | 444 | 353 |
| KET | 849 | 549 |
| PET | 311 | 204 |
Đây là thể lệ ở dạng phụ huynh đọc được trên trang thể lệ công khai. Mọi thứ ở mục 9 đến 14 tồn tại để các quy tắc này đúng. Các con số trích dẫn lấy từ số nền tháng 8/2026 trên bản sao đọc ở mục 2.
| Cấp độ | Học sinh đạt 100 / 100 |
|---|---|
| Starters | 641 |
| Movers | 410 |
| Flyers | 372 |
| KET | 163 |
| PET | 61 |
Cambridge công bố bộ đề Thiếu nhi (Pre A1 Starters, A1 Movers, A2 Flyers) cho trẻ khoảng 6 đến 12 tuổi, mỗi bài hướng đến một khoảng tuổi điển hình trong dải đó. Với A2 Key for Schools và B1 Preliminary for Schools, Cambridge không công bố độ tuổi cố định, chỉ nói bài thi viết cho học sinh trong độ tuổi đi học. Khung dưới đây là khung vận hành của FLYER, bám theo hướng dẫn đó và khối lớp ở Việt Nam. Tuổi tính theo số năm tròn vào ngày cuối của tháng trao giải.
| Cấp độ | Hướng dẫn Cambridge | Khung FLYER (tuổi cuối tháng) | Khối lớp VN điển hình | Năm sinh, cho cuộc đua tháng 9/2026 |
|---|---|---|---|---|
| Starters (Pre A1) | YLE, trẻ 6 đến 12; Starters thường 7 đến 8 | 6 đến 8 | Lớp 1 đến 3 | 2018 đến 2020 |
| Movers (A1) | Thường 8 đến 11 | 8 đến 10 | Lớp 3 đến 5 | 2016 đến 2018 |
| Flyers (A2) | Thường 9 đến 12 | 10 đến 12 | Lớp 5 đến 7 | 2014 đến 2016 |
| KET (A2 Key for Schools) | Độ tuổi đi học; thường THCS | 11 đến 14 | Lớp 6 đến 9 | 2012 đến 2015 |
| PET (B1 Preliminary for Schools) | Độ tuổi đi học; thường cuối THCS, THPT | 13 đến 16 | Lớp 8 đến 11 | 2010 đến 2013 |
Giấy xác nhận là bước kiểm tra bằng con người. Nền tảng tính được tuổi từ ngày sinh, nhưng không biết ngày đó có thật không và học sinh có tự làm bài không. Một giáo viên tiếng Anh có tên, ký vào một mẫu ngắn, làm được cả hai việc, và cho CS một số điện thoại để gọi.
| Trường | Mục đích |
|---|---|
| Họ tên học sinh, ngày sinh, trường, khối và lớp | Đối chiếu với hồ sơ; lệch ngày sinh là từ chối tự động. |
| Cấp độ đã thắng, tháng trao giải | Gắn giấy với một chu kỳ để không tái sử dụng được. |
| Xác nhận 1: học sinh nằm trong khung tuổi FLYER của cấp độ này | Điều kiện mà chương trình được xây dựng trên đó. |
| Xác nhận 2: học sinh hiện đang học tiếng Anh ở hoặc quanh cấp độ này | Bắt trường hợp học sinh mười một tuổi bị đặt vào Starters để lấy giải dù tuổi lọt vào vùng chồng lấn. |
| Xác nhận 3: theo hiểu biết của giáo viên, học sinh tự mình hoàn thành các bài thi | Kiểm tra tính trung thực; biến giáo viên thành người làm chứng, không chỉ là người điền giấy. |
| Họ tên giáo viên, vai trò, trường hoặc trung tâm, số di động, chữ ký, ngày; dấu trường nếu có | Cho CS một người để gọi. Dấu là tuỳ chọn vì giáo viên trung tâm thường không xin được. |
| Họ tên phụ huynh hoặc người giám hộ, số điện thoại, chữ ký, và đồng ý công bố tên, trường và ảnh của người thắng | Cần cho bảng vinh danh và việc giao xe. |
Mẫu là một trang PDF song ngữ, tải từ trang nhận giải. Ảnh chụp trang đã ký bằng điện thoại được chấp nhận; tệp tải lên giới hạn 10 MB và các định dạng JPG, PNG, HEIC, PDF.
Mỗi cấp độ một chu kỳ mỗi tháng, cả năm cấp độ chạy cùng lịch. Bước 1 và 2 là bảng xếp hạng hiện có với công thức mới; bước 3 đến 7 là phần mới.
Thay bảng xếp hạng tháng tạo bằng tay cho năm cấp độ có giải bằng một view mà định nghĩa nằm trong tệp migration, tính điểm mười bài tốt nhất theo mục 5.2, và mang số phiên bản in trên bảng.
monthly_prize_ranks (cấp độ, tháng, hồ sơ học sinh, điểm, số bài khác nhau, tổng thời gian đã chặn, thời điểm đạt điểm cuối, hạng, phiên bản công thức), thêm vào danh sách làm mới mỗi giờ cạnh learner_test_level_ranks.row_number), không bao giờ là nhóm phần trăm. View tổng kết tháng cùng họ dùng ntile(100) trên production và row_number() trên dev; sai lệch đó không được lặp lại ở đây.championship_month và championship_level, ẩn khỏi thư viện và tìm kiếm cho tới khi khung giờ mở, một lượt làm được máy chủ bắt buộc (lần bắt đầu thứ hai bị từ chối rõ ràng, không âm thầm bỏ qua), và được đưa vào thư viện thường sau khi khung giờ đóng. Học thuật bàn giao năm đề như vậy trước ngày 20 mỗi tháng.WITH counted AS ( -- mỗi dòng = (học sinh, đề) với lượt tốt nhất trong tháng
SELECT ua.learner_profile_id, ua.test_level_id, ua.check_point_id,
bool_or(cp.championship_month IS NOT NULL) AS is_championship,
max(round(10 * ua.grade / ua.max_grade, 2)) AS best_grade,
min(least(ua.execution_time, tl.execution_time)) AS best_time,
min(ua.submitted_at) AS first_at
FROM user_attempts ua
JOIN test_level_exam_settings tl ON tl.test_level_id = ua.test_level_id
JOIN check_points cp ON cp.id = ua.check_point_id
WHERE ua.type_attempt = 'exam' AND ua.submitted_at IS NOT NULL
AND ua.grade > 0 AND ua.max_grade > 0
AND (ua.submitted_at AT TIME ZONE 'Asia/Ho_Chi_Minh') >= date_trunc('month', now() AT TIME ZONE 'Asia/Ho_Chi_Minh')
GROUP BY 1,2,3
), ranked AS (
SELECT *, row_number() OVER (PARTITION BY learner_profile_id, test_level_id
ORDER BY best_grade DESC, first_at) AS k
FROM counted WHERE NOT is_championship
), best10 AS (
SELECT learner_profile_id, test_level_id,
sum(best_grade) FILTER (WHERE k <= 10) AS score10,
count(*) AS distinct_tests,
sum(best_time) FILTER (WHERE k <= 10) AS counted_time,
max(first_at) FILTER (WHERE k <= 10) AS final_score_at
FROM ranked GROUP BY 1,2
), champ AS ( -- đề vô địch một lượt của tháng
SELECT learner_profile_id, test_level_id, best_grade * 10 AS champ_score, best_time AS champ_time
FROM counted WHERE is_championship
)
SELECT b.learner_profile_id, b.test_level_id,
b.score10 + coalesce(c.champ_score, 0) AS score,
b.score10, c.champ_score, b.distinct_tests, b.counted_time, c.champ_time, b.final_score_at,
row_number() OVER (PARTITION BY b.test_level_id
ORDER BY b.score10 + coalesce(c.champ_score, 0) DESC,
c.champ_time ASC NULLS LAST, b.distinct_tests DESC,
b.counted_time ASC, b.final_score_at ASC) AS rank,
1 AS formula_version
FROM best10 b LEFT JOIN champ c USING (learner_profile_id, test_level_id);
exam và cho cùng thứ hạng trên dev và production với cùng dữ liệu mẫu.dob_locked_at). Thay đổi cần phiếu hỗ trợ kèm giấy xác nhận của giáo viên hoặc giấy khai sinh. Đây là điều làm cho việc kiểm tra tuổi có ý nghĩa.Một tác vụ chạy hằng đêm (và chạy lại lúc chốt) ghi một dòng điều kiện cho mỗi học sinh đang hoạt động và mỗi cấp độ: đủ hoặc không, kèm danh sách lý do. Lý do là một enum cố định để giao diện giải thích được.
| Mã lý do | Ý nghĩa | Hiển thị cho học sinh |
|---|---|---|
ANONYMOUS | Hồ sơ không có tài khoản người dùng | Tạo tài khoản để đủ điều kiện |
NO_PARENT_PHONE | Chưa có số điện thoại phụ huynh đã xác minh | Thêm số điện thoại của phụ huynh |
NO_DOB | Chưa có ngày sinh, hoặc thêm sau ngày 20 | Thêm ngày sinh trước ngày 20 |
AGE_OUT_OF_BAND | Tuổi cuối tháng ngoài khung của cấp độ | Cấp độ này dành cho 8 đến 10 tuổi; có thể đua ở Flyers |
EXCLUDED | Nằm trong danh sách loại trừ (nhân viên, demo, quản trị đối tác, tạm giữ vì tính trung thực) | Không đủ điều kiện nhận giải |
SECONDARY_PROFILE | Người dùng có hồ sơ khác được đánh dấu là chính | Chỉ hồ sơ chính mới có thể thắng |
WON_THIS_YEAR | Đã nhận xe đạp trong năm dương lịch này | Đã thắng vào tháng 3, xin chúc mừng; năm sau thi tiếp |
@flyer.us hoặc @flyer.vn, danh sách id học sinh demo mà tác vụ lớp mẫu dời ngày, và hồ sơ thuộc tài khoản quản trị đối tác.Một trang trên ranking.flyer.us, đi tới từ thông báo và từ banner trên bảng, dành cho người thắng tạm thời hoặc dự bị đã đăng nhập.
invited, draft, submitted, under_review, verified, rejected, expired, awarded, delivered. Mỗi lần đổi trạng thái đều thông báo cho phụ huynh./prize/winners liệt kê người thắng theo tháng và cấp độ: tên, trường, tỉnh thành, điểm, ảnh trao giải khi được đồng ý./prize/rules hiển thị mục 5 và mục 6 của tài liệu này, song ngữ, kèm phiên bản công thức hiện hành.Danh sách trong admin-web các chu kỳ giải, mỗi dòng một tháng và một cấp độ, với trạng thái (đang mở, đã chốt, đang nhận giải, đang thẩm định, đã công bố, đã đóng), top 10 đã chốt, và chuỗi nhận giải (người thắng, dự bị 1, dự bị 2 và ai đang giữ giải).
Một danh sách phẳng, mới nhất trước, lọc theo trạng thái và cấp độ. Mỗi hồ sơ mở ra một màn hình duy nhất: thẻ hồ sơ học sinh, dòng bảng xếp hạng đã chốt với chi tiết điểm (mười bài được tính), giấy xác nhận đã tải lên, số điện thoại giáo viên, tín hiệu trung thực (mục 13), nhật ký cuộc gọi, và nút Duyệt hoặc Từ chối với danh sách lý do cố định. Duyệt người thắng chuyển chu kỳ sang đã công bố khi cả năm cấp độ đã thẩm định xong hoặc vào ngày 12, tuỳ điều kiện nào đến trước.
Danh sách loại trừ với thêm mới, lý do, ngày hết hạn và nhật ký kiểm toán. Một thao tác ghi đè thủ công để chuyển giải cho dự bị ngoài quy tắc tự động, bắt buộc có ghi chú; các lần ghi đè được liệt kê trên chu kỳ để xem xét về sau.
Nếu học sinh thuộc một lớp trên FLYER Teacher hoặc Teado.ai, hồ sơ nhận giải có thể nêu tên giáo viên đó. Giáo viên nhận yêu cầu trong sản phẩm: “Xác nhận Minh Anh thuộc lớp của giáo viên, 9 tuổi, và tự mình hoàn thành các bài thi.” Một lần chạm để xác nhận; hồ sơ ghi lại tài khoản giáo viên, mạnh hơn một cuộc gọi. Giấy vẫn dùng cho học sinh không có giáo viên trên FLYER.
CSV người thắng theo chu kỳ cho kế toán (giá trị giải, chi phí giao) và báo cáo tháng về hồ sơ theo kết quả và lý do cho buổi rà soát sản phẩm.
Một chiếc xe đạp thu hút nỗ lực. Quy tắc thôi chưa đủ; nền tảng phải đưa ra tín hiệu để CS xem xét có bằng chứng, và mọi quyết định phải dựng lại được.
| Tín hiệu | Quy tắc | Tác động |
|---|---|---|
| Tốc độ bất thường | Bài được tính đạt 10 điểm hoàn thành dưới 25% thời lượng quy định của cấp độ | Gắn cờ trên hồ sơ; CS hỏi giáo viên cụ thể về điểm này |
| Người dùng nhiều hồ sơ | Tài khoản sở hữu hơn một hồ sơ học sinh có bài làm trong tháng | Chỉ hồ sơ chính đủ điều kiện; gắn cờ để xem xét |
| Chung thiết bị hoặc mạng | Nhiều hồ sơ trong top 20 cùng cấp độ dùng chung dấu vân thiết bị hoặc cùng mạng ở phần lớn lượt làm | Gắn cờ; không tự động từ chối, vì anh chị em ruột và lớp học có thật |
| Làm bài dồn dập ban đêm | Hơn năm bài được tính nộp trong khoảng 00:00 đến 05:00 trong tháng | Gắn cờ |
| Ngày sinh nhập muộn | Ngày sinh thêm trong vòng mười ngày trước lúc chốt | Tự động NO_DOB cho tháng đó |
| Trùng mẫu đáp án | Hai hồ sơ có chuỗi đáp án giống hệt ở ba bài trở lên | Gắn cờ cả hai; tạm giữ nếu xác nhận |
exam)| Bảng | Cột chính | Ghi chú |
|---|---|---|
prize_cycles | id, month, test_level_id, status, formula_version, frozen_at, announced_at, closed_at | Mỗi cấp độ mỗi tháng một dòng, tạo bởi tác vụ chốt |
prize_standings | cycle_id, learner_profile_id, rank, score, distinct_tests, counted_time, final_score_at, eligible, reasons (jsonb), counted_tests (jsonb) | Ảnh chụp đã chốt của top 50 mỗi cấp độ cộng dòng riêng của mọi học sinh đủ điều kiện; bất biến |
prize_eligibility | learner_profile_id, test_level_id, month, eligible, reasons (jsonb), computed_at | Hằng đêm; điều khiển nhãn trên bảng |
prize_claims | id, cycle_id, learner_profile_id, position (winner / reserve1 / reserve2), status, letter_file, teacher_name, teacher_role, teacher_org, teacher_phone, parent_name, parent_phone, address, bike_size, consent_publish, reviewer_email, reject_reason, deadline_at, các mốc thời gian | Mỗi học sinh được mời mỗi chu kỳ một dòng |
prize_claim_events | claim_id, actor, action, detail, created_at | Nhật ký chỉ ghi thêm, gồm cả kết quả cuộc gọi |
prize_exclusions | learner_profile_id hoặc user_id, reason, expires_at, created_by | Khởi tạo từ tên miền nhân viên và danh sách học sinh demo |
prize_age_bands | test_level_id, min_age, max_age, effective_from | Học thuật sở hữu; giữ lịch sử |
learner_profiles.dob_locked_at, learner_profiles.is_primary (một hồ sơ mỗi người dùng, mặc định là hồ sơ lâu nhất có bài làm).date_of_birth: kiểm tra khoảng, và từ chối cập nhật sau khi khoá trừ khi người gọi có vai trò hỗ trợ.monthly_prize_ranks cùng các view hiện có.prize_eligibility cho tháng hiện tại.prize_cycles và prize_standings, tạo hồ sơ nhận giải cho người thắng và hai dự bị mỗi cấp độ, gửi thông báo.monthlyPrizeRanks(testLevelId, month, first, after, filterBy) trả về điểm, số bài khác nhau, hạng, đủ điều kiện và lý do; viewerPrizeStatus(testLevelId) cho danh sách kiểm tra; prizeCycle(month, testLevelId) cho banner và bảng vinh danh.GET /v4/student/prize/claims/current, PUT /v4/student/prize/claims/{id}, POST /v4/student/prize/claims/{id}/letter (multipart), POST /v4/student/prize/claims/{id}/submit.webs-ranking: tab Tháng đọc truy vấn mới cho năm cấp độ; nhãn điều kiện và danh sách kiểm tra; banner giải thưởng; các trang /prize/claim/[month]/[level], /prize/winners, /prize/rules; sửa lỗi huy chương theo chỉ số dòng.admin-web: /center/prizes/cycles, /center/prizes/claims, /center/prizes/exclusions, /center/prizes/age-bands, theo mẫu danh sách phẳng của các màn đá tri thức.| Giai đoạn | Nội dung | Kết quả |
|---|---|---|
| Giai đoạn 1, chu kỳ đầu | Yêu cầu A (công thức có phiên bản, đề vô địch dạng check point một lượt có khung giờ, sửa huy chương), Yêu cầu B (nhắc và khoá ngày sinh, tác vụ điều kiện, khởi tạo loại trừ, nhãn trên bảng), Yêu cầu C (trang nhận giải, tải giấy, trang thể lệ, bảng vinh danh), Yêu cầu D (chu kỳ, hàng đợi hồ sơ, loại trừ, khung tuổi), tác vụ chốt và thông báo | Tháng trao giải đầu tiên chạy trên quy tắc phụ huynh đọc được và nhân viên CS thực thi được |
| Giai đoạn 2 | Xác nhận giáo viên trong sản phẩm qua FLYER Teacher và Teado.ai, tín hiệu trung thực trong màn hồ sơ, so trùng mẫu đáp án, dự bị nộp trước, xuất dữ liệu kế toán | Thẩm định mất vài giờ thay vì vài ngày và không còn dựa vào gọi điện |
| Giai đoạn 3 | Áp công thức có phiên bản cho Tuần và Năm, giải phụ theo trường và tỉnh thành, giải nhỏ hằng tuần bằng đá tri thức | Một họ công thức trên mọi tab |
| Ngoài phạm vi | Giải tiền mặt, giải cho cấp độ IELTS hoặc TOEFL, thay đổi tab Ngày, thay đổi cửa hàng đá tri thức |
| Khoản | Giả định | VND mỗi tháng |
|---|---|---|
| Xe đạp | 5 × 2.500.000 đến 3.500.000 (cỡ trẻ em và thiếu niên) | 12.500.000 đến 17.500.000 |
| Giao hàng | 5 × 150.000 đến 400.000, toàn quốc | 750.000 đến 2.000.000 |
| Thời gian CS | 15 hồ sơ gồm dự bị × 30 phút | khoảng 1 ngày công |
| Tổng | khoảng 14.000.000 đến 20.000.000 |
Cỡ xe khác nhau giữa các khung tuổi: trẻ sáu tuổi và học sinh mười sáu tuổi không đi cùng một chiếc xe. Mẫu nhận giải hỏi cỡ xe, và bộ phận mua sắm nên giữ hai mẫu.
| Câu hỏi | Vì sao ảnh hưởng tới thiết kế | Người quyết |
|---|---|---|
| Xác nhận khung tuổi ở mục 6, đặc biệt KET 11 đến 14 và PET 13 đến 16. | Cambridge không công bố độ tuổi cố định cho các bài for Schools; đây là lựa chọn của FLYER và quyết định ai có thể thắng. | Học thuật |
| Có nhận học sinh nhỏ hơn khung không? | Trẻ bảy tuổi thắng Flyers là điểm tốt cho marketing và không bất công với học sinh lớn hơn. Loại trừ thì dễ giải thích hơn. Bản nháp mặc định loại trừ. | Học thuật + Sản phẩm |
| Mười bài, hay mức chặn khác? | Đặt sàn nỗ lực. Mười bài khoảng bốn đến năm giờ ở Starters; nếu Học thuật thấy quá nhiều với trẻ sáu tuổi, Starters có thể dùng tám. | Học thuật |
| Học thuật có soạn được năm đề đầy đủ hoàn toàn mới, chưa công bố, mỗi tháng không? | Đề vô địch chỉ có giá trị khi nội dung chưa ai từng thấy. Nếu năng lực là giới hạn, khung giờ có thể rút ngẫu nhiên một đề từ kho niêm phong hai mươi đề mỗi cấp độ. | Học thuật + Nội dung |
| Khung giờ đề vô địch: cả cuối tuần cuối tháng, hay chỉ Chủ nhật? | Khung dài công bằng hơn với gia đình có lớp học cuối tuần; khung ngắn khiến việc chia sẻ đáp án khó hơn. Bản nháp dùng 08:00 thứ Bảy đến 22:00 Chủ nhật. | Sản phẩm + Học thuật |
| Công thức có phiên bản có thay luôn Ngày, Tuần, Năm từ đầu không? | Gọn hơn, nhưng làm đổi các con số học sinh đang theo dõi và mở rộng giai đoạn 1. Bản nháp giữ trong tab Tháng của năm cấp độ có giải. | Sản phẩm |
| Số điện thoại phụ huynh có sẵn trên đủ nhiều hồ sơ không? | Nếu phần lớn hồ sơ thiếu, bắt buộc ngay tháng đầu sẽ loại gần hết top 10. Cần truy vấn bản sao đọc. | Sản phẩm + Dữ liệu |
| Ai ký cho học sinh học tại nhà hoặc không có giáo viên tiếng Anh? | Bản nháp chấp nhận giáo viên trung tâm và giáo viên chủ nhiệm cho lớp 1 đến 3, nhưng chưa có câu trả lời cho trẻ không có cả hai. | CS + Học thuật |
| Công bố công khai chi tiết điểm của người thắng? | Minh bạch giảm tranh chấp nhưng lộ việc trẻ đã làm những đề nào. Bản nháp chỉ hiển thị chi tiết cho người nhận giải và quản trị. | Sản phẩm + Pháp lý |
| Duyệt ngân sách và mua hai mẫu xe. | Khoảng 14 đến 20 triệu đồng mỗi tháng chưa gồm marketing. | Kế toán |
flyer/webs-ranking (src/pages/index.tsx, src/components/TableRanking, src/api/common.ts, src/providers/auth), flyer/exam (graph/schema/type_extended.graphql, graph/user/type_extended.resolvers.go, ent/schema/views.go, module/mview, module/learnerprofile) và flyer/flyer-go (db/migrations/base/0092_user_score_views.up.sql, service/student/db/queries/score.sql). Định nghĩa thật trên production của learner_test_level_ranks phải đọc từ pg_matviews; nó không nằm trong kho mã nào.