Skip to content

feat(contest): Implement contest scoring and real-time systems - #5

Merged
DavidHLP merged 52 commits into
mainfrom
feat/contest-system
Mar 14, 2026
Merged

feat(contest): Implement contest scoring and real-time systems#5
DavidHLP merged 52 commits into
mainfrom
feat/contest-system

Conversation

@DavidHLP

Copy link
Copy Markdown
Owner

Summary

This PR implements the contest scoring system with real-time updates, anti-cheat detection, and analytics capabilities.

Backend Features

Scoring System

  • Scoring calculation service with configurable rules (base score, time bonus, first solve bonus, penalties)
  • Scoring rules management API (CRUD operations)
  • Redis caching for ranking data with 5-second TTL
  • Performance indexes on ContestParticipant and ContestSubmission tables

Real-time Updates

  • WebSocket gateway for real-time contest updates
  • Real-time service with throttling (max 1 update per second per contest)
  • Socket.io integration for ranking broadcasts

Anti-cheat System

  • Code similarity detection using Jaccard similarity on 3-grams
  • Time anomaly detection (too-fast submissions)
  • Admin endpoints for viewing similarity reports and time anomalies

Analytics Service

  • Contest analytics reports (problem stats, score distribution, top users)
  • User performance history tracking
  • Stored analytics retrieval

Frontend Features (Console)

  • Contest list view with tabs (all, ongoing, upcoming, past)
  • Contest core UI components (timer, problem list, ranking)
  • WebSocket integration via useContestSocket composable
  • Contest Pinia stores with comprehensive tests

Management Features

  • Scoring rules management page
  • Scoring rule selector in contest wizard

Test Plan

  • All 114 contest module tests pass
  • Type checks pass for all packages
  • Database migration to be applied when DATABASE_URL is available

Database Migration Required

When database is available, run:

cd backend && pnpm prisma migrate dev --name add_contest_performance_indexes

DavidHLP and others added 30 commits March 13, 2026 23:57
Add columns.ts for the moderation queue view with:
- FlagStatus type (PENDING, REVIEWED, RESOLVED, DISMISSED)
- ModerationActions interface for callbacks
- Terminal-style status and difficulty badges
- Column definitions: select, problem, flag_status, flag_reason,
  flag_reported_by, flag_reported_at, actions
Add new i18n keys for the moderation queue redesign including:
- columns.problem for table column header
- quickResolve/quickDismiss for batch action buttons
- unknownReporter for anonymous reporter display
- drawerTitle/drawerDescription for detail drawer
- problemDetails/flagInfo/moderationActions for sections
- searchPlaceholder/allDifficulties for filters
Refactored ModerationQueueView to use DataTable layout for improved UX:

- Replaced card-based layout with DataTable component
- Added DataTableToolbar with search and filters (status, difficulty)
- Integrated BaseDetailDrawer for viewing/editing individual flags
- Added batch moderation dialog for bulk operations
- Implemented terminal-style header with stats ticker
- Added batch actions bar for selected rows
- Used useDebounceFn for debounced search
- Added watch for pagination changes
- Improved state management with typed refs
Design for console frontend recommendation page featuring:
- Independent /recommendations route
- Left navigation + content area layout
- 4 recommendation scenarios (daily, weak-points, challenge, similar)
- Tag filtering functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix API import path to use @/utils/request
- Add complete type definitions section
- Add error handling in store actions
- Fix typo in RecommendationNav template
- Add loading/empty states handling
- Add component imports for Combobox
- Clarify SimilarProblemSearch usage context
- Add prerequisites checklist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Detailed plan with 12 tasks covering:
- Types and API layer
- Store with unit tests
- i18n translations
- UI components (ProblemCard, Nav, Filter, Search)
- Main view and routing
- Sidebar integration
- Integration testing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix v-model on computed property in RecommendationsView
- Fix search event handling in SimilarProblemSearch with debounce
- Add ComboboxEmpty to TagFilter component
- Remove unnecessary emit from RecommendationNav

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Chinese and English translation files for the recommendation feature:
- Sidebar navigation labels
- Page title and filter labels
- Card display labels
- Empty state messages
- Search placeholder and no results text
- Add recommendationSidebarData with four navigation items (daily, weak-points, challenge, similar)
- Import Sparkles, Target, GitBranch icons for recommendation types
- Add isRecommendationContext computed property in AppSidebar
- Integrate recommendation sidebar data into context-aware navigation
- Simplify scoring system from Elo to point-based (LeetCode style)
- Add support for 6 contest types: weekly, biweekly, monthly, themed, corporate, campus
- Design realtime features: live ranking, first-solve announcements, code replay
- Add admin features: contest management, scoring rules config, anti-cheat, analytics
- Include comprehensive migration strategy with rollback scripts
- Add feature flags for zero-downtime deployment
- Preserve backward compatibility with existing data
- Chunk 1: Database migration with rollback scripts
- Chunk 2: Backend scoring service with rules management
- Chunk 3: Realtime WebSocket gateway and service
- Chunk 4: Frontend pages (console) with Vue components
- Chunk 5: Admin panel scoring rules management
- Chunk 6: Anti-cheat similarity detection and analytics
- Chunk 7: Integration tests and performance optimization

