Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
.env
.agent
api/data/
7 changes: 2 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# 1. Build Vue Frontend
################################
FROM node:24-alpine AS frontend-builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

WORKDIR /app/web
COPY web/. ./
Expand All @@ -15,14 +13,13 @@ RUN npm install && npm run build
################################
FROM python:3.13-slim

WORKDIR /app
WORKDIR /api

# 拷贝后端
COPY api/ ./

ENV PATH="/app/.venv/bin:$PATH"
ENV PATH="/api/.venv/bin:$PATH"

# 拷贝数据目录(确保 sqlite 文件目录存在) 在容器中的数据保存目录为 /app/data
# 安装后端依赖
RUN mkdir -p data && pip install uv && uv sync

Expand Down
70 changes: 16 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,70 +50,32 @@ docker-compose up -d

## 💻 本地开发

### 前置要求

- Python 3.13
- Node.js 24
- npm

### 环境配置

创建 `api/.env` 文件配置环境变量:

```env
# 数据库配置
DATABASE_URL=sqlite+aiosqlite:///./data/app.db
# DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/dbname

# JWT 配置
SECRET_KEY=your-secret-key-here
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
ALGORITHM=HS256
```sh
# 启动(带日志)
docker compose -f docker-compose.dev.yml up
```

### 后端设置
更多命令

```bash
cd api
```sh
# 后台启动
docker compose -f docker-compose.dev.yml up -d

# 创建虚拟环境
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 查看日志
docker compose -f docker-compose.dev.yml logs -f

# 安装依赖
pip install uv
uv sync

# 运行数据库迁移
alembic upgrade head
# 停止
docker compose -f docker-compose.dev.yml down

# 启动开发服务器
python main.py
# 重新构建(如果修改了依赖)
docker compose -f docker-compose.dev.yml up --build
```

后端将在 http://localhost:8000 运行

### 前端设置

```bash
cd web

# 安装依赖
npm install

# 启动开发服务器
npm run dev
```

前端将在 http://localhost:5173 运行

## 📖 API 文档
前端开发服务器:http://localhost:5173

启动后端服务后,访问以下地址查看自动生成的 API 文档:
后端 API:http://localhost:8000

- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
API 文档:http://localhost:8000/docs

## 📚 数据库迁移

Expand Down
1 change: 0 additions & 1 deletion api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ SECRET_KEY=change-me-generate-a-secure-random-string
ACCESS_TOKEN_EXPIRE_MINUTES=60
REFRESH_TOKEN_EXPIRE_DAYS=7
JWT_ALGORITHM=HS256
# DATABASE_URL 非必填,如果未配置 DATABASE_URL,默认使用 sqlite
# DATABASE_URL=postgres://postgres:password@localhost:5432/postgres
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Make_workspace_id_required_and_update_foreign_key_constraints

Revision ID: 3cc16d4f8bbb
Revises: 54ffc5a9b833
Create Date: 2026-02-01 12:50:58.707755

