初始化

This commit is contained in:
zg
2026-07-15 16:08:17 +08:00
parent f0d70aded4
commit 4e0057ece5
89 changed files with 13512 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""路由包"""

View File

@@ -0,0 +1,61 @@
"""智能导入 API — 文本解析与匹配"""
from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session, joinedload
from models import get_db, Website
from middleware.user_context import UserContext
from urllib.parse import urlparse
router = APIRouter(prefix="/api/import", tags=["智能导入"])
@router.post("/parse")
def parse_import_text(data: dict, db: Session = Depends(get_db), request: Request = None):
"""解析粘贴的文本,返回结构化结果 + 匹配信息"""
user_id = UserContext.get_user_id(request)
text = data.get("text", "").strip()
if not text:
return {"ok": False, "error": "文本为空"}
lines = [l.strip() for l in text.split("\n") if l.strip()]
if len(lines) < 3:
return {"ok": False, "error": "至少需要 3 行:网址+网站名、账号、密码"}
# 解析首行: 网址 + 空格 + 网站名
first_line = lines[0]
space_idx = first_line.find(" ")
if space_idx > 0:
url = first_line[:space_idx].strip()
site_name = first_line[space_idx + 1:].strip()
else:
url = first_line
try:
site_name = urlparse(url).hostname or url
except Exception:
site_name = url
result = {
"ok": True,
"url": url,
"site_name": site_name,
"username": lines[1] if len(lines) > 1 else "",
"password": lines[2] if len(lines) > 2 else "",
"remark": lines[3] if len(lines) > 3 else "",
}
# 匹配已有网站(仅当前用户)
existing = db.query(Website).options(joinedload(Website.accounts)).filter(
Website.url == url, Website.created_by == user_id
).first()
if existing:
result["matched_site"] = {
"id": existing.id,
"name": existing.name,
"folder_id": existing.folder_id,
"account_count": len(existing.accounts),
}
result["action"] = "append"
else:
result["matched_site"] = None
result["action"] = "create"
return result

159
backend/routes/notes.py Normal file
View File

@@ -0,0 +1,159 @@
"""笔记相关路由"""
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from models import get_db, Note
from middleware.user_context import UserContext
router = APIRouter(tags=["笔记"])
@router.get("/api/notes")
def list_notes(folder_id: int = None, db: Session = Depends(get_db), request: Request = None):
"""获取笔记列表(仅当前用户)"""
user_id = UserContext.get_user_id(request)
q = db.query(Note).filter(Note.created_by == user_id)
if folder_id:
q = q.filter(Note.folder_id == folder_id)
return q.order_by(Note.updated_at.desc()).all()
@router.post("/api/notes")
def create_note(data: dict, db: Session = Depends(get_db), request: Request = None):
"""创建笔记"""
user_id = UserContext.get_user_id(request)
note = Note(
title=data.get("title", "无标题"),
content=data.get("content", ""),
folder_id=data.get("folder_id"),
created_by=user_id,
updated_by=user_id,
)
db.add(note)
db.commit()
db.refresh(note)
return {"id": note.id, "title": note.title, "content": note.content,
"folder_id": note.folder_id, "created_by": note.created_by, "updated_by": note.updated_by,
"created_at": str(note.created_at), "updated_at": str(note.updated_at)}
@router.get("/api/notes/{note_id}")
def get_note(note_id: int, db: Session = Depends(get_db), request: Request = None):
"""获取单个笔记"""
user_id = UserContext.get_user_id(request)
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
if not note:
raise HTTPException(404, "笔记不存在")
return {"id": note.id, "title": note.title, "content": note.content,
"folder_id": note.folder_id, "created_by": note.created_by, "updated_by": note.updated_by,
"created_at": str(note.created_at), "updated_at": str(note.updated_at)}
@router.put("/api/notes/{note_id}")
def update_note(note_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
"""更新笔记"""
user_id = UserContext.get_user_id(request)
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
if not note:
raise HTTPException(404, "笔记不存在")
if "title" in data:
note.title = data["title"]
if "content" in data:
note.content = data["content"]
if "folder_id" in data:
note.folder_id = data["folder_id"]
note.updated_by = user_id
db.commit()
db.refresh(note)
return {"ok": True}
@router.delete("/api/notes/{note_id}")
def delete_note(note_id: int, db: Session = Depends(get_db), request: Request = None):
"""删除笔记"""
user_id = UserContext.get_user_id(request)
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
if not note:
raise HTTPException(404, "笔记不存在")
db.delete(note)
db.commit()
return {"ok": True}
# ===== 分享 =====
import secrets
@router.post("/api/notes/{note_id}/share")
def share_note(note_id: int, data: dict = {}, db: Session = Depends(get_db), request: Request = None):
"""生成/刷新分享链接,可设置有效期"""
from datetime import datetime, timedelta
user_id = UserContext.get_user_id(request)
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
if not note:
raise HTTPException(404, "笔记不存在")
note.share_token = secrets.token_urlsafe(16)
# 有效期: 7d / 30d / 1y / forever
ttl = data.get("ttl", "forever")
if ttl == "7d":
note.share_expires_at = datetime.utcnow() + timedelta(days=7)
elif ttl == "30d":
note.share_expires_at = datetime.utcnow() + timedelta(days=30)
elif ttl == "1y":
note.share_expires_at = datetime.utcnow() + timedelta(days=365)
else:
note.share_expires_at = None # 永久
db.commit()
return {
"share_token": note.share_token,
"url": f"/share/{note.share_token}",
"expires_at": note.share_expires_at.strftime("%Y-%m-%d %H:%M") if note.share_expires_at else "永久有效",
}
@router.delete("/api/notes/{note_id}/share")
def unshare_note(note_id: int, db: Session = Depends(get_db), request: Request = None):
"""取消分享"""
user_id = UserContext.get_user_id(request)
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
if not note:
raise HTTPException(404, "笔记不存在")
note.share_token = None
db.commit()
return {"ok": True}
# ===== 移动 =====
@router.put("/api/notes/{note_id}/move")
def move_note(note_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
"""移动笔记到另一个文件夹"""
user_id = UserContext.get_user_id(request)
note = db.query(Note).filter(Note.id == note_id, Note.created_by == user_id).first()
if not note:
raise HTTPException(404, "笔记不存在")
note.folder_id = data.get("folder_id")
db.commit()
return {"ok": True, "folder_id": note.folder_id}
# ===== 分享管理 =====
@router.get("/api/shares")
def list_shares(db: Session = Depends(get_db), request: Request = None):
"""列出当前用户已分享的笔记"""
user_id = UserContext.get_user_id(request)
notes = db.query(Note).filter(
Note.share_token.isnot(None),
Note.created_by == user_id,
).order_by(Note.updated_at.desc()).all()
result = []
for n in notes:
result.append({
"id": n.id,
"title": n.title,
"share_token": n.share_token,
"share_url": f"/share/{n.share_token}",
"share_expires_at": n.share_expires_at.strftime("%Y-%m-%d %H:%M") if n.share_expires_at else "永久有效",
"created_by": n.created_by,
"updated_at": str(n.updated_at),
})
return result

75
backend/routes/share.py Normal file
View File

@@ -0,0 +1,75 @@
"""分享笔记公开查看"""
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from models import get_db, Note
router = APIRouter(tags=["分享"])
CSS = """
:root{--text:#1F2329;--text2:#646A73;--text3:#8F959E;--border:#E5E6EB;--brand:#3370FF;--bg:#F5F6F8;}
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--text);background:var(--bg);min-height:100vh;}
.wrap{max-width:1100px;margin:0 auto;padding:32px 16px;}
.card{background:#fff;border-radius:12px;padding:36px;box-shadow:0 1px 3px rgba(0,0,0,.06);overflow-x:auto;word-wrap:break-word;overflow-wrap:break-word;max-width:100%;}
h1{font-size:26px;font-weight:700;padding-bottom:16px;margin-bottom:16px;border-bottom:2px solid var(--border);line-height:1.3;}
.meta{font-size:12px;color:var(--text3);margin-bottom:28px;display:flex;gap:20px;}
.content{font-size:15px;line-height:1.9;color:var(--text);overflow-x:auto;word-wrap:break-word;overflow-wrap:break-word;}
.content h2{font-size:20px;font-weight:600;margin:24px 0 10px;}
.content h3{font-size:17px;font-weight:600;margin:18px 0 8px;}
.content p{margin:8px 0;}
.content ul,.content ol{padding-left:22px;margin:8px 0;}
.content li{margin:4px 0;}
.content blockquote{border-left:3px solid var(--brand);padding:4px 14px;margin:12px 0;color:var(--text2);}
.content pre{background:#F7F8FA;padding:14px 18px;border-radius:8px;overflow-x:auto;font-size:13px;line-height:1.7;font-family:"SF Mono","Fira Code","Consolas",monospace;}
.content code{background:#F2F3F5;padding:2px 6px;border-radius:3px;font-size:13px;font-family:"SF Mono","Fira Code","Consolas",monospace;}
.content pre code{background:none;padding:0;}
.content table{border-collapse:collapse;width:100%;margin:12px 0;}
.content th,.content td{border:1px solid var(--border);padding:8px 14px;text-align:left;}
.content th{background:#F7F8FA;font-weight:600;font-size:13px;}
.content img{max-width:100%;border-radius:8px;}
.content mark{background:#FFF3CD;padding:2px 4px;border-radius:2px;}
.footer{text-align:center;margin-top:24px;font-size:11px;color:var(--text3);}
.footer a{color:var(--brand);text-decoration:none;}
"""
TEMPLATE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>{title} - MoKeeText</title>
<style>{css}</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1>{title}</h1>
<div class="meta">
<span>创建于 {created_at}</span>
<span>{created_by}</span>
</div>
<div class="content">{content}</div>
</div>
<div class="footer">Powered by <a href="/">MoKeeText</a></div>
</div>
</body>
</html>"""
@router.get("/share/{token}")
def view_shared(token: str, db: Session = Depends(get_db)):
note = db.query(Note).filter_by(share_token=token).first()
if not note:
raise HTTPException(404, "链接无效或已取消分享")
if note.share_expires_at and note.share_expires_at < datetime.utcnow():
raise HTTPException(410, "分享链接已过期")
html = TEMPLATE.format(
title=note.title,
created_at=note.created_at.strftime('%Y-%m-%d %H:%M:%S') if note.created_at else '-',
created_by=note.created_by or 'admin',
content=note.content or '<p style="color:var(--text3);">暂无内容</p>',
css=CSS,
)
return HTMLResponse(html)

101
backend/routes/upload.py Normal file
View File

@@ -0,0 +1,101 @@
"""文件上传/读取 — 数据库存储file_service.py 为可选备用方案,保留不删)"""
from fastapi import APIRouter, UploadFile, File, Depends, HTTPException, Request
from fastapi.responses import Response
from sqlalchemy.orm import Session
from logger import logger
from models import get_db, Upload
from middleware.user_context import UserContext
# 文件服务模块保留备用(不删除),如需切换取消下面注释即可
# from file_service import upload_file, download_file, delete_file
router = APIRouter(tags=["上传"])
ALLOWED = {"image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"}
MAX_SIZE = 20 * 1024 * 1024 # 20MB
@router.post("/api/upload/image")
async def upload_image(file: UploadFile = File(...), db: Session = Depends(get_db), request: Request = None):
"""上传图片 — 存入数据库 LONGBLOB"""
user_id = UserContext.get_user_id(request)
try:
if file.content_type not in ALLOWED:
raise HTTPException(400, f"不支持的图片格式: {file.content_type}")
data = await file.read()
if len(data) > MAX_SIZE:
raise HTTPException(400, f"图片不能超过 20MB当前 {len(data)} 字节")
filename = file.filename or "img"
logger.info(f"上传图片: {filename}, {len(data)} bytes, {file.content_type}, user={user_id}")
upload = Upload(
filename=filename,
content_type=file.content_type,
data=data,
size=len(data),
created_by=user_id,
)
db.add(upload)
db.commit()
db.refresh(upload)
logger.info(f"图片已存入 DB, id={upload.id}")
return {"url": f"/api/file/{upload.id}"}
except HTTPException:
raise
except Exception as e:
logger.error(f"上传失败: {e}")
raise HTTPException(500, f"上传失败: {str(e)}")
@router.get("/api/file/{file_id}")
def get_file(file_id: int, db: Session = Depends(get_db)):
"""读取图片 — 公开访问(不走网关,供编辑器 <img> 标签和分享页加载)
优先从 DB blob 读取;如为文件服务存储则代理下载
"""
upload = db.query(Upload).filter_by(id=file_id).first()
if not upload:
raise HTTPException(404, "文件不存在")
# DB blob主存储方式
if upload.data:
return Response(content=upload.data, media_type=upload.content_type)
# 文件服务存储(兼容之前上传的文件)
if upload.file_id:
from file_service import download_file
try:
data, content_type, _ = download_file(upload.file_id)
return Response(content=data, media_type=content_type)
except FileNotFoundError:
raise HTTPException(404, "文件在文件服务中不存在")
except Exception as e:
logger.error(f"从文件服务下载失败: {e}")
raise HTTPException(500, f"下载文件失败: {str(e)}")
raise HTTPException(404, "文件数据不存在")
@router.delete("/api/file/{file_id}")
def delete_file_api(file_id: int, db: Session = Depends(get_db), request: Request = None):
"""删除图片 — 从 DB 和文件服务中删除"""
user_id = UserContext.get_user_id(request)
upload = db.query(Upload).filter_by(id=file_id).first()
if not upload:
raise HTTPException(404, "文件不存在")
# 如果存在文件服务记录,同步删除
if upload.file_id:
from file_service import delete_file
try:
delete_file(upload.file_id)
logger.info(f"文件服务删除成功, fileId={upload.file_id}")
except Exception as e:
logger.error(f"文件服务删除失败: {e}")
db.delete(upload)
db.commit()
logger.info(f"图片记录已删除, db_id={file_id}, user={user_id}")
return {"ok": True}

221
backend/routes/websites.py Normal file
View File

@@ -0,0 +1,221 @@
"""网站管理相关路由"""
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session, joinedload
from models import get_db, Website, Account, Folder
from middleware.user_context import UserContext
router = APIRouter(tags=["网站管理"])
# ===== 网站 CRUD =====
@router.get("/api/websites")
def list_websites(folder_id: int = None, db: Session = Depends(get_db), request: Request = None):
"""获取网站列表(仅当前用户)"""
user_id = UserContext.get_user_id(request)
q = db.query(Website).options(joinedload(Website.accounts)).filter(Website.created_by == user_id)
if folder_id:
q = q.filter(Website.folder_id == folder_id)
return q.order_by(Website.name).all()
@router.post("/api/websites")
def create_website(data: dict, db: Session = Depends(get_db), request: Request = None):
"""创建网站"""
user_id = UserContext.get_user_id(request)
website = Website(
name=data.get("name", ""),
url=data.get("url", ""),
folder_id=data.get("folder_id"),
created_by=user_id,
updated_by=user_id,
)
db.add(website)
db.commit()
db.refresh(website)
return {"id": website.id, "name": website.name, "url": website.url,
"folder_id": website.folder_id}
@router.get("/api/websites/{website_id}")
def get_website(website_id: int, db: Session = Depends(get_db), request: Request = None):
"""获取网站详情(含账号列表)"""
user_id = UserContext.get_user_id(request)
website = db.query(Website).options(joinedload(Website.accounts)).filter(
Website.id == website_id, Website.created_by == user_id
).first()
if not website:
raise HTTPException(404, "网站不存在")
return website
@router.put("/api/websites/{website_id}")
def update_website(website_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
"""更新网站信息"""
user_id = UserContext.get_user_id(request)
website = db.query(Website).filter(Website.id == website_id, Website.created_by == user_id).first()
if not website:
raise HTTPException(404, "网站不存在")
for field in ["name", "url", "folder_id"]:
if field in data:
setattr(website, field, data[field])
website.updated_by = user_id
db.commit()
db.refresh(website)
return {"ok": True}
@router.delete("/api/websites/{website_id}")
def delete_website(website_id: int, db: Session = Depends(get_db), request: Request = None):
"""删除网站"""
user_id = UserContext.get_user_id(request)
website = db.query(Website).filter(Website.id == website_id, Website.created_by == user_id).first()
if not website:
raise HTTPException(404, "网站不存在")
db.delete(website)
db.commit()
return {"ok": True}
# ===== 账号 CRUD =====
@router.post("/api/websites/{website_id}/accounts")
def add_account(website_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
"""添加账号"""
user_id = UserContext.get_user_id(request)
account = Account(
website_id=website_id,
username=data.get("username", ""),
password=data.get("password", ""),
remark=data.get("remark", ""),
sort_order=data.get("sort_order", 0),
created_by=user_id,
updated_by=user_id,
)
db.add(account)
db.commit()
db.refresh(account)
return {"id": account.id, "username": account.username, "remark": account.remark, "sort_order": account.sort_order}
@router.get("/api/accounts/{account_id}")
def get_account(account_id: int, db: Session = Depends(get_db), request: Request = None):
"""获取单个账号"""
user_id = UserContext.get_user_id(request)
account = db.query(Account).filter(Account.id == account_id, Account.created_by == user_id).first()
if not account:
raise HTTPException(404, "账号不存在")
return {"id": account.id, "username": account.username,
"password": account.password, "remark": account.remark}
@router.put("/api/accounts/{account_id}")
def update_account(account_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
"""更新账号"""
user_id = UserContext.get_user_id(request)
account = db.query(Account).filter(Account.id == account_id, Account.created_by == user_id).first()
if not account:
raise HTTPException(404, "账号不存在")
for field in ["username", "password", "remark", "sort_order"]:
if field in data:
setattr(account, field, data[field])
account.updated_by = user_id
db.commit()
db.refresh(account)
return {"ok": True}
@router.delete("/api/accounts/{account_id}")
def delete_account(account_id: int, db: Session = Depends(get_db), request: Request = None):
"""删除账号"""
user_id = UserContext.get_user_id(request)
account = db.query(Account).filter(Account.id == account_id, Account.created_by == user_id).first()
if not account:
raise HTTPException(404, "账号不存在")
db.delete(account)
db.commit()
return {"ok": True}
@router.put("/api/accounts/{account_id}/move")
def move_account(account_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
"""移动账号顺序: direction = 'up' | 'down'"""
user_id = UserContext.get_user_id(request)
account = db.query(Account).filter(Account.id == account_id, Account.created_by == user_id).first()
if not account:
raise HTTPException(404, "账号不存在")
direction = data.get("direction", "down")
website_id = account.website_id
# 同网站下按 sort_order 排序的所有账号
siblings = db.query(Account).filter_by(website_id=website_id).order_by(Account.sort_order).all()
idx = next((i for i, a in enumerate(siblings) if a.id == account_id), None)
if idx is None:
return {"ok": False}
if direction == "up" and idx > 0:
siblings[idx].sort_order, siblings[idx-1].sort_order = siblings[idx-1].sort_order, siblings[idx].sort_order
elif direction == "down" and idx < len(siblings) - 1:
siblings[idx].sort_order, siblings[idx+1].sort_order = siblings[idx+1].sort_order, siblings[idx].sort_order
db.commit()
return {"ok": True}
# ===== 文件夹 API =====
@router.get("/api/folders")
def list_folders(type: str = "website", db: Session = Depends(get_db), request: Request = None):
"""获取文件夹列表(仅当前用户)"""
user_id = UserContext.get_user_id(request)
return db.query(Folder).filter(
Folder.type == type,
Folder.created_by == user_id,
).order_by(Folder.sort_order).all()
@router.post("/api/folders")
def create_folder(data: dict, db: Session = Depends(get_db), request: Request = None):
"""创建文件夹"""
user_id = UserContext.get_user_id(request)
folder = Folder(
name=data.get("name", "新文件夹"),
type=data.get("type", "website"),
created_by=user_id,
)
db.add(folder)
db.commit()
db.refresh(folder)
return {"id": folder.id, "name": folder.name, "type": folder.type}
@router.put("/api/folders/{folder_id}")
def update_folder(folder_id: int, data: dict, db: Session = Depends(get_db), request: Request = None):
"""重命名文件夹"""
user_id = UserContext.get_user_id(request)
folder = db.query(Folder).filter(Folder.id == folder_id, Folder.created_by == user_id).first()
if not folder:
raise HTTPException(404, "文件夹不存在")
if "name" in data:
folder.name = data["name"]
db.commit()
return {"ok": True, "id": folder.id, "name": folder.name}
@router.delete("/api/folders/{folder_id}")
def delete_folder(folder_id: int, db: Session = Depends(get_db), request: Request = None):
"""删除文件夹(文件夹下的内容变为未分类)"""
user_id = UserContext.get_user_id(request)
folder = db.query(Folder).filter(Folder.id == folder_id, Folder.created_by == user_id).first()
if not folder:
raise HTTPException(404, "文件夹不存在")
# 将该文件夹下的网站/笔记移到未分类(仅限当前用户)
from models import Website, Note
if folder.type == "website":
db.query(Website).filter_by(folder_id=folder_id, created_by=user_id).update({"folder_id": None})
else:
db.query(Note).filter_by(folder_id=folder_id, created_by=user_id).update({"folder_id": None})
db.delete(folder)
db.commit()
return {"ok": True}