Includes feature flags for gradual rollout and complete rollback strategy.
- Add missing foreign key constraint for problem_id in first_solve_records
- Use IF EXISTS syntax for constraint drops in rollback script for better safety
DavidHLP added a commit that referenced this pull request Jun 14, 2026
ApplicationReadyEvent 监听,白名单非法组合反向断言(reviewer P1-1 指导):
- F1 (hard fail): use-port=true + use-judge-outbox=false (port 无 producer → Pending 孤儿)
- W1 (soft warn): use-port=true + envelope-version=1 (dispatcher hard-code v2, flag 是 dead config)
- W2 (soft warn): use-port=true + cutover-at 过期 (配置陈旧)

CI features-off profile 全 false 自然通过。envelope-version 当前无消费者
(JudgeOutboxDispatcher.toEnvelope hard-code v2),ADR-005 §2.4 实施 envelope-aware
写入时 W1 升级为 fail-fast。

Tests: FlagCombinationValidatorTest (6 case) + FeatureFlagsPropertiesBindingTest (3 case) = 9 PASS

Refs: task #5 (P1-1)

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jun 14, 2026
SubmissionServiceImpl.submit 已有 @transactional (line 122), 但 outbox insert
原在 try-catch 中被吞掉, 导致 port 模式下 outbox 是唯一 producer 时 insert
失败会留 Pending 孤儿 (reviewer P1-1 concern).

Fix (方案 A+): 去掉 outbox insert 的 try-catch, 让异常自然冒泡.
@transactional 默认 RuntimeException 回滚, submission + outbox 同生共死 (ADR-003).
异常冒泡到 GlobalExceptionHandler 返回 5xx.

portActive 分支保持 (skip RQueue, 由 outbox dispatcher 取 row enqueue).
recordContestSubmissionIfNeeded try-catch 保留 (supplementary).

Tests: 55 case 全 PASS (含 SubmissionServiceImplIT Testcontainers)

