基础系统版本
commit
72ccd010c1
|
|
@ -0,0 +1,30 @@
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.log
|
||||||
|
.venv/
|
||||||
|
backend/.venv/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.claude/
|
||||||
|
.gemini-clipboard/
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
# Nex Basse 基础平台
|
||||||
|
|
||||||
|
本项目是面向团队快速开发的基础平台,包含用户、角色、权限、码表、系统参数等核心模块。前端采用 React + TypeScript,后端为 FastAPI(Python 3.9+),数据库先实现 MySQL 版本。
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
- 用户管理
|
||||||
|
- 用户列表、创建、编辑、禁用、删除
|
||||||
|
- 重置密码、角色分配
|
||||||
|
- 角色管理
|
||||||
|
- 角色列表、创建
|
||||||
|
- 角色权限分配(权限树勾选)
|
||||||
|
- 权限与菜单
|
||||||
|
- 功能权限树(三级:一级/二级菜单 + 三级按钮)
|
||||||
|
- 菜单动态加载、按角色权限可见
|
||||||
|
- 码表管理
|
||||||
|
- 码表类型与条目管理
|
||||||
|
- 系统参数管理
|
||||||
|
- 多类型参数:string/int/bool/json
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- React 18 + TypeScript
|
||||||
|
- Vite 5
|
||||||
|
- Ant Design 5
|
||||||
|
- 统一组件:`ListTable`、`Toast`、`DetailDrawer`
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- FastAPI
|
||||||
|
- SQLAlchemy 2.0
|
||||||
|
- Alembic
|
||||||
|
- Redis(token/刷新 token/异步任务占位)
|
||||||
|
- PyMySQL
|
||||||
|
|
||||||
|
### 数据库
|
||||||
|
- MySQL 5.7(开发)、8.0+(部署)
|
||||||
|
- 字符集:`utf8mb4`,排序规则:`utf8mb4_unicode_ci`
|
||||||
|
- 状态字段统一:`status TINYINT(1)`
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├─ backend
|
||||||
|
│ ├─ app
|
||||||
|
│ │ ├─ api/v1/endpoints
|
||||||
|
│ │ ├─ core
|
||||||
|
│ │ ├─ models
|
||||||
|
│ │ ├─ schemas
|
||||||
|
│ │ └─ services
|
||||||
|
│ ├─ docs
|
||||||
|
│ │ ├─ db_schema.md
|
||||||
|
│ │ └─ migrations
|
||||||
|
│ ├─ scripts
|
||||||
|
│ ├─ requirements.txt
|
||||||
|
│ └─ ENV.md
|
||||||
|
├─ design
|
||||||
|
│ └─ 页面参考图与数据库设计
|
||||||
|
├─ frontend
|
||||||
|
│ ├─ src
|
||||||
|
│ │ ├─ components
|
||||||
|
│ │ ├─ layout
|
||||||
|
│ │ └─ pages
|
||||||
|
│ ├─ package.json
|
||||||
|
│ └─ vite.config.ts
|
||||||
|
└─ README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 本地启动
|
||||||
|
|
||||||
|
### 1. 后端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 前端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
yarn install
|
||||||
|
yarn dev --host 0.0.0.0 --port 5173
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境配置
|
||||||
|
|
||||||
|
复制示例文件并填写:
|
||||||
|
|
||||||
|
- `backend/.env.example` -> `backend/.env`
|
||||||
|
- `frontend/.env.example` -> `frontend/.env`
|
||||||
|
|
||||||
|
详情见:`backend/ENV.md`
|
||||||
|
|
||||||
|
## 数据库初始化
|
||||||
|
|
||||||
|
执行初始化 SQL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysql -u <user> -p <db_name> < backend/docs/init_mysql.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
若已有旧数据,参考 `backend/docs/migrations/` 中的迁移脚本。
|
||||||
|
|
||||||
|
## 管理员账号
|
||||||
|
|
||||||
|
默认账号密码:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin / 123456
|
||||||
|
```
|
||||||
|
|
||||||
|
可通过脚本重置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
source .venv/bin/activate
|
||||||
|
INIT_ADMIN_PASSWORD=123456 PYTHONPATH=./ /Users/jiliu/WorkSpace/nex_basse/backend/.venv/bin/python scripts/init_admin.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
- 功能页面统一在主框架右侧载入(左侧菜单可收起/展开)
|
||||||
|
- 新增/编辑使用右侧抽屉(DetailDrawer)
|
||||||
|
- 后端权限树为三级结构:一级/二级菜单 + 三级按钮
|
||||||
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# 环境配置说明
|
||||||
|
|
||||||
|
本项目默认使用 `.env` 读取配置。请复制 `backend/.env.example` 到 `backend/.env` 后填写。
|
||||||
|
|
||||||
|
## MySQL
|
||||||
|
- 开发环境 MySQL 5.7,生产 MySQL 8.0+
|
||||||
|
- 表字符集:`utf8mb4`,排序规则:`utf8mb4_unicode_ci`
|
||||||
|
|
||||||
|
## Redis
|
||||||
|
- 主要用于 token、黑名单与异步任务状态
|
||||||
|
|
||||||
|
## Token 过期时间
|
||||||
|
- 默认从环境变量读取
|
||||||
|
- **正式逻辑**:存入系统参数 `sys_param`
|
||||||
|
- `security.access_token_minutes`
|
||||||
|
- `security.refresh_token_minutes`
|
||||||
|
|
||||||
|
## 启动示例
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 初始化数据库
|
||||||
|
1. 先执行 `backend/docs/init_mysql.sql`
|
||||||
|
2. 初始化管理员账号(默认:admin / 123456)
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m scripts.init_admin
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
[alembic]
|
||||||
|
script_location = alembic
|
||||||
|
prepend_sys_path = .
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = INFO
|
||||||
|
handlers = console
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers = console
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
from logging.config import fileConfig
|
||||||
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
from alembic import context
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from app.core.config import get_settings
|
||||||
|
from app.models import Base
|
||||||
|
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_url() -> str:
|
||||||
|
settings = get_settings()
|
||||||
|
return (
|
||||||
|
f"mysql+pymysql://{settings.db_user}:{settings.db_password}"
|
||||||
|
f"@{settings.db_host}:{settings.db_port}/{settings.db_name}"
|
||||||
|
"?charset=utf8mb4"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
url = get_url()
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
compare_type=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
configuration = config.get_section(config.config_ini_section) or {}
|
||||||
|
configuration["sqlalchemy.url"] = get_url()
|
||||||
|
connectable = engine_from_config(
|
||||||
|
configuration,
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
compare_type=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
"""add cascade delete to role permission
|
||||||
|
|
||||||
|
Revision ID: 99adde8b54c2
|
||||||
|
Revises:
|
||||||
|
Create Date: 2026-02-13 16:17:57.673230
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '99adde8b54c2'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('sys_dict_item', 'status',
|
||||||
|
existing_type=mysql.TINYINT(display_width=1),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.drop_index('idx_dict_item_type', table_name='sys_dict_item')
|
||||||
|
op.create_index(op.f('ix_sys_dict_item_type_code'), 'sys_dict_item', ['type_code'], unique=False)
|
||||||
|
op.alter_column('sys_dict_type', 'status',
|
||||||
|
existing_type=mysql.TINYINT(display_width=1),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.drop_index('type_code', table_name='sys_dict_type')
|
||||||
|
op.create_unique_constraint('uk_dict_type_code', 'sys_dict_type', ['type_code'])
|
||||||
|
op.alter_column('sys_log', 'id',
|
||||||
|
existing_type=mysql.BIGINT(),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='日志ID',
|
||||||
|
existing_nullable=False,
|
||||||
|
autoincrement=True)
|
||||||
|
op.alter_column('sys_log', 'user_id',
|
||||||
|
existing_type=mysql.BIGINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='操作用户ID',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'username',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=50),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='用户名',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'operation_type',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=50),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='操作类型',
|
||||||
|
existing_nullable=False)
|
||||||
|
op.alter_column('sys_log', 'resource_type',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=50),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='资源类型',
|
||||||
|
existing_nullable=False)
|
||||||
|
op.alter_column('sys_log', 'resource_id',
|
||||||
|
existing_type=mysql.BIGINT(),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='资源ID',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'detail',
|
||||||
|
existing_type=mysql.TEXT(charset='utf8mb4', collation='utf8mb4_0900_ai_ci'),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='操作详情(JSON)',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'ip_address',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=50),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='IP地址',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'user_agent',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=500),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='用户代理',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'status',
|
||||||
|
existing_type=mysql.TINYINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
nullable=False,
|
||||||
|
comment=None,
|
||||||
|
existing_comment='状态:0-失败 1-成功',
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.alter_column('sys_log', 'error_message',
|
||||||
|
existing_type=mysql.TEXT(charset='utf8mb4', collation='utf8mb4_0900_ai_ci'),
|
||||||
|
comment=None,
|
||||||
|
existing_comment='错误信息',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'created_at',
|
||||||
|
existing_type=mysql.DATETIME(),
|
||||||
|
nullable=False,
|
||||||
|
comment=None,
|
||||||
|
existing_comment='操作时间',
|
||||||
|
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||||
|
op.drop_index('idx_created_at', table_name='sys_log')
|
||||||
|
op.drop_index('idx_resource', table_name='sys_log')
|
||||||
|
op.drop_index('idx_user_id', table_name='sys_log')
|
||||||
|
op.create_index(op.f('ix_sys_log_created_at'), 'sys_log', ['created_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_sys_log_resource_id'), 'sys_log', ['resource_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_sys_log_user_id'), 'sys_log', ['user_id'], unique=False)
|
||||||
|
op.drop_constraint('operation_logs_ibfk_1', 'sys_log', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'sys_log', 'sys_user', ['user_id'], ['user_id'])
|
||||||
|
op.drop_table_comment(
|
||||||
|
'sys_log',
|
||||||
|
existing_comment='操作日志表',
|
||||||
|
schema=None
|
||||||
|
)
|
||||||
|
op.alter_column('sys_param', 'param_type',
|
||||||
|
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=20),
|
||||||
|
type_=sa.Enum('string', 'int', 'bool', 'json', name='paramtypeenum', native_enum=False),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'string'"))
|
||||||
|
op.alter_column('sys_param', 'status',
|
||||||
|
existing_type=mysql.TINYINT(display_width=1),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.drop_index('param_key', table_name='sys_param')
|
||||||
|
op.create_unique_constraint('uk_param_key', 'sys_param', ['param_key'])
|
||||||
|
op.alter_column('sys_permission', 'perm_type',
|
||||||
|
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=20),
|
||||||
|
type_=sa.Enum('menu', 'button', name='permissiontypeenum', native_enum=False),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.alter_column('sys_permission', 'status',
|
||||||
|
existing_type=mysql.TINYINT(display_width=1),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.drop_index('idx_perm_parent', table_name='sys_permission')
|
||||||
|
op.create_index(op.f('ix_sys_permission_parent_id'), 'sys_permission', ['parent_id'], unique=False)
|
||||||
|
op.drop_column('sys_permission', 'component')
|
||||||
|
op.alter_column('sys_role', 'status',
|
||||||
|
existing_type=mysql.TINYINT(display_width=1),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.drop_index('idx_role_perm_perm', table_name='sys_role_permission')
|
||||||
|
op.drop_index('idx_role_perm_role', table_name='sys_role_permission')
|
||||||
|
op.create_index(op.f('ix_sys_role_permission_perm_id'), 'sys_role_permission', ['perm_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_sys_role_permission_role_id'), 'sys_role_permission', ['role_id'], unique=False)
|
||||||
|
op.create_foreign_key(None, 'sys_role_permission', 'sys_permission', ['perm_id'], ['perm_id'], ondelete='CASCADE')
|
||||||
|
op.create_foreign_key(None, 'sys_role_permission', 'sys_role', ['role_id'], ['role_id'])
|
||||||
|
op.alter_column('sys_user', 'status',
|
||||||
|
existing_type=mysql.TINYINT(display_width=1),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.drop_index('idx_user_role_role', table_name='sys_user_role')
|
||||||
|
op.drop_index('idx_user_role_user', table_name='sys_user_role')
|
||||||
|
op.create_index(op.f('ix_sys_user_role_role_id'), 'sys_user_role', ['role_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_sys_user_role_user_id'), 'sys_user_role', ['user_id'], unique=False)
|
||||||
|
op.create_foreign_key(None, 'sys_user_role', 'sys_role', ['role_id'], ['role_id'])
|
||||||
|
op.create_foreign_key(None, 'sys_user_role', 'sys_user', ['user_id'], ['user_id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'sys_user_role', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'sys_user_role', type_='foreignkey')
|
||||||
|
op.drop_index(op.f('ix_sys_user_role_user_id'), table_name='sys_user_role')
|
||||||
|
op.drop_index(op.f('ix_sys_user_role_role_id'), table_name='sys_user_role')
|
||||||
|
op.create_index('idx_user_role_user', 'sys_user_role', ['user_id'], unique=False)
|
||||||
|
op.create_index('idx_user_role_role', 'sys_user_role', ['role_id'], unique=False)
|
||||||
|
op.alter_column('sys_user', 'status',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=mysql.TINYINT(display_width=1),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.drop_constraint(None, 'sys_role_permission', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'sys_role_permission', type_='foreignkey')
|
||||||
|
op.drop_index(op.f('ix_sys_role_permission_role_id'), table_name='sys_role_permission')
|
||||||
|
op.drop_index(op.f('ix_sys_role_permission_perm_id'), table_name='sys_role_permission')
|
||||||
|
op.create_index('idx_role_perm_role', 'sys_role_permission', ['role_id'], unique=False)
|
||||||
|
op.create_index('idx_role_perm_perm', 'sys_role_permission', ['perm_id'], unique=False)
|
||||||
|
op.alter_column('sys_role', 'status',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=mysql.TINYINT(display_width=1),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.add_column('sys_permission', sa.Column('component', mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=255), nullable=True))
|
||||||
|
op.drop_index(op.f('ix_sys_permission_parent_id'), table_name='sys_permission')
|
||||||
|
op.create_index('idx_perm_parent', 'sys_permission', ['parent_id'], unique=False)
|
||||||
|
op.alter_column('sys_permission', 'status',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=mysql.TINYINT(display_width=1),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.alter_column('sys_permission', 'perm_type',
|
||||||
|
existing_type=sa.Enum('menu', 'button', name='permissiontypeenum', native_enum=False),
|
||||||
|
type_=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=20),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.drop_constraint('uk_param_key', 'sys_param', type_='unique')
|
||||||
|
op.create_index('param_key', 'sys_param', ['param_key'], unique=True)
|
||||||
|
op.alter_column('sys_param', 'status',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=mysql.TINYINT(display_width=1),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.alter_column('sys_param', 'param_type',
|
||||||
|
existing_type=sa.Enum('string', 'int', 'bool', 'json', name='paramtypeenum', native_enum=False),
|
||||||
|
type_=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=20),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'string'"))
|
||||||
|
op.create_table_comment(
|
||||||
|
'sys_log',
|
||||||
|
'操作日志表',
|
||||||
|
existing_comment=None,
|
||||||
|
schema=None
|
||||||
|
)
|
||||||
|
op.drop_constraint(None, 'sys_log', type_='foreignkey')
|
||||||
|
op.create_foreign_key('operation_logs_ibfk_1', 'sys_log', 'users', ['user_id'], ['id'], referent_schema='nex_docus', onupdate='RESTRICT', ondelete='SET NULL')
|
||||||
|
op.drop_index(op.f('ix_sys_log_user_id'), table_name='sys_log')
|
||||||
|
op.drop_index(op.f('ix_sys_log_resource_id'), table_name='sys_log')
|
||||||
|
op.drop_index(op.f('ix_sys_log_created_at'), table_name='sys_log')
|
||||||
|
op.create_index('idx_user_id', 'sys_log', ['user_id'], unique=False)
|
||||||
|
op.create_index('idx_resource', 'sys_log', ['resource_type', 'resource_id'], unique=False)
|
||||||
|
op.create_index('idx_created_at', 'sys_log', ['created_at'], unique=False)
|
||||||
|
op.alter_column('sys_log', 'created_at',
|
||||||
|
existing_type=mysql.DATETIME(),
|
||||||
|
nullable=True,
|
||||||
|
comment='操作时间',
|
||||||
|
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||||
|
op.alter_column('sys_log', 'error_message',
|
||||||
|
existing_type=mysql.TEXT(charset='utf8mb4', collation='utf8mb4_0900_ai_ci'),
|
||||||
|
comment='错误信息',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'status',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=mysql.TINYINT(),
|
||||||
|
nullable=True,
|
||||||
|
comment='状态:0-失败 1-成功',
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.alter_column('sys_log', 'user_agent',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=500),
|
||||||
|
comment='用户代理',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'ip_address',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=50),
|
||||||
|
comment='IP地址',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'detail',
|
||||||
|
existing_type=mysql.TEXT(charset='utf8mb4', collation='utf8mb4_0900_ai_ci'),
|
||||||
|
comment='操作详情(JSON)',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'resource_id',
|
||||||
|
existing_type=mysql.BIGINT(),
|
||||||
|
comment='资源ID',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'resource_type',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=50),
|
||||||
|
comment='资源类型',
|
||||||
|
existing_nullable=False)
|
||||||
|
op.alter_column('sys_log', 'operation_type',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=50),
|
||||||
|
comment='操作类型',
|
||||||
|
existing_nullable=False)
|
||||||
|
op.alter_column('sys_log', 'username',
|
||||||
|
existing_type=mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_0900_ai_ci', length=50),
|
||||||
|
comment='用户名',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'user_id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=mysql.BIGINT(),
|
||||||
|
comment='操作用户ID',
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('sys_log', 'id',
|
||||||
|
existing_type=mysql.BIGINT(),
|
||||||
|
comment='日志ID',
|
||||||
|
existing_nullable=False,
|
||||||
|
autoincrement=True)
|
||||||
|
op.drop_constraint('uk_dict_type_code', 'sys_dict_type', type_='unique')
|
||||||
|
op.create_index('type_code', 'sys_dict_type', ['type_code'], unique=True)
|
||||||
|
op.alter_column('sys_dict_type', 'status',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=mysql.TINYINT(display_width=1),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
op.drop_index(op.f('ix_sys_dict_item_type_code'), table_name='sys_dict_item')
|
||||||
|
op.create_index('idx_dict_item_type', 'sys_dict_item', ['type_code'], unique=False)
|
||||||
|
op.alter_column('sys_dict_item', 'status',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=mysql.TINYINT(display_width=1),
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'1'"))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.models import AIModel
|
||||||
|
from app.schemas.ai_model import AIModelOut, AIModelCreate, AIModelUpdate
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/ai-models", tags=["ai-models"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[AIModelOut])
|
||||||
|
def list_ai_models(
|
||||||
|
model_type: Optional[str] = Query(None, pattern="^(asr|llm)$"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
query = db.query(AIModel)
|
||||||
|
if model_type:
|
||||||
|
query = query.filter(AIModel.model_type == model_type)
|
||||||
|
return query.order_by(AIModel.created_at.desc()).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=AIModelOut)
|
||||||
|
def create_ai_model(payload: AIModelCreate, db: Session = Depends(get_db)):
|
||||||
|
# Logic: If set as default, unset others of same type
|
||||||
|
if payload.is_default:
|
||||||
|
db.query(AIModel).filter(AIModel.model_type == payload.model_type).update({"is_default": False})
|
||||||
|
|
||||||
|
item = AIModel(**payload.model_dump())
|
||||||
|
db.add(item)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{model_id}", response_model=AIModelOut)
|
||||||
|
def update_ai_model(model_id: int, payload: AIModelUpdate, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(AIModel).filter(AIModel.model_id == model_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="Model not found")
|
||||||
|
|
||||||
|
update_data = payload.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
|
if update_data.get("is_default"):
|
||||||
|
# Unset others
|
||||||
|
target_type = update_data.get("model_type", item.model_type)
|
||||||
|
db.query(AIModel).filter(AIModel.model_type == target_type).update({"is_default": False})
|
||||||
|
|
||||||
|
for k, v in update_data.items():
|
||||||
|
setattr(item, k, v)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{model_id}")
|
||||||
|
def delete_ai_model(model_id: int, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(AIModel).filter(AIModel.model_id == model_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="Model not found")
|
||||||
|
|
||||||
|
if item.is_default:
|
||||||
|
raise HTTPException(status_code=400, detail="Cannot delete default model")
|
||||||
|
|
||||||
|
db.delete(item)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from redis import Redis
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.core.redis import get_redis
|
||||||
|
from app.schemas.auth import LoginRequest, TokenPair, RefreshRequest, LogoutRequest
|
||||||
|
from app.services.auth_service import (
|
||||||
|
authenticate_user,
|
||||||
|
create_token_pair,
|
||||||
|
refresh_access_token,
|
||||||
|
logout_refresh_token,
|
||||||
|
)
|
||||||
|
from app.services.log_service import create_log
|
||||||
|
from app.core.security import decode_token
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_model=TokenPair)
|
||||||
|
def login(
|
||||||
|
payload: LoginRequest,
|
||||||
|
request: Request,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
redis: Redis = Depends(get_redis)
|
||||||
|
):
|
||||||
|
user = authenticate_user(db, payload.username, payload.password)
|
||||||
|
if not user:
|
||||||
|
# Optionally log failed login attempt here
|
||||||
|
create_log(
|
||||||
|
db=db,
|
||||||
|
user_id=None,
|
||||||
|
username=payload.username,
|
||||||
|
operation_type="LOGIN",
|
||||||
|
resource_type="auth",
|
||||||
|
detail=f"Failed login attempt for user: {payload.username}",
|
||||||
|
ip_address=request.client.host if request.client else None,
|
||||||
|
user_agent=request.headers.get("user-agent"),
|
||||||
|
status=0,
|
||||||
|
error_message="Invalid credentials"
|
||||||
|
)
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||||
|
|
||||||
|
access_token, refresh_token = create_token_pair(db, redis, user)
|
||||||
|
|
||||||
|
create_log(
|
||||||
|
db=db,
|
||||||
|
user_id=user.user_id,
|
||||||
|
username=user.username,
|
||||||
|
operation_type="LOGIN",
|
||||||
|
resource_type="auth",
|
||||||
|
detail="User logged in successfully",
|
||||||
|
ip_address=request.client.host if request.client else None,
|
||||||
|
user_agent=request.headers.get("user-agent"),
|
||||||
|
status=1
|
||||||
|
)
|
||||||
|
|
||||||
|
return TokenPair(access_token=access_token, refresh_token=refresh_token)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/refresh", response_model=TokenPair)
|
||||||
|
def refresh(payload: RefreshRequest, db: Session = Depends(get_db), redis: Redis = Depends(get_redis)):
|
||||||
|
try:
|
||||||
|
access_token, refresh_token = refresh_access_token(db, redis, payload.refresh_token)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
|
||||||
|
return TokenPair(access_token=access_token, refresh_token=refresh_token)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/logout")
|
||||||
|
def logout(
|
||||||
|
payload: LogoutRequest,
|
||||||
|
request: Request,
|
||||||
|
redis: Redis = Depends(get_redis),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
user_id = None
|
||||||
|
username = None
|
||||||
|
|
||||||
|
# Try to extract user info for logging before invalidating token
|
||||||
|
try:
|
||||||
|
token_payload = decode_token(payload.refresh_token)
|
||||||
|
if token_payload and "sub" in token_payload:
|
||||||
|
user_id = int(token_payload["sub"])
|
||||||
|
user = db.query(User).filter(User.user_id == user_id).first()
|
||||||
|
if user:
|
||||||
|
username = user.username
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
logout_refresh_token(redis, payload.refresh_token)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
create_log(
|
||||||
|
db=db,
|
||||||
|
user_id=user_id,
|
||||||
|
username=username,
|
||||||
|
operation_type="LOGOUT",
|
||||||
|
resource_type="auth",
|
||||||
|
detail="User logged out",
|
||||||
|
ip_address=request.client.host if request.client else None,
|
||||||
|
user_agent=request.headers.get("user-agent"),
|
||||||
|
status=1
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import func
|
||||||
|
from redis import Redis
|
||||||
|
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.core.redis import get_redis
|
||||||
|
from app.schemas.dashboard import DashboardSummary, UserStats, StorageStats, ServerStats
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
||||||
|
|
||||||
|
|
||||||
|
AUDIO_EXTENSIONS = {".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg"}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_base_dir() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[5]
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_storage(storage_dir: Path) -> tuple[int, int]:
|
||||||
|
total_bytes = 0
|
||||||
|
file_count = 0
|
||||||
|
if not storage_dir.exists():
|
||||||
|
return 0, 0
|
||||||
|
for root, _dirs, files in os.walk(storage_dir):
|
||||||
|
for name in files:
|
||||||
|
file_path = Path(root) / name
|
||||||
|
try:
|
||||||
|
total_bytes += file_path.stat().st_size
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
file_count += 1
|
||||||
|
return total_bytes, file_count
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cpu_memory() -> tuple[float, float]:
|
||||||
|
try:
|
||||||
|
import psutil # type: ignore
|
||||||
|
cpu = psutil.cpu_percent(interval=0.2)
|
||||||
|
mem = psutil.virtual_memory().percent
|
||||||
|
return float(cpu), float(mem)
|
||||||
|
except Exception:
|
||||||
|
return 0.0, 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _get_disk_usage(path: Path) -> float:
|
||||||
|
try:
|
||||||
|
total, used, _free = shutil.disk_usage(path)
|
||||||
|
if total == 0:
|
||||||
|
return 0.0
|
||||||
|
return round(used / total * 100, 2)
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/summary", response_model=DashboardSummary)
|
||||||
|
def get_dashboard_summary(db: Session = Depends(get_db), redis: Redis = Depends(get_redis)):
|
||||||
|
total_users = db.query(func.count(User.user_id)).filter(User.is_deleted.is_(False)).scalar() or 0
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
start = datetime(now.year, now.month, now.day)
|
||||||
|
end = start + timedelta(days=1)
|
||||||
|
new_today = (
|
||||||
|
db.query(func.count(User.user_id))
|
||||||
|
.filter(User.is_deleted.is_(False), User.created_at >= start, User.created_at < end)
|
||||||
|
.scalar()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
online = len(redis.keys("auth:online:*"))
|
||||||
|
except Exception:
|
||||||
|
online = 0
|
||||||
|
|
||||||
|
base_dir = _get_base_dir()
|
||||||
|
storage_dir = base_dir / "storage"
|
||||||
|
total_bytes, audio_files = _scan_storage(storage_dir)
|
||||||
|
total_gb = round(total_bytes / (1024 ** 3), 2)
|
||||||
|
|
||||||
|
cpu, mem = _get_cpu_memory()
|
||||||
|
disk = _get_disk_usage(storage_dir if storage_dir.exists() else base_dir)
|
||||||
|
|
||||||
|
return DashboardSummary(
|
||||||
|
users=UserStats(total=total_users, new_today=new_today, online=online),
|
||||||
|
storage=StorageStats(total_gb=total_gb, audio_files=audio_files),
|
||||||
|
server=ServerStats(cpu=cpu, memory=mem, disk=disk),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.schemas.dict import (
|
||||||
|
DictTypeOut,
|
||||||
|
DictTypeCreate,
|
||||||
|
DictTypeUpdate,
|
||||||
|
DictItemOut,
|
||||||
|
DictItemCreate,
|
||||||
|
DictItemUpdate,
|
||||||
|
)
|
||||||
|
from app.models import DictType, DictItem
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/dicts", tags=["dicts"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/types", response_model=list[DictTypeOut])
|
||||||
|
def list_types(db: Session = Depends(get_db)):
|
||||||
|
return db.query(DictType).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/types", response_model=DictTypeOut)
|
||||||
|
def create_type(payload: DictTypeCreate, db: Session = Depends(get_db)):
|
||||||
|
exists = db.query(DictType).filter(DictType.type_code == payload.type_code).first()
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Type code exists")
|
||||||
|
item = DictType(
|
||||||
|
type_code=payload.type_code,
|
||||||
|
type_name=payload.type_name,
|
||||||
|
status=payload.status,
|
||||||
|
remark=payload.remark,
|
||||||
|
)
|
||||||
|
db.add(item)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/types/{type_id}", response_model=DictTypeOut)
|
||||||
|
def update_type(type_id: int, payload: DictTypeUpdate, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(DictType).filter(DictType.dict_type_id == type_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Type not found")
|
||||||
|
if payload.type_name is not None:
|
||||||
|
item.type_name = payload.type_name
|
||||||
|
if payload.status is not None:
|
||||||
|
item.status = payload.status
|
||||||
|
if payload.remark is not None:
|
||||||
|
item.remark = payload.remark
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/types/{type_id}")
|
||||||
|
def delete_type(type_id: int, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(DictType).filter(DictType.dict_type_id == type_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Type not found")
|
||||||
|
db.delete(item)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/items", response_model=list[DictItemOut])
|
||||||
|
def list_items(type_code: str, db: Session = Depends(get_db)):
|
||||||
|
return db.query(DictItem).filter(DictItem.type_code == type_code).order_by(DictItem.sort_order).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/items", response_model=DictItemOut)
|
||||||
|
def create_item(payload: DictItemCreate, db: Session = Depends(get_db)):
|
||||||
|
item = DictItem(
|
||||||
|
type_code=payload.type_code,
|
||||||
|
item_label=payload.item_label,
|
||||||
|
item_value=payload.item_value,
|
||||||
|
sort_order=payload.sort_order,
|
||||||
|
status=payload.status,
|
||||||
|
remark=payload.remark,
|
||||||
|
)
|
||||||
|
db.add(item)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/items/{item_id}", response_model=DictItemOut)
|
||||||
|
def update_item(item_id: int, payload: DictItemUpdate, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(DictItem).filter(DictItem.dict_item_id == item_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
|
||||||
|
if payload.item_label is not None:
|
||||||
|
item.item_label = payload.item_label
|
||||||
|
if payload.item_value is not None:
|
||||||
|
item.item_value = payload.item_value
|
||||||
|
if payload.sort_order is not None:
|
||||||
|
item.sort_order = payload.sort_order
|
||||||
|
if payload.status is not None:
|
||||||
|
item.status = payload.status
|
||||||
|
if payload.remark is not None:
|
||||||
|
item.remark = payload.remark
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/items/{item_id}")
|
||||||
|
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(DictItem).filter(DictItem.dict_item_id == item_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
|
||||||
|
db.delete(item)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import Optional
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.models.sys_log import SysLog
|
||||||
|
from app.schemas.sys_log import SysLogPage
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/logs", tags=["logs"])
|
||||||
|
|
||||||
|
@router.get("", response_model=SysLogPage)
|
||||||
|
def list_logs(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
user_id: Optional[int] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
operation_type: Optional[str] = None,
|
||||||
|
status: Optional[int] = None,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
query = db.query(SysLog)
|
||||||
|
|
||||||
|
if username:
|
||||||
|
query = query.filter(SysLog.username.ilike(f"%{username}%"))
|
||||||
|
elif user_id:
|
||||||
|
query = query.filter(SysLog.user_id == user_id)
|
||||||
|
|
||||||
|
if operation_type:
|
||||||
|
query = query.filter(SysLog.operation_type == operation_type)
|
||||||
|
if status is not None:
|
||||||
|
query = query.filter(SysLog.status == status)
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
items = query.order_by(SysLog.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||||
|
|
||||||
|
return {"items": items, "total": total}
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import or_, and_, func, desc
|
||||||
|
from app.core.db import get_db, SessionLocal
|
||||||
|
from app.core.deps import get_current_user
|
||||||
|
from app.models import Meeting, MeetingAttendee, User, TranscriptSegment, SummarizeTask, TranscriptTask
|
||||||
|
from app.schemas.meeting import MeetingOut, MeetingDetailOut, MeetingUpdate, MeetingListOut
|
||||||
|
from app.services.meeting_service import MeetingService
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/meetings", tags=["meetings"])
|
||||||
|
|
||||||
|
async def run_summarize_worker(task_id: str):
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
await MeetingService.process_summarize_task(db, task_id)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@router.post("/{meeting_id}/summarize")
|
||||||
|
async def start_summarize(
|
||||||
|
meeting_id: int,
|
||||||
|
payload: dict,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
task = await MeetingService.create_summarize_task(
|
||||||
|
db,
|
||||||
|
meeting_id,
|
||||||
|
payload.get("prompt_id"),
|
||||||
|
payload.get("model_id"),
|
||||||
|
payload.get("extra_prompt", "")
|
||||||
|
)
|
||||||
|
background_tasks.add_task(run_summarize_worker, task.task_id)
|
||||||
|
return {"task_id": task.task_id, "status": task.status}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@router.get("/tasks", response_model=dict)
|
||||||
|
def list_all_tasks(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
task_type: Optional[str] = Query(None), # summarize, transcript
|
||||||
|
status: Optional[str] = Query(None),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
统一任务监控接口,合并转译和总结任务
|
||||||
|
"""
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
# 1. 获取总结任务
|
||||||
|
sum_query = db.query(SummarizeTask, Meeting.title, User.username).join(
|
||||||
|
Meeting, SummarizeTask.meeting_id == Meeting.meeting_id
|
||||||
|
).join(
|
||||||
|
User, Meeting.user_id == User.user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
sum_query = sum_query.filter(SummarizeTask.status == status)
|
||||||
|
|
||||||
|
for t, m_title, username in sum_query.order_by(desc(SummarizeTask.created_at)).limit(50).all():
|
||||||
|
tasks.append({
|
||||||
|
"task_id": t.task_id,
|
||||||
|
"type": "总结",
|
||||||
|
"meeting_title": m_title,
|
||||||
|
"creator": username,
|
||||||
|
"status": t.status,
|
||||||
|
"progress": t.progress,
|
||||||
|
"created_at": t.created_at,
|
||||||
|
"meeting_id": t.meeting_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. 获取转译任务
|
||||||
|
trans_query = db.query(TranscriptTask, Meeting.title, User.username).join(
|
||||||
|
Meeting, TranscriptTask.meeting_id == Meeting.meeting_id
|
||||||
|
).join(
|
||||||
|
User, Meeting.user_id == User.user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
trans_query = trans_query.filter(TranscriptTask.status == status)
|
||||||
|
|
||||||
|
for t, m_title, username in trans_query.order_by(desc(TranscriptTask.created_at)).limit(50).all():
|
||||||
|
tasks.append({
|
||||||
|
"task_id": t.task_id,
|
||||||
|
"type": "转译",
|
||||||
|
"meeting_title": m_title,
|
||||||
|
"creator": username,
|
||||||
|
"status": t.status,
|
||||||
|
"progress": t.progress,
|
||||||
|
"created_at": t.created_at,
|
||||||
|
"meeting_id": t.meeting_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# 按时间全局排序
|
||||||
|
tasks.sort(key=lambda x: x["created_at"], reverse=True)
|
||||||
|
|
||||||
|
return {"items": tasks}
|
||||||
|
|
||||||
|
@router.get("/tasks/{task_id}")
|
||||||
|
def get_task_progress(
|
||||||
|
task_id: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
task = MeetingService.get_task_status(db, task_id)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
return {
|
||||||
|
"task_id": task.task_id,
|
||||||
|
"status": task.status,
|
||||||
|
"progress": task.progress,
|
||||||
|
"result": task.result,
|
||||||
|
"error": task.error_message
|
||||||
|
}
|
||||||
|
|
||||||
|
# ... 其余接口保持不变
|
||||||
|
@router.get("", response_model=MeetingListOut)
|
||||||
|
def list_meetings(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
scope: str = Query("all", description="all, created, joined"),
|
||||||
|
keyword: Optional[str] = Query(None),
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(10, ge=1, le=100),
|
||||||
|
):
|
||||||
|
query = db.query(Meeting)
|
||||||
|
if scope == "created":
|
||||||
|
query = query.filter(Meeting.user_id == current_user.user_id)
|
||||||
|
elif scope == "joined":
|
||||||
|
query = query.join(MeetingAttendee).filter(MeetingAttendee.user_id == current_user.user_id)
|
||||||
|
else:
|
||||||
|
query = query.outerjoin(MeetingAttendee).filter(
|
||||||
|
or_(Meeting.user_id == current_user.user_id, MeetingAttendee.user_id == current_user.user_id)
|
||||||
|
)
|
||||||
|
if keyword:
|
||||||
|
query = query.filter(Meeting.title.contains(keyword))
|
||||||
|
total = query.distinct().count()
|
||||||
|
results = query.distinct().order_by(Meeting.meeting_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||||
|
items = []
|
||||||
|
for m in results:
|
||||||
|
item = MeetingOut.model_validate(m)
|
||||||
|
item.creator_name = m.creator.display_name if m.creator else "Unknown"
|
||||||
|
item.creator_avatar = m.creator.avatar if m.creator else None
|
||||||
|
attendees = []
|
||||||
|
for att in m.attendees:
|
||||||
|
if att.user:
|
||||||
|
attendees.append({"attendee_id": att.attendee_id,"user_id": att.user_id,"display_name": att.user.display_name,"avatar": att.user.avatar})
|
||||||
|
item.attendees = attendees
|
||||||
|
items.append(item)
|
||||||
|
return {"items": items, "total": total}
|
||||||
|
|
||||||
|
@router.get("/{meeting_id}", response_model=MeetingDetailOut)
|
||||||
|
def get_meeting_detail(
|
||||||
|
meeting_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
meeting = db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first()
|
||||||
|
if not meeting:
|
||||||
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
||||||
|
is_attendee = any(a.user_id == current_user.user_id for a in meeting.attendees)
|
||||||
|
if meeting.user_id != current_user.user_id and not is_attendee:
|
||||||
|
raise HTTPException(status_code=403, detail="Not authorized")
|
||||||
|
item = MeetingDetailOut.model_validate(meeting)
|
||||||
|
item.creator_name = meeting.creator.display_name if meeting.creator else "Unknown"
|
||||||
|
item.creator_avatar = meeting.creator.avatar if meeting.creator else None
|
||||||
|
item.attendees = [
|
||||||
|
{"attendee_id": a.attendee_id, "user_id": a.user_id, "display_name": a.user.display_name if a.user else "Unknown", "avatar": a.user.avatar if a.user else None}
|
||||||
|
for a in meeting.attendees
|
||||||
|
]
|
||||||
|
item.segments = sorted([s for s in meeting.segments], key=lambda x: x.start_time_ms)
|
||||||
|
return item
|
||||||
|
|
||||||
|
@router.delete("/{meeting_id}")
|
||||||
|
def delete_meeting(
|
||||||
|
meeting_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
meeting = db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first()
|
||||||
|
if not meeting:
|
||||||
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
||||||
|
if meeting.user_id != current_user.user_id:
|
||||||
|
role_codes = [ur.role.role_code for ur in current_user.roles]
|
||||||
|
if "admin" not in role_codes and "superuser" not in role_codes:
|
||||||
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
db.delete(meeting)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.core.deps import get_current_user
|
||||||
|
from app.models import Permission, RolePermission, UserRole, User
|
||||||
|
from app.models.enums import StatusEnum, PermissionTypeEnum
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/menus", tags=["menus"])
|
||||||
|
|
||||||
|
|
||||||
|
def build_tree(items):
|
||||||
|
by_parent = {}
|
||||||
|
for item in items:
|
||||||
|
by_parent.setdefault(item.parent_id, []).append(item)
|
||||||
|
|
||||||
|
def make_node(item):
|
||||||
|
return {
|
||||||
|
"id": item.perm_id,
|
||||||
|
"name": item.name,
|
||||||
|
"code": item.code,
|
||||||
|
"type": item.perm_type,
|
||||||
|
"level": item.level,
|
||||||
|
"path": item.path,
|
||||||
|
"icon": item.icon,
|
||||||
|
"children": [make_node(child) for child in by_parent.get(item.perm_id, [])],
|
||||||
|
}
|
||||||
|
|
||||||
|
roots = by_parent.get(None, []) + by_parent.get(0, [])
|
||||||
|
return [make_node(node) for node in roots]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tree")
|
||||||
|
def menu_tree(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
items = (
|
||||||
|
db.query(Permission)
|
||||||
|
.join(RolePermission, RolePermission.perm_id == Permission.perm_id)
|
||||||
|
.join(UserRole, UserRole.role_id == RolePermission.role_id)
|
||||||
|
.filter(UserRole.user_id == current_user.user_id)
|
||||||
|
.filter(Permission.status == int(StatusEnum.ENABLED))
|
||||||
|
.filter(Permission.perm_type.in_([PermissionTypeEnum.MENU, PermissionTypeEnum.BUTTON]))
|
||||||
|
.order_by(Permission.level, Permission.sort_order)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return build_tree(items)
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.schemas.param import ParamOut, ParamCreate, ParamUpdate
|
||||||
|
from app.models import SystemParam
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/params", tags=["params"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[ParamOut])
|
||||||
|
def list_params(db: Session = Depends(get_db)):
|
||||||
|
return db.query(SystemParam).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=ParamOut)
|
||||||
|
def create_param(payload: ParamCreate, db: Session = Depends(get_db)):
|
||||||
|
exists = db.query(SystemParam).filter(SystemParam.param_key == payload.param_key).first()
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Param key exists")
|
||||||
|
item = SystemParam(
|
||||||
|
param_key=payload.param_key,
|
||||||
|
param_value=payload.param_value,
|
||||||
|
param_type=payload.param_type,
|
||||||
|
status=payload.status,
|
||||||
|
is_system=payload.is_system,
|
||||||
|
description=payload.description,
|
||||||
|
)
|
||||||
|
db.add(item)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{param_id}", response_model=ParamOut)
|
||||||
|
def update_param(param_id: int, payload: ParamUpdate, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(SystemParam).filter(SystemParam.param_id == param_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Param not found")
|
||||||
|
if payload.param_value is not None:
|
||||||
|
item.param_value = payload.param_value
|
||||||
|
if payload.param_type is not None:
|
||||||
|
item.param_type = payload.param_type
|
||||||
|
if payload.status is not None:
|
||||||
|
item.status = payload.status
|
||||||
|
if payload.is_system is not None:
|
||||||
|
item.is_system = payload.is_system
|
||||||
|
if payload.description is not None:
|
||||||
|
item.description = payload.description
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{param_id}")
|
||||||
|
def delete_param(param_id: int, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(SystemParam).filter(SystemParam.param_id == param_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Param not found")
|
||||||
|
db.delete(item)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.core.deps import get_current_user
|
||||||
|
from app.schemas.permission import PermissionOut, PermissionCreate, PermissionUpdate
|
||||||
|
from app.models import Permission, RolePermission, UserRole, User
|
||||||
|
from app.models.enums import StatusEnum
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/permissions", tags=["permissions"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[PermissionOut])
|
||||||
|
def list_permissions(db: Session = Depends(get_db)):
|
||||||
|
return db.query(Permission).order_by(Permission.level, Permission.sort_order).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=PermissionOut)
|
||||||
|
def create_permission(payload: PermissionCreate, db: Session = Depends(get_db)):
|
||||||
|
exists = db.query(Permission).filter(Permission.code == payload.code).first()
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Permission code exists")
|
||||||
|
item = Permission(**payload.model_dump())
|
||||||
|
db.add(item)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{perm_id}", response_model=PermissionOut)
|
||||||
|
def update_permission(perm_id: int, payload: PermissionUpdate, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(Permission).filter(Permission.perm_id == perm_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Permission not found")
|
||||||
|
for k, v in payload.model_dump(exclude_unset=True).items():
|
||||||
|
setattr(item, k, v)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{perm_id}")
|
||||||
|
def delete_permission(perm_id: int, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(Permission).filter(Permission.perm_id == perm_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Permission not found")
|
||||||
|
db.delete(item)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me")
|
||||||
|
def my_permissions(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
rows = (
|
||||||
|
db.query(Permission.code)
|
||||||
|
.join(RolePermission, RolePermission.perm_id == Permission.perm_id)
|
||||||
|
.join(UserRole, UserRole.role_id == RolePermission.role_id)
|
||||||
|
.filter(UserRole.user_id == current_user.user_id)
|
||||||
|
.filter(Permission.status == int(StatusEnum.ENABLED))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [r[0] for r in rows]
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import or_, and_
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.core.deps import get_current_user
|
||||||
|
from app.models import User, PromptTemplate, UserPromptConfig
|
||||||
|
from app.schemas.prompt import (
|
||||||
|
PromptTemplateOut,
|
||||||
|
PromptTemplateCreate,
|
||||||
|
PromptTemplateUpdate,
|
||||||
|
UserPromptConfigUpdate
|
||||||
|
)
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/prompts", tags=["prompts"])
|
||||||
|
|
||||||
|
def is_admin(user: User):
|
||||||
|
role_codes = [ur.role.role_code for ur in user.roles] if hasattr(user, 'roles') else []
|
||||||
|
return "admin" in role_codes or "superuser" in role_codes
|
||||||
|
|
||||||
|
@router.get("", response_model=List[PromptTemplateOut])
|
||||||
|
def list_prompts(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
keyword: Optional[str] = Query(None),
|
||||||
|
category: Optional[str] = Query(None),
|
||||||
|
scope: str = Query("personal", description="system or personal")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
根据 scope 返回不同范围的模板
|
||||||
|
"""
|
||||||
|
filters = []
|
||||||
|
|
||||||
|
if scope == "system":
|
||||||
|
# 系统管理入口:仅展示系统级模板
|
||||||
|
if not is_admin(current_user):
|
||||||
|
raise HTTPException(status_code=403, detail="无权访问系统模板库")
|
||||||
|
filters.append(PromptTemplate.is_system == True)
|
||||||
|
else:
|
||||||
|
# 个人管理入口:展示已发布的系统模板 + 自己的个人模板
|
||||||
|
accessibility_filter = or_(
|
||||||
|
and_(PromptTemplate.is_system == True, PromptTemplate.status == 1),
|
||||||
|
PromptTemplate.user_id == current_user.user_id
|
||||||
|
)
|
||||||
|
filters.append(accessibility_filter)
|
||||||
|
|
||||||
|
if keyword:
|
||||||
|
filters.append(or_(
|
||||||
|
PromptTemplate.name.contains(keyword),
|
||||||
|
PromptTemplate.description.contains(keyword),
|
||||||
|
PromptTemplate.content.contains(keyword)
|
||||||
|
))
|
||||||
|
if category:
|
||||||
|
filters.append(PromptTemplate.category == category)
|
||||||
|
|
||||||
|
query = db.query(
|
||||||
|
PromptTemplate,
|
||||||
|
UserPromptConfig.is_active,
|
||||||
|
UserPromptConfig.user_sort_order
|
||||||
|
).outerjoin(
|
||||||
|
UserPromptConfig,
|
||||||
|
(UserPromptConfig.template_id == PromptTemplate.id) & (UserPromptConfig.user_id == current_user.user_id)
|
||||||
|
).filter(and_(*filters))
|
||||||
|
|
||||||
|
results = query.all()
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for template, is_active, user_sort_order in results:
|
||||||
|
item = PromptTemplateOut.model_validate(template)
|
||||||
|
item.is_active = is_active if is_active is not None else True
|
||||||
|
item.user_sort_order = user_sort_order if user_sort_order is not None else template.sort_order
|
||||||
|
out.append(item)
|
||||||
|
|
||||||
|
# 排序:系统管理入口按全局排序,个人入口按个人排序
|
||||||
|
if scope == "system":
|
||||||
|
out.sort(key=lambda x: x.sort_order)
|
||||||
|
else:
|
||||||
|
out.sort(key=lambda x: (x.user_sort_order, x.sort_order))
|
||||||
|
return out
|
||||||
|
|
||||||
|
@router.post("", response_model=PromptTemplateOut)
|
||||||
|
def create_prompt(
|
||||||
|
payload: PromptTemplateCreate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
user_is_admin = is_admin(current_user)
|
||||||
|
item_data = payload.model_dump()
|
||||||
|
|
||||||
|
# 如果显式指定为系统模板,必须是管理员
|
||||||
|
if item_data.get("is_system"):
|
||||||
|
if not user_is_admin:
|
||||||
|
raise HTTPException(status_code=403, detail="仅管理员可创建系统模板")
|
||||||
|
item_data["user_id"] = None
|
||||||
|
else:
|
||||||
|
item_data["user_id"] = current_user.user_id
|
||||||
|
item_data["is_system"] = False
|
||||||
|
|
||||||
|
item = PromptTemplate(**item_data)
|
||||||
|
db.add(item)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
|
||||||
|
res = PromptTemplateOut.model_validate(item)
|
||||||
|
res.is_active = True
|
||||||
|
res.user_sort_order = item.sort_order
|
||||||
|
return res
|
||||||
|
|
||||||
|
@router.put("/{prompt_id}", response_model=PromptTemplateOut)
|
||||||
|
def update_prompt(
|
||||||
|
prompt_id: int,
|
||||||
|
payload: PromptTemplateUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
item = db.query(PromptTemplate).filter(PromptTemplate.id == prompt_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="模板不存在")
|
||||||
|
|
||||||
|
user_is_admin = is_admin(current_user)
|
||||||
|
|
||||||
|
# 系统模板仅管理员可改
|
||||||
|
if item.is_system and not user_is_admin:
|
||||||
|
raise HTTPException(status_code=403, detail="无权修改系统模板")
|
||||||
|
# 个人模板仅主人可改
|
||||||
|
if not item.is_system and item.user_id != current_user.user_id:
|
||||||
|
raise HTTPException(status_code=403, detail="无权修改他人模板")
|
||||||
|
|
||||||
|
update_data = payload.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
|
if "is_system" in update_data:
|
||||||
|
if user_is_admin:
|
||||||
|
new_val = update_data.pop("is_system")
|
||||||
|
item.is_system = new_val
|
||||||
|
item.user_id = None if new_val else current_user.user_id
|
||||||
|
else:
|
||||||
|
update_data.pop("is_system")
|
||||||
|
|
||||||
|
for k, v in update_data.items():
|
||||||
|
setattr(item, k, v)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
|
return PromptTemplateOut.model_validate(item)
|
||||||
|
|
||||||
|
@router.delete("/{prompt_id}")
|
||||||
|
def delete_prompt(
|
||||||
|
prompt_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
item = db.query(PromptTemplate).filter(PromptTemplate.id == prompt_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="模板不存在")
|
||||||
|
|
||||||
|
user_is_admin = is_admin(current_user)
|
||||||
|
if item.is_system and not user_is_admin:
|
||||||
|
raise HTTPException(status_code=403, detail="无权删除系统模板")
|
||||||
|
if not item.is_system and item.user_id != current_user.user_id:
|
||||||
|
raise HTTPException(status_code=403, detail="无权删除他人模板")
|
||||||
|
|
||||||
|
db.delete(item)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
@router.patch("/{prompt_id}/config")
|
||||||
|
def update_user_config(
|
||||||
|
prompt_id: int,
|
||||||
|
payload: UserPromptConfigUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
config = db.query(UserPromptConfig).filter(
|
||||||
|
UserPromptConfig.template_id == prompt_id,
|
||||||
|
UserPromptConfig.user_id == current_user.user_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
template = db.query(PromptTemplate).filter(PromptTemplate.id == prompt_id).first()
|
||||||
|
config = UserPromptConfig(
|
||||||
|
user_id=current_user.user_id,
|
||||||
|
template_id=prompt_id,
|
||||||
|
is_active=True,
|
||||||
|
user_sort_order=template.sort_order if template else 0
|
||||||
|
)
|
||||||
|
db.add(config)
|
||||||
|
|
||||||
|
for k, v in payload.model_dump(exclude_unset=True).items():
|
||||||
|
setattr(config, k, v)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.schemas.role import RoleOut, RoleCreate, RoleUpdate, RolePermissionUpdate
|
||||||
|
from app.models import Role, RolePermission, Permission, UserRole
|
||||||
|
from app.models.enums import StatusEnum
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/roles", tags=["roles"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[RoleOut])
|
||||||
|
def list_roles(db: Session = Depends(get_db)):
|
||||||
|
return db.query(Role).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=RoleOut)
|
||||||
|
def create_role(payload: RoleCreate, db: Session = Depends(get_db)):
|
||||||
|
exists = db.query(Role).filter(Role.role_code == payload.role_code).first()
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Role code exists")
|
||||||
|
role = Role(
|
||||||
|
role_code=payload.role_code,
|
||||||
|
role_name=payload.role_name,
|
||||||
|
status=payload.status,
|
||||||
|
remark=payload.remark,
|
||||||
|
)
|
||||||
|
db.add(role)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(role)
|
||||||
|
return role
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{role_id}", response_model=RoleOut)
|
||||||
|
def update_role(role_id: int, payload: RoleUpdate, db: Session = Depends(get_db)):
|
||||||
|
role = db.query(Role).filter(Role.role_id == role_id).first()
|
||||||
|
if not role:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Role not found")
|
||||||
|
if payload.role_name is not None:
|
||||||
|
role.role_name = payload.role_name
|
||||||
|
if payload.status is not None:
|
||||||
|
role.status = payload.status
|
||||||
|
if payload.remark is not None:
|
||||||
|
role.remark = payload.remark
|
||||||
|
db.commit()
|
||||||
|
db.refresh(role)
|
||||||
|
return role
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{role_id}")
|
||||||
|
def delete_role(role_id: int, db: Session = Depends(get_db)):
|
||||||
|
role = db.query(Role).filter(Role.role_id == role_id).first()
|
||||||
|
if not role:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Role not found")
|
||||||
|
|
||||||
|
# Check if users are assigned
|
||||||
|
user_count = db.query(UserRole).filter(UserRole.role_id == role_id).count()
|
||||||
|
if user_count > 0:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete role with assigned users")
|
||||||
|
|
||||||
|
# Delete permissions
|
||||||
|
db.query(RolePermission).filter(RolePermission.role_id == role_id).delete()
|
||||||
|
|
||||||
|
db.delete(role)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{role_id}/permissions")
|
||||||
|
def get_role_permissions(role_id: int, db: Session = Depends(get_db)):
|
||||||
|
rows = db.query(RolePermission.perm_id).filter(RolePermission.role_id == role_id).all()
|
||||||
|
return [r[0] for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{role_id}/permissions")
|
||||||
|
def update_role_permissions(role_id: int, payload: RolePermissionUpdate, db: Session = Depends(get_db)):
|
||||||
|
exists = db.query(Role).filter(Role.role_id == role_id).first()
|
||||||
|
if not exists:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Role not found")
|
||||||
|
|
||||||
|
db.query(RolePermission).filter(RolePermission.role_id == role_id).delete()
|
||||||
|
for perm_id in payload.perm_ids:
|
||||||
|
db.add(RolePermission(role_id=role_id, perm_id=perm_id))
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{role_id}/users")
|
||||||
|
def get_role_users(role_id: int, db: Session = Depends(get_db)):
|
||||||
|
from app.models import User
|
||||||
|
users = (
|
||||||
|
db.query(User)
|
||||||
|
.join(UserRole, UserRole.user_id == User.user_id)
|
||||||
|
.filter(UserRole.role_id == role_id, User.is_deleted.is_(False))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return users
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.core.deps import get_current_user
|
||||||
|
from app.schemas.user import UserOut, UserMe, UserCreate, UserUpdate, PasswordChange
|
||||||
|
from app.models import User, UserRole, Role
|
||||||
|
from app.core.security import hash_password, verify_password
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/users", tags=["users"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me", response_model=UserMe)
|
||||||
|
def get_me(current_user: User = Depends(get_current_user)):
|
||||||
|
# 优先返回 role_code 用于前端权限判断
|
||||||
|
roles = [ur.role.role_code for ur in current_user.roles]
|
||||||
|
user_dict = UserOut.model_validate(current_user).model_dump()
|
||||||
|
user_dict["roles"] = roles
|
||||||
|
return user_dict
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/me/password")
|
||||||
|
def change_password(
|
||||||
|
payload: PasswordChange,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
if not verify_password(payload.old_password, current_user.password_hash):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Current password incorrect"
|
||||||
|
)
|
||||||
|
|
||||||
|
current_user.password_hash = hash_password(payload.new_password)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/me/avatar")
|
||||||
|
def upload_avatar(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
if not file.content_type.startswith("image/"):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid file type")
|
||||||
|
|
||||||
|
# Path resolution: backend/app/api/v1/endpoints/users.py -> root
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[5]
|
||||||
|
# parents[0] is endpoints, [1] v1, [2] api, [3] app, [4] backend, [5] root?
|
||||||
|
# Let's verify:
|
||||||
|
# file: .../backend/app/api/v1/endpoints/users.py
|
||||||
|
# parent: .../backend/app/api/v1/endpoints
|
||||||
|
# parents[0]: endpoints
|
||||||
|
# parents[1]: v1
|
||||||
|
# parents[2]: api
|
||||||
|
# parents[3]: app
|
||||||
|
# parents[4]: backend
|
||||||
|
# parents[5]: root (where storage is)
|
||||||
|
|
||||||
|
STORAGE_DIR = BASE_DIR / "storage"
|
||||||
|
user_dir = STORAGE_DIR / "users" / str(current_user.user_id) / "avatar"
|
||||||
|
user_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
file_path = user_dir / file.filename
|
||||||
|
with open(file_path, "wb") as buffer:
|
||||||
|
shutil.copyfileobj(file.file, buffer)
|
||||||
|
|
||||||
|
# Assuming backend serves /storage
|
||||||
|
avatar_url = f"/storage/users/{current_user.user_id}/avatar/{file.filename}"
|
||||||
|
|
||||||
|
current_user.avatar = avatar_url
|
||||||
|
db.commit()
|
||||||
|
db.refresh(current_user)
|
||||||
|
|
||||||
|
return {"avatar": avatar_url}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[UserOut])
|
||||||
|
def list_users(db: Session = Depends(get_db)):
|
||||||
|
return db.query(User).filter(User.is_deleted.is_(False)).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=UserOut)
|
||||||
|
def create_user(payload: UserCreate, db: Session = Depends(get_db)):
|
||||||
|
exists = db.query(User).filter(User.username == payload.username).first()
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username exists")
|
||||||
|
|
||||||
|
password = payload.password or "123456"
|
||||||
|
user = User(
|
||||||
|
username=payload.username,
|
||||||
|
display_name=payload.display_name,
|
||||||
|
email=payload.email,
|
||||||
|
phone=payload.phone,
|
||||||
|
password_hash=hash_password(password),
|
||||||
|
status=payload.status,
|
||||||
|
is_deleted=False,
|
||||||
|
)
|
||||||
|
db.add(user)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
for role_id in payload.role_ids:
|
||||||
|
db.add(UserRole(user_id=user.user_id, role_id=role_id))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{user_id}", response_model=UserOut)
|
||||||
|
def update_user(user_id: int, payload: UserUpdate, db: Session = Depends(get_db)):
|
||||||
|
user = db.query(User).filter(User.user_id == user_id, User.is_deleted.is_(False)).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
|
|
||||||
|
if payload.display_name is not None:
|
||||||
|
user.display_name = payload.display_name
|
||||||
|
if payload.email is not None:
|
||||||
|
user.email = payload.email
|
||||||
|
if payload.phone is not None:
|
||||||
|
user.phone = payload.phone
|
||||||
|
if payload.status is not None:
|
||||||
|
user.status = payload.status
|
||||||
|
|
||||||
|
if payload.role_ids is not None:
|
||||||
|
db.query(UserRole).filter(UserRole.user_id == user.user_id).delete()
|
||||||
|
for role_id in payload.role_ids:
|
||||||
|
db.add(UserRole(user_id=user.user_id, role_id=role_id))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{user_id}")
|
||||||
|
def delete_user(user_id: int, db: Session = Depends(get_db)):
|
||||||
|
user = db.query(User).filter(User.user_id == user_id, User.is_deleted.is_(False)).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
|
user.is_deleted = True
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{user_id}/roles")
|
||||||
|
def get_user_roles(user_id: int, db: Session = Depends(get_db)):
|
||||||
|
roles = (
|
||||||
|
db.query(Role.role_id)
|
||||||
|
.join(UserRole, UserRole.role_id == Role.role_id)
|
||||||
|
.filter(UserRole.user_id == user_id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [r[0] for r in roles]
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{user_id}/roles")
|
||||||
|
def update_user_roles(user_id: int, role_ids: list[int], db: Session = Depends(get_db)):
|
||||||
|
db.query(UserRole).filter(UserRole.user_id == user_id).delete()
|
||||||
|
for role_id in role_ids:
|
||||||
|
db.add(UserRole(user_id=user_id, role_id=role_id))
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{user_id}/reset-password")
|
||||||
|
def reset_password(user_id: int, password: str, db: Session = Depends(get_db)):
|
||||||
|
user = db.query(User).filter(User.user_id == user_id, User.is_deleted.is_(False)).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
|
user.password_hash = hash_password(password)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from app.api.v1.endpoints import auth, users, permissions, roles, menus, dicts, params, logs, dashboard, prompts, ai_models, meetings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(auth.router)
|
||||||
|
router.include_router(users.router)
|
||||||
|
router.include_router(roles.router)
|
||||||
|
router.include_router(permissions.router)
|
||||||
|
router.include_router(menus.router)
|
||||||
|
router.include_router(dicts.router)
|
||||||
|
router.include_router(params.router)
|
||||||
|
router.include_router(logs.router)
|
||||||
|
router.include_router(dashboard.router)
|
||||||
|
router.include_router(prompts.router)
|
||||||
|
router.include_router(ai_models.router)
|
||||||
|
router.include_router(meetings.router)
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
ENV_PATH = Path(__file__).resolve().parents[2] / ".env"
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(env_file=str(ENV_PATH), env_file_encoding="utf-8", extra="ignore")
|
||||||
|
|
||||||
|
app_name: str = "nex_basse"
|
||||||
|
env: str = "dev"
|
||||||
|
api_v1_prefix: str = "/api/v1"
|
||||||
|
|
||||||
|
# Database
|
||||||
|
db_host: str = "127.0.0.1"
|
||||||
|
db_port: int = 3306
|
||||||
|
db_user: str = "root"
|
||||||
|
db_password: str = ""
|
||||||
|
db_name: str = "nex_basse"
|
||||||
|
db_echo: bool = False
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
redis_host: str = "127.0.0.1"
|
||||||
|
redis_port: int = 6379
|
||||||
|
redis_db: int = 0
|
||||||
|
redis_password: str | None = None
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
jwt_secret_key: str = "change_me"
|
||||||
|
jwt_algorithm: str = "HS256"
|
||||||
|
jwt_access_token_minutes: int = 60
|
||||||
|
jwt_refresh_token_minutes: int = 60 * 24 * 7
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
return Settings()
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
|
||||||
|
def setup_cors(app: FastAPI):
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from .config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
|
||||||
|
def build_db_url() -> str:
|
||||||
|
return (
|
||||||
|
f"mysql+pymysql://{settings.db_user}:{settings.db_password}"
|
||||||
|
f"@{settings.db_host}:{settings.db_port}/{settings.db_name}"
|
||||||
|
"?charset=utf8mb4"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
build_db_url(),
|
||||||
|
echo=settings.db_echo,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from redis import Redis
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.core.security import decode_token
|
||||||
|
from app.core.redis import get_redis
|
||||||
|
from app.models import User
|
||||||
|
from app.models.enums import StatusEnum
|
||||||
|
|
||||||
|
|
||||||
|
bearer_scheme = HTTPBearer()
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user(
|
||||||
|
creds: HTTPAuthorizationCredentials = Depends(bearer_scheme),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
redis: Redis = Depends(get_redis),
|
||||||
|
) -> User:
|
||||||
|
token = creds.credentials
|
||||||
|
try:
|
||||||
|
payload = decode_token(token)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||||
|
|
||||||
|
if payload.get("type") != "access":
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||||
|
|
||||||
|
user_id = payload.get("sub")
|
||||||
|
# Enforce redis-backed online session; if missing, force re-login
|
||||||
|
online_key = f"auth:online:{user_id}"
|
||||||
|
if not redis.exists(online_key):
|
||||||
|
# Fallback: if refresh token exists for this user, recreate online key
|
||||||
|
cursor = 0
|
||||||
|
found_ttl = None
|
||||||
|
while True:
|
||||||
|
cursor, keys = redis.scan(cursor=cursor, match="auth:refresh:*", count=200)
|
||||||
|
for key in keys:
|
||||||
|
try:
|
||||||
|
val = redis.get(key)
|
||||||
|
if val == str(user_id):
|
||||||
|
ttl = redis.ttl(key)
|
||||||
|
if ttl and ttl > 0:
|
||||||
|
found_ttl = ttl
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if found_ttl is not None or cursor == 0:
|
||||||
|
break
|
||||||
|
if found_ttl is not None:
|
||||||
|
redis.setex(online_key, found_ttl, "1")
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Session expired")
|
||||||
|
user = db.query(User).filter(User.user_id == int(user_id), User.is_deleted.is_(False)).first()
|
||||||
|
if not user or user.status != int(StatusEnum.ENABLED):
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User disabled")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
from .config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
# Async redis client for FastAPI/Async services
|
||||||
|
redis_client = Redis(
|
||||||
|
host=settings.redis_host,
|
||||||
|
port=settings.redis_port,
|
||||||
|
db=settings.redis_db,
|
||||||
|
password=settings.redis_password,
|
||||||
|
decode_responses=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_redis() -> Redis:
|
||||||
|
return redis_client
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from passlib.exc import UnknownHashError
|
||||||
|
from jose import jwt, JWTError
|
||||||
|
from typing import Any
|
||||||
|
from .config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
try:
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
except UnknownHashError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_token(data: dict[str, Any], expires_delta: timedelta) -> str:
|
||||||
|
to_encode = data.copy()
|
||||||
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
return jwt.encode(to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_token(token: str) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
|
||||||
|
return payload
|
||||||
|
except JWTError as exc:
|
||||||
|
raise ValueError("Invalid token") from exc
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
# Add the parent directory of 'app' to sys.path to support 'python app/main.py'
|
||||||
|
backend_dir = Path(__file__).resolve().parent.parent
|
||||||
|
if str(backend_dir) not in sys.path:
|
||||||
|
sys.path.insert(0, str(backend_dir))
|
||||||
|
|
||||||
|
from app.core.config import get_settings
|
||||||
|
from app.core.cors import setup_cors
|
||||||
|
from app.api.v1.router import router as api_router
|
||||||
|
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
app = FastAPI(title=settings.app_name)
|
||||||
|
setup_cors(app)
|
||||||
|
app.include_router(api_router, prefix=settings.api_v1_prefix)
|
||||||
|
|
||||||
|
# Mount storage
|
||||||
|
# Assuming backend is running from backend/ directory or root.
|
||||||
|
# Relative to backend/app/main.py, storage is ../../storage if run from inside app?
|
||||||
|
# Usually we run uvicorn from backend/ dir.
|
||||||
|
# So storage is ../storage relative to backend/ (cwd).
|
||||||
|
# Or if storage is in root of workspace, and we run from backend/, it is ../storage.
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
STORAGE_DIR = BASE_DIR / "storage"
|
||||||
|
STORAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
app.mount(f"{settings.api_v1_prefix}/storage", StaticFiles(directory=str(STORAGE_DIR)), name="storage")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
# When running as 'python app/main.py', the app is in the current module's 'app' variable,
|
||||||
|
# but uvicorn.run("app.main:app") is better for reload support.
|
||||||
|
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
from .base import Base
|
||||||
|
from .user import User
|
||||||
|
from .role import Role
|
||||||
|
from .permission import Permission
|
||||||
|
from .mapping import UserRole, RolePermission
|
||||||
|
from .dict import DictType, DictItem
|
||||||
|
from .param import SystemParam
|
||||||
|
from .sys_log import SysLog
|
||||||
|
from .prompt import PromptTemplate, UserPromptConfig
|
||||||
|
from .ai_model import AIModel
|
||||||
|
from .meeting import Meeting, MeetingAttendee, MeetingAudio, TranscriptTask, TranscriptSegment, SummarizeTask
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Base",
|
||||||
|
"User",
|
||||||
|
"Role",
|
||||||
|
"Permission",
|
||||||
|
"UserRole",
|
||||||
|
"RolePermission",
|
||||||
|
"DictType",
|
||||||
|
"DictItem",
|
||||||
|
"SystemParam",
|
||||||
|
"SysLog",
|
||||||
|
"PromptTemplate",
|
||||||
|
"UserPromptConfig",
|
||||||
|
"AIModel",
|
||||||
|
"Meeting",
|
||||||
|
"MeetingAttendee",
|
||||||
|
"MeetingAudio",
|
||||||
|
"TranscriptTask",
|
||||||
|
"TranscriptSegment",
|
||||||
|
"SummarizeTask",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from sqlalchemy import String, Integer, Boolean, JSON
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from .base import Base, TimestampMixin, MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
|
||||||
|
class AIModel(Base, TimestampMixin):
|
||||||
|
__tablename__ = "biz_ai_model"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
model_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
model_type: Mapped[str] = mapped_column(String(20), nullable=False) # asr / llm
|
||||||
|
provider: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
model_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
api_key: Mapped[str | None] = mapped_column(String(255))
|
||||||
|
base_url: Mapped[str | None] = mapped_column(String(255))
|
||||||
|
api_path: Mapped[str | None] = mapped_column(String(100))
|
||||||
|
config: Mapped[dict | None] = mapped_column(JSON)
|
||||||
|
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
status: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
from sqlalchemy import DateTime
|
||||||
|
|
||||||
|
|
||||||
|
MYSQL_TABLE_ARGS = {"mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TimestampMixin:
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
from sqlalchemy import String, Integer, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from sqlalchemy import Enum, UniqueConstraint
|
||||||
|
from .base import Base, TimestampMixin, MYSQL_TABLE_ARGS
|
||||||
|
from .enums import StatusEnum
|
||||||
|
|
||||||
|
|
||||||
|
class DictType(Base, TimestampMixin):
|
||||||
|
__tablename__ = "sys_dict_type"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("type_code", name="uk_dict_type_code"),
|
||||||
|
MYSQL_TABLE_ARGS,
|
||||||
|
)
|
||||||
|
|
||||||
|
dict_type_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
type_code: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
type_name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
status: Mapped[int] = mapped_column(Integer, default=StatusEnum.ENABLED, nullable=False)
|
||||||
|
remark: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
||||||
|
|
||||||
|
class DictItem(Base, TimestampMixin):
|
||||||
|
__tablename__ = "sys_dict_item"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("type_code", "item_value", name="uk_dict_item"),
|
||||||
|
MYSQL_TABLE_ARGS,
|
||||||
|
)
|
||||||
|
|
||||||
|
dict_item_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
type_code: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||||
|
item_label: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
item_value: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||||
|
status: Mapped[int] = mapped_column(Integer, default=StatusEnum.ENABLED, nullable=False)
|
||||||
|
remark: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import enum
|
||||||
|
|
||||||
|
|
||||||
|
class StatusEnum(int, enum.Enum):
|
||||||
|
ENABLED = 1
|
||||||
|
DISABLED = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionTypeEnum(str, enum.Enum):
|
||||||
|
MENU = "menu"
|
||||||
|
BUTTON = "button"
|
||||||
|
|
||||||
|
|
||||||
|
class ParamTypeEnum(str, enum.Enum):
|
||||||
|
STRING = "string"
|
||||||
|
INT = "int"
|
||||||
|
BOOL = "bool"
|
||||||
|
JSON = "json"
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
from sqlalchemy import Integer, UniqueConstraint, ForeignKey
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from .base import Base, TimestampMixin, MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
|
||||||
|
class UserRole(Base, TimestampMixin):
|
||||||
|
__tablename__ = "sys_user_role"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("user_id", "role_id", name="uk_user_role"),
|
||||||
|
MYSQL_TABLE_ARGS,
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("sys_user.user_id"), nullable=False, index=True)
|
||||||
|
role_id: Mapped[int] = mapped_column(Integer, ForeignKey("sys_role.role_id"), nullable=False, index=True)
|
||||||
|
|
||||||
|
user = relationship("User", back_populates="roles")
|
||||||
|
role = relationship("Role", back_populates="users")
|
||||||
|
|
||||||
|
|
||||||
|
class RolePermission(Base, TimestampMixin):
|
||||||
|
__tablename__ = "sys_role_permission"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("role_id", "perm_id", name="uk_role_perm"),
|
||||||
|
MYSQL_TABLE_ARGS,
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
role_id: Mapped[int] = mapped_column(Integer, ForeignKey("sys_role.role_id"), nullable=False, index=True)
|
||||||
|
perm_id: Mapped[int] = mapped_column(Integer, ForeignKey("sys_permission.perm_id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
|
|
||||||
|
role = relationship("Role", back_populates="permissions")
|
||||||
|
permission = relationship("Permission", back_populates="roles")
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
from sqlalchemy import String, Integer, Text, ForeignKey, DateTime, DECIMAL, Enum
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from datetime import datetime
|
||||||
|
from .base import Base, TimestampMixin, MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
|
||||||
|
class Meeting(Base, TimestampMixin):
|
||||||
|
__tablename__ = "biz_meeting"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
meeting_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("sys_user.user_id"), nullable=True)
|
||||||
|
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
tags: Mapped[str | None] = mapped_column(String(255))
|
||||||
|
meeting_time: Mapped[datetime | None] = mapped_column(DateTime)
|
||||||
|
access_password: Mapped[str | None] = mapped_column(String(10))
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="draft") # draft, transcribing, summarizing, completed
|
||||||
|
summary: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
||||||
|
creator = relationship("User", backref="created_meetings")
|
||||||
|
attendees = relationship("MeetingAttendee", back_populates="meeting", cascade="all, delete-orphan")
|
||||||
|
audios = relationship("MeetingAudio", back_populates="meeting", cascade="all, delete-orphan")
|
||||||
|
segments = relationship("TranscriptSegment", back_populates="meeting", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingAttendee(Base):
|
||||||
|
__tablename__ = "biz_meeting_attendees"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
attendee_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
meeting_id: Mapped[int] = mapped_column(Integer, ForeignKey("biz_meeting.meeting_id", ondelete="CASCADE"), index=True)
|
||||||
|
user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("sys_user.user_id"))
|
||||||
|
|
||||||
|
meeting = relationship("Meeting", back_populates="attendees")
|
||||||
|
user = relationship("User")
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingAudio(Base):
|
||||||
|
__tablename__ = "biz_meeting_audio"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
audio_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
meeting_id: Mapped[int] = mapped_column(Integer, ForeignKey("biz_meeting.meeting_id", ondelete="CASCADE"))
|
||||||
|
file_path: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||||
|
file_name: Mapped[str | None] = mapped_column(String(200))
|
||||||
|
file_size: Mapped[float | None] = mapped_column(DECIMAL(10, 0))
|
||||||
|
duration: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
processing_status: Mapped[str] = mapped_column(String(20), default="uploaded")
|
||||||
|
upload_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
error_message: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
||||||
|
meeting = relationship("Meeting", back_populates="audios")
|
||||||
|
|
||||||
|
|
||||||
|
class TranscriptTask(Base):
|
||||||
|
__tablename__ = "biz_transcript_task"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
task_id: Mapped[str] = mapped_column(String(255), primary_key=True)
|
||||||
|
meeting_id: Mapped[int] = mapped_column(Integer, ForeignKey("biz_meeting.meeting_id", ondelete="CASCADE"), index=True)
|
||||||
|
model_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("biz_ai_model.model_id"))
|
||||||
|
language: Mapped[str] = mapped_column(String(10), default="auto")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="pending") # pending, processing, completed, failed
|
||||||
|
progress: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime)
|
||||||
|
error_message: Mapped[str | None] = mapped_column(Text)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TranscriptSegment(Base):
|
||||||
|
__tablename__ = "biz_transcript_segment"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
segment_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
meeting_id: Mapped[int] = mapped_column(Integer, ForeignKey("biz_meeting.meeting_id", ondelete="CASCADE"))
|
||||||
|
audio_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("biz_meeting_audio.audio_id", ondelete="CASCADE"))
|
||||||
|
speaker_id: Mapped[int | None] = mapped_column(Integer)
|
||||||
|
speaker_tag: Mapped[str | None] = mapped_column(String(50))
|
||||||
|
start_time_ms: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
end_time_ms: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
text_content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
|
||||||
|
meeting = relationship("Meeting", back_populates="segments")
|
||||||
|
|
||||||
|
|
||||||
|
class SummarizeTask(Base):
|
||||||
|
__tablename__ = "biz_summarize_task"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
task_id: Mapped[str] = mapped_column(String(100), primary_key=True)
|
||||||
|
meeting_id: Mapped[int] = mapped_column(Integer, ForeignKey("biz_meeting.meeting_id", ondelete="CASCADE"), index=True)
|
||||||
|
prompt_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("biz_prompt_template.id"))
|
||||||
|
model_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("biz_ai_model.model_id"))
|
||||||
|
user_prompt: Mapped[str | None] = mapped_column(Text)
|
||||||
|
status: Mapped[str] = mapped_column(String(50), default="pending", index=True)
|
||||||
|
progress: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
result: Mapped[str | None] = mapped_column(Text)
|
||||||
|
error_message: Mapped[str | None] = mapped_column(Text)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
|
||||||
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
from sqlalchemy import String, Integer, Text, Boolean
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from sqlalchemy import Enum, UniqueConstraint
|
||||||
|
from .base import Base, TimestampMixin, MYSQL_TABLE_ARGS
|
||||||
|
from .enums import ParamTypeEnum, StatusEnum
|
||||||
|
|
||||||
|
|
||||||
|
class SystemParam(Base, TimestampMixin):
|
||||||
|
__tablename__ = "sys_param"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("param_key", name="uk_param_key"),
|
||||||
|
MYSQL_TABLE_ARGS,
|
||||||
|
)
|
||||||
|
|
||||||
|
param_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
param_key: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
param_value: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
param_type: Mapped[ParamTypeEnum] = mapped_column(
|
||||||
|
Enum(ParamTypeEnum, native_enum=False, values_callable=lambda x: [e.value for e in x]),
|
||||||
|
default=ParamTypeEnum.STRING,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
status: Mapped[int] = mapped_column(Integer, default=StatusEnum.ENABLED, nullable=False)
|
||||||
|
is_system: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
description: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
from sqlalchemy import String, Integer, Boolean, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy import Enum, JSON
|
||||||
|
from .base import Base, TimestampMixin, MYSQL_TABLE_ARGS
|
||||||
|
from .enums import PermissionTypeEnum, StatusEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(Base, TimestampMixin):
|
||||||
|
__tablename__ = "sys_permission"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
perm_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
parent_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
code: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||||
|
perm_type: Mapped[PermissionTypeEnum] = mapped_column(
|
||||||
|
Enum(PermissionTypeEnum, native_enum=False, values_callable=lambda x: [e.value for e in x]),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
level: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
path: Mapped[str | None] = mapped_column(String(255))
|
||||||
|
icon: Mapped[str | None] = mapped_column(String(100))
|
||||||
|
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||||
|
is_visible: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
|
status: Mapped[int] = mapped_column(Integer, default=StatusEnum.ENABLED, nullable=False)
|
||||||
|
description: Mapped[str | None] = mapped_column(Text)
|
||||||
|
meta: Mapped[dict | None] = mapped_column(JSON)
|
||||||
|
|
||||||
|
roles = relationship("RolePermission", back_populates="permission", cascade="all, delete-orphan")
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Integer, Boolean, Text, ForeignKey, UniqueConstraint, DateTime
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from .base import Base, TimestampMixin, MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTemplate(Base, TimestampMixin):
|
||||||
|
__tablename__ = "biz_prompt_template"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
category: Mapped[str] = mapped_column(String(50), nullable=False, default="summary")
|
||||||
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
description: Mapped[str | None] = mapped_column(String(255))
|
||||||
|
user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("sys_user.user_id", ondelete="SET NULL"), index=True)
|
||||||
|
is_system: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
status: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
||||||
|
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||||
|
|
||||||
|
user_configs = relationship("UserPromptConfig", back_populates="template", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
|
||||||
|
class UserPromptConfig(Base, TimestampMixin):
|
||||||
|
__tablename__ = "biz_user_prompt_config"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("user_id", "template_id", name="uk_user_template"),
|
||||||
|
MYSQL_TABLE_ARGS,
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("sys_user.user_id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
|
template_id: Mapped[int] = mapped_column(Integer, ForeignKey("biz_prompt_template.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
|
user_sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||||
|
|
||||||
|
template = relationship("PromptTemplate", back_populates="user_configs")
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from sqlalchemy import String, Integer, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from .base import Base, TimestampMixin, MYSQL_TABLE_ARGS
|
||||||
|
from .enums import StatusEnum
|
||||||
|
from sqlalchemy import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Role(Base, TimestampMixin):
|
||||||
|
__tablename__ = "sys_role"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
role_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
role_code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||||
|
role_name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
status: Mapped[int] = mapped_column(Integer, default=StatusEnum.ENABLED, nullable=False)
|
||||||
|
remark: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
||||||
|
users = relationship("UserRole", back_populates="role")
|
||||||
|
permissions = relationship("RolePermission", back_populates="role")
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
from sqlalchemy import String, Integer, Text, BigInteger
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.sql.schema import ForeignKey
|
||||||
|
from .base import Base, MYSQL_TABLE_ARGS
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
|
||||||
|
class SysLog(Base):
|
||||||
|
__tablename__ = "sys_log"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("sys_user.user_id"), nullable=True, index=True)
|
||||||
|
username: Mapped[str | None] = mapped_column(String(50))
|
||||||
|
operation_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
resource_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
resource_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True)
|
||||||
|
detail: Mapped[str | None] = mapped_column(Text)
|
||||||
|
ip_address: Mapped[str | None] = mapped_column(String(50))
|
||||||
|
user_agent: Mapped[str | None] = mapped_column(String(500))
|
||||||
|
status: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
error_message: Mapped[str | None] = mapped_column(Text)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(default=func.now(), index=True)
|
||||||
|
|
||||||
|
user = relationship("User")
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
from sqlalchemy import String, Boolean, Integer
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from .base import Base, TimestampMixin, MYSQL_TABLE_ARGS
|
||||||
|
from .enums import StatusEnum
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base, TimestampMixin):
|
||||||
|
__tablename__ = "sys_user"
|
||||||
|
__table_args__ = MYSQL_TABLE_ARGS
|
||||||
|
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||||
|
display_name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
email: Mapped[str | None] = mapped_column(String(100), unique=True)
|
||||||
|
phone: Mapped[str | None] = mapped_column(String(30), unique=True)
|
||||||
|
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
avatar: Mapped[str | None] = mapped_column(String(255))
|
||||||
|
status: Mapped[int] = mapped_column(Integer, default=StatusEnum.ENABLED, nullable=False)
|
||||||
|
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
|
roles = relationship("UserRole", back_populates="user")
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
from typing import Optional, Any
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class AIModelBase(BaseModel):
|
||||||
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
|
||||||
|
model_type: str = Field(..., pattern="^(asr|llm)$")
|
||||||
|
provider: str = Field(..., max_length=50)
|
||||||
|
model_name: str = Field(..., max_length=100)
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
base_url: Optional[str] = None
|
||||||
|
api_path: Optional[str] = None
|
||||||
|
config: Optional[dict[str, Any]] = None
|
||||||
|
is_default: bool = False
|
||||||
|
status: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
class AIModelCreate(AIModelBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AIModelUpdate(BaseModel):
|
||||||
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
|
||||||
|
model_type: Optional[str] = None
|
||||||
|
provider: Optional[str] = None
|
||||||
|
model_name: Optional[str] = None
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
base_url: Optional[str] = None
|
||||||
|
api_path: Optional[str] = None
|
||||||
|
config: Optional[dict[str, Any]] = None
|
||||||
|
is_default: Optional[bool] = None
|
||||||
|
status: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AIModelOut(AIModelBase):
|
||||||
|
model_id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshRequest(BaseModel):
|
||||||
|
refresh_token: str
|
||||||
|
|
||||||
|
|
||||||
|
class LogoutRequest(BaseModel):
|
||||||
|
refresh_token: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenPair(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
refresh_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class UserStats(BaseModel):
|
||||||
|
total: int
|
||||||
|
new_today: int
|
||||||
|
online: int
|
||||||
|
|
||||||
|
|
||||||
|
class StorageStats(BaseModel):
|
||||||
|
total_gb: float
|
||||||
|
audio_files: int
|
||||||
|
|
||||||
|
|
||||||
|
class ServerStats(BaseModel):
|
||||||
|
cpu: float
|
||||||
|
memory: float
|
||||||
|
disk: float
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardSummary(BaseModel):
|
||||||
|
users: UserStats
|
||||||
|
storage: StorageStats
|
||||||
|
server: ServerStats
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class DictTypeOut(BaseModel):
|
||||||
|
dict_type_id: int
|
||||||
|
type_code: str
|
||||||
|
type_name: str
|
||||||
|
status: int
|
||||||
|
remark: str | None = None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class DictTypeCreate(BaseModel):
|
||||||
|
type_code: str
|
||||||
|
type_name: str
|
||||||
|
status: int = 1
|
||||||
|
remark: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DictTypeUpdate(BaseModel):
|
||||||
|
type_name: str | None = None
|
||||||
|
status: int | None = None
|
||||||
|
remark: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DictItemOut(BaseModel):
|
||||||
|
dict_item_id: int
|
||||||
|
type_code: str
|
||||||
|
item_label: str
|
||||||
|
item_value: str
|
||||||
|
sort_order: int
|
||||||
|
status: int
|
||||||
|
remark: str | None = None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class DictItemCreate(BaseModel):
|
||||||
|
type_code: str
|
||||||
|
item_label: str
|
||||||
|
item_value: str
|
||||||
|
sort_order: int = 0
|
||||||
|
status: int = 1
|
||||||
|
remark: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DictItemUpdate(BaseModel):
|
||||||
|
item_label: str | None = None
|
||||||
|
item_value: str | None = None
|
||||||
|
sort_order: int | None = None
|
||||||
|
status: int | None = None
|
||||||
|
remark: str | None = None
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingBase(BaseModel):
|
||||||
|
title: str = Field(..., max_length=255)
|
||||||
|
tags: Optional[str] = None
|
||||||
|
meeting_time: Optional[datetime] = None
|
||||||
|
status: str = "draft"
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingCreate(MeetingBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingUpdate(BaseModel):
|
||||||
|
title: Optional[str] = None
|
||||||
|
tags: Optional[str] = None
|
||||||
|
status: Optional[str] = None
|
||||||
|
summary: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeOut(BaseModel):
|
||||||
|
attendee_id: int
|
||||||
|
user_id: Optional[int]
|
||||||
|
display_name: Optional[str] = None
|
||||||
|
avatar: Optional[str] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingOut(MeetingBase):
|
||||||
|
meeting_id: int
|
||||||
|
user_id: Optional[int]
|
||||||
|
creator_name: Optional[str] = None
|
||||||
|
creator_avatar: Optional[str] = None
|
||||||
|
summary: Optional[str] = None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
attendees: List[AttendeeOut] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class TranscriptSegmentOut(BaseModel):
|
||||||
|
segment_id: int
|
||||||
|
speaker_id: Optional[int]
|
||||||
|
speaker_tag: Optional[str]
|
||||||
|
start_time_ms: int
|
||||||
|
end_time_ms: int
|
||||||
|
text_content: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingDetailOut(MeetingOut):
|
||||||
|
segments: List[TranscriptSegmentOut] = []
|
||||||
|
# Add other details like audio list if needed
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingListOut(BaseModel):
|
||||||
|
items: List[MeetingOut]
|
||||||
|
total: int
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ParamOut(BaseModel):
|
||||||
|
param_id: int
|
||||||
|
param_key: str
|
||||||
|
param_value: str
|
||||||
|
param_type: str
|
||||||
|
status: int
|
||||||
|
is_system: bool
|
||||||
|
description: str | None = None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class ParamCreate(BaseModel):
|
||||||
|
param_key: str
|
||||||
|
param_value: str
|
||||||
|
param_type: str = "string"
|
||||||
|
status: int = 1
|
||||||
|
is_system: bool = False
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ParamUpdate(BaseModel):
|
||||||
|
param_value: str | None = None
|
||||||
|
param_type: str | None = None
|
||||||
|
status: int | None = None
|
||||||
|
is_system: bool | None = None
|
||||||
|
description: str | None = None
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionOut(BaseModel):
|
||||||
|
perm_id: int
|
||||||
|
parent_id: int | None
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
perm_type: str
|
||||||
|
level: int
|
||||||
|
path: str | None = None
|
||||||
|
component: str | None = None
|
||||||
|
icon: str | None = None
|
||||||
|
sort_order: int
|
||||||
|
is_visible: bool
|
||||||
|
status: int
|
||||||
|
meta: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionCreate(BaseModel):
|
||||||
|
parent_id: int | None = None
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
perm_type: str
|
||||||
|
level: int
|
||||||
|
path: str | None = None
|
||||||
|
icon: str | None = None
|
||||||
|
sort_order: int = 0
|
||||||
|
is_visible: bool = True
|
||||||
|
status: int = 1
|
||||||
|
description: str | None = None
|
||||||
|
meta: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionUpdate(BaseModel):
|
||||||
|
parent_id: int | None = None
|
||||||
|
name: str | None = None
|
||||||
|
code: str | None = None
|
||||||
|
perm_type: str | None = None
|
||||||
|
level: int | None = None
|
||||||
|
path: str | None = None
|
||||||
|
icon: str | None = None
|
||||||
|
sort_order: int | None = None
|
||||||
|
is_visible: bool | None = None
|
||||||
|
status: int | None = None
|
||||||
|
description: str | None = None
|
||||||
|
meta: dict[str, Any] | None = None
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTemplateBase(BaseModel):
|
||||||
|
name: str = Field(..., max_length=100)
|
||||||
|
category: str = Field("summary", max_length=50)
|
||||||
|
content: str
|
||||||
|
description: Optional[str] = Field(None, max_length=255)
|
||||||
|
sort_order: int = 0
|
||||||
|
status: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTemplateCreate(PromptTemplateBase):
|
||||||
|
is_system: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTemplateUpdate(BaseModel):
|
||||||
|
name: Optional[str] = Field(None, max_length=100)
|
||||||
|
category: Optional[str] = Field(None, max_length=50)
|
||||||
|
content: Optional[str] = None
|
||||||
|
description: Optional[str] = Field(None, max_length=255)
|
||||||
|
sort_order: Optional[int] = None
|
||||||
|
status: Optional[int] = None
|
||||||
|
is_system: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTemplateOut(PromptTemplateBase):
|
||||||
|
id: int
|
||||||
|
user_id: Optional[int]
|
||||||
|
is_system: bool
|
||||||
|
is_active: bool = True # From UserPromptConfig
|
||||||
|
user_sort_order: Optional[int] = None # From UserPromptConfig
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class UserPromptConfigUpdate(BaseModel):
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
user_sort_order: Optional[int] = None
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class RoleOut(BaseModel):
|
||||||
|
role_id: int
|
||||||
|
role_code: str
|
||||||
|
role_name: str
|
||||||
|
status: int
|
||||||
|
remark: str | None = None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class RoleCreate(BaseModel):
|
||||||
|
role_code: str
|
||||||
|
role_name: str
|
||||||
|
status: int = 1
|
||||||
|
remark: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleUpdate(BaseModel):
|
||||||
|
role_name: str | None = None
|
||||||
|
status: int | None = None
|
||||||
|
remark: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RolePermissionUpdate(BaseModel):
|
||||||
|
perm_ids: list[int]
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class SysLogOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
user_id: int | None
|
||||||
|
username: str | None
|
||||||
|
operation_type: str
|
||||||
|
resource_type: str
|
||||||
|
resource_id: int | None
|
||||||
|
detail: str | None
|
||||||
|
ip_address: str | None
|
||||||
|
user_agent: str | None
|
||||||
|
status: int
|
||||||
|
error_message: str | None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class SysLogPage(BaseModel):
|
||||||
|
items: list[SysLogOut]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
class SysLogCreate(BaseModel):
|
||||||
|
user_id: int | None = None
|
||||||
|
username: str | None = None
|
||||||
|
operation_type: str
|
||||||
|
resource_type: str
|
||||||
|
resource_id: int | None = None
|
||||||
|
detail: str | None = None
|
||||||
|
ip_address: str | None = None
|
||||||
|
user_agent: str | None = None
|
||||||
|
status: int = 1
|
||||||
|
error_message: str | None = None
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class UserOut(BaseModel):
|
||||||
|
user_id: int
|
||||||
|
username: str
|
||||||
|
display_name: str
|
||||||
|
email: str | None = None
|
||||||
|
phone: str | None = None
|
||||||
|
avatar: str | None = None
|
||||||
|
status: int
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class UserMe(UserOut):
|
||||||
|
roles: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
username: str
|
||||||
|
display_name: str
|
||||||
|
email: str | None = None
|
||||||
|
phone: str | None = None
|
||||||
|
password: str | None = None
|
||||||
|
status: int = 1
|
||||||
|
role_ids: list[int] = []
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
display_name: str | None = None
|
||||||
|
email: str | None = None
|
||||||
|
phone: str | None = None
|
||||||
|
status: int | None = None
|
||||||
|
role_ids: list[int] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordChange(BaseModel):
|
||||||
|
old_password: str
|
||||||
|
new_password: str
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
from uuid import uuid4
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from redis import Redis
|
||||||
|
from app.core.security import verify_password, create_token, decode_token
|
||||||
|
from app.core.config import get_settings
|
||||||
|
from app.models import User
|
||||||
|
from app.models.enums import StatusEnum
|
||||||
|
from app.services.param_service import get_token_minutes
|
||||||
|
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_user(db: Session, username: str, password: str) -> User | None:
|
||||||
|
user = (
|
||||||
|
db.query(User)
|
||||||
|
.filter(User.username == username, User.status == int(StatusEnum.ENABLED), User.is_deleted.is_(False))
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
if not verify_password(password, user.password_hash):
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def create_token_pair(db: Session, redis: Redis, user: User) -> tuple[str, str]:
|
||||||
|
access_minutes, refresh_minutes = get_token_minutes(
|
||||||
|
db,
|
||||||
|
default_access=settings.jwt_access_token_minutes,
|
||||||
|
default_refresh=settings.jwt_refresh_token_minutes,
|
||||||
|
)
|
||||||
|
|
||||||
|
access_payload = {"sub": str(user.user_id), "type": "access"}
|
||||||
|
refresh_jti = str(uuid4())
|
||||||
|
refresh_payload = {"sub": str(user.user_id), "type": "refresh", "jti": refresh_jti}
|
||||||
|
|
||||||
|
access_token = create_token(access_payload, timedelta(minutes=access_minutes))
|
||||||
|
refresh_token = create_token(refresh_payload, timedelta(minutes=refresh_minutes))
|
||||||
|
|
||||||
|
redis_key = f"auth:refresh:{refresh_jti}"
|
||||||
|
redis.setex(redis_key, refresh_minutes * 60, str(user.user_id))
|
||||||
|
# Track online user with TTL synced to refresh token
|
||||||
|
online_key = f"auth:online:{user.user_id}"
|
||||||
|
redis.setex(online_key, refresh_minutes * 60, "1")
|
||||||
|
|
||||||
|
return access_token, refresh_token
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_access_token(db: Session, redis: Redis, refresh_token: str) -> tuple[str, str]:
|
||||||
|
payload = decode_token(refresh_token)
|
||||||
|
if payload.get("type") != "refresh":
|
||||||
|
raise ValueError("Invalid token type")
|
||||||
|
jti = payload.get("jti")
|
||||||
|
if not jti:
|
||||||
|
raise ValueError("Invalid token")
|
||||||
|
|
||||||
|
redis_key = f"auth:refresh:{jti}"
|
||||||
|
user_id = redis.get(redis_key)
|
||||||
|
if not user_id:
|
||||||
|
raise ValueError("Refresh token expired")
|
||||||
|
|
||||||
|
user = db.query(User).filter(User.user_id == int(user_id), User.is_deleted.is_(False)).first()
|
||||||
|
if not user or user.status != int(StatusEnum.ENABLED):
|
||||||
|
raise ValueError("User not found")
|
||||||
|
|
||||||
|
# Invalidate the old refresh token to avoid stale online counts
|
||||||
|
redis.delete(redis_key)
|
||||||
|
return create_token_pair(db, redis, user)
|
||||||
|
|
||||||
|
|
||||||
|
def logout_refresh_token(redis: Redis, refresh_token: str) -> None:
|
||||||
|
payload = decode_token(refresh_token)
|
||||||
|
if payload.get("type") != "refresh":
|
||||||
|
raise ValueError("Invalid token type")
|
||||||
|
jti = payload.get("jti")
|
||||||
|
if not jti:
|
||||||
|
raise ValueError("Invalid token")
|
||||||
|
redis_key = f"auth:refresh:{jti}"
|
||||||
|
user_id = redis.get(redis_key)
|
||||||
|
redis.delete(redis_key)
|
||||||
|
if user_id:
|
||||||
|
redis.delete(f"auth:online:{user_id}")
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class LLMService:
|
||||||
|
@staticmethod
|
||||||
|
async def chat_completion(
|
||||||
|
api_key: str,
|
||||||
|
base_url: str,
|
||||||
|
model_name: str,
|
||||||
|
messages: List[Dict[str, str]],
|
||||||
|
api_path: str = "/chat/completions",
|
||||||
|
temperature: float = 0.7,
|
||||||
|
top_p: float = 0.9,
|
||||||
|
max_tokens: int = 4000
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generic OpenAI-compatible chat completion caller.
|
||||||
|
"""
|
||||||
|
# Ensure base_url doesn't end with trailing slash if path starts with one
|
||||||
|
url = f"{base_url.rstrip('/')}{api_path}"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model_name,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": temperature,
|
||||||
|
"top_p": top_p,
|
||||||
|
"max_tokens": max_tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||||
|
try:
|
||||||
|
logger.info(f"Sending request to LLM: {url} (Model: {model_name})")
|
||||||
|
response = await client.post(url, headers=headers, json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
content = result["choices"][0]["message"]["content"]
|
||||||
|
return content
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
error_body = e.response.text
|
||||||
|
logger.error(f"LLM API Error ({e.response.status_code}): {error_body}")
|
||||||
|
# Try to parse error message from body
|
||||||
|
try:
|
||||||
|
err_json = json.loads(error_body)
|
||||||
|
detail = err_json.get("error", {}).get("message", error_body)
|
||||||
|
except:
|
||||||
|
detail = error_body
|
||||||
|
raise Exception(f"AI 服务调用失败: {detail}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calling LLM: {str(e)}")
|
||||||
|
raise Exception(f"连接 AI 服务发生错误: {str(e)}")
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.sys_log import SysLog
|
||||||
|
|
||||||
|
def create_log(
|
||||||
|
db: Session,
|
||||||
|
user_id: int | None,
|
||||||
|
username: str | None,
|
||||||
|
operation_type: str,
|
||||||
|
resource_type: str,
|
||||||
|
detail: str | None = None,
|
||||||
|
ip_address: str | None = None,
|
||||||
|
user_agent: str | None = None,
|
||||||
|
status: int = 1,
|
||||||
|
resource_id: int | None = None,
|
||||||
|
error_message: str | None = None
|
||||||
|
):
|
||||||
|
log_entry = SysLog(
|
||||||
|
user_id=user_id,
|
||||||
|
username=username,
|
||||||
|
operation_type=operation_type,
|
||||||
|
resource_type=resource_type,
|
||||||
|
resource_id=resource_id,
|
||||||
|
detail=detail,
|
||||||
|
ip_address=ip_address,
|
||||||
|
user_agent=user_agent,
|
||||||
|
status=status,
|
||||||
|
error_message=error_message
|
||||||
|
)
|
||||||
|
db.add(log_entry)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(log_entry)
|
||||||
|
return log_entry
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models import Meeting, SummarizeTask, PromptTemplate, AIModel, TranscriptSegment
|
||||||
|
from app.services.llm_service import LLMService
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
from app.core.redis import redis_client
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MeetingService:
|
||||||
|
@staticmethod
|
||||||
|
async def create_summarize_task(
|
||||||
|
db: Session,
|
||||||
|
meeting_id: int,
|
||||||
|
prompt_id: int,
|
||||||
|
model_id: int,
|
||||||
|
extra_prompt: str = ""
|
||||||
|
):
|
||||||
|
# 1. 基础数据校验
|
||||||
|
meeting = db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first()
|
||||||
|
if not meeting:
|
||||||
|
raise Exception("Meeting not found")
|
||||||
|
|
||||||
|
# 2. 格式化会议转译内容 (作为 user_prompt 素材)
|
||||||
|
segments = db.query(TranscriptSegment).filter(
|
||||||
|
TranscriptSegment.meeting_id == meeting_id
|
||||||
|
).order_by(TranscriptSegment.start_time_ms).all()
|
||||||
|
|
||||||
|
formatted_content = []
|
||||||
|
for s in segments:
|
||||||
|
secs = int(s.start_time_ms // 1000)
|
||||||
|
m, sc = divmod(secs, 60)
|
||||||
|
timestamp = f"[{m:02d}:{sc:02d}]"
|
||||||
|
speaker = s.speaker_tag or f"发言人{s.speaker_id or '?'}"
|
||||||
|
formatted_content.append(f"{timestamp} {speaker}: {s.text_content}")
|
||||||
|
|
||||||
|
meeting_text = "\n".join(formatted_content)
|
||||||
|
|
||||||
|
# 组合最终 user_prompt (素材 + 用户的附加要求)
|
||||||
|
user_prompt_content = f"### 会议转译内容 ###\n{meeting_text}"
|
||||||
|
if extra_prompt:
|
||||||
|
user_prompt_content += f"\n\n### 用户的额外指令 ###\n{extra_prompt}"
|
||||||
|
|
||||||
|
# 3. 创建任务记录 (按照数据库实际字段)
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
new_task = SummarizeTask(
|
||||||
|
task_id=task_id,
|
||||||
|
meeting_id=meeting_id,
|
||||||
|
prompt_id=prompt_id,
|
||||||
|
model_id=model_id,
|
||||||
|
user_prompt=user_prompt_content,
|
||||||
|
status="pending",
|
||||||
|
progress=0,
|
||||||
|
created_at=datetime.utcnow()
|
||||||
|
)
|
||||||
|
db.add(new_task)
|
||||||
|
|
||||||
|
# 更新会议状态为“总结中”
|
||||||
|
meeting.status = "summarizing"
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# 4. 进入 Redis 队列
|
||||||
|
await redis_client.lpush("meeting:summarize:queue", task_id)
|
||||||
|
|
||||||
|
return new_task
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def process_summarize_task(db: Session, task_id: str):
|
||||||
|
"""
|
||||||
|
后台 Worker 真实执行逻辑
|
||||||
|
"""
|
||||||
|
task = db.query(SummarizeTask).filter(SummarizeTask.task_id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
task.status = "processing"
|
||||||
|
task.progress = 15
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# 1. 获取模型配置
|
||||||
|
model_config = db.query(AIModel).filter(AIModel.model_id == task.model_id).first()
|
||||||
|
if not model_config:
|
||||||
|
raise Exception("AI 模型配置不存在")
|
||||||
|
|
||||||
|
# 2. 实时获取提示词模板内容
|
||||||
|
prompt_tmpl = db.query(PromptTemplate).filter(PromptTemplate.id == task.prompt_id).first()
|
||||||
|
system_prompt = prompt_tmpl.content if prompt_tmpl else "请根据提供的会议转译内容生成准确的总结。"
|
||||||
|
|
||||||
|
task.progress = 30
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# 3. 构建消息结构
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": task.user_prompt}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 解析模型参数
|
||||||
|
config_params = model_config.config or {}
|
||||||
|
temperature = config_params.get("temperature", 0.7)
|
||||||
|
top_p = config_params.get("top_p", 0.9)
|
||||||
|
|
||||||
|
logger.info(f"Task {task_id}: Launching LLM request to {model_config.model_name}")
|
||||||
|
task.progress = 50
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# 4. 调用大模型服务
|
||||||
|
summary_result = await LLMService.chat_completion(
|
||||||
|
api_key=model_config.api_key,
|
||||||
|
base_url=model_config.base_url or "https://api.openai.com/v1",
|
||||||
|
model_name=model_config.model_name,
|
||||||
|
messages=messages,
|
||||||
|
api_path=model_config.api_path or "/chat/completions",
|
||||||
|
temperature=float(temperature),
|
||||||
|
top_p=float(top_p)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 任务完成,回写结果
|
||||||
|
task.result = summary_result
|
||||||
|
task.status = "completed"
|
||||||
|
task.progress = 100
|
||||||
|
task.completed_at = datetime.utcnow()
|
||||||
|
|
||||||
|
# 同步更新会议主表摘要和状态
|
||||||
|
meeting = db.query(Meeting).filter(Meeting.meeting_id == task.meeting_id).first()
|
||||||
|
if meeting:
|
||||||
|
meeting.summary = summary_result
|
||||||
|
meeting.status = "completed"
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
logger.info(f"Task {task_id} completed successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Task {task_id} execution error: {str(e)}")
|
||||||
|
task.status = "failed"
|
||||||
|
task.error_message = str(e)
|
||||||
|
|
||||||
|
# 还原会议状态
|
||||||
|
meeting = db.query(Meeting).filter(Meeting.meeting_id == task.meeting_id).first()
|
||||||
|
if meeting:
|
||||||
|
meeting.status = "draft"
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_task_status(db: Session, task_id: str):
|
||||||
|
return db.query(SummarizeTask).filter(SummarizeTask.task_id == task_id).first()
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models import SystemParam
|
||||||
|
from app.models.enums import ParamTypeEnum
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
ACCESS_TOKEN_KEY = "security.access_token_minutes"
|
||||||
|
REFRESH_TOKEN_KEY = "security.refresh_token_minutes"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_param_value(param: SystemParam):
|
||||||
|
if param.param_type == ParamTypeEnum.INT:
|
||||||
|
return int(param.param_value)
|
||||||
|
if param.param_type == ParamTypeEnum.BOOL:
|
||||||
|
return param.param_value.lower() in {"1", "true", "yes"}
|
||||||
|
if param.param_type == ParamTypeEnum.JSON:
|
||||||
|
return json.loads(param.param_value)
|
||||||
|
return param.param_value
|
||||||
|
|
||||||
|
|
||||||
|
def get_param_value(db: Session, key: str):
|
||||||
|
return db.query(SystemParam).filter(SystemParam.param_key == key).first()
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_minutes(db: Session, default_access: int, default_refresh: int) -> tuple[int, int]:
|
||||||
|
access = get_param_value(db, ACCESS_TOKEN_KEY)
|
||||||
|
refresh = get_param_value(db, REFRESH_TOKEN_KEY)
|
||||||
|
|
||||||
|
access_minutes = default_access
|
||||||
|
refresh_minutes = default_refresh
|
||||||
|
|
||||||
|
if access:
|
||||||
|
access_minutes = int(parse_param_value(access))
|
||||||
|
if refresh:
|
||||||
|
refresh_minutes = int(parse_param_value(refresh))
|
||||||
|
|
||||||
|
return access_minutes, refresh_minutes
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
# 数据库结构文档 (MySQL 5.7/8.0 兼容)
|
||||||
|
|
||||||
|
- 字符集:`utf8mb4`
|
||||||
|
- 排序规则:`utf8mb4_unicode_ci`
|
||||||
|
- 说明:本文件用于持续维护核心表结构与描述(每次变更请同步更新)。
|
||||||
|
|
||||||
|
## 1. 用户与角色
|
||||||
|
|
||||||
|
### 1.1 `sys_user` 用户表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| user_id | INT | PK, AUTO_INCREMENT | 用户ID |
|
||||||
|
| username | VARCHAR(50) | UNIQUE, NOT NULL | 登录名 |
|
||||||
|
| display_name | VARCHAR(50) | NOT NULL | 显示名 |
|
||||||
|
| email | VARCHAR(100) | UNIQUE | 邮箱 |
|
||||||
|
| phone | VARCHAR(30) | UNIQUE | 手机 |
|
||||||
|
| password_hash | VARCHAR(255) | NOT NULL | 密码哈希 |
|
||||||
|
| avatar | VARCHAR(255) | | 头像路径 |
|
||||||
|
| status | TINYINT(1) | NOT NULL | 状态:1 启用 / 0 禁用 |
|
||||||
|
| is_deleted | TINYINT(1) | NOT NULL | 是否软删除 |
|
||||||
|
| created_at | DATETIME | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 更新时间 |
|
||||||
|
|
||||||
|
### 1.2 `sys_role` 角色表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| role_id | INT | PK, AUTO_INCREMENT | 角色ID |
|
||||||
|
| role_code | VARCHAR(50) | UNIQUE, NOT NULL | 角色编码 |
|
||||||
|
| role_name | VARCHAR(50) | NOT NULL | 角色名称 |
|
||||||
|
| status | TINYINT(1) | NOT NULL | 状态:1 启用 / 0 禁用 |
|
||||||
|
| remark | TEXT | | 备注 |
|
||||||
|
| created_at | DATETIME | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 更新时间 |
|
||||||
|
|
||||||
|
### 1.3 `sys_user_role` 用户-角色关联表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | INT | PK, AUTO_INCREMENT | 关联ID |
|
||||||
|
| user_id | INT | INDEX, NOT NULL | 用户ID |
|
||||||
|
| role_id | INT | INDEX, NOT NULL | 角色ID |
|
||||||
|
| created_at | DATETIME | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 更新时间 |
|
||||||
|
| | | UNIQUE(user_id, role_id) | 唯一约束 |
|
||||||
|
|
||||||
|
## 2. 权限
|
||||||
|
|
||||||
|
### 2.1 `sys_permission` 权限表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| perm_id | INT | PK, AUTO_INCREMENT | 权限ID |
|
||||||
|
| parent_id | INT | INDEX | 父级权限ID |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 权限名称 |
|
||||||
|
| code | VARCHAR(100) | UNIQUE, NOT NULL | 权限编码 |
|
||||||
|
| perm_type | VARCHAR(20) | NOT NULL | menu/button |
|
||||||
|
| level | INT | NOT NULL | 1/2/3 级 |
|
||||||
|
| path | VARCHAR(255) | | 前端路由 |
|
||||||
|
| icon | VARCHAR(100) | | 图标 |
|
||||||
|
| sort_order | INT | NOT NULL | 排序 |
|
||||||
|
| is_visible | TINYINT(1) | NOT NULL | 是否展示 |
|
||||||
|
| status | TINYINT(1) | NOT NULL | 1/0 |
|
||||||
|
| description | TEXT | | 描述 |
|
||||||
|
| meta | JSON | | 扩展属性 |
|
||||||
|
| created_at | DATETIME | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 更新时间 |
|
||||||
|
|
||||||
|
### 2.2 `sys_role_permission` 角色-权限关联表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | INT | PK, AUTO_INCREMENT | 关联ID |
|
||||||
|
| role_id | INT | INDEX, NOT NULL | 角色ID |
|
||||||
|
| perm_id | INT | INDEX, NOT NULL | 权限ID |
|
||||||
|
| created_at | DATETIME | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 更新时间 |
|
||||||
|
| | | UNIQUE(role_id, perm_id) | 唯一约束 |
|
||||||
|
|
||||||
|
## 3. 码表
|
||||||
|
|
||||||
|
### 3.1 `sys_dict_type` 码表类型
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| dict_type_id | INT | PK, AUTO_INCREMENT | 类型ID |
|
||||||
|
| type_code | VARCHAR(50) | UNIQUE, NOT NULL | 类型编码 |
|
||||||
|
| type_name | VARCHAR(50) | NOT NULL | 类型名称 |
|
||||||
|
| status | TINYINT(1) | NOT NULL | 1/0 |
|
||||||
|
| remark | TEXT | | 备注 |
|
||||||
|
| created_at | DATETIME | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 更新时间 |
|
||||||
|
|
||||||
|
### 3.2 `sys_dict_item` 码表项
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| dict_item_id | INT | PK, AUTO_INCREMENT | 明细ID |
|
||||||
|
| type_code | VARCHAR(50) | INDEX, NOT NULL | 类型编码 |
|
||||||
|
| item_label | VARCHAR(100) | NOT NULL | 展示值 |
|
||||||
|
| item_value | VARCHAR(100) | NOT NULL | 存储值 |
|
||||||
|
| sort_order | INT | NOT NULL | 排序 |
|
||||||
|
| status | TINYINT(1) | NOT NULL | 1/0 |
|
||||||
|
| remark | TEXT | | 备注 |
|
||||||
|
| created_at | DATETIME | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 更新时间 |
|
||||||
|
| | | UNIQUE(type_code, item_value) | 唯一约束 |
|
||||||
|
|
||||||
|
## 4. 系统参数
|
||||||
|
|
||||||
|
### 4.1 `sys_param` 系统参数表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| param_id | INT | PK, AUTO_INCREMENT | 参数ID |
|
||||||
|
| param_key | VARCHAR(100) | UNIQUE, NOT NULL | 参数键 |
|
||||||
|
| param_value | TEXT | NOT NULL | 参数值 |
|
||||||
|
| param_type | VARCHAR(20) | NOT NULL | string/int/bool/json |
|
||||||
|
| status | TINYINT(1) | NOT NULL | 1/0 |
|
||||||
|
| is_system | TINYINT(1) | NOT NULL | 是否系统级 |
|
||||||
|
| description | TEXT | | 描述 |
|
||||||
|
| created_at | DATETIME | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 更新时间 |
|
||||||
|
|
||||||
|
## 5. 日志
|
||||||
|
|
||||||
|
### 5.1 `sys_log` 操作日志表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGINT | PK, AUTO_INCREMENT | 日志ID |
|
||||||
|
| user_id | INT | INDEX | 操作用户ID |
|
||||||
|
| username | VARCHAR(50) | | 用户名 |
|
||||||
|
| operation_type | VARCHAR(50) | NOT NULL | 操作类型 |
|
||||||
|
| resource_type | VARCHAR(50) | NOT NULL | 资源类型 |
|
||||||
|
| resource_id | BIGINT | INDEX | 资源ID |
|
||||||
|
| detail | TEXT | | 操作详情(JSON) |
|
||||||
|
| ip_address | VARCHAR(50) | | IP地址 |
|
||||||
|
| user_agent | VARCHAR(500) | | 用户代理 |
|
||||||
|
| status | TINYINT | DEFAULT 1 | 状态: 1成功/0失败 |
|
||||||
|
| error_message | TEXT | | 错误信息 |
|
||||||
|
| created_at | DATETIME | NOT NULL | 操作时间 |
|
||||||
|
|
||||||
|
## 6. 业务 - 提示词管理
|
||||||
|
|
||||||
|
### 6.1 `biz_prompt_template` 提示词模板表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | INT | PK, AUTO_INCREMENT | 模板ID |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 模板名称 |
|
||||||
|
| category | VARCHAR(50) | NOT NULL | 分类 |
|
||||||
|
| content | TEXT | NOT NULL | 提示词内容 |
|
||||||
|
| description | VARCHAR(255) | | 描述 |
|
||||||
|
| user_id | INT | INDEX | 创建者ID (系统级为NULL) |
|
||||||
|
| is_system | TINYINT(1) | NOT NULL | 是否系统内置 |
|
||||||
|
| status | TINYINT(1) | NOT NULL | 全局状态:1启用 / 0禁用 |
|
||||||
|
| sort_order | INT | NOT NULL | 默认排序 |
|
||||||
|
| created_at | DATETIME | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 更新时间 |
|
||||||
|
|
||||||
|
### 6.2 `biz_user_prompt_config` 用户提示词配置表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | INT | PK, AUTO_INCREMENT | 配置ID |
|
||||||
|
| user_id | INT | INDEX, NOT NULL | 用户ID |
|
||||||
|
| template_id | INT | INDEX, NOT NULL | 模板ID |
|
||||||
|
| is_active | TINYINT(1) | NOT NULL | 用户是否启用 |
|
||||||
|
| user_sort_order | INT | NOT NULL | 用户自定义排序 |
|
||||||
|
| updated_at | DATETIME | NOT NULL | 修改时间 |
|
||||||
|
| | | UNIQUE(user_id, template_id) | 唯一约束 |
|
||||||
|
|
||||||
|
## 7. 业务 - 会议管理
|
||||||
|
|
||||||
|
### 7.1 `biz_meeting` 会议主表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| meeting_id | INT | PK, AUTO_INCREMENT | 会议ID |
|
||||||
|
| user_id | INT | | 创建者ID |
|
||||||
|
| title | VARCHAR(255) | NOT NULL | 会议标题 |
|
||||||
|
| tags | VARCHAR(255) | | 标签(逗号分隔) |
|
||||||
|
| meeting_time | TIMESTAMP | | 会议时间 |
|
||||||
|
| access_password| VARCHAR(10) | | 访问密码 |
|
||||||
|
| status | VARCHAR(20) | DEFAULT 'draft' | 状态: draft, transcribing, summarizing, completed |
|
||||||
|
| summary | TEXT | | 最终总结内容 |
|
||||||
|
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||||
|
|
||||||
|
### 7.2 `biz_meeting_attendees` 会议参会人表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| attendee_id | INT | PK, AUTO_INCREMENT | 参会ID |
|
||||||
|
| meeting_id | INT | INDEX | 会议ID |
|
||||||
|
| user_id | INT | | 系统用户ID |
|
||||||
|
|
||||||
|
### 7.3 `biz_meeting_audio` 会议音频表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| audio_id | INT | PK, AUTO_INCREMENT | 音频ID |
|
||||||
|
| meeting_id | INT | | 会议ID |
|
||||||
|
| file_path | VARCHAR(512) | NOT NULL | 文件存储路径 |
|
||||||
|
| file_name | VARCHAR(200) | | 原始文件名 |
|
||||||
|
| file_size | DECIMAL(10,0) | | 文件大小(字节) |
|
||||||
|
| duration | INT | DEFAULT 0 | 音频时长(秒) |
|
||||||
|
| processing_status| VARCHAR(20) | DEFAULT 'uploaded' | 处理状态 |
|
||||||
|
| upload_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 上传时间 |
|
||||||
|
| error_message | TEXT | | 错误信息 |
|
||||||
|
|
||||||
|
### 7.4 `biz_transcript_task` 转译任务表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| task_id | VARCHAR(255) | PK | 任务ID |
|
||||||
|
| meeting_id | INT | INDEX, NOT NULL | 会议ID |
|
||||||
|
| model_id | INT | | 模型配置ID (关联 biz_ai_model) |
|
||||||
|
| language | VARCHAR(10) | DEFAULT 'auto' | 识别语言 |
|
||||||
|
| status | ENUM | INDEX, DEFAULT 'pending' | pending/processing/completed/failed |
|
||||||
|
| progress | INT | DEFAULT 0 | 处理进度(0-100) |
|
||||||
|
| completed_at | TIMESTAMP | | 完成时间 |
|
||||||
|
| error_message | TEXT | | 错误信息 |
|
||||||
|
| created_at | TIMESTAMP | INDEX, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||||
|
|
||||||
|
### 7.5 `biz_transcript_segment` 转译片段表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| segment_id | INT | PK, AUTO_INCREMENT | 片段ID |
|
||||||
|
| meeting_id | INT | | 会议ID |
|
||||||
|
| audio_id | INT | | 音频ID |
|
||||||
|
| speaker_id | INT | | 发言人ID |
|
||||||
|
| speaker_tag | VARCHAR(50) | | 发言人标签(如: A, B) |
|
||||||
|
| start_time_ms | INT | NOT NULL | 开始时间(毫秒) |
|
||||||
|
| end_time_ms | INT | NOT NULL | 结束时间(毫秒) |
|
||||||
|
| text_content | TEXT | NOT NULL | 文本内容 |
|
||||||
|
|
||||||
|
### 7.6 `biz_summarize_task` 总结任务表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| task_id | VARCHAR(100) | PK | 任务ID |
|
||||||
|
| meeting_id | INT | INDEX, NOT NULL | 会议ID |
|
||||||
|
| prompt_id | INT | | 使用的提示词模板ID |
|
||||||
|
| model_id | INT | | 模型配置ID (关联 biz_ai_model) |
|
||||||
|
| user_prompt | TEXT | | 任务执行时的最终Prompt |
|
||||||
|
| status | VARCHAR(50) | INDEX, DEFAULT 'pending' | 任务状态 |
|
||||||
|
| progress | INT | DEFAULT 0 | 进度 |
|
||||||
|
| result | TEXT | | 总结结果 |
|
||||||
|
| error_message | TEXT | | 错误信息 |
|
||||||
|
| created_at | TIMESTAMP | INDEX, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||||
|
| completed_at | TIMESTAMP | | 完成时间 |
|
||||||
|
|
||||||
|
### 7.7 `biz_ai_model` AI 模型配置表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| model_id | INT | PK, AUTO_INCREMENT | 模型配置ID |
|
||||||
|
| model_type | VARCHAR(20) | NOT NULL | asr/llm |
|
||||||
|
| provider | VARCHAR(50) | NOT NULL | 提供商 (Qwen, OpenAI等) |
|
||||||
|
| model_name | VARCHAR(100) | NOT NULL | 模型标识 |
|
||||||
|
| api_key | VARCHAR(255) | | API密钥 |
|
||||||
|
| base_url | VARCHAR(255) | | 基础URL |
|
||||||
|
| api_path | VARCHAR(100) | | 接口路径 |
|
||||||
|
| config | JSON | | 差异化参数(temp, top_p等) |
|
||||||
|
| is_default | TINYINT(1) | NOT NULL | 是否默认 |
|
||||||
|
| status | TINYINT(1) | NOT NULL | 启用状态 |
|
||||||
|
| created_at | TIMESTAMP | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP | NOT NULL | 更新时间 |
|
||||||
|
|
||||||
|
## 7. 业务 - 会议管理
|
||||||
|
|
||||||
|
### 7.1 `biz_meeting` 会议主表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| meeting_id | INT | PK, AUTO_INCREMENT | 会议ID |
|
||||||
|
| user_id | INT | | 创建者ID |
|
||||||
|
| title | VARCHAR(255) | NOT NULL | 会议标题 |
|
||||||
|
| tags | VARCHAR(255) | | 标签(逗号分隔) |
|
||||||
|
| meeting_time | TIMESTAMP | | 会议时间 |
|
||||||
|
| access_password| VARCHAR(10) | | 访问密码 |
|
||||||
|
| summary | TEXT | | 最终总结内容 |
|
||||||
|
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||||
|
|
||||||
|
### 7.2 `biz_meeting_attendees` 会议参会人表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| attendee_id | INT | PK, AUTO_INCREMENT | 参会ID |
|
||||||
|
| meeting_id | INT | INDEX | 会议ID |
|
||||||
|
| user_id | INT | | 系统用户ID |
|
||||||
|
|
||||||
|
### 7.3 `biz_meeting_audio` 会议音频表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| audio_id | INT | PK, AUTO_INCREMENT | 音频ID |
|
||||||
|
| meeting_id | INT | | 会议ID |
|
||||||
|
| file_path | VARCHAR(512) | NOT NULL | 文件存储路径 |
|
||||||
|
| file_name | VARCHAR(200) | | 原始文件名 |
|
||||||
|
| file_size | DECIMAL(10,0) | | 文件大小(字节) |
|
||||||
|
| processing_status| VARCHAR(20) | DEFAULT 'uploaded' | 处理状态 |
|
||||||
|
| upload_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 上传时间 |
|
||||||
|
| error_message | TEXT | | 错误信息 |
|
||||||
|
|
||||||
|
### 7.4 `biz_transcript_task` 转译任务表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| task_id | VARCHAR(255) | PK | 任务ID |
|
||||||
|
| meeting_id | INT | INDEX, NOT NULL | 会议ID |
|
||||||
|
| status | ENUM | INDEX, DEFAULT 'pending' | pending/processing/completed/failed |
|
||||||
|
| progress | INT | DEFAULT 0 | 处理进度(0-100) |
|
||||||
|
| completed_at | TIMESTAMP | | 完成时间 |
|
||||||
|
| error_message | TEXT | | 错误信息 |
|
||||||
|
| created_at | TIMESTAMP | INDEX, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||||
|
|
||||||
|
### 7.5 `biz_transcript_segment` 转译片段表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| segment_id | INT | PK, AUTO_INCREMENT | 片段ID |
|
||||||
|
| meeting_id | INT | | 会议ID |
|
||||||
|
| speaker_id | INT | | 发言人ID |
|
||||||
|
| speaker_tag | VARCHAR(50) | | 发言人标签(如: A, B) |
|
||||||
|
| start_time_ms | INT | NOT NULL | 开始时间(毫秒) |
|
||||||
|
| end_time_ms | INT | NOT NULL | 结束时间(毫秒) |
|
||||||
|
| text_content | TEXT | NOT NULL | 文本内容 |
|
||||||
|
|
||||||
|
### 7.6 `biz_summarize_task` 总结任务表
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| task_id | VARCHAR(100) | PK | 任务ID |
|
||||||
|
| meeting_id | INT | INDEX, NOT NULL | 会议ID |
|
||||||
|
| prompt_id | INT | | 使用的提示词模板ID |
|
||||||
|
| user_prompt | TEXT | | 任务执行时的最终Prompt |
|
||||||
|
| status | VARCHAR(50) | INDEX, DEFAULT 'pending' | 任务状态 |
|
||||||
|
| progress | INT | DEFAULT 0 | 进度 |
|
||||||
|
| result | TEXT | | 总结结果 |
|
||||||
|
| error_message | TEXT | | 错误信息 |
|
||||||
|
| created_at | TIMESTAMP | INDEX, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||||
|
| completed_at | TIMESTAMP | | 完成时间 |
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
-- Nex Basse init schema (MySQL 5.7/8.0)
|
||||||
|
-- Charset: utf8mb4, Collation: utf8mb4_unicode_ci
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_user (
|
||||||
|
user_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
display_name VARCHAR(50) NOT NULL,
|
||||||
|
email VARCHAR(100) UNIQUE,
|
||||||
|
phone VARCHAR(30) UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
status TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
is_deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_role (
|
||||||
|
role_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
role_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
role_name VARCHAR(50) NOT NULL,
|
||||||
|
status TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
remark TEXT,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_user_role (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
role_id INT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_user_role (user_id, role_id),
|
||||||
|
INDEX idx_user_role_user (user_id),
|
||||||
|
INDEX idx_user_role_role (role_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_permission (
|
||||||
|
perm_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
parent_id INT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
code VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
perm_type VARCHAR(20) NOT NULL,
|
||||||
|
level INT NOT NULL,
|
||||||
|
path VARCHAR(255),
|
||||||
|
component VARCHAR(255),
|
||||||
|
icon VARCHAR(100),
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
is_visible TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
status TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
description TEXT,
|
||||||
|
meta JSON,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_perm_parent (parent_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_role_permission (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
role_id INT NOT NULL,
|
||||||
|
perm_id INT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_role_perm (role_id, perm_id),
|
||||||
|
INDEX idx_role_perm_role (role_id),
|
||||||
|
INDEX idx_role_perm_perm (perm_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_dict_type (
|
||||||
|
dict_type_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
type_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
type_name VARCHAR(50) NOT NULL,
|
||||||
|
status TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
remark TEXT,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_dict_item (
|
||||||
|
dict_item_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
type_code VARCHAR(50) NOT NULL,
|
||||||
|
item_label VARCHAR(100) NOT NULL,
|
||||||
|
item_value VARCHAR(100) NOT NULL,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
status TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
remark TEXT,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_dict_item (type_code, item_value),
|
||||||
|
INDEX idx_dict_item_type (type_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_param (
|
||||||
|
param_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
param_key VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
param_value TEXT NOT NULL,
|
||||||
|
param_type VARCHAR(20) NOT NULL DEFAULT 'string',
|
||||||
|
status TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
is_system TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
description TEXT,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Default system params
|
||||||
|
INSERT INTO sys_param (param_key, param_value, param_type, status, is_system, description)
|
||||||
|
VALUES
|
||||||
|
('security.access_token_minutes', '60', 'int', 1, 1, 'Access token 有效期(分钟)'),
|
||||||
|
('security.refresh_token_minutes', '10080', 'int', 1, 1, 'Refresh token 有效期(分钟)')
|
||||||
|
ON DUPLICATE KEY UPDATE param_value=VALUES(param_value);
|
||||||
|
|
||||||
|
-- Default admin role
|
||||||
|
INSERT IGNORE INTO sys_role (role_code, role_name, status, remark)
|
||||||
|
VALUES ('admin', '管理员', 1, '系统默认管理员');
|
||||||
|
|
||||||
|
-- Seed permissions (3-level menu/button)
|
||||||
|
INSERT IGNORE INTO sys_permission (perm_id, parent_id, name, code, perm_type, level, path, component, icon, sort_order, is_visible, status, description, meta)
|
||||||
|
VALUES
|
||||||
|
(1, NULL, 'AI 战略洞察', 'strategy.ai', 'menu', 1, '/strategy', NULL, 'strategy', 1, 1, 1, '一级菜单', NULL),
|
||||||
|
(2, NULL, '工作空间', 'workspace', 'menu', 1, '/workspace', NULL, 'workspace', 2, 1, 1, '一级菜单', NULL),
|
||||||
|
(3, NULL, '知识库', 'ai_agent', 'menu', 1, '/ai-agent', NULL, 'ai', 3, 1, 1, '一级菜单', NULL),
|
||||||
|
(4, NULL, '系统管理', 'system', 'menu', 1, '/system', NULL, 'system', 4, 1, 1, '一级菜单', NULL),
|
||||||
|
|
||||||
|
(10, 1, 'AI 战略洞察', 'strategy.ai.home', 'menu', 2, '/strategy/insights', 'StrategyHome', 'insight', 1, 1, 1, '二级菜单', NULL),
|
||||||
|
(11, 2, '实时会议', 'workspace.realtime', 'menu', 2, '/workspace/meeting', 'MeetingLive', 'meeting', 1, 1, 1, '二级菜单', NULL),
|
||||||
|
(12, 2, '历史记录', 'workspace.history', 'menu', 2, '/workspace/history', 'MeetingHistory', 'history', 2, 1, 1, '二级菜单', NULL),
|
||||||
|
(13, 2, '声纹档案', 'workspace.voice', 'menu', 2, '/workspace/voice', 'VoiceProfile', 'voice', 3, 1, 1, '二级菜单', NULL),
|
||||||
|
(14, 3, '知识库管理', 'ai_agent.kb', 'menu', 2, '/ai-agent/kb', 'KnowledgeBase', 'kb', 1, 1, 1, '二级菜单', NULL),
|
||||||
|
(15, 3, '总结模板管理', 'ai_agent.templates', 'menu', 2, '/ai-agent/templates', 'PromptTemplates', 'template', 2, 1, 1, '二级菜单', NULL),
|
||||||
|
(16, 3, '热词管理', 'ai_agent.hotwords', 'menu', 2, '/ai-agent/hotwords', 'Hotwords', 'hot', 3, 1, 1, '二级菜单', NULL),
|
||||||
|
(17, 4, '系统设置', 'system.setting', 'menu', 2, '/system/settings', 'SystemSettings', 'setting', 1, 1, 1, '二级菜单', NULL),
|
||||||
|
(18, 4, '用户管理', 'system.users', 'menu', 2, '/system/users', 'UserManage', 'user', 2, 1, 1, '二级菜单', NULL),
|
||||||
|
(19, 4, '角色管理', 'system.roles', 'menu', 2, '/system/roles', 'RoleManage', 'role', 3, 1, 1, '二级菜单', NULL),
|
||||||
|
(20, 4, '系统指标', 'system.metrics', 'menu', 2, '/system/metrics', 'SystemMetrics', 'metrics', 4, 1, 1, '二级菜单', NULL),
|
||||||
|
|
||||||
|
(101, 18, '创建用户', 'system.users.create', 'button', 3, NULL, NULL, NULL, 1, 1, 1, '按钮权限', NULL),
|
||||||
|
(102, 18, '编辑用户', 'system.users.edit', 'button', 3, NULL, NULL, NULL, 2, 1, 1, '按钮权限', NULL),
|
||||||
|
(103, 18, '删除用户', 'system.users.delete', 'button', 3, NULL, NULL, NULL, 3, 1, 1, '按钮权限', NULL),
|
||||||
|
(111, 19, '创建角色', 'system.roles.create', 'button', 3, NULL, NULL, NULL, 1, 1, 1, '按钮权限', NULL),
|
||||||
|
(112, 19, '编辑角色', 'system.roles.edit', 'button', 3, NULL, NULL, NULL, 2, 1, 1, '按钮权限', NULL),
|
||||||
|
(113, 19, '分配权限', 'system.roles.assign', 'button', 3, NULL, NULL, NULL, 3, 1, 1, '按钮权限', NULL);
|
||||||
|
|
||||||
|
-- Grant all permissions to admin role
|
||||||
|
INSERT IGNORE INTO sys_role_permission (role_id, perm_id)
|
||||||
|
SELECT r.role_id, p.perm_id
|
||||||
|
FROM sys_role r
|
||||||
|
JOIN sys_permission p
|
||||||
|
WHERE r.role_code = 'admin';
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Add status column to sys_user and backfill from is_active
|
||||||
|
ALTER TABLE sys_user ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'enabled';
|
||||||
|
UPDATE sys_user SET status = CASE WHEN is_active = 1 THEN 'enabled' ELSE 'disabled' END;
|
||||||
|
|
||||||
|
-- Optional cleanup (uncomment if you want to remove is_active)
|
||||||
|
-- ALTER TABLE sys_user DROP COLUMN is_active;
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- Convert status columns to TINYINT(1) and backfill (safe order)
|
||||||
|
UPDATE sys_user SET status = CASE WHEN status IN ('enabled', '1') THEN '1' ELSE '0' END;
|
||||||
|
ALTER TABLE sys_user MODIFY COLUMN status TINYINT(1) NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
UPDATE sys_role SET status = CASE WHEN status IN ('enabled', '1') THEN '1' ELSE '0' END;
|
||||||
|
ALTER TABLE sys_role MODIFY COLUMN status TINYINT(1) NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
UPDATE sys_permission SET status = CASE WHEN status IN ('enabled', '1') THEN '1' ELSE '0' END;
|
||||||
|
ALTER TABLE sys_permission MODIFY COLUMN status TINYINT(1) NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
UPDATE sys_dict_type SET status = CASE WHEN status IN ('enabled', '1') THEN '1' ELSE '0' END;
|
||||||
|
ALTER TABLE sys_dict_type MODIFY COLUMN status TINYINT(1) NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
UPDATE sys_dict_item SET status = CASE WHEN status IN ('enabled', '1') THEN '1' ELSE '0' END;
|
||||||
|
ALTER TABLE sys_dict_item MODIFY COLUMN status TINYINT(1) NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
UPDATE sys_param SET status = CASE WHEN status IN ('enabled', '1') THEN '1' ELSE '0' END;
|
||||||
|
ALTER TABLE sys_param MODIFY COLUMN status TINYINT(1) NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
-- Optional: drop legacy is_active if exists
|
||||||
|
-- ALTER TABLE sys_user DROP COLUMN is_active;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- Drop legacy is_active column if exists (MySQL 5.7/8.0 compatible)
|
||||||
|
SET @exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA='nex_basse'
|
||||||
|
AND TABLE_NAME='sys_user'
|
||||||
|
AND COLUMN_NAME='is_active'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@exists=1, 'ALTER TABLE sys_user DROP COLUMN is_active', 'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- add permission nodes for permissions/dicts/params pages
|
||||||
|
INSERT IGNORE INTO sys_permission (perm_id, parent_id, name, code, perm_type, level, path, component, icon, sort_order, is_visible, status, description, meta)
|
||||||
|
VALUES
|
||||||
|
(21, 4, '权限管理', 'system.permissions', 'menu', 2, '/system/permissions', 'PermissionTree', 'lock', 5, 1, 1, '二级菜单', NULL),
|
||||||
|
(22, 4, '码表管理', 'system.dicts', 'menu', 2, '/system/dicts', 'DictManage', 'dict', 6, 1, 1, '二级菜单', NULL),
|
||||||
|
(23, 4, '系统参数', 'system.params', 'menu', 2, '/system/params', 'ParamManage', 'param', 7, 1, 1, '二级菜单', NULL);
|
||||||
|
|
||||||
|
-- NOTE: 不自动给角色授权权限,请在角色权限分配页面手动分配
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
-- Add LOG_OPERATION_TYPE dict
|
||||||
|
INSERT INTO sys_dict_type (type_code, type_name, status, remark) VALUES ('LOG_OPERATION_TYPE', '操作类型', 1, '系统日志操作类型');
|
||||||
|
|
||||||
|
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order, status) VALUES
|
||||||
|
('LOG_OPERATION_TYPE', '登录', 'LOGIN', 1, 1),
|
||||||
|
('LOG_OPERATION_TYPE', '登出', 'LOGOUT', 2, 1),
|
||||||
|
('LOG_OPERATION_TYPE', '创建', 'CREATE', 3, 1),
|
||||||
|
('LOG_OPERATION_TYPE', '更新', 'UPDATE', 4, 1),
|
||||||
|
('LOG_OPERATION_TYPE', '删除', 'DELETE', 5, 1),
|
||||||
|
('LOG_OPERATION_TYPE', '查询', 'QUERY', 6, 1);
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- Remove auto-granted permissions for newly added menu items (manual assignment only)
|
||||||
|
DELETE FROM sys_role_permission WHERE perm_id IN (21,22,23);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE sys_user ADD COLUMN avatar VARCHAR(255) DEFAULT NULL;
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- 提示词模板表
|
||||||
|
CREATE TABLE `biz_prompt_template` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`name` VARCHAR(100) NOT NULL COMMENT '模板名称',
|
||||||
|
`category` VARCHAR(50) NOT NULL COMMENT '分类',
|
||||||
|
`content` TEXT NOT NULL COMMENT '提示词内容',
|
||||||
|
`description` VARCHAR(255) COMMENT '描述',
|
||||||
|
`user_id` INT COMMENT '创建者ID (系统级为NULL)',
|
||||||
|
`is_system` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否系统内置',
|
||||||
|
`status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '全局状态:1启用 / 0禁用',
|
||||||
|
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '默认排序',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
INDEX `idx_prompt_user_id` (`user_id`),
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`user_id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 用户提示词配置表
|
||||||
|
CREATE TABLE `biz_user_prompt_config` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`user_id` INT NOT NULL COMMENT '用户ID',
|
||||||
|
`template_id` INT NOT NULL COMMENT '模板ID',
|
||||||
|
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '用户是否启用',
|
||||||
|
`user_sort_order` INT NOT NULL DEFAULT 0 COMMENT '用户自定义排序',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
UNIQUE KEY `uk_user_template` (`user_id`, `template_id`),
|
||||||
|
INDEX `idx_config_user_id` (`user_id`),
|
||||||
|
INDEX `idx_config_template_id` (`template_id`),
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`user_id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`template_id`) REFERENCES `biz_prompt_template` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
-- 2. 创建 AI 模型配置表
|
||||||
|
CREATE TABLE `biz_ai_model` (
|
||||||
|
`model_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`model_type` VARCHAR(20) NOT NULL COMMENT '类型: asr/llm',
|
||||||
|
`provider` VARCHAR(50) NOT NULL COMMENT '提供商: Qwen, OpenAI, Aliyun等',
|
||||||
|
`model_name` VARCHAR(100) NOT NULL COMMENT '模型标识/Model Name',
|
||||||
|
`api_key` VARCHAR(255) COMMENT 'API密钥',
|
||||||
|
`base_url` VARCHAR(255) COMMENT '接口基础地址',
|
||||||
|
`api_path` VARCHAR(100) COMMENT '接口路径',
|
||||||
|
`config` JSON COMMENT '差异化配置(temp, top_p, language等)',
|
||||||
|
`is_default` TINYINT(1) DEFAULT 0 COMMENT '是否为该类型的默认模型',
|
||||||
|
`status` TINYINT(1) DEFAULT 1 COMMENT '1启用/0禁用',
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 3. 调整会议主表:增加状态字段
|
||||||
|
ALTER TABLE `biz_meeting`
|
||||||
|
ADD COLUMN `status` VARCHAR(20) DEFAULT 'draft' COMMENT '状态: draft, transcribing, summarizing, completed' AFTER `access_password`;
|
||||||
|
|
||||||
|
-- 4. 调整会议音频表:增加时长
|
||||||
|
ALTER TABLE `biz_meeting_audio`
|
||||||
|
ADD COLUMN `duration` INT DEFAULT 0 COMMENT '音频时长(秒)' AFTER `file_size`;
|
||||||
|
|
||||||
|
-- 5. 调整转译任务表:关联模型配置、增加语言
|
||||||
|
ALTER TABLE `biz_transcript_task`
|
||||||
|
ADD COLUMN `model_id` INT COMMENT '使用的模型配置ID' AFTER `meeting_id`,
|
||||||
|
ADD COLUMN `language` VARCHAR(10) DEFAULT 'auto' COMMENT '识别语言',
|
||||||
|
ADD INDEX `idx_task_model` (`model_id`);
|
||||||
|
|
||||||
|
-- 6. 调整总结任务表:关联模型配置
|
||||||
|
ALTER TABLE `biz_summarize_task`
|
||||||
|
ADD COLUMN `model_id` INT COMMENT '使用的模型配置ID' AFTER `prompt_id`,
|
||||||
|
ADD INDEX `idx_sum_model` (`model_id`);
|
||||||
|
|
||||||
|
-- 7. 调整转译片段表:关联音频ID
|
||||||
|
ALTER TABLE `biz_transcript_segment`
|
||||||
|
ADD COLUMN `audio_id` INT COMMENT '所属音频ID' AFTER `meeting_id`,
|
||||||
|
ADD INDEX `idx_seg_audio` (`audio_id`);
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
fastapi==0.115.6
|
||||||
|
uvicorn[standard]==0.30.6
|
||||||
|
sqlalchemy==2.0.36
|
||||||
|
pydantic==2.9.2
|
||||||
|
pydantic-settings==2.6.1
|
||||||
|
alembic==1.13.3
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
bcrypt==4.1.3
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
redis==5.1.1
|
||||||
|
pymysql==1.1.1
|
||||||
|
psutil==5.9.8
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import os
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.config import get_settings
|
||||||
|
from app.core.security import hash_password
|
||||||
|
from app.models import User, Role, UserRole
|
||||||
|
|
||||||
|
|
||||||
|
def build_db_url() -> str:
|
||||||
|
settings = get_settings()
|
||||||
|
return (
|
||||||
|
f"mysql+pymysql://{settings.db_user}:{settings.db_password}"
|
||||||
|
f"@{settings.db_host}:{settings.db_port}/{settings.db_name}"
|
||||||
|
"?charset=utf8mb4"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
username = os.getenv("INIT_ADMIN_USERNAME", "admin")
|
||||||
|
password = os.getenv("INIT_ADMIN_PASSWORD", "123456")
|
||||||
|
|
||||||
|
engine = create_engine(build_db_url(), pool_pre_ping=True)
|
||||||
|
with Session(engine) as db:
|
||||||
|
role = db.query(Role).filter(Role.role_code == "admin").first()
|
||||||
|
if not role:
|
||||||
|
role = Role(role_code="admin", role_name="管理员")
|
||||||
|
db.add(role)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
user = db.query(User).filter(User.username == username).first()
|
||||||
|
if not user:
|
||||||
|
user = User(
|
||||||
|
username=username,
|
||||||
|
display_name="Administrator",
|
||||||
|
email=None,
|
||||||
|
phone=None,
|
||||||
|
password_hash=hash_password(password),
|
||||||
|
status=1,
|
||||||
|
is_deleted=False,
|
||||||
|
)
|
||||||
|
db.add(user)
|
||||||
|
db.flush()
|
||||||
|
else:
|
||||||
|
user.password_hash = hash_password(password)
|
||||||
|
user.status = 1
|
||||||
|
|
||||||
|
exist_link = db.query(UserRole).filter(UserRole.user_id == user.user_id, UserRole.role_id == role.role_id).first()
|
||||||
|
if not exist_link:
|
||||||
|
db.add(UserRole(user_id=user.user_id, role_id=role.role_id))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
print(f"Admin ready: {username}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Activate a Python virtual environment for the current PowerShell session.
|
||||||
|
|
||||||
|
.Description
|
||||||
|
Pushes the python executable for a virtual environment to the front of the
|
||||||
|
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||||
|
in a Python virtual environment. Makes use of the command line switches as
|
||||||
|
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||||
|
|
||||||
|
.Parameter VenvDir
|
||||||
|
Path to the directory that contains the virtual environment to activate. The
|
||||||
|
default value for this is the parent of the directory that the Activate.ps1
|
||||||
|
script is located within.
|
||||||
|
|
||||||
|
.Parameter Prompt
|
||||||
|
The prompt prefix to display when this virtual environment is activated. By
|
||||||
|
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||||
|
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Verbose
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and shows extra information about the activation as it executes.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||||
|
Activates the Python virtual environment located in the specified location.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Prompt "MyPython"
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and prefixes the current prompt with the specified string (surrounded in
|
||||||
|
parentheses) while the virtual environment is active.
|
||||||
|
|
||||||
|
.Notes
|
||||||
|
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||||
|
execution policy for the user. You can do this by issuing the following PowerShell
|
||||||
|
command:
|
||||||
|
|
||||||
|
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
|
||||||
|
For more information on Execution Policies:
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||||
|
|
||||||
|
#>
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$VenvDir,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$Prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
<# Function declarations --------------------------------------------------- #>
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Remove all shell session elements added by the Activate script, including the
|
||||||
|
addition of the virtual environment's Python executable from the beginning of
|
||||||
|
the PATH variable.
|
||||||
|
|
||||||
|
.Parameter NonDestructive
|
||||||
|
If present, do not remove this function from the global namespace for the
|
||||||
|
session.
|
||||||
|
|
||||||
|
#>
|
||||||
|
function global:deactivate ([switch]$NonDestructive) {
|
||||||
|
# Revert to original values
|
||||||
|
|
||||||
|
# The prior prompt:
|
||||||
|
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||||
|
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||||
|
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PYTHONHOME:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PATH:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the VIRTUAL_ENV altogether:
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||||
|
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave deactivate function in the global namespace if requested:
|
||||||
|
if (-not $NonDestructive) {
|
||||||
|
Remove-Item -Path function:deactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Description
|
||||||
|
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||||
|
given folder, and returns them in a map.
|
||||||
|
|
||||||
|
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||||
|
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||||
|
then it is considered a `key = value` line. The left hand string is the key,
|
||||||
|
the right hand is the value.
|
||||||
|
|
||||||
|
If the value starts with a `'` or a `"` then the first and last character is
|
||||||
|
stripped from the value before being captured.
|
||||||
|
|
||||||
|
.Parameter ConfigDir
|
||||||
|
Path to the directory that contains the `pyvenv.cfg` file.
|
||||||
|
#>
|
||||||
|
function Get-PyVenvConfig(
|
||||||
|
[String]
|
||||||
|
$ConfigDir
|
||||||
|
) {
|
||||||
|
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||||
|
|
||||||
|
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||||
|
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||||
|
|
||||||
|
# An empty map will be returned if no config file is found.
|
||||||
|
$pyvenvConfig = @{ }
|
||||||
|
|
||||||
|
if ($pyvenvConfigPath) {
|
||||||
|
|
||||||
|
Write-Verbose "File exists, parse `key = value` lines"
|
||||||
|
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||||
|
|
||||||
|
$pyvenvConfigContent | ForEach-Object {
|
||||||
|
$keyval = $PSItem -split "\s*=\s*", 2
|
||||||
|
if ($keyval[0] -and $keyval[1]) {
|
||||||
|
$val = $keyval[1]
|
||||||
|
|
||||||
|
# Remove extraneous quotations around a string value.
|
||||||
|
if ("'""".Contains($val.Substring(0, 1))) {
|
||||||
|
$val = $val.Substring(1, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyvenvConfig[$keyval[0]] = $val
|
||||||
|
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pyvenvConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<# Begin Activate script --------------------------------------------------- #>
|
||||||
|
|
||||||
|
# Determine the containing directory of this script
|
||||||
|
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||||
|
|
||||||
|
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||||
|
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||||
|
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||||
|
|
||||||
|
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||||
|
# First, get the location of the virtual environment, it might not be
|
||||||
|
# VenvExecDir if specified on the command line.
|
||||||
|
if ($VenvDir) {
|
||||||
|
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||||
|
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||||
|
Write-Verbose "VenvDir=$VenvDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||||
|
# as `prompt`.
|
||||||
|
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||||
|
|
||||||
|
# Next, set the prompt from the command line, or the config file, or
|
||||||
|
# just use the name of the virtual environment folder.
|
||||||
|
if ($Prompt) {
|
||||||
|
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||||
|
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||||
|
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||||
|
$Prompt = $pyvenvCfg['prompt'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||||
|
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||||
|
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Prompt = '$Prompt'"
|
||||||
|
Write-Verbose "VenvDir='$VenvDir'"
|
||||||
|
|
||||||
|
# Deactivate any currently active virtual environment, but leave the
|
||||||
|
# deactivate function in place.
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||||
|
# that there is an activated venv.
|
||||||
|
$env:VIRTUAL_ENV = $VenvDir
|
||||||
|
|
||||||
|
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||||
|
|
||||||
|
Write-Verbose "Setting prompt to '$Prompt'"
|
||||||
|
|
||||||
|
# Set the prompt to include the env name
|
||||||
|
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||||
|
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||||
|
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||||
|
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||||
|
|
||||||
|
function global:prompt {
|
||||||
|
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||||
|
_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear PYTHONHOME
|
||||||
|
if (Test-Path -Path Env:PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
Remove-Item -Path Env:PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the venv to the PATH
|
||||||
|
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||||
|
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# You cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||||
|
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||||
|
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||||
|
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||||
|
export VIRTUAL_ENV=$(cygpath "/Users/jiliu/WorkSpace/nex_meeting/backend/venv")
|
||||||
|
else
|
||||||
|
# use the path as-is
|
||||||
|
export VIRTUAL_ENV="/Users/jiliu/WorkSpace/nex_meeting/backend/venv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
PS1="(venv) ${PS1:-}"
|
||||||
|
export PS1
|
||||||
|
VIRTUAL_ENV_PROMPT="(venv) "
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV "/Users/jiliu/WorkSpace/nex_meeting/backend/venv"
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
|
set prompt = "(venv) $prompt"
|
||||||
|
setenv VIRTUAL_ENV_PROMPT "(venv) "
|
||||||
|
endif
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||||
|
# (https://fishshell.com/). You cannot run it directly.
|
||||||
|
|
||||||
|
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
# prevents error when using nested fish instances (Issue #93858)
|
||||||
|
if functions -q _old_fish_prompt
|
||||||
|
functions -e fish_prompt
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
set -e VIRTUAL_ENV_PROMPT
|
||||||
|
if test "$argv[1]" != "nondestructive"
|
||||||
|
# Self-destruct!
|
||||||
|
functions -e deactivate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV "/Users/jiliu/WorkSpace/nex_meeting/backend/venv"
|
||||||
|
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
||||||
|
|
||||||
|
# Unset PYTHONHOME if set.
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# fish uses a function instead of an env var to generate the prompt.
|
||||||
|
|
||||||
|
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
# With the original prompt function renamed, we can override with our own.
|
||||||
|
function fish_prompt
|
||||||
|
# Save the return status of the last command.
|
||||||
|
set -l old_status $status
|
||||||
|
|
||||||
|
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||||
|
printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal)
|
||||||
|
|
||||||
|
# Restore the return status of the previous command.
|
||||||
|
echo "exit $old_status" | .
|
||||||
|
# Output the original/"old" prompt.
|
||||||
|
_old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT "(venv) "
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from alembic.config import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from dotenv.__main__ import cli
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(cli())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from fastapi.cli import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from httpx import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from mako.cmd import cmdline
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(cmdline())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from rsa.cli import decrypt
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(decrypt())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from rsa.cli import encrypt
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(encrypt())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from rsa.cli import keygen
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(keygen())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from rsa.util import private_to_public
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(private_to_public())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from rsa.cli import sign
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(sign())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from rsa.cli import verify
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(verify())
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
python3.12
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
python3.12
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from uvicorn.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/Users/jiliu/WorkSpace/nex_meeting/backend/venv/bin/python3.12
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from watchfiles.cli import cli
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(cli())
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue