Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
Q
qiyouxue
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
qiyouxue
Commits
a91e4cea
Commit
a91e4cea
authored
Aug 14, 2026
by
tao355667
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor: decouple CMS content from Git
parent
6dc1c171
Pipeline
#468
passed with stage
in 25 seconds
Changes
13
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
24 additions
and
204 deletions
+24
-204
.gitignore
.gitignore
+1
-0
CMS_README.md
CMS_README.md
+5
-3
README.md
README.md
+1
-1
content.config.ts
src/content.config.ts
+3
-27
how-to-read-learning-profile.md
src/content/articles/how-to-read-learning-profile.md
+0
-22
how-to-review-mistakes.md
src/content/articles/how-to-review-mistakes.md
+0
-16
online-one-to-one-fit.md
src/content/articles/online-one-to-one-fit.md
+0
-22
categories.json
src/content/categories.json
+0
-6
how-to-read-learning-profile.md
src/content/published/how-to-read-learning-profile.md
+0
-24
how-to-review-mistakes.md
src/content/published/how-to-review-mistakes.md
+0
-32
online-one-to-one-fit.md
src/content/published/online-one-to-one-fit.md
+0
-26
article-store.ts
src/lib/article-store.ts
+5
-25
article-ssr.test.ts
tests/article-ssr.test.ts
+9
-0
No files found.
.gitignore
View file @
a91e4cea
...
...
@@ -6,5 +6,6 @@ dist/
.env
!.env.example
public/uploads/
src/content/
.DS_Store
.firecrawl/
CMS_README.md
View file @
a91e4cea
...
...
@@ -40,15 +40,17 @@ CMS_CONTENT_LOCK_STALE_MS=900000
## 内容工作流
-
工作稿:
`src/content/articles/`
-
已发布:
`src/content/published/`
-
工作稿:
`${CMS_DATA_DIR}/articles/`
-
已发布:
`${CMS_DATA_DIR}/published/`
-
分类数据:
`${CMS_DATA_DIR}/categories.json`
-
上传文件:
`${CMS_DATA_DIR}/uploads/`
-
默认分类:学情指南、学习方法、家长课堂、服务说明
-
支持新建、编辑、发布、下架、删除与未发布修改回退
-
支持分类新增、重命名、删除与文章迁移
-
支持 Markdown、Word
`.docx`
导入和内嵌图片提取
-
支持图片上传、Markdown 预览和后台密码修改
生产环境配置
`CMS_DATA_DIR`
后,内容持久化到仓库外,不随单目录部署被覆盖。发布、下架、删除与分类变更在
`CMS_CONTENT_LOCK`
临界区内通过同目录临时文件和原子 rename 写入;已有写操作进行中时接口立即返回 HTTP 409,不等待整站构建。锁文件记录 PID、创建时间与操作名称,支持异常进程和超时锁恢复。
文章内容不纳入 Git 版本管理。本地默认保存在
`.runtime/cms-data/`
;
生产环境配置
`CMS_DATA_DIR`
后,内容持久化到仓库外,不随单目录部署被覆盖。发布、下架、删除与分类变更在
`CMS_CONTENT_LOCK`
临界区内通过同目录临时文件和原子 rename 写入;已有写操作进行中时接口立即返回 HTTP 409,不等待整站构建。锁文件记录 PID、创建时间与操作名称,支持异常进程和超时锁恢复。
文章公开入口采用 SSR:下一次请求会读取已发布目录并立即反映新内容。
`llms.txt`
、
`llms-full.txt`
和
`/article-sitemap.xml`
同样动态生成并支持 ETag。进程内缓存使用文件的
`mtimeMs`
与大小校验,多进程也能发现文件替换或删除。
...
...
README.md
View file @
a91e4cea
...
...
@@ -34,7 +34,7 @@ npm start
## 内容管理
学习资讯使用 Markdown 存储,并由同端口
`/admin/`
后台管理。
工作稿位于
`src/content/articles/`
,线上稿位于
`src/content/published/`
;生产环境通过
`CMS_DATA_DIR`
将内容持久化到仓库外
。后台支持草稿、发布、下架、删除、分类管理、Markdown/Word 导入、图片上传、预览、修改密码与 Bearer API。
学习资讯使用 Markdown 存储,并由同端口
`/admin/`
后台管理。
文章、分类和上传文件统一保存在
`CMS_DATA_DIR`
,默认使用被 Git 忽略的
`.runtime/cms-data/`
;生产环境使用
`/root/qiyouxue/shared/cms-data`
,内容与代码版本完全独立
。后台支持草稿、发布、下架、删除、分类管理、Markdown/Word 导入、图片上传、预览、修改密码与 Bearer API。
完整说明见
[
CMS_README.md
](
./CMS_README.md
)
。
...
...
src/content.config.ts
View file @
a91e4cea
import
{
defineCollection
,
z
}
from
"astro:content"
;
import
{
glob
}
from
"astro/loaders"
;
const
articleSchema
=
z
.
object
({
title
:
z
.
string
(),
slug
:
z
.
string
(),
date
:
z
.
coerce
.
date
(),
updated
:
z
.
coerce
.
date
(),
category
:
z
.
string
(),
author
:
z
.
string
(),
excerpt
:
z
.
string
(),
readingMinutes
:
z
.
number
().
int
().
positive
().
optional
(),
featured
:
z
.
boolean
().
optional
(),
status
:
z
.
enum
([
"draft"
,
"published"
]),
});
const
articles
=
defineCollection
({
loader
:
glob
({
pattern
:
"**/*.md"
,
base
:
"./src/content/articles"
}),
schema
:
articleSchema
,
});
const
published
=
defineCollection
({
loader
:
glob
({
pattern
:
"**/*.md"
,
base
:
"./src/content/published"
}),
schema
:
articleSchema
,
});
export
const
collections
=
{
articles
,
published
};
// CMS content is loaded from CMS_DATA_DIR at request time. Keep Astro's build-time
// content collections empty so local legacy files under src/content are not scanned.
export
const
collections
=
{};
src/content/articles/how-to-read-learning-profile.md
deleted
100644 → 0
View file @
6dc1c171
---
title
:
"
学情分析在分析什么:从一次错题看见真正的问题"
slug
:
"
how-to-read-learning-profile"
date
:
2026-08-08
updated
:
2026-08-12
category
:
"
学情指南"
author
:
"
启优学教研中心"
excerpt
:
"
同一道错题背后,可能是知识缺口、方法不熟,也可能是审题和学习习惯问题。有效的学情分析需要先把原因分清楚。"
status
:
"
published"
---
# 学情分析在分析什么:从一次错题看见真正的问题
孩子说“这道题不会”,并不等于再讲一遍答案就能解决。一次错误可能来自概念没有理解、题型方法不熟、条件遗漏,或计算和表达习惯不稳定。
## 先看错误发生在哪一步
有效的学情沟通会结合近期试卷、错题、教材进度和学生自己的解题过程,判断问题更接近知识、方法还是习惯。只有原因清楚,近期任务才不会变成泛泛刷题。
## 把宽泛目标拆小
“提高数学”很难直接执行,“本周检查一次函数取值范围的分类讨论”则更具体。阶段方案需要说明先做什么、如何检查,以及什么时候根据表现调整。
src/content/articles/how-to-review-mistakes.md
deleted
100644 → 0
View file @
6dc1c171
---
title
:
"
错题复盘不只是重做:把失分原因分成四类"
slug
:
"
how-to-review-mistakes"
date
:
2026-08-10
updated
:
2026-08-12
category
:
"
学习方法"
author
:
"
启优学教研中心"
excerpt
:
"
知识概念、解题方法、审题表达和计算习惯需要不同的改进方式。错题复盘的第一步,是不再把所有错误归为粗心。"
status
:
"
published"
---
# 错题复盘不只是重做:把失分原因分成四类
把错题答案抄一遍,并不一定能减少下一次错误。更有效的方式,是先判断错误发生的类型:知识概念、解题方法、审题表达和计算习惯,需要不同的改进方式。
复盘的目标不是收藏更多错题,而是发现重复出现的原因,并安排下一次可以检查的任务。
src/content/articles/online-one-to-one-fit.md
deleted
100644 → 0
View file @
6dc1c171
---
title
:
"
在线一对一适合哪些学生?先看三个判断维度"
slug
:
"
online-one-to-one-fit"
date
:
2026-08-09
updated
:
2026-08-12
category
:
"
家长课堂"
author
:
"
启优学教研中心"
excerpt
:
"
一对一更适合问题较明确、需要个别讲解节奏或持续反馈的学生,是否适合还要结合目标、配合情况与学习周期判断。"
status
:
"
published"
---
# 在线一对一适合哪些学生?先看三个判断维度
在线一对一并不是所有学习问题的统一答案。判断是否适合,可以先看问题是否明确、学生是否需要个别节奏,以及家庭是否能为持续练习与反馈留出空间。
## 学习问题是否可以被具体描述
单科短板、某一章节基础断层、校内进度衔接困难,通常比“想全面提高”更容易形成明确方案。
## 是否需要更多互动检查
如果学生在大班课堂中不容易表达思路,或需要教师根据作答即时调整讲解,一对一互动会提供更集中的反馈机会。
src/content/categories.json
deleted
100644 → 0
View file @
6dc1c171
[
"学情指南"
,
"学习方法"
,
"家长课堂"
,
"服务说明"
]
src/content/published/how-to-read-learning-profile.md
deleted
100644 → 0
View file @
6dc1c171
---
title
:
"
学情分析在分析什么:从一次错题看见真正的问题"
slug
:
"
how-to-read-learning-profile"
date
:
2026-08-08
updated
:
2026-08-12
category
:
"
学情指南"
author
:
"
启优学教研中心"
excerpt
:
"
同一道错题背后,可能是知识缺口、方法不熟,也可能是审题和学习习惯问题。有效的学情分析需要先把原因分清楚。"
status
:
"
published"
---
# 学情分析在分析什么:从一次错题看见真正的问题
孩子说“这道题不会”,并不等于再讲一遍答案就能解决。一次错误可能来自概念没有理解、题型方法不熟、条件遗漏,或计算和表达习惯不稳定。
## 先看错误发生在哪一步
有效的学情沟通会结合近期试卷、错题、教材进度和学生自己的解题过程,判断问题更接近知识、方法还是习惯。只有原因清楚,近期任务才不会变成泛泛刷题。
## 把宽泛目标拆小
“提高数学”很难直接执行,“本周检查一次函数取值范围的分类讨论”则更具体。阶段方案需要说明先做什么、如何检查,以及什么时候根据表现调整。
学习结果受基础、频率、练习与学习周期等多种因素影响。学情分析的价值,是让下一步安排更有依据,而不是给出统一答案。
src/content/published/how-to-review-mistakes.md
deleted
100644 → 0
View file @
6dc1c171
---
title
:
"
错题复盘不只是重做:把失分原因分成四类"
slug
:
"
how-to-review-mistakes"
date
:
2026-08-10
updated
:
2026-08-12
category
:
"
学习方法"
author
:
"
启优学教研中心"
excerpt
:
"
知识概念、解题方法、审题表达和计算习惯需要不同的改进方式。错题复盘的第一步,是不再把所有错误归为粗心。"
status
:
"
published"
---
# 错题复盘不只是重做:把失分原因分成四类
把错题答案抄一遍,并不一定能减少下一次错误。更有效的方式,是先判断错误发生的类型。
## 知识概念
定义、公式或概念边界没有理解,需要回到基础知识并用相近问题检查。
## 解题方法
知道知识点,却不知道从什么条件切入,需要梳理题型结构和步骤。
## 审题与表达
遗漏条件、答非所问或步骤不完整,需要把读题标记与答题规范纳入练习。
## 计算与学习习惯
符号、单位、抄写和检查中的重复错误,需要用固定的自查流程逐步减少。
复盘的目标不是收藏更多错题,而是发现重复出现的原因,并安排下一次可以检查的任务。
src/content/published/online-one-to-one-fit.md
deleted
100644 → 0
View file @
6dc1c171
---
title
:
"
在线一对一适合哪些学生?先看三个判断维度"
slug
:
"
online-one-to-one-fit"
date
:
2026-08-09
updated
:
2026-08-12
category
:
"
家长课堂"
author
:
"
启优学教研中心"
excerpt
:
"
一对一更适合问题较明确、需要个别讲解节奏或持续反馈的学生,是否适合还要结合目标、配合情况与学习周期判断。"
status
:
"
published"
---
# 在线一对一适合哪些学生?先看三个判断维度
在线一对一并不是所有学习问题的统一答案。判断是否适合,可以先看问题是否明确、学生是否需要个别节奏,以及家庭是否能为持续练习与反馈留出空间。
## 学习问题是否可以被具体描述
单科短板、某一章节基础断层、校内进度衔接困难,通常比“想全面提高”更容易形成明确方案。
## 是否需要更多互动检查
如果学生在大班课堂中不容易表达思路,或需要教师根据作答即时调整讲解,一对一互动会提供更集中的反馈机会。
## 是否能配合阶段任务
课堂只是学习的一部分。课后巩固、错题复盘与阶段沟通同样重要。具体课程与教师安排,应在学情沟通后由双方确认。
src/lib/article-store.ts
View file @
a91e4cea
...
...
@@ -5,14 +5,11 @@ import matter from "gray-matter";
import
{
marked
}
from
"marked"
;
const
ROOT
=
process
.
cwd
();
const
DATA_ROOT
=
process
.
env
.
CMS_DATA_DIR
?
path
.
resolve
(
process
.
env
.
CMS_DATA_DIR
)
:
null
;
const
SOURCE_ARTICLES_DIR
=
path
.
join
(
ROOT
,
"src"
,
"content"
,
"articles"
);
const
SOURCE_PUBLISHED_DIR
=
path
.
join
(
ROOT
,
"src"
,
"content"
,
"published"
);
export
const
ARTICLES_DIR
=
DATA_ROOT
?
path
.
join
(
DATA_ROOT
,
"articles"
)
:
path
.
join
(
ROOT
,
"src"
,
"content"
,
"articles"
);
export
const
PUBLISHED_DIR
=
DATA_ROOT
?
path
.
join
(
DATA_ROOT
,
"published"
)
:
path
.
join
(
ROOT
,
"src"
,
"content"
,
"published"
);
export
const
CATEGORIES_FILE
=
DATA_ROOT
?
path
.
join
(
DATA_ROOT
,
"categories.json"
)
:
path
.
join
(
ROOT
,
"src"
,
"content"
,
"categories.json"
);
export
const
UPLOADS_DIR
=
DATA_ROOT
?
path
.
join
(
DATA_ROOT
,
"uploads"
)
:
path
.
join
(
ROOT
,
"public"
,
"uploads"
);
const
DATA_SEED_MARKER
=
DATA_ROOT
?
path
.
join
(
DATA_ROOT
,
".content-initialized"
)
:
null
;
const
DATA_ROOT
=
path
.
resolve
(
process
.
env
.
CMS_DATA_DIR
||
path
.
join
(
ROOT
,
".runtime"
,
"cms-data"
));
export
const
ARTICLES_DIR
=
path
.
join
(
DATA_ROOT
,
"articles"
);
export
const
PUBLISHED_DIR
=
path
.
join
(
DATA_ROOT
,
"published"
);
export
const
CATEGORIES_FILE
=
path
.
join
(
DATA_ROOT
,
"categories.json"
);
export
const
UPLOADS_DIR
=
path
.
join
(
DATA_ROOT
,
"uploads"
);
const
DEFAULT_CATEGORIES
=
[
"学情指南"
,
"学习方法"
,
"家长课堂"
,
"服务说明"
];
const
DEFAULT_AUTHOR
=
"启优学教研中心"
;
...
...
@@ -143,23 +140,6 @@ async function ensureStore(): Promise<void> {
fs
.
mkdir
(
PUBLISHED_DIR
,
{
recursive
:
true
}),
fs
.
mkdir
(
UPLOADS_DIR
,
{
recursive
:
true
}),
]);
if
(
DATA_ROOT
&&
DATA_SEED_MARKER
&&
!
await
fileExists
(
DATA_SEED_MARKER
))
{
const
seedDirectory
=
async
(
source
:
string
,
target
:
string
)
=>
{
const
seeds
=
await
fs
.
readdir
(
source
).
catch
(()
=>
[]
as
string
[]);
await
Promise
.
all
(
seeds
.
filter
((
file
)
=>
file
.
endsWith
(
".md"
)).
map
(
async
(
file
)
=>
{
await
writeAtomic
(
path
.
join
(
target
,
file
),
await
fs
.
readFile
(
path
.
join
(
source
,
file
)));
}));
};
const
[
workingFiles
,
publishedFiles
]
=
await
Promise
.
all
([
fs
.
readdir
(
ARTICLES_DIR
),
fs
.
readdir
(
PUBLISHED_DIR
)]);
const
hasExistingContent
=
[...
workingFiles
,
...
publishedFiles
].
some
((
file
)
=>
file
.
endsWith
(
".md"
));
if
(
!
hasExistingContent
)
{
await
Promise
.
all
([
seedDirectory
(
SOURCE_ARTICLES_DIR
,
ARTICLES_DIR
),
seedDirectory
(
SOURCE_PUBLISHED_DIR
,
PUBLISHED_DIR
),
]);
}
await
writeAtomic
(
DATA_SEED_MARKER
,
`
${
new
Date
().
toISOString
()}
\n`
);
}
if
(
!
await
fileExists
(
CATEGORIES_FILE
))
{
await
writeAtomic
(
CATEGORIES_FILE
,
`
${
JSON
.
stringify
(
DEFAULT_CATEGORIES
,
null
,
2
)}
\n`
);
}
...
...
tests/article-ssr.test.ts
View file @
a91e4cea
...
...
@@ -79,6 +79,15 @@ after(async () => {
});
test
(
"article lists read metadata without rendering every Markdown body"
,
async
()
=>
{
const
created
=
await
apiRequest
(
"articles"
,
"POST"
,
{
title
:
"元数据读取测试文章"
,
category
:
"学习方法"
,
body
:
"# 元数据正文
\n\n
列表读取时不应渲染这段 Markdown 正文。"
,
});
assert
.
equal
(
created
.
status
,
201
);
const
result
=
await
created
.
json
()
as
{
slug
:
string
};
const
published
=
await
apiRequest
(
`articles/
${
result
.
slug
}
/publish`
,
"POST"
);
assert
.
equal
(
published
.
status
,
200
);
articleStore
.
resetArticleStoreDiagnostics
();
const
articles
=
await
articleStore
.
getPublishedArticles
();
assert
.
ok
(
articles
.
length
>
0
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment