feat: 完善WordPress文章分类标签联动

This commit is contained in:
sunlei 2026-05-19 22:27:28 +08:00
parent 33ec71e9a7
commit 8139279fb6
5 changed files with 58 additions and 10 deletions

10
API.md
View File

@ -415,8 +415,8 @@ Query
| pageSize | number | 否 | 每页条数,默认 `10` | | pageSize | number | 否 | 每页条数,默认 `10` |
| search | string | 否 | 关键词搜索 | | search | string | 否 | 关键词搜索 |
| status | string | 否 | 文章状态,默认 `any` | | status | string | 否 | 文章状态,默认 `any` |
| categories | string | 否 | 分类 ID多个用逗号分隔 | | categories | string | 否 | 分类 ID多个用逗号分隔,也兼容重复传参 |
| tags | string | 否 | 标签 ID多个用逗号分隔 | | tags | string | 否 | 标签 ID多个用逗号分隔,也兼容重复传参 |
新增/编辑 Body 常用字段: 新增/编辑 Body 常用字段:
@ -435,6 +435,8 @@ Query
} }
``` ```
`categories``tags` 直接对应 WordPress 文章 REST 字段,传入 ID 数组即可把文章绑定到对应分类和标签;传空数组表示清空当前文章的对应绑定。
### WordPress Tag ### WordPress Tag
| 方法 | 路径 | 说明 | | 方法 | 路径 | 说明 |
@ -445,6 +447,8 @@ Query
| POST | `/wordpress/tag/update` | 编辑标签 | | POST | `/wordpress/tag/update` | 编辑标签 |
| POST | `/wordpress/tag/remove?id=1&force=true` | 删除标签 | | POST | `/wordpress/tag/remove?id=1&force=true` | 删除标签 |
WordPress 标签 term 不支持回收站,删除时必须使用 `force=true`
新增/编辑 Body 新增/编辑 Body
```json ```json
@ -466,6 +470,8 @@ Query
| POST | `/wordpress/category/update` | 编辑分类 | | POST | `/wordpress/category/update` | 编辑分类 |
| POST | `/wordpress/category/remove?id=1&force=true` | 删除分类 | | POST | `/wordpress/category/remove?id=1&force=true` | 删除分类 |
WordPress 分类 term 不支持回收站,删除时必须使用 `force=true`;删除分类不会删除文章,文章会按 WordPress 自身规则解除或迁移分类关系。
新增/编辑 Body 新增/编辑 Body
```json ```json

View File

@ -126,6 +126,7 @@ pnpm test:e2e # e2e 测试
- Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`;登录、刷新 token、退出登录和部分示例状态测试接口通过 `@Public()` 放行。 - Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`;登录、刷新 token、退出登录和部分示例状态测试接口通过 `@Public()` 放行。
- WordPress 管理接口同样先走本系统 `JwtAuthGuard`,再透传客户端 WordPress 登录态访问 WordPress REST API当前 WordPress 只有单管理员账号且不开放注册,账号配置放在 env 中,但不作为 BasicAuth 发送。 - WordPress 管理接口同样先走本系统 `JwtAuthGuard`,再透传客户端 WordPress 登录态访问 WordPress REST API当前 WordPress 只有单管理员账号且不开放注册,账号配置放在 env 中,但不作为 BasicAuth 发送。
- Admin 前端只调用现有 `/auth/login`;后端会在登录流程里自动尝试登录 WordPress把 WordPress cookie 写入本系统 httpOnly cookie前端只持久化 REST nonce 和用户信息。WordPress 远程不可用时不会阻塞 Admin 主登录,后端会返回 `wordpressAuth=null` 并在菜单和按钮权限接口中过滤博客管理相关入口。 - Admin 前端只调用现有 `/auth/login`;后端会在登录流程里自动尝试登录 WordPress把 WordPress cookie 写入本系统 httpOnly cookie前端只持久化 REST nonce 和用户信息。WordPress 远程不可用时不会阻塞 Admin 主登录,后端会返回 `wordpressAuth=null` 并在菜单和按钮权限接口中过滤博客管理相关入口。
- WordPress 文章的 `categories``tags` 按原生 REST API 语义透传 ID 数组;分类和标签 term 支持新增、编辑、强制删除,删除 term 不会删除文章。
- WordPress 客户端登录态优先通过 `X-WordPress-Authorization` 透传,也支持 `X-WP-Nonce` 加 WordPress 登录 cookie 的 REST cookie 认证。 - WordPress 客户端登录态优先通过 `X-WordPress-Authorization` 透传,也支持 `X-WP-Nonce` 加 WordPress 登录 cookie 的 REST cookie 认证。
- 如果 WordPress 服务器未开启 rewrite 导致 `/wp-json/*` 返回 404后端会自动回退到 `?rest_route=/...` 形式继续访问 REST API。 - 如果 WordPress 服务器未开启 rewrite 导致 `/wp-json/*` 返回 404后端会自动回退到 `?rest_route=/...` 形式继续访问 REST API。
- `kt-template-admin` 登录会写入 access token 与刷新 token cookie`kt-template-online-web` 和 `kt-template-online-playground` 可在回跳后通过刷新 token 重新持久化登录态。 - `kt-template-admin` 登录会写入 access token 与刷新 token cookie`kt-template-online-web` 和 `kt-template-online-playground` 可在回跳后通过刷新 token 重新持久化登录态。

