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:
1 parent
5d16c8c8d6
commit
5e9d328841
12 files changed
+234
-15
No files matched your search
@@ -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
|
||||
```
|
||||
|
||||
## 本地联调
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user