This commit is contained in:
wangjiming
2026-07-27 10:38:16 +08:00
parent b4ff5db17b
commit 4c7d66030c
4 changed files with 279 additions and 20 deletions

View File

@@ -68,14 +68,54 @@ YG_FT/
> 说明:`audit`、`retention` 模块当前未拆为独立 REST 路由——审计接口挂在 `system` 下、留存策略嵌套于租户资源;用户「页面权限」精细控制 UI 仍为占位,待后续版本补齐。数据层当前为内存/本地存储实现,生产环境按 `docs/postgres-schema.sql` 迁移 PostgreSQL。
## 首次环境准备(新人 / 新机器)
> 后端与算力服务都运行在 **WSL2** 中Windows 原生 Python / anaconda 依赖不兼容)。第一次拉代码、机器上没有任何环境时,按下面顺序一次性准备好;日常启动见下方「后端启动」「算力服务启动」。
### 1. 基础运行时
- **WSL2**Windows 终端执行 `wsl --install`(默认 Ubuntu重启生效。
- **Python ≥ 3.9(推荐 3.12**,在 WSL 内确认:
```bash
python3 --version
```
版本过低或未安装:`sudo apt update && sudo apt install -y python3.12 python3.12-venv python3-pip`。
- **Node.js 18+**:前端在 **Windows 终端**安装并运行(不要放进 WSL否则热更新跨 `/mnt` 文件系统很慢)。
### 2. 数据库 PostgreSQL首次需自建
后端默认连接 `postgresql+psycopg://yg_ft:change_me@localhost:15432/yg_ft`。首次没有数据库时,推荐用 Docker 一键起:
```bash
docker run -d --name yg_ft_pg -p 15432:5432 \
-e POSTGRES_USER=yg_ft -e POSTGRES_PASSWORD=change_me -e POSTGRES_DB=yg_ft \
postgres:16
```
无 Docker 时,可在 WSL 内 `sudo apt install -y postgresql` 后手动建库与用户。后端首次启动会自动建表并写入内置管理员账号(详见「后端启动」)。
### 3. 后端 / 算力 venv 首次创建
后端与算力各有独立 venv首次需在 WSL 中创建并安装依赖(完整命令见下方启动小节):
- 后端:`cd /mnt/e/yg_ft/backend && python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt`
- 算力:`cd /mnt/e/yg_ft && source compute/.venv/bin/activate && pip install -r compute/requirements.txt`compute 使用绝对导入 `compute.*`,须在仓库根目录操作)
### 4. 前端首次安装
在 Windows 终端:
```powershell
cd e:\yg_ft\frontend
npm install
npm run dev
```
---
## 后端启动
> 后端需在 **WSL 终端** 中启动Windows 的 anaconda 环境依赖不兼容,且项目 venv 为 Linux 版(使用 `bin/activate` 而非 `Scripts`)。若 `.wslconfig` 使用 `networkingMode=mirrored`uvicorn 必须绑定 `--host 0.0.0.0` 才能被 Windows 侧 `localhost` 访问。
```bash
cd backend
python -m venv .venv
.venv\Scripts\activate
# 在 WSL 终端中执行
cd /mnt/e/yg_ft/backend
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 17861
uvicorn app.main:app --host 0.0.0.0 --port 17861 --reload
```
默认接口前缀为 `/modelTF`,例如:
@@ -118,14 +158,14 @@ npm run dev
## 算力服务启动
算力服务位于 `compute/` 目录,拥有**独立虚拟环境(不复用后端 venv**。
代码使用绝对导入 `compute.*`,因此必须从**仓库根目录(`yg_ft/`**启动,
不能先 `cd compute` 再启动(否则报 `No module named 'compute'`)。
> 算力服务同样需在 **WSL 终端** 中启动,且拥有**独立虚拟环境(不复用后端 venv**。代码使用绝对导入 `compute.*`,因此必须从**仓库根目录(`/mnt/e/yg_ft`**执行,不能先 `cd compute` 再启动(否则报 `No module named 'compute'`)。若 `.wslconfig` 使用 `networkingMode=mirrored`uvicorn 需绑定 `--host 0.0.0.0` 才能被 Windows 侧 `localhost` 访问。
```bash
# 在仓库根目录 yg_ft/ 下执行
source compute/.venv/bin/activate # 激活独立 venvWindows 用 compute\.venv\Scripts\activate
pip install -r compute/requirements.txt # 首次安装依赖
# 在 WSL 终端中执行(必须位于仓库根目录 yg_ft/
cd /mnt/e/yg_ft
python3 -m venv compute/.venv
source compute/.venv/bin/activate
pip install -r compute/requirements.txt
uvicorn compute.api.main:app --host 0.0.0.0 --port 19100 --reload
```

View File

@@ -49,7 +49,29 @@ npm run dev
---
## 1.5 用户设置 → 重置密码(系统设置)
## 1.4 用户设置 → 创建用户(系统设置)
> 入口:左侧菜单「系统设置 → 用户设置」页右上「创建用户」按钮(或 `/user-settings/create`),对应后端 `POST /modelTF/users`。新建的账号用于后续 1.5 各项验证(重置密码 / 启停 / 权限)。
### 1.4.1 创建普通账号
1. 进入「用户设置」,点「**创建用户**」
2. 填:
- 账号:`test-user-manual`
- 显示名称:`测试用户`
- 初始密码:留默认 `platform123`(或自定义)
- 角色:选「操作员」(非 admin便于后续测启停/权限)
- 状态:保持「启用」
- 页面权限:默认已勾 `dashboard`,按需增删
3. 点「**保存**」
4. 预期:提示「用户已创建」,跳回列表,列表出现 `test-user-manual`
5. 验证:退出后该账号 + `platform123` 应能登录成功;登录后左侧菜单按角色/权限显示(操作员无「用户设置」)
### 1.4.2 创建时即禁用(可选)
- 新建时「状态」选「禁用」再保存,预期列表该账号状态为「禁用」,且用该账号登录会失败(后端 `login` 校验 `status != 'active'` 拒绝)。
---
## 1.5 用户设置 → 账号操作(系统设置)
> 入口:左侧菜单「系统设置 → 用户设置」。该页是账号列表,每行「操作」列有「重置密码」按钮,对应后端 `POST /modelTF/users/{id}/reset-password`。
@@ -69,6 +91,32 @@ npm run dev
- `admin` 是内置保护账号protected不可被重置。点 `admin` 行「重置密码」并提交后端会返回错误提示「protected user cannot be reset」前端弹「重置失败」。
- 验证:点 `admin` 行「重置密码」→ 确定,预期出现错误提示,且 `admin` 密码不变(仍可用 `admin123` 登录)。
### 1.5.4 启用 / 禁用账号
> 操作列对 `admin` 保护账号**隐藏**启停按钮(避免锁死管理员自身);普通账号可见「禁用 / 启用」按钮,对应后端 `PUT /modelTF/users/{id}`status 字段)。
1. 在任一**非 admin** 普通账号行,点「**禁用**」
2. 预期:状态标签由「启用」变「禁用」,操作按钮变「启用」
3. 验证:用该账号尝试登录,预期**登录失败**(后端 `login` 校验 `status != 'active'` 直接拒绝)
4. 再点「**启用**」,预期状态恢复「启用」,该账号可重新登录
### 1.5.5 页面权限精细控制
> 操作列「权限」按钮进入「权限设置」页(`/user-settings/:id/permission`),勾选可访问页面码后保存,对应后端 `PUT /modelTF/users/{id}`permissions 字段,直接覆盖该用户权限清单)。
1. 在目标账号行点「**权限**」,进入「权限设置」页
2. 页内列出 12 个页面权限码(仪表盘 / 模型微调 / … / 用户设置),当前已勾选项为该用户现有权限
3. 取消某几项(如去掉 `user-settings`),点「**保存**」
4. 预期:提示「权限已保存」,跳回用户列表
5. 验证:用该账号登录后,其权限菜单应**不再包含**被取消的页面(前端 `hasPermission` 基于 `users.permissions` 实时判断)
### 1.5.6 删除账号
> 操作列「删除」按钮仅对**非保护账号、且非当前登录自己**显示,点击触发二次确认,对应后端 `DELETE /modelTF/users/{id}`(带 `current_username` 防删自己)。
1. 在 1.4.1 创建的 `test-user-manual`(或任一非 admin 普通账号)行,点「**删除**」
2. 弹出二次确认「确定删除该账号?此操作不可恢复」,点「**删除**」
3. 预期:提示「用户已删除」,列表不再显示该账号
4. 验证:用该账号 + `platform123` 登录,预期失败(账号已不存在)
5. 边界:`admin` 保护账号行**无**「删除」按钮;当前登录账号自身也**无**「删除」按钮(防误删自己)
---
## 2. 租户管理(第 2 周)
@@ -237,7 +285,7 @@ npm run dev
- [ ] 4. 项目成员:添加 / 改角色 / 移除 均成功
- [ ] 5. 资源 ACL添加授权项 + 勾选权限 + 点「保存」成功,重开仍在
- [ ] 6. 归档:点后状态变 `archived`
- [ ] 7. 用户设置重置密码自定义密码生效 / 留空回退 `platform123` / `admin` 保护账号被拒
- [ ] 7. 用户设置:创建用户(账号/显示名/初始密码/角色/状态/页面权限均可设,列表出现新账号)、重置密码自定义生效 / 留空回退 `platform123` / `admin` 被拒)、启用禁用账号(禁用后登录失败、启用后恢复)、页面权限精细控制(取消权限后登录即生效)、删除账号(二次确认后列表移除、登录失败)
**第 3 周(审计)**
- [ ] 8. 审计日志:按 动作/操作人/项目 过滤均能返回正确结果

View File

@@ -1,12 +1,134 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getUsers, updateUserAccess } from '@/api/modules/system'
import type { PermissionCode, SystemUser } from '@/types'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const saving = ref(false)
const userId = route.params.id as string
const target = ref<SystemUser | null>(null)
const checked = ref<PermissionCode[]>([])
const options: PermissionCode[] = [
'dashboard',
'fine-tune',
'model-eval',
'model-inference',
'model-manage',
'dataset',
'data-process',
'data-convert',
'compute',
'hardware',
'logs',
'user-settings',
]
const labels: Record<PermissionCode, string> = {
dashboard: '仪表盘',
'fine-tune': '模型微调',
'model-eval': '模型评估',
'model-inference': '模型推理',
'model-manage': '模型管理',
dataset: '数据集',
'data-process': '数据处理',
'data-convert': '数据转换',
compute: '算力',
hardware: '平台性能',
logs: '日志',
'user-settings': '用户设置',
}
async function load() {
loading.value = true
try {
const users = await getUsers()
const u = users.find((x) => x.id === userId)
if (!u) {
ElMessage.error('用户不存在')
return
}
target.value = u
checked.value = [...u.permissions]
} finally {
loading.value = false
}
}
async function save() {
saving.value = true
try {
await updateUserAccess(userId, { permissions: checked.value })
ElMessage.success('权限已保存')
router.push('/user-settings')
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || '保存失败')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<template>
<section class="simple-page">
<h1>权限设置</h1>
<p>第一版已支持账号页面权限读取与保存精细化项目/模型/数据集权限将在后续版本补齐</p>
<section class="permission-page" v-loading="loading">
<header class="page-header">
<div>
<h1>权限设置</h1>
<p v-if="target">
为账号 <b>{{ target.username }}</b>{{ target.display_name }}配置可访问的页面权限
</p>
</div>
<el-button @click="$router.push('/user-settings')">返回</el-button>
</header>
<el-form label-width="90px" class="perm-form">
<el-form-item label="账号">{{ target?.username }}</el-form-item>
<el-form-item label="角色">{{ target?.role }}</el-form-item>
<el-form-item label="页面权限">
<el-checkbox-group v-model="checked">
<el-checkbox v-for="opt in options" :key="opt" :value="opt">
{{ labels[opt] }}{{ opt }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item>
<el-button @click="$router.push('/user-settings')">取消</el-button>
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
</el-form-item>
</el-form>
</section>
</template>
<style scoped>
.simple-page {
.permission-page {
padding: 24px;
}
.page-header {
display: flex;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.page-header h1 {
margin: 0;
font-size: 24px;
}
.page-header p {
margin: 8px 0 0;
color: #64748b;
}
.perm-form {
max-width: 760px;
}
</style>

View File

@@ -1,8 +1,11 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getUsers, resetUserPassword } from '@/api/modules/system'
import type { SystemUser } from '@/types'
import { getUsers, resetUserPassword, updateUserAccess, deleteUser } from '@/api/modules/system'
import { useAuthStore } from '@/stores/auth'
import type { SystemUser, UserStatus } from '@/types'
const auth = useAuthStore()
const loading = ref(false)
const users = ref<SystemUser[]>([])
@@ -41,6 +44,27 @@ async function confirmReset() {
}
}
async function toggleStatus(row: SystemUser) {
const next: UserStatus = row.status === 'active' ? 'disabled' : 'active'
try {
await updateUserAccess(row.id, { status: next })
ElMessage.success(next === 'active' ? '已启用' : '已禁用')
await loadUsers()
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || '操作失败')
}
}
async function removeUser(row: SystemUser) {
try {
await deleteUser(row.id, auth.username)
ElMessage.success('用户已删除')
await loadUsers()
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || '删除失败')
}
}
onMounted(loadUsers)
</script>
@@ -58,14 +82,39 @@ onMounted(loadUsers)
<el-table-column prop="username" label="账号" min-width="140" />
<el-table-column prop="display_name" label="显示名称" min-width="160" />
<el-table-column prop="role" label="角色" width="120" />
<el-table-column prop="status" label="状态" width="120" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'info'">
{{ row.status === 'active' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="权限数" width="120">
<template #default="{ row }">{{ row.permissions?.length || 0 }}</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" min-width="180" />
<el-table-column label="操作" width="120" fixed="right">
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openReset(row)">重置密码</el-button>
<el-button link type="primary" size="small" @click="$router.push(`/user-settings/${row.id}/permission`)">权限</el-button>
<el-button
v-if="!row.protected"
link
:type="row.status === 'active' ? 'danger' : 'success'"
size="small"
@click="toggleStatus(row)"
>{{ row.status === 'active' ? '禁用' : '启用' }}</el-button>
<el-popconfirm
v-if="!row.protected && row.username !== auth.username"
title="确定删除该账号?此操作不可恢复"
confirm-button-text="删除"
cancel-button-text="取消"
@confirm="removeUser(row)"
>
<template #reference>
<el-button link type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>