View File

@ -137,6 +137,10 @@ VALUES
(2041700000000120201, 2041700000000100202, 'SystemDeptCreate', NULL, NULL, NULL, 'System:Dept:Create', 'button', '{"title":"common.create"}', 1, 0), (2041700000000120201, 2041700000000100202, 'SystemDeptCreate', NULL, NULL, NULL, 'System:Dept:Create', 'button', '{"title":"common.create"}', 1, 0),
(2041700000000120202, 2041700000000100202, 'SystemDeptEdit', NULL, NULL, NULL, 'System:Dept:Edit', 'button', '{"title":"common.edit"}', 1, 0), (2041700000000120202, 2041700000000100202, 'SystemDeptEdit', NULL, NULL, NULL, 'System:Dept:Edit', 'button', '{"title":"common.edit"}', 1, 0),
(2041700000000120203, 2041700000000100202, 'SystemDeptDelete', NULL, NULL, NULL, 'System:Dept:Delete', 'button', '{"title":"common.delete"}', 1, 0), (2041700000000120203, 2041700000000100202, 'SystemDeptDelete', NULL, NULL, NULL, 'System:Dept:Delete', 'button', '{"title":"common.delete"}', 1, 0),
(2041700000000100203, 2041700000000100002, 'SystemKtTableDemo', '/system/ktTableDemo', '/system/ktTableDemo/list', NULL, 'System:KtTableDemo:List', 'menu', '{"icon":"lucide:table-2","title":"system.ktTableDemo.title"}', 1, 3),
(2041700000000120204, 2041700000000100203, 'SystemKtTableDemoCreate', NULL, NULL, NULL, 'System:KtTableDemo:Create', 'button', '{"title":"common.create"}', 1, 0),
(2041700000000120205, 2041700000000100203, 'SystemKtTableDemoEdit', NULL, NULL, NULL, 'System:KtTableDemo:Edit', 'button', '{"title":"common.edit"}', 1, 0),
(2041700000000120206, 2041700000000100203, 'SystemKtTableDemoDelete', NULL, NULL, NULL, 'System:KtTableDemo:Delete', 'button', '{"title":"common.delete"}', 1, 0),
(2041700000000100300, 0, 'Blog', '/blog', NULL, '/blog/article', NULL, 'catalog', '{"icon":"lucide:newspaper","order":100,"title":"博客管理"}', 1, 100), (2041700000000100300, 0, 'Blog', '/blog', NULL, '/blog/article', NULL, 'catalog', '{"icon":"lucide:newspaper","order":100,"title":"博客管理"}', 1, 100),
(2041700000000100301, 2041700000000100300, 'BlogArticle', '/blog/article', '/blog/article/list', NULL, 'Blog:Article:List', 'menu', '{"icon":"lucide:file-text","title":"文章管理"}', 1, 0), (2041700000000100301, 2041700000000100300, 'BlogArticle', '/blog/article', '/blog/article/list', NULL, 'Blog:Article:List', 'menu', '{"icon":"lucide:file-text","title":"文章管理"}', 1, 0),
(2041700000000120301, 2041700000000100301, 'BlogArticleCreate', NULL, NULL, NULL, 'Blog:Article:Create', 'button', '{"title":"common.create"}', 1, 0), (2041700000000120301, 2041700000000100301, 'BlogArticleCreate', NULL, NULL, NULL, 'Blog:Article:Create', 'button', '{"title":"common.create"}', 1, 0),

