Compare commits

..
2 Commits
Author SHA1 Message Date
Stev_Wang cda4b1825b chore(openapi): 同步部门管理接口的生成类型
ci / 质量门禁 (push) Canceled after 0s
背景:
- 上一批部门管理接口(disable / enable、update、树与字段说明)合入后没有重新导出 OpenAPI,
  apps/web/src/api/generated/openapi.d.ts 仍停留在「部门改名」版本,前端类型与真实契约不一致;
- pnpm ci:local 第 9 步 openapi:export 会自动重生成该文件,导致每次跑完工作区变脏。

改动:
- 执行 pnpm openapi:export,重新生成 apps/web/src/api/generated/openapi.d.ts
  (新增 /departments/{id}/disable、/departments/{id}/enable、update 操作与字段说明)。

影响面:
- 仅前端生成类型,无运行时行为变化;openapi/lingxi.openapi.json 无差异。

回滚方式:
- 回滚本提交即恢复旧生成文件。

验证证据:
- pnpm openapi:diff 通过;
- pnpm ci:local 全部门禁通过。
2026-11-03 16:26:10 +08:00
Stev_Wang 087f27936e fix(ci): 修复本地 CI 端到端冒烟的 demo 会话与 A0 遗留用例
背景:
- .gitea/workflows/ci.yml 在 job env 中设置 VITE_PUBLIC_DEMO_SESSION=true,
  e2e 用 vite preview + 演示会话登录;scripts/ci-local.mjs 没有同步这组 env,
  本地 CI 第 10 步必失败;
- e2e/smoke.spec.ts 是 A0 用例:还在找「请输入账号或邮箱」占位符、断言登录后落在工作台;
  A1 接入真实登录后占位符为「请输入账号或手机号」,且登录默认落地 /system/users。

改动:
- ci-local:构建与 e2e 步骤注入 VITE_PUBLIC_DEMO_SESSION / VITE_PUBLIC_API_BASE_URL
  (与 ci.yml 对齐),只作用于这两个步骤,避免影响单测对真实登录流程的断言;
- turbo.json:声明 globalEnv,保证构建产物随这两个 VITE 变量变化,不复用非 demo 构建缓存;
- smoke.spec:登录占位符改为「请输入账号或手机号」;登录后按 A1 现状断言落地 /system/users,
  再从顶部「工作台」进入首页并校验模块导航;
- README:单独执行 e2e 时补充 demo 会话环境变量,并注明 ci:local 已内置。

影响面:
- 仅本地 CI 与 e2e 冒烟;业务代码与 CI 工作流行为不变(ci.yml 本就在 job env 注入这些变量);
- 本地构建缓存会因 globalEnv 变更失效一次。

回滚方式:
- 回滚本提交即恢复原 ci-local 步骤、冒烟用例与 README。

验证证据:
- pnpm ci:local 全部门禁通过(用时 218.8s);
- e2e smoke 4 项通过(登录 / 403 / 模块切换 / 404;A1 账号周期 7 项按预期 skip)。
2026-11-03 16:25:55 +08:00
5 changed files with 110 additions and 15 deletions

No files matched your search