"""

from typing import Sequence, Union

import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "3cc16d4f8bbb"
down_revision: Union[str, Sequence[str], None] = "54ffc5a9b833"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"files", "workspace_id", existing_type=sa.VARCHAR(length=36), nullable=False
)
op.alter_column(
"folders", "workspace_id", existing_type=sa.VARCHAR(length=36), nullable=False
)
op.alter_column(
"notes", "workspace_id", existing_type=sa.VARCHAR(length=36), nullable=False
)
op.alter_column(
"workspace_user_association",
"created_at",
existing_type=postgresql.TIMESTAMP(),
nullable=True,
)
op.drop_constraint(
op.f("workspaces_created_by_fkey"), "workspaces", type_="foreignkey"
)
op.create_foreign_key(None, "workspaces", "users", ["created_by"], ["id"])
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "workspaces", type_="foreignkey")
op.create_foreign_key(
op.f("workspaces_created_by_fkey"),
"workspaces",
"users",
["created_by"],
["id"],
ondelete="SET NULL",
)
op.alter_column(
"workspace_user_association",
"created_at",
existing_type=postgresql.TIMESTAMP(),
nullable=False,
)
op.alter_column(
"notes", "workspace_id", existing_type=sa.VARCHAR(length=36), nullable=True
)
op.alter_column(
"folders", "workspace_id", existing_type=sa.VARCHAR(length=36), nullable=True
)
op.alter_column(
"files", "workspace_id", existing_type=sa.VARCHAR(length=36), nullable=True
)
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ def upgrade() -> None:
)

# 删除 users 表的 role 列
# PostgreSQL 支持直接删除列,SQLite 需要使用 batch 模式
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.drop_column("role")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ def upgrade() -> None:

def downgrade() -> None:
"""删除存储后端配置表"""
# 使用 batch mode 来支持 SQLite
with op.batch_alter_table("files", schema=None) as batch_op:
batch_op.drop_constraint("fk_files_storage_backend_id", type_="foreignkey")
batch_op.drop_column("storage_backend_id")
Expand Down
10 changes: 10 additions & 0 deletions api/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

Expand All @@ -29,6 +30,15 @@ async def lifespan(app: FastAPI):

app = FastAPI(lifespan=lifespan)

# 配置 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(auth.router)
app.include_router(users.router)
app.include_router(workspaces.router)
Expand Down
83 changes: 35 additions & 48 deletions api/app/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,62 +15,49 @@

load_dotenv()

# 默认使用异步 sqlite 驱动 aiosqlite
_DEFAULT_SQLITE = "sqlite+aiosqlite:///./data/app.sqlite"

# 支持通过环境变量 `DATABASE_URL` 切换为 Postgres(推荐带 asyncpg 驱动)
# 如果用户提供常见的 postgres URI(postgres:// 或 postgresql://),
# 会自动把 scheme 转换为 `postgresql+asyncpg://` 以使用 asyncpg
raw_db_url = os.getenv("DATABASE_URL", "").strip()
connect_args = {}

if raw_db_url:
# 如果是 sqlite,直接使用,不进行后续的 URL 重组(避免 urlunparse 丢失 /// 问题)
if raw_db_url.startswith("sqlite"):
DATABASE_URL = raw_db_url
else:
if raw_db_url.startswith("postgres://"):
raw_db_url = raw_db_url.replace("postgres://", "postgresql+asyncpg://", 1)
elif raw_db_url.startswith("postgresql://"):
raw_db_url = raw_db_url.replace("postgresql://", "postgresql+asyncpg://", 1)

# 解析 URL 处理 asyncpg 不支持的参数
parsed = urlparse(raw_db_url)
query_params = parse_qs(parsed.query)

# 处理 sslmode
if "sslmode" in query_params:
ssl_mode = query_params.pop("sslmode")[0]
if ssl_mode == "require":
connect_args["ssl"] = "require"
elif ssl_mode == "disable":
connect_args["ssl"] = False

# 处理 channel_binding (asyncpg 不支持此参数作为 kwarg,移除以避免报错)
if "channel_binding" in query_params:
query_params.pop("channel_binding")

# 重组 URL
new_query = urlencode(query_params, doseq=True)
parsed = parsed._replace(query=new_query)
DATABASE_URL = urlunparse(parsed)
else:
DATABASE_URL = _DEFAULT_SQLITE

# 对 sqlite 使用特定 connect_args(aiosqlite 的 check_same_thread)
if DATABASE_URL.startswith("sqlite"):
engine = create_async_engine(
DATABASE_URL, connect_args={"check_same_thread": False}
)
if raw_db_url.startswith("postgres://"):
raw_db_url = raw_db_url.replace("postgres://", "postgresql+asyncpg://", 1)
elif raw_db_url.startswith("postgresql://"):
raw_db_url = raw_db_url.replace("postgresql://", "postgresql+asyncpg://", 1)

# 解析 URL 处理 asyncpg 不支持的参数
parsed = urlparse(raw_db_url)
query_params = parse_qs(parsed.query)

# 处理 sslmode
if "sslmode" in query_params:
ssl_mode = query_params.pop("sslmode")[0]
if ssl_mode == "require":
connect_args["ssl"] = "require"
elif ssl_mode == "disable":
connect_args["ssl"] = False

# 处理 channel_binding (asyncpg 不支持此参数作为 kwarg,移除以避免报错)
if "channel_binding" in query_params:
query_params.pop("channel_binding")

# 重组 URL
new_query = urlencode(query_params, doseq=True)
parsed = parsed._replace(query=new_query)
DATABASE_URL = urlunparse(parsed)
else:
engine = create_async_engine(
DATABASE_URL,
connect_args=connect_args,
pool_size=20, # 增加连接池大小
max_overflow=40, # 允许超出连接池的额外连接数
pool_pre_ping=True, # 连接前ping确保连接有效
pool_recycle=3600, # 1小时后回收连接
)
raise ValueError("DATABASE_URL environment variable is required")

engine = create_async_engine(
DATABASE_URL,
connect_args=connect_args,
pool_size=20, # 增加连接池大小
max_overflow=40, # 允许超出连接池的额外连接数
pool_pre_ping=True, # 连接前ping确保连接有效
pool_recycle=3600, # 1小时后回收连接
)

async_session_maker = async_sessionmaker(engine, expire_on_commit=False)

Expand Down
2 changes: 1 addition & 1 deletion api/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class StorageBackendConfig(Base):
id = Column(
String(36), primary_key=True, index=True, default=lambda: str(uuid.uuid4())
)
workspace_id = Column(String(36), ForeignKey("workspaces.id"), nullable=False)
workspace_id = Column(String(36), ForeignKey("workspaces.id"), nullable=True)
name: Mapped[str] = mapped_column(String, unique=True, nullable=False, index=True)
backend_type: Mapped[str] = mapped_column(String, nullable=False) # local or s3
is_active: Mapped[bool] = mapped_column(Integer, default=0) # 0: False, 1: True
Expand Down
1 change: 0 additions & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"aiosqlite>=0.21.0",
"fastapi>=0.123.4",
"pydantic>=2.12.5",
"python-multipart>=0.0.20",
Expand Down
Loading