fix(时区): 日历口径收回东八区,读出的时刻统一成 ISO 并保留微秒

旧栈 Django 按 Asia/Shanghai 算日历,OJ2 重写时这个锚点丢了:容器和数据库会话
都是 UTC,于是「今日提交」在北京时间 0–8 点是空的,「凌晨/早起提交次数」整体偏
8 小时,热力图、AC 趋势年份、近两年活跃人数也各按进程时区切。

- 新增 apps/api/src/time.ts 作为唯一锚点(固定 +8 偏移,不依赖进程 TZ / tzdata),
  todayStart、成就小时/日期键、热力图、月份平移、年份夹逼全部改走它;
  SQL 里按日历切的一律显式 at time zone。
- db/index.ts:连接会话时区设为东八区(兜底);给 timestamptz(1184) 挂 parser,
  读出统一成 ISO 8601 UTC,撤掉为拿 PG 文本形状写的 ::text。parser 保留微秒 ——
  生产库 12.3 万条提交几乎全带微秒,截成毫秒会让翻页分界行和班级 AC 排名的
  <= min(create_time) 把自己排除(翻页每页丢一条、排名少 1)。
- 前端 parseTime/zonedParts/zonedYear 按 Asia/Shanghai 渲染,n-date-picker 做
  toPickerValue/fromPickerValue 平移,站内不再按浏览器时区取时间部件。
- Dockerfile 设 TZ=Asia/Shanghai 作为第二道兜底。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
This commit is contained in:
2026-09-14 05:55:17 -06:00
parent 6e63866cc9
commit 4c0c38445c
23 changed files with 491 additions and 106 deletions

View File

@@ -22,6 +22,7 @@
</template>
<script lang="ts" setup>
import { useBreakpoints } from "shared/composables/breakpoints"
import { zonedYear } from "utils/functions"
const route = useRoute()
const { isMobile } = useBreakpoints()
@@ -29,7 +30,9 @@ const hiddenICP = computed(() =>
["problem", "contest problem"].includes(route.name as string),
)
const currentYear = new Date().getFullYear()
// 版权年份也走东八区:站内不留任何一处按浏览器时区取时间部件的代码,
// 免得下一个人照着抄
const currentYear = zonedYear()
const copyrightText = `© 2022 - ${currentYear} 判题狗 保留所有权利`
function goICP() {

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from "vue"
import { parseTime } from "utils/functions"
import { getNodeTypeConfig } from "./useNodeStyles"
import { currentDragNodeType } from "./useDnD"
@@ -60,7 +61,8 @@ const saveStatusTitle = computed(() => {
} else if (props.hasUnsavedChanges) {
return "有未保存的更改"
} else if (props.lastSaved) {
return `已保存 - ${new Date(props.lastSaved).toLocaleTimeString()}`
// 和站内其它时间同一口径(东八区),不用 toLocaleTimeString() 跟着浏览器走
return `已保存 - ${parseTime(props.lastSaved, "HH:mm:ss")}`
} else {
return "已保存"
}