完整注册业务链路注册功能采用标准的手机号验证码流程验证码获取阶段前端传入手机号后端校验手机号未注册生成 6 位数字验证码存入 Redis 并设置 2 分钟有效期验证码校验阶段前端提交手机号与验证码后端读取 Redis 缓存完成验证码有效性校验账号创建阶段校验通过后接收用户设置的密码通过 Tortoise-ORM 完成用户数据入库生成 JWT 登录令牌返回前端核心功能轻量化实现本项目聚焦手机号验证码注册核心流程仅保留必需字段与逻辑✅ 手机号验证码注册✅ JWT 令牌生成✅ Redis 缓存验证码❌ 去除头像、邮箱等非核心字段❌ 去除第三方登录等复杂业务fromcontextlibimportasynccontextmanagerfromfastapiimportFastAPI,QueryfrompydanticimportBaseModelfromtortoiseimportTortoise,fieldsfromtortoise.modelsimportModelimportredisimportrandomfromdatetimeimportdatetime,timedeltafromjoseimportjwt# 全局基础配置 # MySQL数据库连接地址根据本地数据库账号密码自行修改MYSQL_URLmysql://root:root127.0.0.1:3306/simple_boss# Redis客户端初始化用于存储短信验证码redis_clientredis.Redis(host127.0.0.1,port6379,db0,decode_responsesTrue)# JWT加密密钥线上项目需更换为复杂随机字符串SECRET_KEYregister-system-secret-key-2026# Tortoise-ORM 数据库模型 classJobUser(Model):求职者用户数据表存储注册账号基础信息idfields.IntField(pkTrue,description用户唯一主键ID)mobilefields.CharField(max_length11,uniqueTrue,description11位手机号全局唯一不可重复)passwordfields.CharField(max_length20,description账号登录密码)create_timefields.DatetimeField(auto_now_addTrue,description账号注册创建时间)classMeta:tablejob_usertable_description求职者注册账号表# Pydantic 请求参数校验模型 # 验证码校验入参模型classCheckCodeReq(BaseModel):mobile:strcode:str# 账号注册提交入参模型classRegisterReq(BaseModel):mobile:strcode:strpassword:str# 通用工具函数 defcreate_sms_code()-str:生成6位纯数字短信验证码return.join(random.choices(0123456789,k6))defcreate_login_token(user_id:int,mobile:str):根据用户ID、手机号生成有效期2小时的JWT登录令牌expire_timedatetime.utcnow()timedelta(hours2)payload{sub:user_id,mobile:mobile,exp:expire_time}tokenjwt.encode(payload,SECRET_KEY,algorithmHS256)returntoken# 业务服务层注册核心业务逻辑 classRegisterService:staticmethodasyncdefsend_register_code(mobile:str): 下发注册场景短信验证码 业务逻辑校验手机号未注册 → 生成验证码 → Redis缓存存储2分钟 # 查询数据库校验手机号是否已完成注册exist_userawaitJobUser.get_or_none(mobilemobile)ifexist_user:raiseException(该手机号已注册账号请直接登录)# 生成6位验证码并存入Rediscodecreate_sms_code()redis_cache_keyfregister_code:{mobile}redis_client.set(redis_cache_key,code,ex120)# 本地调试打印验证码线上项目替换为真实短信服务商接口print(f【注册验证码】手机号{mobile}验证码{code})returnTruestaticmethodasyncdefverify_sms_code(mobile:str,code:str):校验注册场景短信验证码有效性redis_cache_keyfregister_code:{mobile}real_cache_coderedis_client.get(redis_cache_key)ifnotreal_cache_code:raiseException(验证码已过期请重新获取)ifreal_cache_code!code:raiseException(输入的验证码与下发验证码不一致)returnTruestaticmethodasyncdefregister_account(mobile:str,code:str,password:str): 完成账号注册全流程 1. 二次校验验证码有效性 2. 校验手机号不存在防止并发重复注册 3. ORM写入数据库创建用户 4. 清除Redis缓存验证码生成登录令牌 # 第一步校验验证码有效awaitRegisterService.verify_sms_code(mobile,code)# 第二步二次校验手机号规避并发重复注册问题exist_userawaitJobUser.get_or_none(mobilemobile)ifexist_user:raiseException(该手机号已注册账号无法重复注册)# 第三步Tortoise-ORM完成用户数据入库new_userawaitJobUser.create(mobilemobile,passwordpassword)# 第四步清除Redis中已使用的验证码避免复用redis_client.delete(fregister_code:{mobile})# 第五步生成登录令牌返回前端login_tokencreate_login_token(new_user.id,mobile)return{login_token:login_token,user_id:new_user.id,mobile:mobile}# 项目生命周期管理数据库初始化与释放 asynccontextmanagerasyncdeflifespan(app:FastAPI):# 项目启动时初始化数据库连接自动生成数据表awaitTortoise.init(db_urlMYSQL_URL,modules{models:[__main__]})awaitTortoise.generate_schemas()yield# 项目关闭时断开数据库连接释放资源awaitTortoise.close_connections()# 接口路由层对外暴露HTTP接口 appFastAPI(title轻量化注册系统,lifespanlifespan)app.get(/)asyncdefroot():return{message:轻量化注册系统已启动}app.get(/api/register/send-code)asyncdefsend_register_code(mobile:strQuery(...,min_length11,max_length11)): 发送注册验证码接口 - 参数mobile (11位手机号) - 返回发送结果 try:awaitRegisterService.send_register_code(mobile)return{code:200,message:验证码发送成功,data:None}exceptExceptionase:return{code:400,message:str(e),data:None}app.post(/api/register/verify-code)asyncdefverify_register_code(req:CheckCodeReq): 验证码校验接口 - 参数mobile, code - 返回校验结果 try:awaitRegisterService.verify_sms_code(req.mobile,req.code)return{code:200,message:验证码校验成功,data:None}exceptExceptionase:return{code:400,message:str(e),data:None}app.post(/api/register/submit)asyncdefsubmit_register(req:RegisterReq): 提交注册信息接口 - 参数mobile, code, password - 返回注册结果及登录令牌 try:resultawaitRegisterService.register_account(req.mobile,req.code,req.password)return{code:200,message:注册成功,data:result}exceptExceptionase:return{code:400,message:str(e),data:None}