基础系统版本
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