diff --git a/.env.example b/.env.example index 1d9a749..62bff5d 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,8 @@ WORDPRESS_BASE_URL=http://localhost WORDPRESS_ADMIN_USERNAME=admin WORDPRESS_ADMIN_PASSWORD= WORDPRESS_TIMEOUT_MS=15000 +WORDPRESS_LOGIN_TIMEOUT_MS=3000 +WORDPRESS_AVAILABILITY_TTL_MS=60000 ADMIN_TOKEN_SECRET=change-me ADMIN_COOKIE_SECURE=false diff --git a/API.md b/API.md index 326b9e3..2bf5f12 100644 --- a/API.md +++ b/API.md @@ -24,14 +24,14 @@ ## 功能模块 -| 模块 | 说明 | -| --------- | --------------------------------------------------------------------- | +| 模块 | 说明 | +| --------- | ----------------------------------------------------------------------------------------- | | Component | Admin 下受保护的组件/图表模板列表、详情、新增、编辑、逻辑删除,数据表为 `admin_component` | -| Dict | 基于新 `admin_dict` 表的数据库字典查询,以及组件一级类型到二级类型的数据库关系映射 | -| Admin | Vben Admin 真实接口,包含认证、用户、菜单、角色、部门、时区和上传适配 | -| MinIO | Bucket 检查/创建、文件上传、列表、临时访问地址、下载和删除 | -| WordPress | WordPress 文章、标签、分类管理,复用客户端 WordPress 登录态访问 REST API | -| Common | 统一响应 Swagger 注解、字典翻译注解、`POST */save` 请求体规范化拦截器 | +| Dict | 基于新 `admin_dict` 表的数据库字典查询,以及组件一级类型到二级类型的数据库关系映射 | +| Admin | Vben Admin 真实接口,包含认证、用户、菜单、角色、部门、时区和上传适配 | +| MinIO | Bucket 检查/创建、文件上传、列表、临时访问地址、下载和删除 | +| WordPress | WordPress 文章、标签、分类管理,复用客户端 WordPress 登录态访问 REST API | +| Common | 统一响应 Swagger 注解、字典翻译注解、`POST */save` 请求体规范化拦截器 | ## 通用规则 @@ -61,49 +61,53 @@ WordPress 侧只使用客户端登录态,后端不走 BasicAuth。当前 WordP 环境变量: -| 变量 | 说明 | -| --- | --- | -| `WORDPRESS_BASE_URL` | WordPress 站点根地址,例如 `http://192.168.31.224:8080` | -| `WORDPRESS_ADMIN_USERNAME` | WordPress 单管理员账号用户名 | -| `WORDPRESS_ADMIN_PASSWORD` | WordPress 单管理员账号密码,仅放真实 env,不提交到仓库 | -| `WORDPRESS_TIMEOUT_MS` | WordPress REST API 请求超时时间,默认 `15000` | +| 变量 | 说明 | +| ------------------------------- | ------------------------------------------------------------------------------------------ | +| `WORDPRESS_BASE_URL` | WordPress 站点根地址,例如 `http://192.168.31.224:8080` | +| `WORDPRESS_ADMIN_USERNAME` | WordPress 单管理员账号用户名 | +| `WORDPRESS_ADMIN_PASSWORD` | WordPress 单管理员账号密码,仅放真实 env,不提交到仓库 | +| `WORDPRESS_TIMEOUT_MS` | WordPress REST API 请求超时时间,默认 `15000` | +| `WORDPRESS_LOGIN_TIMEOUT_MS` | Admin 登录链路里 WordPress 自动认证的短超时时间,默认 `3000`,避免远程不可用阻塞主系统登录 | +| `WORDPRESS_AVAILABILITY_TTL_MS` | WordPress 可用性缓存时间,默认 `60000`;远程不可用时用于过滤博客菜单和按钮权限 | 支持的 WordPress 登录态来源: -| Header/Cookie | 说明 | -| --- | --- | -| `X-WordPress-Authorization` | 优先透传的 WordPress 授权头,例如客户端登录拿到的 `Bearer ` | -| `Authorization` | 仅当它不是本系统 Admin access token 时才会透传,避免和后台认证冲突 | -| `X-WP-Nonce` | WordPress REST cookie 认证 nonce | -| `Cookie` | 只会过滤并透传 `wordpress_*`、`wordpress_logged_in_*`、`wp-settings-*` 等 WordPress 登录相关 cookie | -| `X-WordPress-Cookie` | 显式传入 WordPress cookie,适合非浏览器客户端联调 | -| `kt_wordpress_auth` | 后端自动认证后写入的 httpOnly cookie,前端不可读取,后端会自动转成 WordPress cookie 透传 | +| Header/Cookie | 说明 | +| --------------------------- | --------------------------------------------------------------------------------------------------- | +| `X-WordPress-Authorization` | 优先透传的 WordPress 授权头,例如客户端登录拿到的 `Bearer ` | +| `Authorization` | 仅当它不是本系统 Admin access token 时才会透传,避免和后台认证冲突 | +| `X-WP-Nonce` | WordPress REST cookie 认证 nonce | +| `Cookie` | 只会过滤并透传 `wordpress_*`、`wordpress_logged_in_*`、`wp-settings-*` 等 WordPress 登录相关 cookie | +| `X-WordPress-Cookie` | 显式传入 WordPress cookie,适合非浏览器客户端联调 | +| `kt_wordpress_auth` | 后端自动认证后写入的 httpOnly cookie,前端不可读取,后端会自动转成 WordPress cookie 透传 | 如果 WordPress 所在 Apache/Nginx 未开启 rewrite,`/wp-json/*` 可能返回 404。后端会自动回退到 WordPress 原生 `?rest_route=/...` 形式,避免因为固定链接配置阻断文章、标签和分类管理接口。 +Admin 主登录不依赖 WordPress 可用性:本系统账号验证通过后会先写入 Admin token;WordPress 自动认证失败时登录仍返回成功,`wordpressAuth` 为 `null`、`wordpressAvailable=false`,并清理旧 WordPress cookie。随后 `/menu/all` 与 `/auth/codes` 会基于最近一次 WordPress 可用性状态过滤 `Blog*` 菜单和 `Blog:*` 按钮权限码,避免前端展示不可用的文章、分类、标签管理入口。 + ### 数据库字典翻译 组件数据维护在 `admin_component` 表中,字典数据维护在新的 `admin_dict` 表中。`Component.typeMsg`、`Component.componentTypeMsg` 会在 TypeORM `AfterLoad` 阶段根据字典缓存自动映射;旧 `/dict/*` 接口路径保持兼容,但仍需要登录态。 `admin_dict` 表核心字段: -| 字段 | 类型 | 说明 | -| ----------- | ------- | ------------------------------------------------------ | -| id | string | 字典数字 ID | -| dictCode | string | 字典分组,例如 `COMPONENT_TYPE`、`CHART`、`COMPONENT` | -| label | string | 展示文本 | -| value | string | 字典值 | -| childrenCode | string | 子字典分组,例如 `COMPONENT_TYPE.value=1` 指向 `CHART` | -| sort | number | 排序 | -| status | number | 启停状态,`1` 启用 | -| isDeleted | boolean | 逻辑删除标记 | +| 字段 | 类型 | 说明 | +| ------------ | ------- | ------------------------------------------------------ | +| id | string | 字典数字 ID | +| dictCode | string | 字典分组,例如 `COMPONENT_TYPE`、`CHART`、`COMPONENT` | +| label | string | 展示文本 | +| value | string | 字典值 | +| childrenCode | string | 子字典分组,例如 `COMPONENT_TYPE.value=1` 指向 `CHART` | +| sort | number | 排序 | +| status | number | 启停状态,`1` 启用 | +| isDeleted | boolean | 逻辑删除标记 | 当前数据库示例关系: | dictCode | value | label | childrenCode | -| -------------- | ----- | ----- | ----------- | -| COMPONENT_TYPE | 1 | 图表 | CHART | -| COMPONENT_TYPE | 2 | 组件 | COMPONENT | +| -------------- | ----- | ----- | ------------ | +| COMPONENT_TYPE | 1 | 图表 | CHART | +| COMPONENT_TYPE | 2 | 组件 | COMPONENT | ## 数据结构 @@ -323,35 +327,35 @@ Query: 核心接口: -| 方法 | 路径 | 说明 | -| --- | --- | --- | -| POST | `/auth/login` | 登录,返回 `accessToken` 与 `wordpressAuth`,并写入 access token、刷新 token 和 WordPress 授权 cookie | -| POST | `/auth/refresh` | 通过刷新 token cookie 刷新 accessToken,并更新 token cookie | -| POST | `/auth/logout` | 退出登录并清理 access token、刷新 token 与 WordPress 授权 cookie | -| GET | `/auth/codes` | 获取当前用户权限码 | -| GET | `/user/info` | 获取当前用户信息 | -| GET | `/menu/all` | 获取当前用户可访问菜单 | -| GET | `/system/menu/list` | 获取系统菜单树 | -| GET | `/system/menu/name-exists` | 校验菜单 name 是否重复 | -| GET | `/system/menu/path-exists` | 校验菜单 path 是否重复 | -| POST | `/system/menu` | 新增菜单 | -| PUT | `/system/menu/:id` | 更新菜单 | -| DELETE | `/system/menu/:id` | 删除菜单及子菜单 | -| GET | `/system/role/list` | 分页查询角色 | -| POST | `/system/role` | 新增角色 | -| PUT | `/system/role/:id` | 更新角色 | -| DELETE | `/system/role/:id` | 删除角色 | -| GET | `/system/dept/list` | 获取部门树 | -| POST | `/system/dept` | 新增部门 | -| PUT | `/system/dept/:id` | 更新部门 | -| DELETE | `/system/dept/:id` | 删除部门 | -| GET | `/timezone/getTimezoneOptions` | 获取时区选项 | -| GET | `/timezone/getTimezone` | 获取当前用户时区 | -| POST | `/timezone/setTimezone` | 设置当前用户时区 | -| POST | `/upload` | Vben Upload 适配接口,真实上传到 MinIO 并返回 `{ url }` | -| GET | `/table/list` | Vben 示例远程表格数据 | -| GET | `/status` | Vben 状态码测试接口 | -| GET | `/demo/bigint` | Vben BigInt JSON 测试接口 | +| 方法 | 路径 | 说明 | +| ------ | ------------------------------ | ----------------------------------------------------------------------------------------------------- | +| POST | `/auth/login` | 登录,返回 `accessToken` 与 `wordpressAuth`,并写入 access token、刷新 token 和 WordPress 授权 cookie | +| POST | `/auth/refresh` | 通过刷新 token cookie 刷新 accessToken,并更新 token cookie | +| POST | `/auth/logout` | 退出登录并清理 access token、刷新 token 与 WordPress 授权 cookie | +| GET | `/auth/codes` | 获取当前用户权限码 | +| GET | `/user/info` | 获取当前用户信息 | +| GET | `/menu/all` | 获取当前用户可访问菜单 | +| GET | `/system/menu/list` | 获取系统菜单树 | +| GET | `/system/menu/name-exists` | 校验菜单 name 是否重复 | +| GET | `/system/menu/path-exists` | 校验菜单 path 是否重复 | +| POST | `/system/menu` | 新增菜单 | +| PUT | `/system/menu/:id` | 更新菜单 | +| DELETE | `/system/menu/:id` | 删除菜单及子菜单 | +| GET | `/system/role/list` | 分页查询角色 | +| POST | `/system/role` | 新增角色 | +| PUT | `/system/role/:id` | 更新角色 | +| DELETE | `/system/role/:id` | 删除角色 | +| GET | `/system/dept/list` | 获取部门树 | +| POST | `/system/dept` | 新增部门 | +| PUT | `/system/dept/:id` | 更新部门 | +| DELETE | `/system/dept/:id` | 删除部门 | +| GET | `/timezone/getTimezoneOptions` | 获取时区选项 | +| GET | `/timezone/getTimezone` | 获取当前用户时区 | +| POST | `/timezone/setTimezone` | 设置当前用户时区 | +| POST | `/upload` | Vben Upload 适配接口,真实上传到 MinIO 并返回 `{ url }` | +| GET | `/table/list` | Vben 示例远程表格数据 | +| GET | `/status` | Vben 状态码测试接口 | +| GET | `/demo/bigint` | Vben BigInt JSON 测试接口 | 初始化 SQL: @@ -395,24 +399,24 @@ Query: ### WordPress Article -| 方法 | 路径 | 说明 | -| --- | --- | --- | -| GET | `/wordpress/article/list` | 获取文章分页列表 | -| GET | `/wordpress/article/detail?id=1` | 获取文章详情 | -| POST | `/wordpress/article/save` | 新增文章 | -| POST | `/wordpress/article/update` | 编辑文章 | -| POST | `/wordpress/article/remove?id=1&force=true` | 删除文章 | +| 方法 | 路径 | 说明 | +| ---- | ------------------------------------------- | ---------------- | +| GET | `/wordpress/article/list` | 获取文章分页列表 | +| GET | `/wordpress/article/detail?id=1` | 获取文章详情 | +| POST | `/wordpress/article/save` | 新增文章 | +| POST | `/wordpress/article/update` | 编辑文章 | +| POST | `/wordpress/article/remove?id=1&force=true` | 删除文章 | 列表 Query: -| 参数 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| pageNo | number | 否 | 页码,默认 `1` | -| pageSize | number | 否 | 每页条数,默认 `10` | -| search | string | 否 | 关键词搜索 | -| status | string | 否 | 文章状态,默认 `any` | -| categories | string | 否 | 分类 ID,多个用逗号分隔 | -| tags | string | 否 | 标签 ID,多个用逗号分隔 | +| 参数 | 类型 | 必填 | 说明 | +| ---------- | ------ | ---- | ----------------------- | +| pageNo | number | 否 | 页码,默认 `1` | +| pageSize | number | 否 | 每页条数,默认 `10` | +| search | string | 否 | 关键词搜索 | +| status | string | 否 | 文章状态,默认 `any` | +| categories | string | 否 | 分类 ID,多个用逗号分隔 | +| tags | string | 否 | 标签 ID,多个用逗号分隔 | 新增/编辑 Body 常用字段: @@ -433,13 +437,13 @@ Query: ### WordPress Tag -| 方法 | 路径 | 说明 | -| --- | --- | --- | -| GET | `/wordpress/tag/list` | 获取标签分页列表 | -| GET | `/wordpress/tag/detail?id=1` | 获取标签详情 | -| POST | `/wordpress/tag/save` | 新增标签 | -| POST | `/wordpress/tag/update` | 编辑标签 | -| POST | `/wordpress/tag/remove?id=1&force=true` | 删除标签 | +| 方法 | 路径 | 说明 | +| ---- | --------------------------------------- | ---------------- | +| GET | `/wordpress/tag/list` | 获取标签分页列表 | +| GET | `/wordpress/tag/detail?id=1` | 获取标签详情 | +| POST | `/wordpress/tag/save` | 新增标签 | +| POST | `/wordpress/tag/update` | 编辑标签 | +| POST | `/wordpress/tag/remove?id=1&force=true` | 删除标签 | 新增/编辑 Body: @@ -454,13 +458,13 @@ Query: ### WordPress Category -| 方法 | 路径 | 说明 | -| --- | --- | --- | -| GET | `/wordpress/category/list` | 获取分类分页列表 | -| GET | `/wordpress/category/detail?id=1` | 获取分类详情 | -| POST | `/wordpress/category/save` | 新增分类 | -| POST | `/wordpress/category/update` | 编辑分类 | -| POST | `/wordpress/category/remove?id=1&force=true` | 删除分类 | +| 方法 | 路径 | 说明 | +| ---- | -------------------------------------------- | ---------------- | +| GET | `/wordpress/category/list` | 获取分类分页列表 | +| GET | `/wordpress/category/detail?id=1` | 获取分类详情 | +| POST | `/wordpress/category/save` | 新增分类 | +| POST | `/wordpress/category/update` | 编辑分类 | +| POST | `/wordpress/category/remove?id=1&force=true` | 删除分类 | 新增/编辑 Body: diff --git a/README.md b/README.md index 9c171b2..9cd9de8 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ ## 功能模块 -| 模块 | 说明 | -| --- | --- | +| 模块 | 说明 | +| ----------- | ----------------------------------------------------------------------------------------- | | `component` | Admin 下受保护的组件/图表模板列表、详情、新增、编辑、逻辑删除,数据表为 `admin_component` | -| `dict` | 基于新 `admin_dict` 表的字典查询,维护组件一级类型和二级类型关系 | -| `admin` | Vben Admin 真实接口,包含登录、用户、菜单、角色、部门、时区、上传和示例表格 | -| `minio` | Bucket 检查/创建、文件上传、列表、临时访问地址、下载和删除 | -| `wordpress` | WordPress 文章、标签、分类管理接口,复用客户端 WordPress 登录态访问 REST API | -| `common` | 响应注解、字典翻译、`POST */save` 请求体规范化等通用能力 | +| `dict` | 基于新 `admin_dict` 表的字典查询,维护组件一级类型和二级类型关系 | +| `admin` | Vben Admin 真实接口,包含登录、用户、菜单、角色、部门、时区、上传和示例表格 | +| `minio` | Bucket 检查/创建、文件上传、列表、临时访问地址、下载和删除 | +| `wordpress` | WordPress 文章、标签、分类管理接口,复用客户端 WordPress 登录态访问 REST API | +| `common` | 响应注解、字典翻译、`POST */save` 请求体规范化等通用能力 | ## 目录结构 @@ -57,6 +57,8 @@ WORDPRESS_BASE_URL=http://localhost WORDPRESS_ADMIN_USERNAME=admin WORDPRESS_ADMIN_PASSWORD= WORDPRESS_TIMEOUT_MS=15000 +WORDPRESS_LOGIN_TIMEOUT_MS=3000 +WORDPRESS_AVAILABILITY_TTL_MS=60000 ADMIN_TOKEN_SECRET=change-me ADMIN_COOKIE_SECURE=false @@ -113,7 +115,7 @@ pnpm test:e2e # e2e 测试 - 如果旧版本曾写入 `admin_user.id=0`,先执行 `sql/fix-admin-user-zero-id.sql` 修复脏数据,再重启服务。 - Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`;登录、刷新 token、退出登录和部分示例状态测试接口通过 `@Public()` 放行。 - WordPress 管理接口同样先走本系统 `JwtAuthGuard`,再透传客户端 WordPress 登录态访问 WordPress REST API;当前 WordPress 只有单管理员账号且不开放注册,账号配置放在 env 中,但不作为 BasicAuth 发送。 -- Admin 前端只调用现有 `/auth/login`;后端会在登录流程里自动登录 WordPress,把 WordPress cookie 写入本系统 httpOnly cookie,前端只持久化 REST nonce 和用户信息。 +- Admin 前端只调用现有 `/auth/login`;后端会在登录流程里自动尝试登录 WordPress,把 WordPress cookie 写入本系统 httpOnly cookie,前端只持久化 REST nonce 和用户信息。WordPress 远程不可用时不会阻塞 Admin 主登录,后端会返回 `wordpressAuth=null` 并在菜单和按钮权限接口中过滤博客管理相关入口。 - WordPress 客户端登录态优先通过 `X-WordPress-Authorization` 透传,也支持 `X-WP-Nonce` 加 WordPress 登录 cookie 的 REST cookie 认证。 - 如果 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 重新持久化登录态。 diff --git a/sql/vben-admin-init.sql b/sql/vben-admin-init.sql index 9bdd2ac..442d0da 100644 --- a/sql/vben-admin-init.sql +++ b/sql/vben-admin-init.sql @@ -137,6 +137,19 @@ VALUES (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), (2041700000000120203, 2041700000000100202, 'SystemDeptDelete', NULL, NULL, NULL, 'System:Dept:Delete', 'button', '{"title":"common.delete"}', 1, 0), + (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), + (2041700000000120301, 2041700000000100301, 'BlogArticleCreate', NULL, NULL, NULL, 'Blog:Article:Create', 'button', '{"title":"common.create"}', 1, 0), + (2041700000000120302, 2041700000000100301, 'BlogArticleEdit', NULL, NULL, NULL, 'Blog:Article:Edit', 'button', '{"title":"common.edit"}', 1, 0), + (2041700000000120303, 2041700000000100301, 'BlogArticleDelete', NULL, NULL, NULL, 'Blog:Article:Delete', 'button', '{"title":"common.delete"}', 1, 0), + (2041700000000100302, 2041700000000100300, 'BlogCategory', '/blog/category', '/blog/category/list', NULL, 'Blog:Category:List', 'menu', '{"icon":"lucide:folder-tree","title":"分类管理"}', 1, 1), + (2041700000000120311, 2041700000000100302, 'BlogCategoryCreate', NULL, NULL, NULL, 'Blog:Category:Create', 'button', '{"title":"common.create"}', 1, 0), + (2041700000000120312, 2041700000000100302, 'BlogCategoryEdit', NULL, NULL, NULL, 'Blog:Category:Edit', 'button', '{"title":"common.edit"}', 1, 0), + (2041700000000120313, 2041700000000100302, 'BlogCategoryDelete', NULL, NULL, NULL, 'Blog:Category:Delete', 'button', '{"title":"common.delete"}', 1, 0), + (2041700000000100303, 2041700000000100300, 'BlogTag', '/blog/tag', '/blog/tag/list', NULL, 'Blog:Tag:List', 'menu', '{"icon":"lucide:tags","title":"标签管理"}', 1, 2), + (2041700000000120321, 2041700000000100303, 'BlogTagCreate', NULL, NULL, NULL, 'Blog:Tag:Create', 'button', '{"title":"common.create"}', 1, 0), + (2041700000000120322, 2041700000000100303, 'BlogTagEdit', NULL, NULL, NULL, 'Blog:Tag:Edit', 'button', '{"title":"common.edit"}', 1, 0), + (2041700000000120323, 2041700000000100303, 'BlogTagDelete', NULL, NULL, NULL, 'Blog:Tag:Delete', 'button', '{"title":"common.delete"}', 1, 0), (2041700000000100009, 0, 'Project', '/vben-admin', NULL, NULL, NULL, 'catalog', '{"badgeType":"dot","icon":"carbon:data-center","order":9998,"title":"demos.vben.title"}', 1, 9998), (2041700000000100901, 2041700000000100009, 'VbenDocument', '/vben-admin/document', 'IFrameView', NULL, NULL, 'embedded', '{"icon":"carbon:book","iframeSrc":"https://doc.vben.pro","title":"demos.vben.document"}', 1, 0), (2041700000000100902, 2041700000000100009, 'VbenGithub', '/vben-admin/github', 'IFrameView', NULL, NULL, 'link', '{"icon":"carbon:logo-github","link":"https://github.com/vbenjs/vue-vben-admin","title":"Github"}', 1, 0), diff --git a/src/admin/auth/admin-auth.controller.ts b/src/admin/auth/admin-auth.controller.ts index 1c86469..d57192a 100644 --- a/src/admin/auth/admin-auth.controller.ts +++ b/src/admin/auth/admin-auth.controller.ts @@ -39,18 +39,26 @@ export class AdminAuthController { body.password, ); const wordpressLogin = - await this.wordpressService.loginWithConfiguredAdmin(); + await this.wordpressService.tryLoginWithConfiguredAdmin(); this.authService.setAccessTokenCookie(res, accessToken); this.authService.setRefreshTokenCookie(res, refreshToken); - this.wordpressService.setAuthCookie(res, wordpressLogin.cookie); + if (wordpressLogin.available) { + this.wordpressService.setAuthCookie(res, wordpressLogin.result.cookie); + } else { + this.wordpressService.clearAuthCookie(res); + } return vbenSuccess({ ...this.userService.serializeUser(user), accessToken, - wordpressAuth: { - ...wordpressLogin.auth, - user: wordpressLogin.user, - }, + wordpressAuth: wordpressLogin.available + ? { + ...wordpressLogin.result.auth, + user: wordpressLogin.result.user, + } + : null, + wordpressAvailable: wordpressLogin.available, + wordpressError: wordpressLogin.error, }); } diff --git a/src/admin/menu/admin-menu.service.ts b/src/admin/menu/admin-menu.service.ts index 9cd3993..9495d38 100644 --- a/src/admin/menu/admin-menu.service.ts +++ b/src/admin/menu/admin-menu.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { toTree } from '@/common'; +import { WordpressService } from '@/wordpress/wordpress.service'; import { AdminUser } from '../user/admin-user.entity'; import { AdminMenu, AdminMenuMeta } from './admin-menu.entity'; @@ -15,17 +16,20 @@ export class AdminMenuService { constructor( @InjectRepository(AdminMenu) private readonly menuRepository: Repository, + private readonly wordpressService: WordpressService, ) {} async getAccessCodes(user: AdminUser) { - const menus = await this.getAllowedMenus(user); - return menus - .map((menu) => menu.authCode) - .filter((authCode) => !!authCode); + const menus = this.filterUnavailableFeatureMenus( + await this.getAllowedMenus(user), + ); + return menus.map((menu) => menu.authCode).filter((authCode) => !!authCode); } async getRouteMenus(user: AdminUser) { - const menus = await this.getAllowedMenus(user); + const menus = this.filterUnavailableFeatureMenus( + await this.getAllowedMenus(user), + ); return this.buildMenuTree(menus.filter((menu) => menu.type !== 'button')); } @@ -114,6 +118,22 @@ export class AdminMenuService { return [...menuMap.values()]; } + private filterUnavailableFeatureMenus(menus: AdminMenu[]) { + if (!menus.some((menu) => this.isWordpressMenu(menu))) return menus; + if (this.wordpressService.isAdminIntegrationAvailable()) return menus; + + // WordPress 是外部增强能力,远程不可用时不影响 Admin 主系统登录和系统管理菜单。 + return menus.filter((menu) => !this.isWordpressMenu(menu)); + } + + private isWordpressMenu(menu: AdminMenu) { + return ( + menu.name?.startsWith('Blog') || + menu.path?.startsWith('/blog') || + menu.authCode?.startsWith('Blog:') + ); + } + private normalizeMenuInput( data: MenuInput, includeEmptyMeta: boolean, diff --git a/src/wordpress/wordpress-article.controller.ts b/src/wordpress/wordpress-article.controller.ts index 237a317..e9a8e2a 100644 --- a/src/wordpress/wordpress-article.controller.ts +++ b/src/wordpress/wordpress-article.controller.ts @@ -13,7 +13,7 @@ import { import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import type { Request } from 'express'; import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard'; -import { ToolsService } from '@/common'; +import { vbenSuccess } from '@/common'; import { WordpressArticleBodyDto, WordpressArticleListQueryDto, @@ -35,10 +35,7 @@ import { WordpressService } from './wordpress.service'; @Controller('wordpress/article') @UseGuards(JwtAuthGuard) export class WordpressArticleController { - constructor( - private readonly toolsService: ToolsService, - private readonly wordpressService: WordpressService, - ) {} + constructor(private readonly wordpressService: WordpressService) {} @Get('list') @ApiOperation({ summary: '获取 WordPress 文章分页列表' }) @@ -50,7 +47,7 @@ export class WordpressArticleController { const auth = this.wordpressService.getAuthContext(req); const list = await this.wordpressService.articleList(query, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list)); + return res.send(vbenSuccess(list)); } @Get('detail') @@ -60,7 +57,7 @@ export class WordpressArticleController { const auth = this.wordpressService.getAuthContext(req); const detail = await this.wordpressService.articleDetail(id, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', detail)); + return res.send(vbenSuccess(detail)); } @Post('save') @@ -74,7 +71,7 @@ export class WordpressArticleController { const auth = this.wordpressService.getAuthContext(req); const result = await this.wordpressService.articleSave(body, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result)); + return res.send(vbenSuccess(result)); } @Post('update') @@ -88,7 +85,7 @@ export class WordpressArticleController { const auth = this.wordpressService.getAuthContext(req); const result = await this.wordpressService.articleUpdate(body, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result)); + return res.send(vbenSuccess(result)); } @Post('remove') @@ -109,6 +106,6 @@ export class WordpressArticleController { auth, ); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result)); + return res.send(vbenSuccess(result)); } } diff --git a/src/wordpress/wordpress-auth.controller.ts b/src/wordpress/wordpress-auth.controller.ts index f4aa2bc..81fd4b5 100644 --- a/src/wordpress/wordpress-auth.controller.ts +++ b/src/wordpress/wordpress-auth.controller.ts @@ -1,7 +1,6 @@ import { Controller, Get, - HttpStatus, Post, Req, Res, @@ -10,7 +9,7 @@ import { import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger'; import type { Request, Response } from 'express'; import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard'; -import { Public, ToolsService } from '@/common'; +import { Public, vbenSuccess } from '@/common'; import { WordpressService } from './wordpress.service'; @ApiTags('wordpress-auth') @@ -27,10 +26,7 @@ import { WordpressService } from './wordpress.service'; @Controller('wordpress/auth') @UseGuards(JwtAuthGuard) export class WordpressAuthController { - constructor( - private readonly toolsService: ToolsService, - private readonly wordpressService: WordpressService, - ) {} + constructor(private readonly wordpressService: WordpressService) {} @Post('login') @ApiOperation({ summary: '使用环境变量中的 WordPress 管理员账号自动认证' }) @@ -39,7 +35,7 @@ export class WordpressAuthController { await this.wordpressService.loginWithConfiguredAdmin(); this.wordpressService.setAuthCookie(res, cookie); - return this.toolsService.res(HttpStatus.OK, '操作成功', { + return vbenSuccess({ auth, user, }); @@ -51,7 +47,7 @@ export class WordpressAuthController { logout(@Res({ passthrough: true }) res: Response) { this.wordpressService.clearAuthCookie(res); - return this.toolsService.res(HttpStatus.OK, '操作成功', true); + return vbenSuccess(true); } @Get('check') @@ -60,6 +56,6 @@ export class WordpressAuthController { const auth = this.wordpressService.getAuthContext(req); const user = await this.wordpressService.checkAuth(auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', user)); + return res.send(vbenSuccess(user)); } } diff --git a/src/wordpress/wordpress-category.controller.ts b/src/wordpress/wordpress-category.controller.ts index a53b0bb..0bea339 100644 --- a/src/wordpress/wordpress-category.controller.ts +++ b/src/wordpress/wordpress-category.controller.ts @@ -13,7 +13,7 @@ import { import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import type { Request } from 'express'; import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard'; -import { ToolsService } from '@/common'; +import { vbenSuccess } from '@/common'; import { WordpressTermBodyDto, WordpressTermListQueryDto, @@ -35,10 +35,7 @@ import { WordpressService } from './wordpress.service'; @Controller('wordpress/category') @UseGuards(JwtAuthGuard) export class WordpressCategoryController { - constructor( - private readonly toolsService: ToolsService, - private readonly wordpressService: WordpressService, - ) {} + constructor(private readonly wordpressService: WordpressService) {} @Get('list') @ApiOperation({ summary: '获取 WordPress 分类分页列表' }) @@ -50,7 +47,7 @@ export class WordpressCategoryController { const auth = this.wordpressService.getAuthContext(req); const list = await this.wordpressService.categoryList(query, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list)); + return res.send(vbenSuccess(list)); } @Get('detail') @@ -60,7 +57,7 @@ export class WordpressCategoryController { const auth = this.wordpressService.getAuthContext(req); const detail = await this.wordpressService.categoryDetail(id, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', detail)); + return res.send(vbenSuccess(detail)); } @Post('save') @@ -74,7 +71,7 @@ export class WordpressCategoryController { const auth = this.wordpressService.getAuthContext(req); const result = await this.wordpressService.categorySave(body, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result)); + return res.send(vbenSuccess(result)); } @Post('update') @@ -88,7 +85,7 @@ export class WordpressCategoryController { const auth = this.wordpressService.getAuthContext(req); const result = await this.wordpressService.categoryUpdate(body, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result)); + return res.send(vbenSuccess(result)); } @Post('remove') @@ -109,6 +106,6 @@ export class WordpressCategoryController { auth, ); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result)); + return res.send(vbenSuccess(result)); } } diff --git a/src/wordpress/wordpress-tag.controller.ts b/src/wordpress/wordpress-tag.controller.ts index 451d652..f8546da 100644 --- a/src/wordpress/wordpress-tag.controller.ts +++ b/src/wordpress/wordpress-tag.controller.ts @@ -13,7 +13,7 @@ import { import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import type { Request } from 'express'; import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard'; -import { ToolsService } from '@/common'; +import { vbenSuccess } from '@/common'; import { WordpressTermBodyDto, WordpressTermListQueryDto, @@ -35,10 +35,7 @@ import { WordpressService } from './wordpress.service'; @Controller('wordpress/tag') @UseGuards(JwtAuthGuard) export class WordpressTagController { - constructor( - private readonly toolsService: ToolsService, - private readonly wordpressService: WordpressService, - ) {} + constructor(private readonly wordpressService: WordpressService) {} @Get('list') @ApiOperation({ summary: '获取 WordPress 标签分页列表' }) @@ -50,7 +47,7 @@ export class WordpressTagController { const auth = this.wordpressService.getAuthContext(req); const list = await this.wordpressService.tagList(query, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list)); + return res.send(vbenSuccess(list)); } @Get('detail') @@ -60,7 +57,7 @@ export class WordpressTagController { const auth = this.wordpressService.getAuthContext(req); const detail = await this.wordpressService.tagDetail(id, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', detail)); + return res.send(vbenSuccess(detail)); } @Post('save') @@ -74,7 +71,7 @@ export class WordpressTagController { const auth = this.wordpressService.getAuthContext(req); const result = await this.wordpressService.tagSave(body, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result)); + return res.send(vbenSuccess(result)); } @Post('update') @@ -88,7 +85,7 @@ export class WordpressTagController { const auth = this.wordpressService.getAuthContext(req); const result = await this.wordpressService.tagUpdate(body, auth); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result)); + return res.send(vbenSuccess(result)); } @Post('remove') @@ -109,6 +106,6 @@ export class WordpressTagController { auth, ); - return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result)); + return res.send(vbenSuccess(result)); } } diff --git a/src/wordpress/wordpress.module.ts b/src/wordpress/wordpress.module.ts index a640c76..700899a 100644 --- a/src/wordpress/wordpress.module.ts +++ b/src/wordpress/wordpress.module.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module'; -import { ToolsService } from '@/common'; import { WordpressArticleController } from './wordpress-article.controller'; import { WordpressAuthController } from './wordpress-auth.controller'; import { WordpressCategoryController } from './wordpress-category.controller'; @@ -15,7 +14,7 @@ import { WordpressTagController } from './wordpress-tag.controller'; WordpressTagController, WordpressCategoryController, ], - providers: [ToolsService, WordpressService], + providers: [WordpressService], exports: [WordpressService], }) export class WordpressModule {} diff --git a/src/wordpress/wordpress.service.ts b/src/wordpress/wordpress.service.ts index 25d9e1d..56c3db2 100644 --- a/src/wordpress/wordpress.service.ts +++ b/src/wordpress/wordpress.service.ts @@ -23,6 +23,30 @@ export type WordpressLoginResult = { user: any; }; +export type WordpressOptionalLoginResult = + | { + available: false; + error: WordpressAvailabilityError; + result: null; + } + | { + available: true; + error: null; + result: WordpressLoginResult & { cookie: string }; + }; + +export type WordpressAvailabilityError = { + error: any; + message: string; + status: number; +}; + +type WordpressAvailabilityCache = { + available: boolean; + checkedAt: number; + error?: WordpressAvailabilityError; +}; + type WordpressRequestOptions = { auth: WordpressAuthContext; body?: Record; @@ -46,6 +70,8 @@ const WORDPRESS_AUTH_COOKIE = 'kt_wordpress_auth'; @Injectable() export class WordpressService { + private availabilityCache: null | WordpressAvailabilityCache = null; + constructor(private readonly configService: ConfigService) {} getAuthContext(request: Request): WordpressAuthContext { @@ -79,9 +105,45 @@ export class WordpressService { return response.data; } - async loginWithConfiguredAdmin(): Promise< - WordpressLoginResult & { cookie: string } - > { + async tryLoginWithConfiguredAdmin(): Promise { + try { + const result = await this.loginWithConfiguredAdmin({ + timeoutMs: this.getLoginTimeout(), + }); + this.rememberAvailability(true); + + return { + available: true, + error: null, + result, + }; + } catch (err) { + const error = this.normalizeAvailabilityError(err); + this.rememberAvailability(false, error); + + return { + available: false, + error, + result: null, + }; + } + } + + isAdminIntegrationAvailable() { + if (!this.availabilityCache) return true; + if ( + Date.now() - this.availabilityCache.checkedAt > + this.getAvailabilityTtl() + ) { + return true; + } + + return this.availabilityCache.available; + } + + async loginWithConfiguredAdmin( + options: { timeoutMs?: number } = {}, + ): Promise { const username = this.configService.get('WORDPRESS_ADMIN_USERNAME'); const password = this.configService.get('WORDPRESS_ADMIN_PASSWORD'); @@ -93,8 +155,12 @@ export class WordpressService { ); } - const cookie = await this.loginByPassword(username, password); - const nonce = await this.fetchRestNonce(cookie); + const cookie = await this.loginByPassword( + username, + password, + options.timeoutMs, + ); + const nonce = await this.fetchRestNonce(cookie, options.timeoutMs); if (!nonce) { throwVbenError( @@ -138,7 +204,10 @@ export class WordpressService { }); } - async articleList(query: WordpressArticleListQueryDto, auth: WordpressAuthContext) { + async articleList( + query: WordpressArticleListQueryDto, + auth: WordpressAuthContext, + ) { const response = await this.request('/wp-json/wp/v2/posts', { auth, query: { @@ -179,7 +248,10 @@ export class WordpressService { return response.data; } - async articleUpdate(body: WordpressArticleBodyDto & { id: number }, auth: WordpressAuthContext) { + async articleUpdate( + body: WordpressArticleBodyDto & { id: number }, + auth: WordpressAuthContext, + ) { const response = await this.request(`/wp-json/wp/v2/posts/${body.id}`, { auth, body: this.getArticleBody(body), @@ -189,7 +261,11 @@ export class WordpressService { return response.data; } - async articleRemove(id: string | number, force: boolean, auth: WordpressAuthContext) { + async articleRemove( + id: string | number, + force: boolean, + auth: WordpressAuthContext, + ) { const response = await this.request(`/wp-json/wp/v2/posts/${id}`, { auth, method: 'DELETE', @@ -213,15 +289,25 @@ export class WordpressService { return this.termSave('/wp-json/wp/v2/tags', body, auth); } - async tagUpdate(body: WordpressTermBodyDto & { id: number }, auth: WordpressAuthContext) { + async tagUpdate( + body: WordpressTermBodyDto & { id: number }, + auth: WordpressAuthContext, + ) { return this.termUpdate('/wp-json/wp/v2/tags', body, auth); } - async tagRemove(id: string | number, force: boolean, auth: WordpressAuthContext) { + async tagRemove( + id: string | number, + force: boolean, + auth: WordpressAuthContext, + ) { return this.termRemove('/wp-json/wp/v2/tags', id, force, auth); } - async categoryList(query: WordpressTermListQueryDto, auth: WordpressAuthContext) { + async categoryList( + query: WordpressTermListQueryDto, + auth: WordpressAuthContext, + ) { return this.termList('/wp-json/wp/v2/categories', query, auth); } @@ -233,11 +319,18 @@ export class WordpressService { return this.termSave('/wp-json/wp/v2/categories', body, auth); } - async categoryUpdate(body: WordpressTermBodyDto & { id: number }, auth: WordpressAuthContext) { + async categoryUpdate( + body: WordpressTermBodyDto & { id: number }, + auth: WordpressAuthContext, + ) { return this.termUpdate('/wp-json/wp/v2/categories', body, auth); } - async categoryRemove(id: string | number, force: boolean, auth: WordpressAuthContext) { + async categoryRemove( + id: string | number, + force: boolean, + auth: WordpressAuthContext, + ) { return this.termRemove('/wp-json/wp/v2/categories', id, force, auth); } @@ -346,7 +439,11 @@ export class WordpressService { const data = await this.parseResponse(response); // 兼容未开启 Apache rewrite 的 WordPress:/wp-json 404 时自动回退到 ?rest_route=。 - if (!response.ok && response.status === 404 && index < urls.length - 1) { + if ( + !response.ok && + response.status === 404 && + index < urls.length - 1 + ) { continue; } @@ -380,14 +477,22 @@ export class WordpressService { } } - private async loginByPassword(username: string, password: string) { - const response = await this.formRequest('/wp-login.php', { - log: username, - pwd: password, - redirect_to: this.getUrl('/wp-admin/'), - testcookie: '1', - 'wp-submit': 'Log In', - }); + private async loginByPassword( + username: string, + password: string, + timeoutMs?: number, + ) { + const response = await this.formRequest( + '/wp-login.php', + { + log: username, + pwd: password, + redirect_to: this.getUrl('/wp-admin/'), + testcookie: '1', + 'wp-submit': 'Log In', + }, + timeoutMs, + ); const setCookies = this.getSetCookieHeaders(response.headers); const cookie = this.toCookieHeader(setCookies); @@ -403,15 +508,23 @@ export class WordpressService { return cookie; } - private async fetchRestNonce(cookie: string) { - const adminPaths = ['/wp-admin/', '/wp-admin/post-new.php', '/wp-admin/edit.php']; + private async fetchRestNonce(cookie: string, timeoutMs?: number) { + const adminPaths = [ + '/wp-admin/', + '/wp-admin/post-new.php', + '/wp-admin/edit.php', + ]; for (const path of adminPaths) { - const response = await this.rawRequest(path, { - headers: { - Cookie: cookie, + const response = await this.rawRequest( + path, + { + headers: { + Cookie: cookie, + }, }, - }); + timeoutMs, + ); const html = await response.text().catch(() => ''); const nonce = this.extractRestNonce(html); @@ -421,23 +534,38 @@ export class WordpressService { return ''; } - private async formRequest(path: string, body: Record) { + private async formRequest( + path: string, + body: Record, + timeoutMs?: number, + ) { const form = new URLSearchParams(body); - return this.rawRequest(path, { - body: form, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Cookie: 'wordpress_test_cookie=WP Cookie check', + return this.rawRequest( + path, + { + body: form, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Cookie: 'wordpress_test_cookie=WP Cookie check', + }, + method: 'POST', + redirect: 'manual', }, - method: 'POST', - redirect: 'manual', - }); + timeoutMs, + ); } - private async rawRequest(path: string, init: RequestInit = {}) { + private async rawRequest( + path: string, + init: RequestInit = {}, + timeoutMs?: number, + ) { const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), this.getTimeout()); + const timer = setTimeout( + () => controller.abort(), + timeoutMs || this.getTimeout(), + ); try { return await fetch(this.getUrl(path), { @@ -497,7 +625,8 @@ export class WordpressService { } private getCookieOptions() { - const secure = this.configService.get('ADMIN_COOKIE_SECURE') === 'true'; + const secure = + this.configService.get('ADMIN_COOKIE_SECURE') === 'true'; return { httpOnly: true, @@ -551,6 +680,31 @@ export class WordpressService { return Number(this.configService.get('WORDPRESS_TIMEOUT_MS') || 15000); } + private getLoginTimeout() { + return Number( + this.configService.get('WORDPRESS_LOGIN_TIMEOUT_MS') || + this.configService.get('WORDPRESS_TIMEOUT_MS') || + 3000, + ); + } + + private getAvailabilityTtl() { + return Number( + this.configService.get('WORDPRESS_AVAILABILITY_TTL_MS') || 60_000, + ); + } + + private rememberAvailability( + available: boolean, + error?: WordpressAvailabilityError, + ) { + this.availabilityCache = { + available, + checkedAt: Date.now(), + error, + }; + } + private getPageQuery(query: WordpressPagedQueryDto) { return { order: query.order, @@ -642,6 +796,30 @@ export class WordpressService { ); } + private normalizeAvailabilityError(err: unknown): WordpressAvailabilityError { + if (err instanceof HttpException) { + const response = err.getResponse(); + const responseBody = + response && typeof response === 'object' + ? (response as Record) + : {}; + + return { + error: responseBody.error || response, + message: + responseBody.message || + (typeof response === 'string' ? response : err.message), + status: err.getStatus(), + }; + } + + return { + error: err instanceof Error ? err.name : 'WordPressUnavailable', + message: err instanceof Error ? err.message : 'WordPress 暂不可用', + status: HttpStatus.BAD_GATEWAY, + }; + } + private getErrorCause(err: unknown) { const cause = (err as { cause?: { code?: string; message?: string } }) ?.cause; @@ -650,11 +828,16 @@ export class WordpressService { } private getLoginErrorMessage(html: string) { - const match = html.match(/]*id=["']login_error["'][^>]*>([\s\S]*?)<\/div>/i); + const match = html.match( + /]*id=["']login_error["'][^>]*>([\s\S]*?)<\/div>/i, + ); if (!match?.[1]) return 'WordPress 管理员登录失败'; - return match[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim(); + return match[1] + .replace(/<[^>]+>/g, '') + .replace(/\s+/g, ' ') + .trim(); } private getSetCookieHeaders(headers: Headers) { @@ -778,4 +961,6 @@ export class WordpressService { } } -type WordpressPagedQueryDto = WordpressArticleListQueryDto | WordpressTermListQueryDto; +type WordpressPagedQueryDto = + | WordpressArticleListQueryDto + | WordpressTermListQueryDto; diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 4630db9..baebf62 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -563,9 +563,8 @@ const routeTestCases: Record = { expect(wordpressServiceMock.checkAuth).toHaveBeenCalledWith( wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressUser, }); }, @@ -584,9 +583,8 @@ const routeTestCases: Record = { expect.anything(), wordpressLoginResult.cookie, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: { auth: wordpressLoginResult.auth, user: wordpressUser, @@ -602,9 +600,8 @@ const routeTestCases: Record = { expect(wordpressServiceMock.clearAuthCookie).toHaveBeenCalledWith( expect.anything(), ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: true, }); }, @@ -632,9 +629,8 @@ const routeTestCases: Record = { }, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: { list: [wordpressArticle], total: 1, @@ -654,9 +650,8 @@ const routeTestCases: Record = { '1', wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressArticle, }); }, @@ -680,9 +675,8 @@ const routeTestCases: Record = { }, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressArticle, }); }, @@ -705,9 +699,8 @@ const routeTestCases: Record = { }, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressArticle, }); }, @@ -728,9 +721,8 @@ const routeTestCases: Record = { false, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: true, }); }, @@ -758,9 +750,8 @@ const routeTestCases: Record = { }, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: { list: [wordpressTerm], total: 1, @@ -780,9 +771,8 @@ const routeTestCases: Record = { '1', wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressTerm, }); }, @@ -804,9 +794,8 @@ const routeTestCases: Record = { }, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressTerm, }); }, @@ -829,9 +818,8 @@ const routeTestCases: Record = { }, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressTerm, }); }, @@ -849,9 +837,8 @@ const routeTestCases: Record = { true, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: true, }); }, @@ -879,9 +866,8 @@ const routeTestCases: Record = { }, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: { list: [wordpressTerm], total: 1, @@ -901,9 +887,8 @@ const routeTestCases: Record = { '1', wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressTerm, }); }, @@ -927,9 +912,8 @@ const routeTestCases: Record = { }, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressTerm, }); }, @@ -954,9 +938,8 @@ const routeTestCases: Record = { }, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: wordpressTerm, }); }, @@ -974,9 +957,8 @@ const routeTestCases: Record = { true, wordpressAuthContext, ); - expect(response.body).toEqual({ - code: 200, - msg: '操作成功', + expect(response.body).toMatchObject({ + code: 0, data: true, }); },