fix(排序): 同日文章 date 补全时分秒 + EdgeOne 部署重试增强

现象:小乖同一天发的第二篇排到了第一篇后面。

根因: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)。
This commit is contained in:
zqlit committed 2026-10-05 21:54:03 +08:00
1 parent 5d16c8c8d6
commit 5e9d328841
12 files changed
+234 -15

No files matched your search

+50 -1
View File
@@ -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);