+2 -2
View File
@@ -113,9 +113,9 @@ pnpm dev # 并行启动 api(3000) / web(5173) / worker
```bash ```bash
pnpm lint && pnpm typecheck && pnpm test # 本地门禁 pnpm lint && pnpm typecheck && pnpm test # 本地门禁
pnpm build && pnpm test:e2e # 构建与端到端冒烟 VITE_PUBLIC_DEMO_SESSION=true pnpm test:e2e # 构建 + 端到端冒烟(demo 会话)
pnpm openapi:export && pnpm openapi:diff # 契约快照与破坏性变更检查 pnpm openapi:export && pnpm openapi:diff # 契约快照与破坏性变更检查
pnpm ci:local # 本地跑完整 CI 门禁 pnpm ci:local # 本地跑完整 CI 门禁(已内置 e2e 所需 env)
``` ```
> Gitea 暂未部署 Actions runner,推送后远程 CI 会停留在 queued;当前以 `pnpm ci:local` 为准,runner 就绪后 `.gitea/workflows/ci.yml` 会自动接管。 > Gitea 暂未部署 Actions runner,推送后远程 CI 会停留在 queued;当前以 `pnpm ci:local` 为准,runner 就绪后 `.gitea/workflows/ci.yml` 会自动接管。
+88 -6
View File
@@ -204,7 +204,10 @@ export interface paths {
path?: never; path?: never;
cookie?: never; cookie?: never;
}; };
/** 部门树(两级,含成员数与负责人) */ /**
* 部门树(两级)
* @description memberCount 含下级成员、directMemberCount 仅本部门直属成员;默认只返回启用部门,管理页传 include_disabled=true
*/
get: operations['DepartmentsController_tree']; get: operations['DepartmentsController_tree'];
put?: never; put?: never;
post?: never; post?: never;
@@ -225,7 +228,7 @@ export interface paths {
put?: never; put?: never;
/** /**
* 新建部门 / 小组 * 新建部门 / 小组
* @description 最多两级,在小组下新建返回 42201 * @description 最多两级;父级必须启用;负责人必须是启用用户;同级重名返回 40903
*/ */
post: operations['DepartmentsController_create']; post: operations['DepartmentsController_create'];
delete?: never; delete?: never;
@@ -247,8 +250,51 @@ export interface paths {
delete?: never; delete?: never;
options?: never; options?: never;
head?: never; head?: never;
/** 部门改名 */ /**
patch: operations['DepartmentsController_rename']; * 更新部门(名称 / 负责人 / 排序)
* @description 上级关系创建后不可移动;负责人必须是启用用户
*/
patch: operations['DepartmentsController_update'];
trace?: never;
};
'/api/v1/departments/{id}/disable': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* 停用部门
* @description 本部门仍有在职成员、或一级部门存在启用中的下级小组 / 下级在职成员时返回 42201;已停用返回 40903
*/
post: operations['DepartmentsController_disable'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/api/v1/departments/{id}/enable': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* 启用部门
* @description 上级必须启用;已启用返回 40903
*/
post: operations['DepartmentsController_enable'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never; trace?: never;
}; };
'/api/v1/roles': { '/api/v1/roles': {
@@ -657,7 +703,9 @@ export interface operations {
}; };
DepartmentsController_tree: { DepartmentsController_tree: {
parameters: { parameters: {
query?: never; query?: {
include_disabled?: 'true' | 'false';
};
header?: never; header?: never;
path?: never; path?: never;
cookie?: never; cookie?: never;
@@ -689,7 +737,41 @@ export interface operations {
}; };
}; };
}; };
DepartmentsController_rename: { DepartmentsController_update: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
DepartmentsController_disable: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
DepartmentsController_enable: {
parameters: { parameters: {
query?: never; query?: never;
header?: never; header?: never;
+4 -1
View File
@@ -2,7 +2,7 @@ import { expect, type Page, test } from '@playwright/test';
async function signIn(page: Page): Promise<void> { async function signIn(page: Page): Promise<void> {
await page.goto('/login'); await page.goto('/login');
await page.getByPlaceholder('请输入账号或邮箱').fill('admin'); await page.getByPlaceholder('请输入账号或手机号').fill('admin');
await page.getByPlaceholder('请输入密码').fill('demo1234'); await page.getByPlaceholder('请输入密码').fill('demo1234');
await page.getByTestId('login-submit').click(); await page.getByTestId('login-submit').click();
await expect(page.getByTestId('app-shell')).toBeVisible(); await expect(page.getByTestId('app-shell')).toBeVisible();
@@ -12,6 +12,9 @@ test('登录页打开并进入工作台', async ({ page }) => {
await page.goto('/login'); await page.goto('/login');
await expect(page.getByRole('heading', { name: '登录灵犀' })).toBeVisible(); await expect(page.getByRole('heading', { name: '登录灵犀' })).toBeVisible();
await signIn(page); await signIn(page);
// A1 起登录默认落地 /system/users,再从顶部模块导航进入工作台
await expect(page).toHaveURL(/\/system\/users/);
await page.getByRole('button', { name: '工作台', exact: true }).click();
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await expect(page.getByRole('heading', { name: '工作台', exact: true })).toBeVisible(); await expect(page.getByRole('heading', { name: '工作台', exact: true })).toBeVisible();
await expect(page.getByRole('navigation', { name: '模块导航' })).toBeVisible(); await expect(page.getByRole('navigation', { name: '模块导航' })).toBeVisible();
+15 -6
View File
@@ -7,26 +7,35 @@ import { spawnSync } from 'node:child_process';
const pnpm = process.env.npm_execpath ? [process.execPath, process.env.npm_execpath] : ['pnpm']; const pnpm = process.env.npm_execpath ? [process.execPath, process.env.npm_execpath] : ['pnpm'];
/** 每一步可包含多条 pnpm 命令,任一失败立即退出。 */ /**
* 端到端冒烟走 vite preview + demo 会话,需与 .gitea/workflows/ci.yml 的 job env 对齐;
* 只加在构建与 e2e 步骤,避免影响单测里对真实登录流程的断言。
*/
const e2eEnv = {
VITE_PUBLIC_API_BASE_URL: process.env.VITE_PUBLIC_API_BASE_URL ?? 'http://127.0.0.1:3000/api/v1',
VITE_PUBLIC_DEMO_SESSION: process.env.VITE_PUBLIC_DEMO_SESSION ?? 'true',
};
/** 每一步可包含多条 pnpm 命令,任一失败立即退出;env 为该步骤追加的环境变量。 */
const steps = [ const steps = [
{ label: '检查本地依赖(PG / Redis)', commands: [['check:dev']] }, { label: '检查本地依赖(PG / Redis)', commands: [['check:dev']] },
{ label: '格式化检查', commands: [['format:check']] }, { label: '格式化检查', commands: [['format:check']] },
{ label: 'Lint', commands: [['lint']] }, { label: 'Lint', commands: [['lint']] },
{ label: '类型检查', commands: [['typecheck']] }, { label: '类型检查', commands: [['typecheck']] },
{ label: '单元测试', commands: [['test']] }, { label: '单元测试', commands: [['test']] },
{ label: '构建', commands: [['build']] }, { label: '构建', commands: [['build']], env: e2eEnv },
{ {
label: '数据库迁移与种子', label: '数据库迁移与种子',
commands: [['exec', 'prisma', 'migrate', 'deploy'], ['db:seed']], commands: [['exec', 'prisma', 'migrate', 'deploy'], ['db:seed']],
}, },
{ label: '集成测试', commands: [['test:int']] }, { label: '集成测试', commands: [['test:int']] },
{ label: 'OpenAPI 导出与契约检查', commands: [['openapi:export'], ['openapi:diff']] }, { label: 'OpenAPI 导出与契约检查', commands: [['openapi:export'], ['openapi:diff']] },
{ label: '端到端测试', commands: [['test:e2e']] }, { label: '端到端测试', commands: [['test:e2e']], env: e2eEnv },
]; ];
function runPnpm(args) { function runPnpm(args, env = {}) {
const result = spawnSync(pnpm[0], [...pnpm.slice(1), ...args], { const result = spawnSync(pnpm[0], [...pnpm.slice(1), ...args], {
env: process.env, env: { ...process.env, ...env },
stdio: 'inherit', stdio: 'inherit',
}); });
if (result.error) { if (result.error) {
@@ -43,7 +52,7 @@ const startedAt = Date.now();
for (const [index, step] of steps.entries()) { for (const [index, step] of steps.entries()) {
console.log(`\n[${index + 1}/${steps.length}] ${step.label}`); console.log(`\n[${index + 1}/${steps.length}] ${step.label}`);
for (const args of step.commands) { for (const args of step.commands) {
runPnpm(args); runPnpm(args, step.env);
} }
} }
const seconds = ((Date.now() - startedAt) / 1000).toFixed(1); const seconds = ((Date.now() - startedAt) / 1000).toFixed(1);
+1
View File
@@ -1,6 +1,7 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"ui": "stream", "ui": "stream",
"globalEnv": ["VITE_PUBLIC_API_BASE_URL", "VITE_PUBLIC_DEMO_SESSION"],
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],