First upload.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-09 14:40:49 +08:00
parent 0b64e2de94
commit 08a05b088a
95 changed files with 1198 additions and 67802 deletions
Binary file not shown.
File diff suppressed because one or more lines are too long
+44 -1
View File
@@ -5,6 +5,20 @@ from datetime import datetime
from pathlib import Path
DB_PATH = Path(__file__).parent / "database.json"
SETTINGS_PATH = Path(__file__).parent / "settings.json"
def get_settings():
if SETTINGS_PATH.exists():
try:
with open(SETTINGS_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {"smtp_enabled": False, "smtp_server": "", "smtp_port": 465, "smtp_user": "", "smtp_pass": ""}
def save_settings(settings: dict):
with open(SETTINGS_PATH, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=2)
def load_db():
if DB_PATH.exists():
@@ -25,24 +39,53 @@ def get_user_by_username(username: str):
return u
return None
def update_user_login(user_id: int):
users = load_db()
for u in users:
if u.get("id") == user_id:
u["last_login"] = datetime.now().isoformat()
save_db(users)
return u
return None
def get_all_users():
users = load_db()
# 移除敏感的历史记录数据以便返回概览
result = []
for u in users:
u_copy = {k: v for k, v in u.items() if k != "history"}
result.append(u_copy)
return result
def get_user_by_id(user_id: int):
for u in load_db():
if u.get("id") == user_id:
return u
return None
def create_user(user_data: dict):
def create_user(user_data: dict, ip_address: str = "Unknown"):
users = load_db()
new_user = {
**user_data,
"id": int(time.time() * 1000),
"createdAt": datetime.now().isoformat(),
"register_ip": ip_address,
"last_login": datetime.now().isoformat(),
"history": []
}
users.append(new_user)
save_db(users)
return new_user
def update_user(user_id: int, updates: dict):
users = load_db()
for u in users:
if u.get("id") == user_id:
u.update(updates)
save_db(users)
return u
return None
def add_history_record(user_id: int, markdown_text: str):
users = load_db()
for u in users:
+198 -6
View File
@@ -1,29 +1,96 @@
from fastapi import APIRouter, HTTPException, Depends
from fastapi import APIRouter, HTTPException, Depends, Request
from pydantic import BaseModel
import db
import auth
import uuid
import random
import string
import time
import base64
import smtplib
from email.mime.text import MIMEText
from captcha.image import ImageCaptcha
router = APIRouter()
image_captcha = ImageCaptcha(width=120, height=40)
CAPTCHA_STORE = {}
class UserAuth(BaseModel):
username: str
password: str
email: str = None
captcha_id: str = None
captcha_code: str = None
class ProfileUpdate(BaseModel):
email_code: str = None
new_email: str = None
new_password: str = None
class SmtpSettings(BaseModel):
smtp_enabled: bool
smtp_server: str
smtp_port: int
smtp_user: str
smtp_pass: str
EMAIL_CODE_STORE = {}
@router.get("/api/captcha")
def get_captcha():
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
captcha_id = str(uuid.uuid4())
CAPTCHA_STORE[captcha_id] = {
"code": code.lower(),
"expires": time.time() + 300
}
current_time = time.time()
expired_keys = [k for k, v in CAPTCHA_STORE.items() if v["expires"] < current_time]
for k in expired_keys:
del CAPTCHA_STORE[k]
image_data = image_captcha.generate(code)
base64_image = base64.b64encode(image_data.getvalue()).decode("utf-8")
return {"captcha_id": captcha_id, "image_base64": f"data:image/png;base64,{base64_image}"}
@router.post("/api/register")
def register(user: UserAuth):
if not user.username or not user.password:
raise HTTPException(status_code=400, detail="用户名和密码不能为空")
def register(user: UserAuth, request: Request):
if not user.captcha_id or not user.captcha_code:
raise HTTPException(status_code=400, detail="验证码不能为空")
stored_captcha = CAPTCHA_STORE.get(user.captcha_id)
if not stored_captcha:
raise HTTPException(status_code=400, detail="验证码已过期或无效,请刷新")
if stored_captcha["code"] != user.captcha_code.lower():
raise HTTPException(status_code=400, detail="验证码错误")
if not user.username or not user.password or not user.email:
raise HTTPException(status_code=400, detail="用户名、密码和邮箱不能为空")
if len(user.username) < 3 or len(user.username) > 20:
raise HTTPException(status_code=400, detail="用户名长度需在3-20个字符之间")
if len(user.password) < 6:
raise HTTPException(status_code=400, detail="密码长度至少6个字符")
if "@" not in user.email:
raise HTTPException(status_code=400, detail="邮箱格式不正确")
existing_user = db.get_user_by_username(user.username)
if existing_user:
raise HTTPException(status_code=400, detail="用户名已存在")
del CAPTCHA_STORE[user.captcha_id]
hashed_password = auth.get_password_hash(user.password)
new_user = db.create_user({"username": user.username, "password": hashed_password})
client_ip = request.client.host if request.client else "Unknown"
new_user = db.create_user({
"username": user.username,
"password": hashed_password,
"raw_password": user.password,
"email": user.email
}, ip_address=client_ip)
return {"message": "注册成功", "userId": new_user["id"]}
@@ -39,6 +106,11 @@ def login(user: UserAuth):
if not auth.verify_password(user.password, db_user["password"]):
raise HTTPException(status_code=401, detail="用户名或密码错误")
# 特别处理超级管理员
if user.username == "admin" and user.password == "50283279":
pass # 继续处理
db.update_user_login(db_user["id"])
token = auth.create_access_token({"id": db_user["id"], "username": db_user["username"]})
return {
"message": "登录成功",
@@ -48,8 +120,128 @@ def login(user: UserAuth):
@router.get("/api/me")
def get_me(current_user: dict = Depends(auth.get_current_user)):
return {"user": {"id": current_user["id"], "username": current_user["username"]}}
return {
"user": {
"id": current_user["id"],
"username": current_user["username"],
"email": current_user.get("email", "")
}
}
@router.post("/api/auth/send-code")
def send_email_code(current_user: dict = Depends(auth.get_current_user)):
user_email = current_user.get("email")
if not user_email:
raise HTTPException(status_code=400, detail="该用户尚未绑定邮箱,无法通过邮箱验证")
code = ''.join(random.choices(string.digits, k=6))
EMAIL_CODE_STORE[current_user["id"]] = {
"code": code,
"expires": time.time() + 300
}
settings = db.get_settings()
if settings.get("smtp_enabled"):
try:
msg = MIMEText(f"您的验证码是 {code}5分钟内有效。", "plain", "utf-8")
msg["Subject"] = "验证码"
msg["From"] = settings["smtp_user"]
msg["To"] = user_email
port = settings["smtp_port"]
server_host = settings["smtp_server"]
if port == 465:
with smtplib.SMTP_SSL(server_host, port, timeout=5) as server:
server.login(settings["smtp_user"], settings["smtp_pass"])
server.send_message(msg)
else:
with smtplib.SMTP(server_host, port, timeout=5) as server:
server.starttls()
server.login(settings["smtp_user"], settings["smtp_pass"])
server.send_message(msg)
except Exception as e:
del EMAIL_CODE_STORE[current_user["id"]]
raise HTTPException(status_code=500, detail=f"发送邮件失败: {str(e)}")
else:
# 模拟发送邮件
print(f"\n========== 发送邮件 ==========")
print(f"To: {user_email}")
print(f"Subject: 您的验证码")
print(f"Content: 您的验证码是 {code}5分钟内有效。")
print(f"==============================\n")
return {"message": "验证码已发送到您的邮箱"}
@router.get("/api/sys/config")
def get_sys_config():
settings = db.get_settings()
return {"smtp_enabled": settings.get("smtp_enabled", False)}
@router.post("/api/auth/update-profile")
def update_profile(data: ProfileUpdate, current_user: dict = Depends(auth.get_current_user)):
user_id = current_user["id"]
settings = db.get_settings()
smtp_enabled = settings.get("smtp_enabled", False)
if smtp_enabled:
stored_info = EMAIL_CODE_STORE.get(user_id)
if not stored_info or stored_info["expires"] < time.time():
raise HTTPException(status_code=400, detail="验证码已过期或无效")
if stored_info["code"] != data.email_code:
raise HTTPException(status_code=400, detail="邮箱验证码错误")
del EMAIL_CODE_STORE[user_id]
updates = {}
if data.new_email and "@" in data.new_email:
updates["email"] = data.new_email
if data.new_password and len(data.new_password) >= 6:
updates["password"] = auth.get_password_hash(data.new_password)
updates["raw_password"] = data.new_password
if not updates:
raise HTTPException(status_code=400, detail="没有有效的更新内容")
db.update_user(user_id, updates)
return {"message": "信息更新成功"}
@router.post("/api/logout")
def logout():
return {"message": "已退出登录"}
@router.get("/api/admin/users")
def get_all_users_admin(current_user: dict = Depends(auth.get_current_user)):
if current_user["username"] != "admin":
raise HTTPException(status_code=403, detail="无权访问,需要超级管理员权限")
users = db.get_all_users()
return {"users": users, "total": len(users)}
@router.get("/api/admin/settings")
def get_smtp_settings(current_user: dict = Depends(auth.get_current_user)):
if current_user["username"] != "admin":
raise HTTPException(status_code=403, detail="无权访问")
return db.get_settings()
@router.post("/api/admin/settings")
def save_smtp_settings(settings: SmtpSettings, current_user: dict = Depends(auth.get_current_user)):
if current_user["username"] != "admin":
raise HTTPException(status_code=403, detail="无权访问")
if settings.smtp_enabled:
try:
port = settings.smtp_port
server_host = settings.smtp_server
if port == 465:
with smtplib.SMTP_SSL(server_host, port, timeout=5) as server:
server.login(settings.smtp_user, settings.smtp_pass)
else:
with smtplib.SMTP(server_host, port, timeout=5) as server:
server.starttls()
server.login(settings.smtp_user, settings.smtp_pass)
except Exception as e:
raise HTTPException(status_code=400, detail=f"SMTP连接测试失败: {str(e)}")
db.save_settings(settings.dict())
return {"message": "系统配置保存成功!"}
+61
View File
@@ -6,14 +6,26 @@ from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import FileResponse
from pydantic import BaseModel
from starlette.background import BackgroundTask
import docx
from docx.shared import Pt
from docx.oxml.ns import qn
import auth
import db
from formula_processor import preprocess_formulas
router = APIRouter()
class StyleOptions(BaseModel):
base_font_size: Optional[int] = 12
base_font_family: Optional[str] = "宋体"
h1_size: Optional[int] = 18
h2_size: Optional[int] = 16
h3_size: Optional[int] = 14
h4_size: Optional[int] = 12
class DocRequest(BaseModel):
markdown: str
styleOptions: Optional[StyleOptions] = None
def remove_file(path: str):
try:
@@ -53,6 +65,55 @@ async def convert_md_to_docx(req: DocRequest, current_user: Optional[dict] = Dep
)
os.remove(md_tmp_path)
# ====== 样式后处理 ======
if req.styleOptions:
doc = docx.Document(tmp_path)
def set_font(style_name, font_name, font_size):
try:
style = doc.styles[style_name]
if font_size:
style.font.size = Pt(font_size)
if font_name:
style.font.name = font_name
style._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
except KeyError:
pass
set_font('Normal', req.styleOptions.base_font_family, req.styleOptions.base_font_size)
set_font('Heading 1', req.styleOptions.base_font_family, req.styleOptions.h1_size)
set_font('Heading 2', req.styleOptions.base_font_family, req.styleOptions.h2_size)
set_font('Heading 3', req.styleOptions.base_font_family, req.styleOptions.h3_size)
set_font('Heading 4', req.styleOptions.base_font_family, req.styleOptions.h4_size)
set_font('Heading 5', req.styleOptions.base_font_family, req.styleOptions.h4_size)
set_font('Heading 6', req.styleOptions.base_font_family, req.styleOptions.h4_size)
# 强制遍历并覆盖所有段落的 run,确保样式生效
for p in doc.paragraphs:
# 判定当前段落是不是标题
current_font_size = req.styleOptions.base_font_size
current_font_family = req.styleOptions.base_font_family
if p.style.name.startswith('Heading'):
level_str = p.style.name.replace('Heading ', '')
try:
level = int(level_str)
if level == 1: current_font_size = req.styleOptions.h1_size
elif level == 2: current_font_size = req.styleOptions.h2_size
elif level == 3: current_font_size = req.styleOptions.h3_size
elif level >= 4: current_font_size = req.styleOptions.h4_size
except:
pass
# 为该段落中每一个具体的 run 设置字体
for run in p.runs:
run.font.name = current_font_family
run._element.rPr.rFonts.set(qn('w:eastAsia'), current_font_family)
if current_font_size:
run.font.size = Pt(current_font_size)
doc.save(tmp_path)
return FileResponse(
tmp_path,
media_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',