Refs: task #5 (P1-1)

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jun 18, 2026
F-06 (SubmissionServiceImpl.recordContestSubmissionIfNeeded):
timeFromStart was computed from contest.startTime (planned) — wrong for
virtual sessions (their clock is participant.startedAt, not the real
contest's start). Now picks the right clock per participant.isVirtual;
for real contests also uses actualStartTime (fallback to startTime)
so submissions before the scheduler triggers RUNNING don't compute
negative offsets.

F-01 audit (F-01-STATE_MACHINE_AUDIT.md): documents the status-machine
audit, finding one violation (#5 finishVirtualContest direct write)
and one pre-existing concern (B.1, addressed in R6.5).

F-01 fix (ContestSchedulerServiceImpl.finishVirtualContest): routes
through participantMapper.bulkFinishByIds(...) instead of
updateById(participant) so the 'auto-finish central dispatch'
invariant from the audit doc holds.

F-07 (ContestServiceImpl.submitContestProblem): virtual participants
get a hard deadline at started_at + duration_minutes; previously the
auto-finish only kicked in on the next 10s tick, leaving a window where
late submissions slipped through. 409 CONTEST_ENDED on miss.
DavidHLP added a commit that referenced this pull request Jun 18, 2026
新增 EXECUTION_PLAN_R10.md(9 项,3.5-4 人日):
- R10.1 per-contest evict 真实现
- R10.2 i18n view 模板接线
- R10.3 i18n key 同步审计
- R10.4 旧 getGlobalRankingsPaginated 签名删除
- R10.5 M1 contestMapper.selectById 优化(独立 PR)
- R10.6/10.7 F-01 状态机复核销项(doc-only)
- R10.8 F-SEC-10 迁移期 checklist
- R10.9 F-SEC-13 log retention 文档

F-01-STATE_MACHINE_AUDIT.md R10 销项:
- §3.1 finishVirtualContest 复核通过(已走 bulkFinishByIds:251-255)
- §6.4 F-06 timeFromStart 复核通过(三元分支:1387-1395,虚拟用 p.startedAt,真实用 contest.actualStartTime)
- §5 结论表 #5 行从'需复核'→'已复核通过'
- §7 行动项两行标 ✅
- 头部加 R10 销项声明

REVIEW_V3.md §12 同步:
- 'F-01 状态机待复核'→'F-01 状态机(已 R10 销项)'+ 复核证据
- R10 deferred 项加 EXECUTION_PLAN_R10.md 链接

README.md 加 R10 计划到'实施计划 (中游)'表格。

完成模块 v4.3 收口入口;R10 实施 = v4.3 完结。
DavidHLP added a commit that referenced this pull request Jul 3, 2026
feat(contest): Implement contest scoring and real-time systems
DavidHLP added a commit that referenced this pull request Jul 3, 2026
管理后台鉴权初始化(CRITICAL #1):
- management/src/main.ts: 新增完整 auth bootstrap 流程
- management/src/stores/auth.ts: 新增 setupSessionExpiredCallback + clearUser

WebSocket Token 安全泄露(CRITICAL #2):
- console/src/lib/socket.ts: 移除 URL ?token= 参数,token 改走 connectHeaders
- console/src/composables/contest/useContestSocket.ts: 同上

OAuth 重定向 URL 统一(CRITICAL #4):
- management/src/views/auth/components/OAuthButton.vue: 相对路径→绝对路径

Router Guard 错误处理(HIGH #5):
- console/src/router/index.ts: catch 块增加 ApiError instanceof 判断

WebSocket Token 传递优化(HIGH #6):
- useContestSocket.ts: 已正确从 socket.ts 导入 getTokenFromCookie

verifyAuth 绕过 axios 拦截器(HIGH #7):
- console/src/utils/auth.ts: fetch() → apiGet()

性能优化(HIGH #9):
- console/src/contexts/AuthContext.ts: setInterval(1000) → watch(isAuthenticated)

代码质量提升(MEDIUM):
- console/src/stores/auth.ts: 移除 LOGOUT_KEY localStorage 追踪
- 新增 hasAuthCookie() 提前检查,173 行净减少
- clearUser() 重置 status="idle" 允许重新初始化
- 移除所有调试 console.log

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…_ADMIN + rankings 404 + is_deleted filter + Unicode title + @Audited + new ErrorCode + controller XSS guard + mutator tests)

修复 docs/contests-api-test-report.md 报告的 7 处缺陷 (1 HIGH + 6 MEDIUM/LOW):

缺陷 #1 (HIGH) — DELETE 软删除 is_deleted 字段未持久化
  - 根因:Contest.isDeleted 字段 @TableLogic + mapper.updateById(entity) 自动忽略逻辑删除字段
  - 修复:改用 mapper.update(null, LambdaUpdateWrapper.set(...)) 显式写入

缺陷 #2 — PATCH on FINISHED/RUNNING 比赛无状态守卫
  - 修复:service 层加 status guard,仅允许 UPCOMING 修改
  - 新增 ErrorCode CONTEST_ONLY_UPDATE_UPCOMING (70006) 区分注册与修改语义

缺陷 #3 — service 层 hasRole("ADMIN") 拒绝 SUPER_ADMIN,与控制器 @PreAuthorize 不一致
  - 修复:SecurityUtil 新增 hasAnyRole(String...) 工具方法,ContestServiceImpl 7 处 mutator 替换

缺陷 #4 — CreateContestDTO/UpdateContestDTO title @pattern 拒绝 CJK 字符
  - 修复:正则放宽为 [\p{L}\p{N}\s\p{P}]+ 接受 Unicode 字母
  - AdminContestController 新增 rejectUnsafeTitleChars helper 显式排除 < 和 >

缺陷 #5 — GET /admin/contest/{id}/rankings 对不存在 contest 返 200+空
  - 修复:getAdminContestRanking 入口加 contest 存在性 + is_deleted 校验

缺陷 #6 — addProblem/removeProblem/startContest/endContest 未校验 is_deleted
  - 修复:4 个 mutator 入口加 .filter(c -> !isDeleted).orElseThrow(COMPETITION_NOT_FOUND)

缺陷 #7@Audited 注解缺失导致 audit_logs 无 contest 记录
  - 修复:7 个 mutator 方法加 @Audited + AuditContext.setOldValues/setNewValues/setUserId
  - 复用 AuditActionUtil.CREATE_CONTEST/UPDATE_CONTEST/DELETE_CONTEST 常量

新增测试 ContestServiceImplMutatorTest:5 case 覆盖缺陷 #2/#5/#6 的状态守卫 + 软删除过滤 + 404 路径。
缺陷 #1 的 LambdaUpdateWrapper.getSqlSet() 断言需 @SpringBootTest 上下文,保留为 follow-up,
运行时 curl 验证已确认三列同改 (docs/contests-api-test-report.md §3.5)。

验证:
- mvnw compile -B:0 错误
- mvnw test -Dtest=ContestServiceImplMutatorTest:5/5 通过,2.0s
- 运行时 curl 5/5 critical 缺陷修复确认 (PM2 重启 ulticode-9001)

Artifacts:
- Plan: .claude/PRPs/plans/completed/admin-contests-mutator-fixes.plan.md
- Report: .claude/PRPs/reports/admin-contests-mutator-fixes-report.md
- Review v1: .claude/reviews/admin-contests-mutator-review.md
- Review v2: .claude/reviews/admin-contests-mutator-review-v2.md
- Test report: docs/contests-api-test-report.md
DavidHLP added a commit that referenced this pull request Jul 3, 2026
Backend UserVO uses @JsonInclude(NON_NULL), so admin (no bio set)
returns without bio field, and /users/{id} public endpoint
strips email via toPublicVO(). Frontend type declared both as
required, causing type/runtime mismatch.

- console/src/api/user.ts: UserProfile.bio?: string, email?: string

UI components already guard with v-if='profile.bio', no template
changes needed.

Verified: pnpm type-check 0 errors, pnpm test 241/241 pass, pnpm build OK.

Refs: docs/api-test-report-user-userstats.md (Defect #5)
DavidHLP added a commit that referenced this pull request Jul 3, 2026
… handler, compute real progress

Fixes 6 issues from docs/achievement-api-test-report-2026-06-11.md
plus 2 newly-discovered typeHandler bugs (Bug #7, Bug #8) surfaced
during validation.

CRITICAL #1 — escape `key` column via @TableField(value = "`key`")
  - Achievement.java:19 — affects MyBatis-Plus auto-generated column
    lists in selectById / selectPage / selectBatchIds.
  - Without this, every SQL that lists the achievements columns fails
    with 'You have an error in your SQL syntax ... near key,...'.

CRITICAL #2 — escape `key` in @select SQL
  - AchievementMapper.findByKey:24 — WHERE clause now backticks.
  - Fixes admin POST /achievements which called findByKey for dedup.

HIGH #3 — T2b /achievements/{invalid} now returns 404 (was 500)
  - Follows from CRITICAL #1+#2: the BadSqlGrammarException
    no longer fires before the business-level ACHIEVEMENT_NOT_FOUND
    can be raised.

MEDIUM #4 — BadSqlGrammarException → DATABASE_ERROR (50001)
  - GlobalExceptionHandler:227 — new handler preserves root cause
    in server log while returning generic 'Database error' to clients.
  - Pairs with existing MyBatisSystemException / BindingException /
    DataIntegrityViolationException handlers (no fallback to 50000
    'Unknown error').

LOW #5 — getUserAchievements progress is no longer hard-coded to 0
  - AchievementServiceImpl:280 — mirrors getUserProgress logic,
    reads criteria.type from JSON Map, dispatches to SubmissionMapper
    counter.

LOW #6 — re-scoped Javadoc: getUserAchievements and
  getUserProgress are NOT duplicates (verified UserController
  /me/achievements/progress uses the latter). Both serve different
  endpoints with different DTO shapes.

Bug #7 — findAllActive missing JacksonTypeHandler
  - AchievementMapper:46 — @select bypassed @TableField(typeHandler=
    JacksonTypeHandler.class), returning criteria=null.
  - Converted to default method via BaseMapper.selectList.

Bug #8 — findByKey same typeHandler bypass
  - Discovered by AchievementMapperIT (L2) on first run;
    findByKey_criteriaIsDeserializedAsMap failed.
  - AchievementMapper:24 — also converted to default method.
  - Latent: only caller (create) never read criteria.

Refactors
  - M1 (review): getUserAchievements extracted buildProgressDTO helper
    (52 → 16 lines).
  - L1 (review): findAllActive uses LambdaQueryWrapper<Achievement>
    method refs instead of string column names.
  - L3 (review): Javadoc clarified the two service methods.

Tests
  + AchievementMapperSQLGuardTest: 3 reflection-guard tests
    (CRITICAL #1, CRITICAL #2, no @select on findAllActive/findByKey).
  + AchievementMapperIT: 5 Testcontainers MySQL 8.0 IT tests
    verifying JacksonTypeHandler runtime behavior + Bug #7/#8
    regression guards.
  + AchievementServiceTest: 3 progress tests + 2 new mock fields
    (SubmissionMapper, ContestParticipantMapper).
  + GlobalExceptionHandlerTest: 2 BadSqlGrammarException tests.

Validation
  - compile / package: BUILD SUCCESS
  - Unit tests: 36/36 pass
  - Testcontainers IT: 5/5 pass
  - 8 curl integration tests on worktree:9091: all match expected
    HTTP codes (200/400/404/409) and real progress values
    (admin 6 accepted problems → progress=6 for all 3 rows).

Refs: docs/achievement-api-test-report-2026-06-11.md,
      .claude/PRPs/plans/completed/achievement-api-fixes.plan.md,
      .claude/PRPs/reports/achievement-api-fixes-report.md,
      .claude/reviews/achievement-api-fixes-review.md
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…dation

Merges the fix/achievement-api worktree changes:
- CRITICAL #1, #2 (MySQL reserved word `key`)
- HIGH #3 (T2b 404)
- MEDIUM #4 (BadSqlGrammarException handler)
- LOW #5 (real progress computation)
- LOW #6 (Javadoc re-scoped)
- Bug #7 (findAllActive typeHandler)
- Bug #8 (findByKey typeHandler, IT-discovered)

M1/L1/L2/L3/L4 from .claude/reviews/achievement-api-fixes-review.md
all addressed. Test coverage: 36 unit + 5 Testcontainers IT.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…nt dispatch

- SubmissionServiceImpl: 2 dispatch sites (updateSubmissionResult +
  triggerPostVerdictSideEffects) flag-gated to use SubmissionCompletedIntent.
  Legacy path is the ADR-004 §2.5 fallback; new path is the new dispatcher.
- AchievementNotificationListener: flag-gated to dispatch
  AchievementEarnedIntent. The legacy path keeps the manual realtimeService
  push; the new path relies on WebSocketNotificationChannel to emit the
  BadgeEarnedPayload (no double-push).
- FollowServiceImpl: flag-gated to dispatch FollowReceivedIntent.
- ContestScheduler.sendContestReminder: flag-gated to dispatch
  ContestStartingIntent. The 24h and 1h reminders naturally get separate
  intent ids via ContestStartingIntent.intentId() derivation.
- NotificationDispatchService + Impl: @deprecated. Javadoc points to the
  new dispatcher. Removal is M4d's job.

Test updates (4 files):
- AchievementNotificationListenerTest, FollowServiceImplTest,
  SubmissionServiceImplTest, SubmissionServiceImplIT: add @mock for the
  new NotificationDispatcher field, mock FeatureFlagsProperties with
  lenient().when(isUseNotificationIntent).thenReturn(false) so existing
  assertions on the legacy dispatch path keep working.

Grep audit (ADR-004 §4 #5): business modules no longer import
EmailService or RealtimeService.

mvn compile + 4 unit test classes: green.

Refs: docs/adr/ADR-004-notification-intents.md §2.4, §2.6, §4 #5.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
- docs/adr/ADR-004-notification-intents.md: status Proposed → Accepted;
  §4 validation checkboxes all checked; commit list added in header.
- docs/adr/README.md: ADR-004 row updated to 'Accepted (2026-06-13)' with
  the M4a-M4d commit hashes.

M4 status:
- M4a: e38e340 (ledger + intent records + dispatcher skeleton)
- M4b: bf02f48 (3 channel impls + EmailTemplates)
- M4c: 9ecf10e (migrate 4 callers to typed intent dispatch)
- M4d: 62a4dca (dispatcher contract + idempotency + perf + per-channel tests)

Grep audit (ADR-004 §4 #5): business modules no longer import
EmailService or RealtimeService directly. Legacy NotificationDispatchService
+ Impl kept with @deprecated; removal is a follow-up chore once the
useNotificationIntent flag has been on in production for ≥1 cycle.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…nedAt)

2 review findings about intentId() collisions on the ledger UNIQUE
(intent_id, channel_id) constraint:

- SubmissionCompletedIntent: factory of() now throws IllegalStateException
  when submission.getGeneration() is null, instead of silently falling
  back to 0L. Two distinct submissions (one real g=0, one with null
  generation) would have produced identical intentIds, and the second
  dispatch would have been silently dropped by tryClaim returning 0.
  The null-generation case indicates a hydration bug, not user data, so
  fail-fast is correct. Fixes finding #5.
- AchievementEarnedIntent: added earnedAt field, intentId now includes
  the millisecond timestamp (:at<epochMs>). Without this, a re-issued
  event (tier-up promotion, system re-trigger) for the same
  achievement collapsed under the existing intentId and was silently
  dropped on every channel. The natural key for achievement
  notifications now includes the issue time, matching the
  AchievementEarnedEvent one-publish-per-earn invariant. Fixes
  finding #6.

Test updates: 5 test files updated to pass the new
earnedAt=Instant.now() arg. The sample intents in the channel contract
test and the per-channel tests use the new field; the legacy
follow-system test path is unchanged (FollowReceivedIntent has no
earnedAt).

mvn test on the notification/follow/submission/email slice: green.

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review findings
#5, #6).
DavidHLP added a commit that referenced this pull request Jul 3, 2026
ADR-005 §4 #5: 之前 DB 迁移文件名完全靠人工/CLAUDE.md 文档约束, 零自动化.
本次新增 flyway-filename-lint job, 在 init-db/migrations 下扫所有 V*.sql
文件, 校验符合 V<14-digit-ts>__<desc>.sql 格式 (兼容 V20260604_110000
与 V20260604110000 两种时间戳写法).

正则在本地 dry-run: 现有 28 个迁移文件 0 fail. 故意改名会触发 ::error.

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md §4 Row #5
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…rationProperties)

Pre-P0-2: FeatureFlagsProperties 用 flat 字段 judgeQueueUsePort/judgeQueueEnvelopeVersion,
但 application.yml 用 nested key app.features.judge-queue.use-port。Spring Binder 的
kebab→camel 转换只作用于叶节点名,不作用于路径段,导致 YAML override 永远不生效,
M3c cutover flag 静默卡在 false。

Fix: 改为 @NestedConfigurationProperty JudgeQueue inner class (usePort/envelopeVersion/cutoverAt),
与 app.features 整体 prefix 一致,方便后续 FlagCombinationValidator 单 bean 聚合校验。
cutover-at (F13 watermark) 之前被 Binder 静默忽略,本轮一并补字段。

调用方更新 (2 处):
- JudgeWorkerProcessor.pollAndProcessFromPort
- SubmissionServiceImpl.submit (portActive)

Note: JudgeOutboxDispatcher 仍用 @value 直接读 YAML key (reviewer 补充 #1),
task #7 统一切到 FeatureFlagsProperties,避免 YAML 双解析。
@ConditionalOnProperty 4 处 (RedissonStreamsJudgeQueueAdapter/QueueConfig/...) 不动
(reviewer 补充 #2 — 它们直接读 YAML key,P0-2 只修 API 暴露层)。

Tests: FeatureFlagsPropertiesBindingTest (3 case, 真实 Binder 端到端验证) +
回归 41 case 全 PASS (JudgeWorkerProcessorTest + P0-1 测试集)。

Refs: task #5 (P0-2)

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 3, 2026
ApplicationReadyEvent 监听,白名单非法组合反向断言(reviewer P1-1 指导):
- F1 (hard fail): use-port=true + use-judge-outbox=false (port 无 producer → Pending 孤儿)
- W1 (soft warn): use-port=true + envelope-version=1 (dispatcher hard-code v2, flag 是 dead config)
- W2 (soft warn): use-port=true + cutover-at 过期 (配置陈旧)

CI features-off profile 全 false 自然通过。envelope-version 当前无消费者
(JudgeOutboxDispatcher.toEnvelope hard-code v2),ADR-005 §2.4 实施 envelope-aware
写入时 W1 升级为 fail-fast。

Tests: FlagCombinationValidatorTest (6 case) + FeatureFlagsPropertiesBindingTest (3 case) = 9 PASS

Refs: task #5 (P1-1)

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 3, 2026
SubmissionServiceImpl.submit 已有 @transactional (line 122), 但 outbox insert
原在 try-catch 中被吞掉, 导致 port 模式下 outbox 是唯一 producer 时 insert
失败会留 Pending 孤儿 (reviewer P1-1 concern).

Fix (方案 A+): 去掉 outbox insert 的 try-catch, 让异常自然冒泡.
@transactional 默认 RuntimeException 回滚, submission + outbox 同生共死 (ADR-003).
异常冒泡到 GlobalExceptionHandler 返回 5xx.

portActive 分支保持 (skip RQueue, 由 outbox dispatcher 取 row enqueue).
recordContestSubmissionIfNeeded try-catch 保留 (supplementary).

Tests: 55 case 全 PASS (含 SubmissionServiceImplIT Testcontainers)

Refs: task #5 (P1-1)

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 3, 2026
F-06 (SubmissionServiceImpl.recordContestSubmissionIfNeeded):
timeFromStart was computed from contest.startTime (planned) — wrong for
virtual sessions (their clock is participant.startedAt, not the real
contest's start). Now picks the right clock per participant.isVirtual;
for real contests also uses actualStartTime (fallback to startTime)
so submissions before the scheduler triggers RUNNING don't compute
negative offsets.

F-01 audit (F-01-STATE_MACHINE_AUDIT.md): documents the status-machine
audit, finding one violation (#5 finishVirtualContest direct write)
and one pre-existing concern (B.1, addressed in R6.5).

F-01 fix (ContestSchedulerServiceImpl.finishVirtualContest): routes
through participantMapper.bulkFinishByIds(...) instead of
updateById(participant) so the 'auto-finish central dispatch'
invariant from the audit doc holds.

F-07 (ContestServiceImpl.submitContestProblem): virtual participants
get a hard deadline at started_at + duration_minutes; previously the
auto-finish only kicked in on the next 10s tick, leaving a window where
late submissions slipped through. 409 CONTEST_ENDED on miss.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
新增 EXECUTION_PLAN_R10.md(9 项,3.5-4 人日):
- R10.1 per-contest evict 真实现
- R10.2 i18n view 模板接线
- R10.3 i18n key 同步审计
- R10.4 旧 getGlobalRankingsPaginated 签名删除
- R10.5 M1 contestMapper.selectById 优化(独立 PR)
- R10.6/10.7 F-01 状态机复核销项(doc-only)
- R10.8 F-SEC-10 迁移期 checklist
- R10.9 F-SEC-13 log retention 文档

F-01-STATE_MACHINE_AUDIT.md R10 销项:
- §3.1 finishVirtualContest 复核通过(已走 bulkFinishByIds:251-255)
- §6.4 F-06 timeFromStart 复核通过(三元分支:1387-1395,虚拟用 p.startedAt,真实用 contest.actualStartTime)
- §5 结论表 #5 行从'需复核'→'已复核通过'
- §7 行动项两行标 ✅
- 头部加 R10 销项声明

REVIEW_V3.md §12 同步:
- 'F-01 状态机待复核'→'F-01 状态机(已 R10 销项)'+ 复核证据
- R10 deferred 项加 EXECUTION_PLAN_R10.md 链接

README.md 加 R10 计划到'实施计划 (中游)'表格。

完成模块 v4.3 收口入口;R10 实施 = v4.3 完结。
DavidHLP added a commit that referenced this pull request Jul 4, 2026
把 OAuth state 生命周期(安全不变量 #5:HttpOnly cookie 绑定 + Redis 原子消费)从 OAuthService 提取为独立 deep module OAuthStatePort + OAuthStateModule,与既有 AuthSessionPort 并列。

- 新增 OAuthStatePort(issueState/validateAndConsume) + OAuthStateModule(@component)
- OAuthService 瘦身:移除 state 相关 6 个 helper + 3 个 dead dependency
  (CsrfService/StringRedisTemplate/JwtProperties 注入但未用) + dead private
  method getCookie + 4 个 public 方法的 dead param HttpServletRequest
- AuthController github/google callback 同步移除 dead HttpServletRequest 形参
- 删除 OAuthServiceTest(state 契约迁移),新建 OAuthStateModuleTest(7 个契约测试)

state Redis key/TTL/cookie Path/SameSite/Secure 耦合 + getAndDelete 原子消费行为与原代码逐字一致;HTTP 端点签名不变(仅 Java 形参清理)。
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…rough port

Architecture review candidate #5 (top recommendation):

AdminProblemServiceImpl used to @Autowired ProblemService (problem
module) and SubmissionMapper (submission module) directly, violating
the port pattern established across ADR-0001/06/07/08 and 46 existing
port/projection interfaces.

Create AdminProblemPort interface (owned by admin) with 6 methods:
- Read: toVO, findBySlug, findSubmissionsByProblemId
- Write: publishProblem, unpublishProblem, deleteProblem

Create AdminProblemAdapter (@component) that delegates to the existing
ProblemService and SubmissionMapper — same runtime behavior, but admin
no longer imports those types at compile time.

AdminProblemServiceImpl now depends on AdminProblemPort only. Test
surface shrinks: one port mock replaces two cross-module service mocks.

Verified: mvn compile succeeds. Pre-existing test failures in
AdminProblemListControllerTest (undefined DTO setters) are unrelated.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…imitAspect

Architecture review candidate #5 — the @ratelimit annotation carried 3
fields (key/limit/period) but its enforcing aspect embedded:
  - the Redis Lua script (INCR + EXPIRE)
  - the 'rate-limit:' key prefix
  - direct StringRedisTemplate injection
Callers (155 @ratelimit sites across 33 controllers) could not see what
they were getting; the aspect could not be unit-tested without Redis.

Deep-module extraction — splits the aspect's two responsibilities:

1. Request-context key generation (placeholder substitution, user/IP
   detection) STAYS in RateLimitAspect. This legitimately needs the
   ProceedingJoinPoint and HttpServletRequest — it cannot move behind
   the port.

2. Rate-check mechanism MOVES behind a new port:
     common/ratelimiter/RateLimiter             (interface, 1 method)
     common/ratelimiter/AcquisitionVerdict      (record: allowed + retryAfter)
     common/ratelimiter/RedisRateLimiter        (@component, prod adapter)
     common/ratelimiter/InMemoryRateLimiter     (NOT a bean; test adapter)

Two adapters justify the seam (architecture-review glossary):
  - RedisRateLimiter: atomic INCR+EXPIRE Lua script (preserves exact
    pre-refactor behavior — the 'sliding-since-last-activity' window
    is documented in javadoc as out-of-scope for this extraction)
  - InMemoryRateLimiter: synchronized ConcurrentHashMap; constructed
    manually in unit tests, NOT auto-registered as a bean

RateLimitAspect now injects RateLimiter and delegates the actual check.
155 @ratelimit sites untouched — annotation contract is unchanged.

Verified: ./mvnw compile clean, AuditPolicyCoverageTest 2/2 pass.
RateLimitIT (Testcontainers) requires Docker daemon to run, unchanged
by this refactor — it tests behavior, which is preserved.

Wins (glossary terms): locality — rate-limit policy in one module;
leverage — aspect drops to a delegator; tests run without Redis;
two adapters justify the seam.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
#1 SecurityUtil → CurrentUserProvider port
  - New CurrentUserProvider interface in common/auth/
  - SecurityCurrentUserProvider @component adapter (wraps SecurityContextHolder)
  - 37 files migrated from static SecurityUtil calls to injected interface
  - Eliminates duplicated userId==null guards across controller layer

#2 DashboardMapper+Service → DashboardStatsProjection
  - New DashboardStatsProjection interface + DefaultDashboardStatsProjection
  - Absorbs 7 private sub-aggregators + 6 mapper default methods
  - DashboardService + DashboardServiceImpl deleted
  - DashboardController now injects the projection directly
  - Mirrors ADR-0011 *Projection pattern

#3 AdminCommentService → CommentModerator polymorphic seam
  - New CommentModerator interface with ForumCommentModerator + SolutionCommentModerator
  - AdminCommentServiceImpl refactored to thin router
  - Eliminates 5x duplicated forum/solution type-string switch

#5 SubmissionMapper.calculateStreak → SubmissionStreakCalculator
  - New SubmissionStreakCalculator interface + JdbcSubmissionStreakCalculator
  - Streak algorithm (recursive CTE) now behind a JVM-testable interface
  - 3 production callers redirected + 3 test classes updated

#7 Frontend wrapper cleanup
  - 8 re-export wrapper files deleted (markdown, useTheme, useTypographyDensity, cn)
  - tsconfig path remaps @/lib/utils → @/shared/auth-core/src/utils
  - shared/sidebar-menu cn() consolidated to import from auth-core
  - LOCALE_HEADER_KEY duplicate declarations removed

#8 OAuthService → OAuthClient port + adapters
  - New OAuthClient interface + OAuthTokenResponse/OAuthUserInfo DTOs
  - GithubOAuthClient + GoogleOAuthClient @component adapters
  - OAuthService refactored to thin coordinator (221→195 LoC)
  - 11 unit tests covering dispatch, callback, DB upsert

Production code compiles (BUILD SUCCESS). 53 test compilation errors
from constructor signature changes — mechanical @mock field additions.
Candidates #4 (analytics projections) and #6 (contest.ts split) pending.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
… casing leak

Arch review #5. Production callers of @ulticode/domain-types import only
PageResult; the Problem/Contest/Comment DTOs are declared but unused (a "fake
canonical" superset).

- Problem: drop `acceptance_rate` (snake) — backend serves camelCase JSON only
  (Spring Boot default Jackson, no SNAKE_CASE strategy; verified `acceptanceRate`
  across ProfileVO/UserStatsDTO/ProblemVO). Keep `acceptanceRate?` (camel). The
  snake field was transport-casing leakage, never returned by the API.
- File doc: declare the contract proven-canonical for PageResult only; the other
  DTOs are the intended home but not yet consumed — migrate one-by-one as a
  caller proves the shared shape, and do not balloon into a superset.

The parallel Problem definitions in console/types and management/api stay (they
are the actual consumers today); this change fixes only the shared contract's
casing correctness and narrows its stated scope.

Verified: console + management vue-tsc type-check clean.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…nagement

Arch review 2026-07-10 candidate #5: console/src/types/sandbox.ts
and management/src/types/sandbox.ts were byte-identical pure
re-exports of @ulticode/sandbox-types with zero callers (grep
verified). Apps consume the shared module directly via
@/shared/sandbox-types/src/index.

The shim was a no-strategy adapter — it just renamed imports,
which the path alias already does. Delete is the right call.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…view #5)

Three auth services (AuthServiceImpl, OAuthService,
PasswordResetService) used to build LambdaQueryWrapper against the
User entity directly and read password-reset / OAuth columns
inline. The same column knowledge and the same query construction
appeared in three files; deletion of any one copy regenerated it
in the other two.

Move all user-table / reset-token / OAuth persistence behind a
single AuthAccountPort interface. The default implementation
lives in com.ulticode.modules.auth.account.DefaultAuthAccountAdapter
and is the only file on the auth side that imports UserMapper.

- Login / register / refresh: User lookups go through
  findByUsername / findByEmail / findById. Insertions and password
  updates go through create / updatePassword.
- Password reset: token storage is hidden behind
  findPasswordReset / savePasswordReset / clearPasswordReset /
  findUsersWithActivePasswordReset — the auth service no longer
  knows the column names tokenHash / expiresAt.
- OAuth: the email match is the only persistence step the auth
  service knows about; the adapter decides whether to look it up
  by email or by a future identity table.

The auth side keeps its existing AuthSessionPort for the post-auth
tail; the new AuthAccountPort is purely the pre-auth persistence
seam.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…pace (arch-review #5)

ForumPostDetailView and CommentDetailView each inlined the same
detail lifecycle: isInitialLoad, the isLoaded staggered-reveal
setTimeout, the onMounted fetch, a loadData() wrapper, and the
action-refresh pattern (call loadData after a mutation). Detail
correctness depended on each view remembering which reads to refresh.

Concentrate that lifecycle in one deep module — useDetailWorkspace
(isInitialLoad / isLoaded / refresh) — mirroring the useDataTable
collection-workspace pattern. Each view becomes an adapter supplying
its entityId + fetch (+ optional secondary refresh: Forum passes
audit history). Domain tabs, actions, permissions and dialog state
stay in the view. Also drops the redundant double audit-fetch the
old unflagPost/handleFlagSuccess paths had.

Behaviour preserved: 100ms animation, first-load skeleton gating,
silent error handoff to the store, fire-and-forget secondary refresh.

Validated: management vue-tsc 0 errors, eslint clean, forum/comments/
composables tests 22/22.

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 16, 2026
Implements candidates from /tmp/architecture-review-1783827842.html:

#1 AdminBulkExecutor (admin/bulk): lifts the duplicated per-item try/catch
loop + counting + optional existence-guard out of AdminForum/Comment/
Solution/Problem bulk actions into one consumer-owned executor with a
canonical ItemOutcome/Run. batchRejudge withdrawn with evidence (rich
RejudgeResult shape ≠ void-action aggregated contract).

#2 ProblemExportService + ExportPayload: moves format validation, the 10k
size cap, CSV header/escaping, and the LocalDate.now() time-leak out of
AdminProblemController into a Clock-injected service; controller shrinks
to response-writing.

#3 LegacyRejudgeStrategy + RejudgePolicy.rejudge dispatcher: moves the
50-line inline legacy rejudge state machine out of AdminSubmissionServiceImpl
behind the policy port; fenced vs legacy selection now lives in
DefaultRejudgePolicy. AdminSubmissionServiceImpl.rejudge = 3-line dispatch.

#4 createAuthStore factory (shared/auth-core): owns the duplicated
login/logout/fetchUser/loadPermissions/initialize/clearUser/hasPermission
chain + CSRF contract once; management store migrated. Console deferred
(already composable-shaped with a richer status state machine).

#6 AdminContestReadPort + adapter: ADR-0011 phase 2 (contest) — admin
contest reads cross the seam instead of reaching into ContestProblemMapper.

#5 (AdminCrudListView wrapper) withdrawn: scaffolded then deleted after
code-review flagged it as dead code (0 views wired); the genuine shared
seam already lives in DataTable + useDataTable + DataTableToolbar.

Validation: mvn compile + test-compile green; 37 targeted admin/submission
tests pass (incl. new AdminBulkExecutorTest); management eslint green on
auth store. code-review skill run (Standards pass, Spec gaps documented
in-code).

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 16, 2026
…fecycle

Two architecture-review candidates sharing the ContestScheduler /
ContestLifecycleServiceImpl seam.

#1 Notification delivery collapse: delete NotificationDispatchService, the
createNotification WS-push wrapper, and the useNotificationIntent flag.
The four per-user producers (achievement, follow, submission-judged,
contest reminder) now dispatch only a typed NotificationIntent; the
dispatcher owns preference gating, channel fan-out, and ledger
idempotency. Admin broadcast stays a batch exception.

#5 Contest lifecycle deepening: ContestScheduler is a thin trigger adapter
(285->67 lines); tick()/sendReminders() and the transition policy
concentrate in ContestLifecycleServiceImpl behind a @lazy self proxy that
preserves @transactional on batchStart/autoFinish (self-invocation would
otherwise bypass the proxy).

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP pushed a commit that referenced this pull request Jul 16, 2026
Candidate 1 — Submission judging execution
- New JudgingLanguageSupport port + DefaultJudgingLanguageSupport.
- CodeExecutionHelper.SUPPORTED_LANGUAGES / DFORM_SUPPORTED_LANGUAGES
  stop leaking to problem/projection and submission/sandbox modules;
  DefaultProblemProjection, InMemorySandboxAdapter, CodeExecutionService
  cross the port.

Candidate 2 — Problem List workflow
- New useProblemListMutations shared composable concentrating the
  HTTP-call + toast + reload policy.
- useProblemLists composable now delegates mutation boilerplate.
  (useSidebarLists / useProblemListOperations migration deferred —
  literal toast strings need a raw-mode flag on the helper.)

Candidate 3 — Problem solving session (top recommendation)
- New useProblemSession deep composable owning navigation, problem
  load, contest context, layout, panel state, panel component map,
  and the provide() setup order.
- ProblemDetailView shrinks from 309 → 150 lines; zero inline
  connector components; setup order is internal to the session.

Candidate 4 — Notification delivery
- New AnnouncementBroadcaster port + DefaultAnnouncementBroadcaster.
- AdminNotificationServiceImpl.createSystemNotification delegates the
  79-line inline fan-out (target resolution, preference filter,
  batch row insert) to the broadcaster. ADR-004 §2.3 force-delivery
  policy preserved; admin broadcast remains the documented exception
  outside SystemAlertIntent.

Candidate 5 — Audit capture
- New AuditRecorder port + DefaultAuditRecorder mirroring the
  @Audited aspect's metadata-capture contract (performer / IP /
  user-agent) and forwarding to AuditSinkPort.
- ForumFlagPolicyImpl, ForumPostFieldToggleImpl,
  AdminForumServiceImpl.deletePost, UserManagementServiceImpl.bulkDelete
  migrate off the deprecated AuditHelper. AdminContestMutationServiceImpl
  dead injection removed.

Verification
- ./mvnw test: 1610 / 1610 (4 pre-existing skips)
- console pnpm test: 436 / 436
- console pnpm type-check: clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant