mirror of
https://github.com/KwiTsukasa/kt-template-online-api.git
synced 2026-05-27 15:44:54 +08:00
feat: 完善WordPress文章分类标签联动
This commit is contained in:
parent
33ec71e9a7
commit
8139279fb6
10
API.md
10
API.md
@ -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
|
||||||
|
|||||||
@ -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 重新持久化登录态。
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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 网关会拦截 DELETE;REST 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]) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user