如何在3分钟内快速安装国家图书馆ISBN检索插件:Calibre元数据管理终极指南
2026/5/22 4:44:26
本文档提供了一个完整的、可直接落地的用户认证体系 MVP 套件,包括:
可直接将文档放入仓库docs/目录,将代码放入src/或根目录。
功能名称:用户注册、登录与个人主页
目标:构建一个简洁、安全的用户账户体系,支持邮箱+密码注册、登录,以及登录后查看/编辑个人主页(昵称、简介、头像 URL)。作为后续社交、内容等功能的用户基础模块。
目标用户:所有新用户及需要账户体系的业务线。
当前系统缺少账户体系,用户无法保存个性化信息、偏好设置或发布内容。需要一个最小可行产品(MVP)快速验证用户对注册及完善个人信息的意愿。
核心目标:4 周内完成注册、登录、个人主页编辑功能并上线。
成功指标(上线后可量化跟踪):
MVP 必选功能:
POST /signup(邮箱 + 密码,密码哈希存储,返回 201)POST /login(返回 JWT access_token)GET /me(需认证)PUT /me(需认证,支持更新昵称、简介、头像 URL)可选后续迭代:
/me返回 401。范围:仅实现上述 4 个核心 API(后端),可选最简前端演示页面。
时间箱:4 周(含设计、开发、测试、验证上线)。
风险:简单实现可能存在安全薄弱点(如 JWT 密钥管理不当)。
缓解:
建议存放路径:docs/adr/
日期:2025-12-17
状态:Accepted
背景:需要快速实现前后端分离的认证机制,用户量中等,短期内无需复杂会话一致性。
决策:采用 JWT(HS256 对称签名),token 中包含sub(用户 ID)和exp(过期时间)。
考虑的备选方案:
后果:
日期:2025-12-17
状态:Accepted
背景:MVP 需要快速、可单机运行的 PoC,降低开发与部署成本。
决策:使用 SQLite 文件数据库,后续按需迁移至 MySQL/PostgreSQL。
考虑的备选方案:
后果:
建议存放路径:docs/spec/openapi.yaml
openapi:3.0.1info:title:用户系统 APIversion:"1.0.0"description:用户注册、登录与个人主页 APIservers:-url:http://localhost:8000paths:/signup:post:summary:用户注册requestBody:required:truecontent:application/json:schema:$ref:'#/components/schemas/SignupReq'responses:'201':description:注册成功content:application/json:schema:$ref:'#/components/schemas/MessageResp''400':description:参数错误或用户已存在/login:post:summary:用户登录requestBody:required:truecontent:application/json:schema:$ref:'#/components/schemas/LoginReq'responses:'200':description:登录成功,返回 access_tokencontent:application/json:schema:$ref:'#/components/schemas/LoginResp''401':description:凭证错误/me:get:summary:获取当前用户信息security:-bearerAuth:[]responses:'200':description:用户信息content:application/json:schema:$ref:'#/components/schemas/User''401':description:未认证或 token 无效put:summary:更新当前用户信息security:-bearerAuth:[]requestBody:required:truecontent:application/json:schema:$ref:'#/components/schemas/UserUpdateReq'responses:'200':description:更新成功,返回最新用户content:application/json:schema:$ref:'#/components/schemas/User'components:securitySchemes:bearerAuth:type:httpscheme:bearerbearerFormat:JWTschemas:SignupReq:type:objectrequired:-email-passwordproperties:email:type:stringformat:emailpassword:type:stringminLength:8LoginReq:type:objectrequired:-email-passwordproperties:email:type:stringformat:emailpassword:type:stringLoginResp:type:objectproperties:access_token:type:stringtoken_type:type:stringdefault:BearerMessageResp:type:objectproperties:message:type:stringUser:type:objectproperties:id:type:integeremail:type:stringformat:emailnickname:type:stringbio:type:stringavatar_url:type:stringUserUpdateReq:type:objectproperties:nickname:type:stringbio:type:stringavatar_url:type:stringmvp-user-auth/ ├─ README.md ├─ requirements.txt ├─ app.py ├─ db.py ├─ utils.py └─ schemas.pyfastapi uvicorn[standard] sqlalchemy passlib[bcrypt] pyjwt pydanticfromsqlalchemyimportcreate_engine,MetaData,Table,Column,Integer,String,Textfromsqlalchemy.sqlimportselect DATABASE_URL="sqlite:///./users.db"engine=create_engine(DATABASE_URL,connect_args={"check_same_thread":False})metadata=MetaData()users=Table('users',metadata,Column('id',Integer,primary_key=True,autoincrement=True),Column('email',String(255),unique=True,nullable=False),Column('password_hash',String(255),nullable=False),Column('nickname',String(100),nullable=True),Column('bio',Text,nullable=True),Column('avatar_url',String(512),nullable=True),)metadata.create_all(engine)defget_user_by_email(conn,email):q=select(users).where(users.c.email==email)returnconn.execute(q).fetchone()defcreate_user(conn,email,password_hash):ins=users.insert().values(email=email,password_hash=password_hash)res=conn.execute(ins)returnres.inserted_primary_key[0]defget_user_by_id(conn,user_id):q=select(users).where(users.c.id==user_id)returnconn.execute(q).fetchone()defupdate_user(conn,user_id,**fields):upd=users.update().where(users.c.id==user_id).values(**fields)conn.execute(upd)returnget_user_by_id(conn,user_id)frompasslib.contextimportCryptContextimportjwtimporttime PWD_CTX=CryptContext(schemes=["bcrypt"],deprecated="auto")SECRET="change_this_to_a_long_random_secret"# 生产环境务必更换为安全密钥ALGORITHM="HS256"ACCESS_TOKEN_EXPIRE_SECONDS=7*24*3600# 7 天defhash_password(password:str)->str:returnPWD_CTX.hash(password)defverify_password(plain:str,hashed:str)->bool:returnPWD_CTX.verify(plain,hashed)defcreate_access_token(subject:str):now=int(time.time())payload={"sub":subject,"iat":now,"exp":now+ACCESS_TOKEN_EXPIRE_SECONDS}returnjwt.encode(payload,SECRET,algorithm=ALGORITHM)defdecode_access_token(token:str):try:returnjwt.decode(token,SECRET,algorithms=[ALGORITHM])exceptjwt.ExpiredSignatureError:raiseexceptException:raisefrompydanticimportBaseModel,EmailStr,FieldfromtypingimportOptionalclassSignupReq(BaseModel):email:EmailStr password:str=Field(...,min_length=8)classLoginReq(BaseModel):email:EmailStr password:strclassLoginResp(BaseModel):access_token:strtoken_type:str="Bearer"classMessageResp(BaseModel):message:strclassUser(BaseModel):id:intemail:EmailStr nickname:Optional[str]=Nonebio:Optional[str]=Noneavatar_url:Optional[str]=NoneclassUserUpdateReq(BaseModel):nickname:Optional[str]=Nonebio:Optional[str]=Noneavatar_url:Optional[str]=NonefromfastapiimportFastAPI,HTTPException,Dependsfromfastapi.securityimportHTTPBearer,HTTPAuthorizationCredentialsfromsqlalchemyimportcreate_engineimportdbas_dbimportschemasas_schemasimportutilsas_utils app=FastAPI(title="User Auth MVP")engine=create_engine(_db.DATABASE_URL,connect_args={"check_same_thread":False})bearer_scheme=HTTPBearer()@app.post('/signup',status_code=201,response_model=_schemas.MessageResp)defsignup(req:_schemas.SignupReq):withengine.connect()asconn:if_db.get_user_by_email(conn,req.email):raiseHTTPException(status_code=400,detail="user exists")pw_hash=_utils.hash_password(req.password)_db.create_user(conn,req.email,pw_hash)return{"message":"created"}@app.post('/login',response_model=_schemas.LoginResp)deflogin(req:_schemas.LoginReq):withengine.connect()asconn:user=_db.get_user_by_email(conn,req.email)ifnotuserornot_utils.verify_password(req.password,user['password_hash']):raiseHTTPException(status_code=401,detail="invalid credentials")token=_utils.create_access_token(str(user['id']))return{"access_token":token}defget_current_user(creds:HTTPAuthorizationCredentials=Depends(bearer_scheme)):token=creds.credentialstry:payload=_utils.decode_access_token(token)except:raiseHTTPException(status_code=401,detail="invalid or expired token")user_id=int(payload["sub"])withengine.connect()asconn:user=_db.get_user_by_id(conn,user_id)ifnotuser:raiseHTTPException(status_code=401,detail="user not found")returnuser@app.get('/me',response_model=_schemas.User)defme(user=Depends(get_current_user)):return{k:user[k]forkinuser.keys()ifkin('id','email','nickname','bio','avatar_url')}@app.put('/me',response_model=_schemas.User)defupdate_me(req:_schemas.UserUpdateReq,user=Depends(get_current_user)):fields={k:vfork,vinreq.dict().items()ifvisnotNone}ifnotfields:returnme(user)withengine.connect()asconn:updated=_db.update_user(conn,user['id'],**fields)return{k:updated[k]forkinupdated.keys()ifkin('id','email','nickname','bio','avatar_url')}# User Auth MVP ## 快速启动 1. 创建并激活虚拟环境 ```bash python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activatepipinstall-rrequirements.txtuvicorn app:app--reload--port8000--- ## 五、后续扩展建议(生产化路线) - 数据库迁移至 PostgreSQL/MySQL,使用 Alembic 管理迁移。 - 引入 Redis 实现 token 黑名单与接口限流。 - 使用密钥管理服务(KMS)存储 JWT secret,或改为非对称签名(RS256)。 - 增加邮箱验证、密码找回、头像上传(S3/Object Storage)等功能。 - 添加单元测试与集成测试覆盖核心流程。 --- 至此,一个完整、可运行的用户注册、登录、个人主页 MVP 已交付,可直接用于开发验证或作为后续功能的基础。