feat: 取消显示名称,统一用户名,大小写不敏感去重,一次性改名
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 16s
Test / e2e-test (push) Successful in 52s
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 16s
Test / e2e-test (push) Successful in 52s
- 注册: 去掉display_name字段,用户名大小写不敏感去重 - 登录: 大小写不敏感匹配 - 用户菜单: 显示用户名+改名按钮(只能改一次) - /api/me/username: 一次性改名API - 启动时: sync display_name=username, 发通知告知用户 - 前端: 所有display_name显示改为username Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user