From fdc7b2092923571ea24d9f8532fc4301f7456fa4 Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Sun, 12 Apr 2026 22:20:23 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=96=E6=B6=88=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=EF=BC=8C=E7=BB=9F=E4=B8=80=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=90=8D=EF=BC=8C=E5=A4=A7=E5=B0=8F=E5=86=99=E4=B8=8D=E6=95=8F?= =?UTF-8?q?=E6=84=9F=E5=8E=BB=E9=87=8D=EF=BC=8C=E4=B8=80=E6=AC=A1=E6=80=A7?= =?UTF-8?q?=E6=94=B9=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 注册: 去掉display_name字段,用户名大小写不敏感去重 - 登录: 大小写不敏感匹配 - 用户菜单: 显示用户名+改名按钮(只能改一次) - /api/me/username: 一次性改名API - 启动时: sync display_name=username, 发通知告知用户 - 前端: 所有display_name显示改为username Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/database.py | 2 + backend/main.py | 60 +++++++++++++++++++++++--- frontend/src/App.vue | 2 +- frontend/src/components/LoginModal.vue | 15 +------ frontend/src/components/UserMenu.vue | 30 ++++++++++++- frontend/src/stores/auth.js | 6 +-- 6 files changed, 89 insertions(+), 26 deletions(-) diff --git a/backend/database.py b/backend/database.py index bedec5a..88c2045 100644 --- a/backend/database.py +++ b/backend/database.py @@ -165,6 +165,8 @@ def init_db(): c.execute("ALTER TABLE users ADD COLUMN brand_align TEXT DEFAULT 'center'") if "role_changed_at" not in user_cols: c.execute("ALTER TABLE users ADD COLUMN role_changed_at TEXT") + if "username_changed" not in user_cols: + c.execute("ALTER TABLE users ADD COLUMN username_changed INTEGER DEFAULT 0") # Migration: add tags to user_diary diary_cols = [row[1] for row in c.execute("PRAGMA table_info(user_diary)").fetchall()] diff --git a/backend/main.py b/backend/main.py index 6bdf449..75a85ec 100644 --- a/backend/main.py +++ b/backend/main.py @@ -132,7 +132,7 @@ def get_version(): @app.get("/api/me") def get_me(user=Depends(get_current_user)): - return {"username": user["username"], "role": user["role"], "display_name": user.get("display_name", ""), "id": user.get("id"), "has_password": bool(user.get("password")), "business_verified": bool(user.get("business_verified"))} + return {"username": user["username"], "role": user["role"], "display_name": user["username"], "id": user.get("id"), "has_password": bool(user.get("password")), "business_verified": bool(user.get("business_verified")), "username_changed": bool(user.get("username_changed"))} # ── Bug Reports ───────────────────────────────────────── @@ -350,20 +350,24 @@ def symptom_search(body: dict, user=Depends(get_current_user)): def register(body: dict): username = body.get("username", "").strip() password = body.get("password", "").strip() - display_name = body.get("display_name", "").strip() if not username or len(username) < 2: raise HTTPException(400, "用户名至少2个字符") if not password or len(password) < 4: raise HTTPException(400, "密码至少4位") - token = _secrets.token_hex(24) + # Case-insensitive uniqueness check conn = get_db() + existing = conn.execute("SELECT id FROM users WHERE LOWER(username) = LOWER(?)", (username,)).fetchone() + if existing: + conn.close() + raise HTTPException(400, "用户名已被占用") + token = _secrets.token_hex(24) try: conn.execute( "INSERT INTO users (username, token, role, display_name, password) VALUES (?, ?, ?, ?, ?)", - (username, token, "viewer", display_name or username, hash_password(password)) + (username, token, "viewer", username, hash_password(password)) ) uid = conn.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone() - log_audit(conn, uid["id"] if uid else None, "register", "user", username, display_name or username, None) + log_audit(conn, uid["id"] if uid else None, "register", "user", username, username, None) conn.commit() except Exception: conn.close() @@ -380,7 +384,7 @@ def login(body: dict): if not username or not password: raise HTTPException(400, "请输入用户名和密码") conn = get_db() - user = conn.execute("SELECT id, token, password, display_name, role FROM users WHERE username = ?", (username,)).fetchone() + user = conn.execute("SELECT id, token, password, display_name, role, username FROM users WHERE LOWER(username) = LOWER(?)", (username,)).fetchone() if not user: conn.close() raise HTTPException(401, "用户名不存在") @@ -452,6 +456,30 @@ def set_password(body: dict, user=Depends(get_current_user)): return {"ok": True} +@app.put("/api/me/username") +def change_username(body: dict, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + conn = get_db() + u = conn.execute("SELECT username_changed FROM users WHERE id = ?", (user["id"],)).fetchone() + if u and u["username_changed"]: + conn.close() + raise HTTPException(400, "用户名只能修改一次") + new_name = body.get("username", "").strip() + if not new_name or len(new_name) < 2: + conn.close() + raise HTTPException(400, "用户名至少2个字符") + existing = conn.execute("SELECT id FROM users WHERE LOWER(username) = LOWER(?) AND id != ?", (new_name, user["id"])).fetchone() + if existing: + conn.close() + raise HTTPException(400, "用户名已被占用") + conn.execute("UPDATE users SET username = ?, display_name = ?, username_changed = 1 WHERE id = ?", + (new_name, new_name, user["id"])) + conn.commit() + conn.close() + return {"ok": True, "username": new_name} + + # ── Business Verification ────────────────────────────── @app.post("/api/business-apply", status_code=201) def business_apply(body: dict, user=Depends(get_current_user)): @@ -2058,6 +2086,26 @@ def startup(): data = json.load(f) seed_defaults(data["oils_meta"], data["recipes"]) + # One-time migration: sync display_name = username, notify about username change + conn = get_db() + needs_sync = conn.execute("SELECT id, username, display_name FROM users WHERE display_name != username AND display_name IS NOT NULL").fetchall() + if needs_sync: + for row in needs_sync: + conn.execute("UPDATE users SET display_name = ? WHERE id = ?", (row["username"], row["id"])) + # Send notification once (check if already sent) + already_notified = conn.execute("SELECT id FROM notifications WHERE title = '📢 用户名变更通知'").fetchone() + if not already_notified: + all_users = conn.execute("SELECT id FROM users").fetchall() + for u in all_users: + conn.execute( + "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)", + ("viewer", "📢 用户名变更通知", + "系统已取消显示名称,统一使用用户名。你可以在个人设置中修改一次用户名。", u["id"]) + ) + conn.commit() + print(f"[INIT] Synced display_name for {len(needs_sync)} users") + conn.close() + # Auto-fill missing en_name for existing recipes conn = get_db() missing = conn.execute("SELECT id, name FROM recipes WHERE en_name IS NULL OR en_name = ''").fetchall() diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c90807f..0a7e668 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -15,7 +15,7 @@