View File

@ -39,14 +39,14 @@ export class WordpressArticleListQueryDto extends WordpressPagedQueryDto {
status?: string; status?: string;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: '分类 ID多个使用逗号分隔', description: '分类 ID多个使用逗号分隔或重复传参',
}) })
categories?: string; categories?: string | string[];
@ApiPropertyOptional({ @ApiPropertyOptional({
description: '标签 ID多个使用逗号分隔', description: '标签 ID多个使用逗号分隔或重复传参',
}) })
tags?: string; tags?: string | string[];
@ApiPropertyOptional({ @ApiPropertyOptional({
description: '作者 ID', description: '作者 ID',

View File

@ -213,11 +213,11 @@ export class WordpressService {
query: { query: {
...this.getPageQuery(query), ...this.getPageQuery(query),
author: query.author, author: query.author,
categories: query.categories, categories: this.normalizeIdQuery(query.categories),
context: 'edit', context: 'edit',
search: query.search, search: query.search,
status: query.status || 'any', status: query.status || 'any',
tags: query.tags, tags: this.normalizeIdQuery(query.tags),
}, },
}); });
@ -429,14 +429,25 @@ export class WordpressService {
const urls = this.getRequestUrls(path, options.query); const urls = this.getRequestUrls(path, options.query);
for (let index = 0; index < urls.length; index += 1) { for (let index = 0; index < urls.length; index += 1) {
const response = await fetch(urls[index], { let response = await fetch(urls[index], {
body: options.body ? JSON.stringify(options.body) : undefined, body: options.body ? JSON.stringify(options.body) : undefined,
headers: this.getHeaders(options.auth, !!options.body), headers: this.getHeaders(options.auth, !!options.body),
method: options.method || 'GET', method: options.method || 'GET',
redirect: 'follow', redirect: 'follow',
signal: controller.signal, signal: controller.signal,
}); });
const data = await this.parseResponse(response); let data = await this.parseResponse(response);
// 部分 WordPress 网关会拦截 DELETEREST API 官方支持用 _method=DELETE 通过 POST 兜底。
if (!response.ok && response.status === 405 && options.method === 'DELETE') {
response = await fetch(this.getMethodOverrideUrl(urls[index], 'DELETE'), {
headers: this.getHeaders(options.auth, false),
method: 'POST',
redirect: 'follow',
signal: controller.signal,
});
data = await this.parseResponse(response);
}
// 兼容未开启 Apache rewrite 的 WordPress/wp-json 404 时自动回退到 ?rest_route=。 // 兼容未开启 Apache rewrite 的 WordPress/wp-json 404 时自动回退到 ?rest_route=。
if ( if (
@ -676,6 +687,14 @@ export class WordpressService {
return url.toString(); return url.toString();
} }
private getMethodOverrideUrl(url: string, method: 'DELETE') {
const overrideUrl = new URL(url);
overrideUrl.searchParams.set('_method', method);
return overrideUrl.toString();
}
private getTimeout() { private getTimeout() {
return Number(this.configService.get('WORDPRESS_TIMEOUT_MS') || 15000); return Number(this.configService.get('WORDPRESS_TIMEOUT_MS') || 15000);
} }
@ -747,6 +766,24 @@ export class WordpressService {
.filter((item) => !Number.isNaN(item)); .filter((item) => !Number.isNaN(item));
} }
private normalizeIdQuery(value?: string | string[]) {
if (Array.isArray(value)) {
return value
.flatMap((item) => item.split(','))
.map((item) => item.trim())
.filter(Boolean)
.join(',');
}
if (typeof value !== 'string') return value;
return value
.split(',')
.map((item) => item.trim())
.filter(Boolean)
.join(',');
}
private pickDefined(payload: Record<string, unknown>) { private pickDefined(payload: Record<string, unknown>) {
return Object.entries(payload).reduce<Record<string, unknown>>( return Object.entries(payload).reduce<Record<string, unknown>>(
(acc, [key, value]) => { (acc, [key, value]) => {