From 5e9d328841a54aa5fabaa1cf04db86ed24647aa4 Mon Sep 17 00:00:00 2001 From: zqlit Date: Mon, 5 Oct 2026 21:54:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=8E=92=E5=BA=8F):=20=E5=90=8C=E6=97=A5?= =?UTF-8?q?=E6=96=87=E7=AB=A0=20date=20=E8=A1=A5=E5=85=A8=E6=97=B6?= =?UTF-8?q?=E5=88=86=E7=A7=92=20+=20EdgeOne=20=E9=83=A8=E7=BD=B2=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:小乖同一天发的第二篇排到了第一篇后面。 根因:front matter 的 date 只写到年月日(写入侧只收得到日期), 同一天多篇的时间戳完全并列 → 排序退化成比标题,前台/后台都错。 改动: - 数据(6 篇):用目录名/slug 里的时间戳把 date 补全为 'YYYY-MM-DDTHH:MM:SS+08:00',slug 即写作时刻。每篇仅 1 行 diff。 2026-10-05 两篇(必须)、2026-06-02 / 06-03 四篇(同一 bug 的历史遗留)。 - 写入侧根治 - editor-api/src/posts.mjs:新建时「只到天」补北京时间此刻; 保存时「同日保真」——没换日子则原有时分秒一字符不动。 - write-server/src/lib/posts.ts:parseDate 按 Asia/Shanghai 归日, 列表同天排序不再受 UTC 时区漂移影响。 - 回归测试:新增 editor-api/test/date-time.mjs(8 项,自带一次性临时仓库)。 - .cnb.yml:EdgeOne stage 重试 3 次/20s → 5 次/30s。 构建节点到 api.edgeone.ai 偶发 15s 超时会被 CLI 误报 「Invalid EDGEONE_PAGES_API_TOKEN」,纯网络抖动,令牌没坏。 - .gitignore:忽略 *.zip 与 .edgeone/(误留的 blog.zip 达 361MB)。 --- .cnb.yml | 18 +-- .gitignore | 3 + .../index.md | 2 +- .../index.md | 2 +- .../2026-06-03-0603-linux-运维面试题/index.md | 2 +- .../index.md | 2 +- .../index.md | 2 +- .../index.md | 2 +- editor-api/README.md | 3 + editor-api/src/posts.mjs | 51 +++++++- editor-api/test/date-time.mjs | 113 ++++++++++++++++++ write-server/src/lib/posts.ts | 49 +++++++- 12 files changed, 234 insertions(+), 15 deletions(-) create mode 100644 editor-api/test/date-time.mjs diff --git a/.cnb.yml b/.cnb.yml index 0850f4f4..c2d19945 100644 --- a/.cnb.yml +++ b/.cnb.yml @@ -178,21 +178,25 @@ trap record EXIT test -n "$EDGEONE_API_TOKEN" || { echo "❌ 缺 EDGEONE_API_TOKEN"; exit 1; } - # ★ 重试 3 次:EdgeOne CLI 第一步要访问 api.edgeone.ai 做 region 检查, - # 构建节点到该域名的偶发超时(实测 15s AxiosError timeout)会让 CLI 直接 - # 报「Invalid EDGEONE_PAGES_API_TOKEN」—— 令牌其实没坏,纯网络抖动。 + # ★ 重试 5 次 / 退避 30s:EdgeOne CLI 第一步要访问 api.edgeone.ai 做 region + # 检查,构建节点到该域名的偶发超时(实测 15s AxiosError timeout)会让 CLI + # 直接报「Invalid EDGEONE_PAGES_API_TOKEN」—— 令牌其实没坏,纯网络抖动。 # 2026-10-04 复现:同一令牌在本机部署秒过,CNB 节点那一次 15s 超时误报。 + # 2026-10-05 build 4 又踩同一次抖动(3 次重试全挂 → 白报一封失败邮件)。 + # 最坏耗时 5×(15s 超时 + 30s 退避) ≈ 4 分钟,远小于本 stage 的 20m。 # 失败重试比让整条流水线变红、白发一封失败邮件划算得多。 ok_deploy=0 - for i in 1 2 3; do + for i in 1 2 3 4 5; do if npx edgeone pages deploy ./public -n hugo-blog -t "$EDGEONE_API_TOKEN" --area overseas; then ok_deploy=1 break fi - echo "⚠️ EdgeOne 第 $i 次部署失败(多为构建节点网络抖动),20s 后重试…" - sleep 20 + if [ "$i" -lt 5 ]; then + echo "⚠️ EdgeOne 第 $i 次部署失败(多为构建节点网络抖动),30s 后重试…" + sleep 30 + fi done - test "$ok_deploy" = 1 || { echo "❌ EdgeOne 连续 3 次部署失败"; exit 1; } + test "$ok_deploy" = 1 || { echo "❌ EdgeOne 连续 5 次部署失败"; exit 1; } echo "✅ EdgeOne 部署完成" - name: 上报部署状态 diff --git a/.gitignore b/.gitignore index 35647b84..16c2e650 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,8 @@ cnb-secrets.yml # 编辑器本地验证用的临时目录(探针 / 日志 / 回收站),绝不入库 .editor-tmp/ .editor-trash/ +# 部署打包产物 + EdgeOne CLI 本地状态(blog.zip 曾达 361MB,误入库会撑爆仓库) +*.zip +.edgeone/ # Cloudflare 部署自检产物 blog-admin/.deploy-check/ diff --git a/content/posts/2026/2026-06-02-尝试接入-telegram-机器人/index.md b/content/posts/2026/2026-06-02-尝试接入-telegram-机器人/index.md index fd9ec6a8..4caae437 100644 --- a/content/posts/2026/2026-06-02-尝试接入-telegram-机器人/index.md +++ b/content/posts/2026/2026-06-02-尝试接入-telegram-机器人/index.md @@ -1,6 +1,6 @@ --- title: 尝试接入 telegram 机器人 -date: '2026-06-02T00:00:00.000Z' +date: '2026-06-02T11:39:00+08:00' slug: '202606021139' draft: false categories: diff --git a/content/posts/2026/2026-06-02-深圳已经迎来六月-我该如何面对房东的三级能耗空调/index.md b/content/posts/2026/2026-06-02-深圳已经迎来六月-我该如何面对房东的三级能耗空调/index.md index 32ad4977..e518732d 100644 --- a/content/posts/2026/2026-06-02-深圳已经迎来六月-我该如何面对房东的三级能耗空调/index.md +++ b/content/posts/2026/2026-06-02-深圳已经迎来六月-我该如何面对房东的三级能耗空调/index.md @@ -1,6 +1,6 @@ --- title: 深圳已经迎来六月,我该如何面对房东的三级能耗 -date: '2026-06-02T00:00:00.000Z' +date: '2026-06-02T16:30:41+08:00' slug: '20260602163041' draft: false categories: diff --git a/content/posts/2026/2026-06-03-0603-linux-运维面试题/index.md b/content/posts/2026/2026-06-03-0603-linux-运维面试题/index.md index 3b443a70..40280857 100644 --- a/content/posts/2026/2026-06-03-0603-linux-运维面试题/index.md +++ b/content/posts/2026/2026-06-03-0603-linux-运维面试题/index.md @@ -1,6 +1,6 @@ --- title: 0603 Linux 运维面试题 -date: '2026-06-03T00:00:00.000Z' +date: '2026-06-03T15:27:00+08:00' slug: '202606031527' draft: false categories: diff --git a/content/posts/2026/2026-06-03-分享一个好用的跳开屏广告-app/index.md b/content/posts/2026/2026-06-03-分享一个好用的跳开屏广告-app/index.md index 8ebf1d81..d66f1b98 100644 --- a/content/posts/2026/2026-06-03-分享一个好用的跳开屏广告-app/index.md +++ b/content/posts/2026/2026-06-03-分享一个好用的跳开屏广告-app/index.md @@ -1,6 +1,6 @@ --- title: 分享一个好用的跳开屏广告 app -date: '2026-06-03T00:00:00.000Z' +date: '2026-06-03T18:19:47+08:00' slug: '20260603181947' draft: false categories: diff --git a/content/posts/2026/2026-10-05-博客停更三个月的来龙去脉-20261005113439/index.md b/content/posts/2026/2026-10-05-博客停更三个月的来龙去脉-20261005113439/index.md index ae8150d3..c0762f43 100644 --- a/content/posts/2026/2026-10-05-博客停更三个月的来龙去脉-20261005113439/index.md +++ b/content/posts/2026/2026-10-05-博客停更三个月的来龙去脉-20261005113439/index.md @@ -1,6 +1,6 @@ --- title: 博客停更三个月的来龙去脉 -date: 2026-10-05 +date: '2026-10-05T11:34:39+08:00' slug: 20261005113439 author: 小赵 layout: post diff --git a/content/posts/2026/2026-10-05-测试一下多人写作-20261005213010/index.md b/content/posts/2026/2026-10-05-测试一下多人写作-20261005213010/index.md index dc65a4f5..8c4b6f88 100644 --- a/content/posts/2026/2026-10-05-测试一下多人写作-20261005213010/index.md +++ b/content/posts/2026/2026-10-05-测试一下多人写作-20261005213010/index.md @@ -1,6 +1,6 @@ --- title: 测试一下多人写作 -date: 2026-10-05 +date: '2026-10-05T21:30:10+08:00' slug: 20261005213010 author: 小乖 author_id: 502 diff --git a/editor-api/README.md b/editor-api/README.md index cceede50..aab44fe6 100644 --- a/editor-api/README.md +++ b/editor-api/README.md @@ -135,6 +135,9 @@ node test/api-e2e.mjs http://127.0.0.1:8017 devtoken # ④ 角色权限矩阵(自带一次性 git 仓库,不碰真仓库) node test/role-perm.mjs + +# ⑤ date 时分秒(自带一次性临时仓库):新建补时刻、同一天不再撞、保存不抹时刻 +node test/date-time.mjs ``` ## 本地联调 diff --git a/editor-api/src/posts.mjs b/editor-api/src/posts.mjs index 51e4df8e..86bbc79d 100644 --- a/editor-api/src/posts.mjs +++ b/editor-api/src/posts.mjs @@ -272,6 +272,17 @@ export function makePosts(cfg) { const incoming = pickDefined(patch.frontMatter); const merged = { ...(post.frontMatter || {}), ...incoming }; + // date 保真:编辑器的日期框只有「年月日」,保存时会把 date 回写成 2026-10-05, + // 把原有的时分秒抹掉 —— 那样同一天的文章又会撞成一片。只要「哪一天」没变, + // 就沿用原来那串(deepEqual 因此成立,front matter 原文照抄,零风险); + // 用户真的换了日子才补新时刻。 + if (incoming.date !== undefined && post.frontMatter && post.frontMatter.date != null) { + merged.date = + dayKey(incoming.date) === dayKey(post.frontMatter.date) + ? post.frontMatter.date + : withTime(incoming.date); + } + // 老文章(改造前没有 author_id):第一次由编辑本人保存时补上,正式划到自己名下。 // 已有则不覆写 —— 避免把别人名下的文章顺手改成自己的。 if (viewer && viewer.role === 'editor') { @@ -315,6 +326,43 @@ export function makePosts(cfg) { ); } + /** + * date 补时分秒 —— 防止「同一天发的两篇文章排序颠倒」。 + * + * 起因:编辑器的日期框只有年月日,新建时 date 被写成 `2026-10-05`。于是同一天 + * 发的两篇 date 完全一样,前台(Hugo 按 Date 排)和后台列表(下面按 date 排) + * 都只剩标题能当兜底比较 —— 结果 21:30 发的新文章排在了 11:34 发的旧文章 + * **后面**(2026-10-05 实测:首页第 2 位)。 + * + * 规则:date 只到「天」(或时刻正好是 00:00:00)→ 补上北京时间此刻的时分秒, + * 写成带 +08:00 偏移的完整时间戳;已经带非零时刻的一律原样保留(不去动老文章)。 + */ + const CN_OFFSET_MS = 8 * 60 * 60 * 1000; + + /** 北京时间的 YYYY-MM-DDTHH:mm:ss(不带偏移) */ + function cnStamp() { + return new Date(Date.now() + CN_OFFSET_MS).toISOString().slice(0, 19); + } + + /** 'YYYY-MM-DD' / 'YYYY-MM-DDTHH:mm:ss.000Z'(时刻为 0)→ 补当前时刻;其它原样返回 */ + function withTime(dateStr) { + const s = String(dateStr == null ? '' : dateStr).trim(); + if (!s) return cnStamp() + '+08:00'; + const m = /^(\d{4}-\d{2}-\d{2})(?:[T ]00:00:00(?:\.000)?Z?)?$/.exec(s); + if (!m) return s; + return m[1] + 'T' + cnStamp().slice(11) + '+08:00'; + } + + /** 只取「哪一天」(北京时间的年月日),用来判断用户到底有没有改日子 */ + function dayKey(value) { + const d = value instanceof Date ? value : new Date(String(value == null ? '' : value).trim()); + if (!isNaN(d.getTime())) { + return new Date(d.getTime() + CN_OFFSET_MS).toISOString().slice(0, 10); + } + const hit = /^(\d{4}-\d{2}-\d{2})/.exec(String(value == null ? '' : value)); + return hit ? hit[1] : ''; + } + /** 目录名里的标题段:与 write-server 的 computeDirPath/titlePart 同一套规则 */ function titlePartOf(title) { return ( @@ -335,7 +383,8 @@ export function makePosts(cfg) { const clash = listPosts({ perPage: 200 }).posts.find((r) => r.slug === slug); if (clash) throw bad('slug「' + slug + '」已被占用(' + clash.dirName + '),请换一个', 409); - const date = String(frontMatter.date || new Date().toISOString().slice(0, 10)); + // 只到「天」的 date 补上此刻时分秒:否则同一天的文章按 date 排不分先后(见 withTime 注释) + const date = withTime(frontMatter.date); const ymd = date.slice(0, 10); const year = ymd.slice(0, 4); diff --git a/editor-api/test/date-time.mjs b/editor-api/test/date-time.mjs new file mode 100644 index 00000000..0a52fdee --- /dev/null +++ b/editor-api/test/date-time.mjs @@ -0,0 +1,113 @@ +/** + * date 时分秒回归测试(2026-10-05 加)。 + * + * node test/date-time.mjs + * + * 背景:编辑器的日期框只有年月日,写出来的 front matter 是 `date: 2026-10-05`。 + * 同一天发两篇 → 两篇 date 一模一样 → 前台(Hugo 按 Date 排)和后台列表 + * (posts.mjs 的 listPosts 按 date 排)都只能拿标题当兜底比较,于是 21:30 发的 + * 新文章排在了 11:34 发的旧文章**后面**(真实事故:首页第 2 位)。 + * + * 这里在一个一次性临时仓库里跑,不碰真仓库。断言 5 件事: + * ① 新建时 date 只给「天」→ 落盘必须带时分秒 + 北京时区偏移 + * ② 同一天连发两篇 → 时间戳必须不同,列表里新的在前 + * ③「时间正好 00:00:00」的写法也按「只到天」处理,同样补时刻 + * ④ 保存时不改日子 → 原有的时分秒一个字符都不能动 + * ⑤ 保存时真的换了日子 → date 换成新日子 + 新时刻 + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { makePosts } from '../src/posts.mjs'; + +const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'editor-date-')); +const contentDir = path.join(TMP, 'content', 'posts'); +fs.mkdirSync(contentDir, { recursive: true }); + +const api = makePosts({ + contentDir, + trashDir: path.join(TMP, 'trash'), + defaultAuthor: '测试', + repoRoot: TMP, + blogBase: '', +}); + +const admin = { uid: 1, name: '测试', role: 'admin' }; +let pass = 0; +let fail = 0; + +function check(name, ok, extra = '') { + if (ok) { + pass++; + console.log(' ✓ ' + name + (extra ? ' ' + extra : '')); + } else { + fail++; + console.log(' ✗ ' + name + (extra ? ' ' + extra : '')); + } +} + +/** 读回落盘的 date 值(序列化会把带 ':' 的值加单引号,剥掉) */ +function dateOf(dirName) { + const raw = fs.readFileSync(path.join(contentDir, '2026', dirName, 'index.md'), 'utf8'); + const v = ((/^date:\s*(.+)$/m.exec(raw) || [])[1] || '').trim(); + return /^'.*'$/.test(v) ? v.slice(1, -1) : v; +} + +const CN = /^2026-10-0[57]T\d{2}:\d{2}:\d{2}\+08:00$/; + +try { + // ---- ① 只给「天」---- + const a = api.createPost( + { frontMatter: { title: '同日甲', date: '2026-10-05', slug: '20261005090000' }, content: '甲' }, + admin + ); + const da = dateOf(a.dirName); + check('① 新建:date 补上了时分秒', CN.test(da), da); + + // ---- ② 同一天再发一篇(隔 1 秒,保证时刻严格更大)---- + await new Promise((r) => setTimeout(r, 1100)); + const b = api.createPost( + { frontMatter: { title: '同日乙', date: '2026-10-05', slug: '20261005103000' }, content: '乙' }, + admin + ); + const db = dateOf(b.dirName); + check('② 第二篇也有时分秒', CN.test(db), db); + check('② 两篇时间戳不再相同', da !== db, da + ' vs ' + db); + + const order = api.listPosts({ perPage: 50 }, admin).posts.map((p) => p.title); + check('② 列表里新的排在前面', order[0] === '同日乙' && order[1] === '同日甲', JSON.stringify(order)); + + // ---- ③ 时刻正好 00:00:00 的老写法 ---- + const c = api.createPost( + { + frontMatter: { title: '零点写法', date: '2026-10-06T00:00:00.000Z', slug: '20261006000000' }, + content: '丙', + }, + admin + ); + check('③ 00:00:00 也按「只到天」补时刻', /^2026-10-06T\d{2}:\d{2}:\d{2}\+08:00$/.test(dateOf(c.dirName)), dateOf(c.dirName)); + + // ---- ④ 保存时不改日子(编辑器只会回传 2026-10-05)---- + api.savePost(b.dirName, { content: '乙(改了正文)', frontMatter: { date: '2026-10-05' } }, admin); + check('④ 不改日子 → 时分秒保留', dateOf(b.dirName) === db, dateOf(b.dirName)); + + // ---- ⑤ 真的换了日子 ---- + api.savePost(b.dirName, { frontMatter: { date: '2026-10-07' } }, admin); + check('⑤ 改日子 → 新日子 + 时刻', /^2026-10-07T\d{2}:\d{2}:\d{2}\+08:00$/.test(dateOf(b.dirName)), dateOf(b.dirName)); + + // ---- ⑥ 明确给了时刻的(含手工指定的历史时间)不能被覆盖 ---- + const d = api.createPost( + { + frontMatter: { title: '指定时刻', date: '2026-10-05T08:15:30+08:00', slug: '20261005081530' }, + content: '丁', + }, + admin + ); + check('⑥ 明确给了时刻则原样保留', dateOf(d.dirName) === '2026-10-05T08:15:30+08:00', dateOf(d.dirName)); +} finally { + fs.rmSync(TMP, { recursive: true, force: true, maxRetries: 3 }); +} + +console.log('\n================================'); +console.log('通过 ' + pass + ' / 失败 ' + fail); +process.exit(fail ? 1 : 0); diff --git a/write-server/src/lib/posts.ts b/write-server/src/lib/posts.ts index abf23fe7..1e4fc44e 100644 --- a/write-server/src/lib/posts.ts +++ b/write-server/src/lib/posts.ts @@ -39,6 +39,43 @@ function parseDate(dateStr: string): Date { return isNaN(d.getTime()) ? new Date(0) : d; } +/** + * date 补时分秒 —— 防止「同一天发的两篇文章排序颠倒」。 + * + * 起因:PostEditor 的日期框是 ,只拿得到 YYYY-MM-DD,提交时又 + * 被转成 `2026-10-05T00:00:00.000Z`。于是同一天发的两篇 date 完全一样,前台 + * (Hugo 按 Date 排)和后台(listPosts 按 date 排)都只剩标题能当兜底比较 —— + * 结果 21:30 发的新文章排在了 11:34 发的旧文章**后面**(2026-10-05 实测)。 + * + * 规则:date 只到「天」(或时刻正好是 00:00:00)→ 补上北京时间此刻的时分秒, + * 写成带 +08:00 偏移的完整时间戳;已经带非零时刻的一律原样保留(不去动老文章)。 + */ +const CN_OFFSET_MS = 8 * 60 * 60 * 1000; + +/** 北京时间的 YYYY-MM-DDTHH:mm:ss(不带偏移) */ +function cnStamp(): string { + return new Date(Date.now() + CN_OFFSET_MS).toISOString().slice(0, 19); +} + +/** 'YYYY-MM-DD' / 'YYYY-MM-DDTHH:mm:ss.000Z'(时刻为 0)→ 补当前时刻;其它原样返回 */ +function withTime(dateStr: unknown): string { + const s = String(dateStr ?? "").trim(); + if (!s) return cnStamp() + "+08:00"; + const m = /^(\d{4}-\d{2}-\d{2})(?:[T ]00:00:00(?:\.000)?Z?)?$/.exec(s); + if (!m) return s; + return `${m[1]}T${cnStamp().slice(11)}+08:00`; +} + +/** 只取「哪一天」(北京时间的年月日),用来判断用户到底有没有改日子 */ +function dayKey(value: unknown): string { + const d = value instanceof Date ? value : new Date(String(value ?? "").trim()); + if (!isNaN(d.getTime())) { + return new Date(d.getTime() + CN_OFFSET_MS).toISOString().slice(0, 10); + } + const hit = /^(\d{4}-\d{2}-\d{2})/.exec(String(value ?? "")); + return hit ? hit[1] : ""; +} + function findPostDirectories(): string[] { const result: string[] = []; if (!fs.existsSync(CONTENT_DIR)) return result; @@ -176,7 +213,7 @@ export function createPost(frontMatter: PostFrontMatter, content: string): strin const fm: Record = { title: frontMatter.title, - date: frontMatter.date, + date: withTime(frontMatter.date), slug: frontMatter.slug, draft: frontMatter.draft ?? false, categories: frontMatter.categories || [], @@ -202,6 +239,16 @@ export function updatePost( const merged = { ...post.frontMatter, ...frontMatter }; + // date 保真:日期框只有「年月日」,保存时会把 date 回写成当天 00:00,抹掉原有的 + // 时分秒 —— 那样同一天的文章又会撞成一片。只要「哪一天」没变,就沿用原来那串 + // (原值可能是 Date 对象,也可能是带偏移的字符串,都别动);换了日子才补新时刻。 + if (frontMatter.date !== undefined && post.frontMatter.date !== undefined) { + merged.date = + dayKey(frontMatter.date) === dayKey(post.frontMatter.date) + ? post.frontMatter.date + : withTime(frontMatter.date); + } + // If slug changed, we need to rename the directory if (frontMatter.slug && frontMatter.slug !== post.frontMatter.slug) { const newDate = new Date(merged.date || post.frontMatter.date);