From bd5b2e5cbb88bc72b9e9405e609ef3209392d974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=96=B9=E6=88=90?= Date: Thu, 2 Oct 2025 09:44:54 +0800 Subject: [PATCH] =?UTF-8?q?app=5Fuser=EF=BC=9A=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aaa.txt | 302 ------------------------------ app/api/v1/__init__.py | 2 + app/api/v1/app_users/__init__.py | 5 + app/api/v1/app_users/app_users.py | 134 +++++++++++++ app/controllers/app_user.py | 124 ++++++++++++ app/models/__init__.py | 1 + app/models/user.py | 25 +++ app/schemas/app_user.py | 69 +++++++ app/utils/app_user_jwt.py | 83 ++++++++ 9 files changed, 443 insertions(+), 302 deletions(-) delete mode 100644 aaa.txt create mode 100644 app/api/v1/app_users/__init__.py create mode 100644 app/api/v1/app_users/app_users.py create mode 100644 app/controllers/app_user.py create mode 100644 app/models/user.py create mode 100644 app/schemas/app_user.py create mode 100644 app/utils/app_user_jwt.py diff --git a/aaa.txt b/aaa.txt deleted file mode 100644 index 104d6f0..0000000 --- a/aaa.txt +++ /dev/null @@ -1,302 +0,0 @@ -企业软件著作权查询: - 接口地址 : https://openapi.chinaz.net/v1/1036/copyrightsoftware - 返回格式 : JSON - 请求方式 : POST -请求参数: - 名称 - 类型 - 必填 - 说明 - sign string 是 填写由"634xz年月日"生成的32位md5加密值("年月日"指调用api的日期)。 举例:如果是2021年5月6日调用api,则对634xz20210506进行md5加密,得到的32位加密值就是sign - entName string 是 企业名称 - pageNo string 是 页码,值从1开始(表示第1页) - range string 是 每页条数,取值介于在1到300之间(含1和300) - APIKey string 是 申请接口时获取的APIKey值 - ChinazVer string 是 接口版本号,取值:2.0 - 返回参数说明: - 名称 - 类型 - 说明 - rc string 状态码 - msg string 状态信息 - data object 数据对象 - data.total string 总数量 - data.totalPage string 总页数 - data.dataList object 软件著作权列表 - data.dataList[].SNAME string 软件全称 - data.dataList[].SHORTNAME string 软件简称 - data.dataList[].ENTNAME string 著作权人 - data.dataList[].SNUM string 登记号 - data.dataList[].REGDATE string 首次发表日期 - data.dataList[].ANNDATE string 登记批准日期 - data.dataList[].ANNTYPE string 分类号名称 - data.dataList[].TYPENUM string 分类号编号 - data.dataList[].VNUM string 版本号 - data.dataList[].cdate string 完成日期 -正确的返回示例: - -{ - "orderNo":"202308281517124830015", - "data":{ - "total":12, - "totalPage":25, - "dataList":[ - { - "cdate":"2023-08-30", - "ANNDATE":"2023-11-29", - "SNUM":"2023SR1531019", - "SHORTNAME":"ModelMate", - "REGDATE":"2023-11-29", - "SNAME":"AI模型使能平台", - "VNUM":"24.0", - "ENTNAME":"****有限公司", - "ANNTYPE":"****", - "TYPENUM":"438951" - } - ] - }, - "rc":"0000", - "msg":"查询成功" -} - -错误的返回示例: -{ - "rc":"1002", - "msg":"sign校验失败" -} - - -企业专利信息 - -接口地址 : https://openapi.chinaz.net/v1/1036/patent -返回格式 : JSON -请求方式 : POST -请求参数: -名称 -类型 -必填 -说明 -sign string 是 填写由"634xz年月日"生成的32位md5加密值("年月日"指调用api的日期)。 举例:如果是2021年5月6日调用api,则对634xz20210506进行md5加密,得到的32位加密值就是sign -searchKey string 是 查询关键词(企业名称、企业统代、企业注册号) -searchType string 否 查询关键字类型;默认为0:支持的所有类型;1:企业名称;2:统一社会信用代码;3:注册号; -pageNo string 是 页码,值从1开始(表示第1页) -range string 是 每页条数,取值介于在1到300之间(含1和300) -APIKey string 是 申请接口时获取的APIKey值 -ChinazVer string 是 接口版本号,取值:2.0 -返回参数说明: -名称 -类型 -说明 -rc string 状态码 -msg string 状态信息 -data object 数据列表 -data.total string 总数量 -data.totalPage string 总页数 -data.dataList string 专利信息列表 -data.dataList[].patentflls string 专利法律状态信息列表 -data.dataList[].patentflls[].flzt string 法律状态 -data.dataList[].patentflls[].flztxq string 法律状态详情 -data.dataList[].patentflls[].flztggrq string 法律状态公告日期 -data.dataList[].SQH string 专利申请号 -data.dataList[].PATNAME string 专利标题 -data.dataList[].SQR string 申请/专利权人 -data.dataList[].SQRQ string 申请日期 -data.dataList[].GKGGH string 公开/公告号 -data.dataList[].FMR string 发明/设计人 -data.dataList[].GKGGR string 公开/公告日 -data.dataList[].FCFL string 范畴分类 -data.dataList[].FLH string 分类号 -data.dataList[].PTYPE string 专利分类 -data.dataList[].DLR string 代理人 -data.dataList[].DZ string 地址 -data.dataList[].FASQ string 分案申请 -data.dataList[].GJGB string 国际公布 -data.dataList[].GJSQ string 国际申请 -data.dataList[].GSDM string 国省代码 -data.dataList[].JRGJRQ string 进入国家日期 -data.dataList[].YXQ string 优先权 -data.dataList[].YZWX string 引证文献 -data.dataList[].ZFLH string 主分类号 -data.dataList[].ZLDLJG string 专利代理机构 -data.dataList[].ZQX string 主权项 -data.dataList[].ZY string 摘要 -data.dataList[].IPC string 专利行业分类 -data.dataList[].PID string 专利ID -data.dataList[].PIC string 代表图片 -data.dataList[].SQGKGGH string 授权公告号 -data.dataList[].SQGKGGR string 授权公开日期 -data.dataList[].ENTNAME string 申请企业 -data.dataList[].ZFLNAME string 主分类名称 - - - -司法综合数据查询 -接口地址 : https://openapi.chinaz.net/v1/1036/judgementdetailv4 -返回格式 : JSON -请求方式 : POST -调用说明 : 司法数据综合查询接口数据非实时,可能存在部分数据延迟。该数据仅供参考,不作为判定具体结果使用。 -请求参数: -名称 -类型 -必填 -说明 -sign string 是 填写由"634xz年月日"生成的32位md5加密值("年月日"指调用api的日期)。 举例:如果是2021年5月6日调用api,则对634xz20210506进行md5加密,得到的32位加密值就是sign -q string 否 名称,选填,列表检索时(即查详情,也即id不填时)必填 -idCardNo string 否 身份证号,选填,一般查个人时填写查询会更加精确 -datatype string 否 查询数据类型,多个以',' 分割,共有zxgg(被执行人)、sxgg(失信被执行人)、ktgg(开庭公告)、fygg(法院公告)、cpws(裁判文书)、splc(审判流程)六种数据类型,如果列表检索时,该字段默认为六种数据类型都填 -id string 否 详情id,该id从检索结果中获取,请配合查询数据类型使用。如查询某条被执行人详情时,datatype必须填'zxgg' -pageNo string 否 页码,列表检索时输入的字段,默认为1 -APIKey string 是 申请接口时获取的APIKey值 -ChinazVer string 是 接口版本号,取值:1.0 -返回参数说明: -名称 -类型 -说明 -rc string 状态码 -msg string 状态信息 -orderNo string 订单号 -data string 返回查询结果 -count string 检索出每种数据类型的总量统计 -count[].bzxrCount string 被执行人总量 -count[].sxrCount string 失信被执行人总量 -count[].ktggCount string 开庭公告总量 -count[].fyggCount string 法院公告数量 -count[].cpwsCount string 裁判文书数量 -count[].splcCount string 审判流程数量 -list string 检索出每种数据类型数据列表 -list[].bzxrList string 被执行人列表 -bzxrList[].id string 被执行人ID -bzxrList[].entityName string 主体名称 -bzxrList[].entityId string 主体代码 -bzxrList[].courtName string 法院 -bzxrList[].caseCode string 案号 -bzxrList[].regDate string 立案时间 -bzxrList[].relateType string 数据类别 -list[].sxrList string 失信被执行人列表 -sxrList[].id string 失信被执行人ID -sxrList[].entityName string 主体名称 -sxrList[].entityId string 主体代码 -sxrList[].courtName string 法院 -sxrList[].caseCode string 案号 -sxrList[].regDate string 立案时间 -sxrList[].relateType string 数据类别 -list[].ktggList string 开庭公告列表 -ktggList[].id string 开庭公告ID -ktggList[].content string 开庭公告文书内容 -list[].fyggList string 法院公告列表 -fyggList[].id string 法院公告ID -fyggList[].content string 法院公告文书内容 -list[].cpwsList string 裁判文书列表 -cpwsList[].id string 裁判文书ID -cpwsList[].title string 标题 -cpwsList[].caseCode string 案号 -cpwsList[].court string 法院 -cpwsList[].standPoint string 查询主体立场 -cpwsList[].judgeDate string 裁判日期 -cpwsList[].content string 文书缩略内容 -cpwsList[].resultMatch string 个人查询匹配度,个人查询时有值,企业查询返回是空值 -list[].splcList string 审判流程列表 -splcList[].id string 审判流程ID -splcList[].litigant string 当事人 -splcList[].courtName string 法院 -splcList[].caseCode string 案号 -entityName string 主体名称 -entityId string 主体代码 -courtName string 法院 -caseState string 案件状态 -caseCode string 案号 -execMoney string 执行标的 -regDate string 立案时间 -gistId string 依据文号 -entityName string 主体名称 -entityId string 主体代码 -caseCode string 案号 -age string 年龄 -sex string 性别 -bussinessEntity string 法定代表人或者负责人姓名 -courtName string 法院 -areaName string 省份 -partyTypeName string 类型 -gistId string 执行依据文号 -regDate string 立案时间 -gistUnit string 做出执行依据单位 -duty string 生效法律文书确定的义务 -performance string 被执行人的履行情况 -performedPart string 已履行 -unPerformPart string 未履行 -disruptTypeName string 失信类型 -publishDate string 发布日期 -status string 下架状态:已下架,未下架 -ktggId string 开庭公告ID -areaName string 地区名称 -court string 审理法院 -caseNo string 案号 -content string 公告内容 -openTime string 开庭时间 -litigant string 当事人,多个以顿号分开 -plaintiff string 原告,多个以顿号分开 -defendant string 被告,多个以顿号分开 -caseCause string 案由 -type string 公告类型 -announcer string 公告人 -litigant string 当事人 -content string 公告内容 -publishDate string 公告时间 -baseInfo string 案件详情 -baseInfo.caseCode string 案号 -baseInfo.judgeDate string 裁判日期 -baseInfo.title string 标题 -baseInfo.publishDate string 发布日期 -baseInfo.caseType string 案件类型 -baseInfo.docType string 文书类型 -baseInfo.caseCause string 案由 -baseInfo.court string 法院 -baseInfo.province string 省份 -baseInfo.program string 审判程序 -baseInfo.clauseContent string 审判依据 -baseInfo.resultContent string 审判结果 -baseInfo.content string 文书缩略内容 -baseInfo.id string 裁判文书ID -roleList[] string 角色详情 -roleList[].roleName string 角色 -roleList[].standpoint string 立场 -roleList[].entityName string 名称 -roleList[].birthday string 生日 -roleList[].gender string 性别 -roleList[].nativePlace string 籍贯 -roleList[].address string 地址 -roleList[].workUnit string 工作单位 -roleList[].nation string 民族 -roleList[].organizationCode string 组织机构代码 -roleList[].socialCreditCode string 统一社会信用代码 -roleList[].idcard string 身份证 -roleList[].jobTitle string 职位 -judgeResultList[] string 判决结果 -judgeResultList[].price string 涉案金额 -judgeResultList[].realPrice string 实际金额 -judgeResultList[].fromRole string 负担角色 -judgeResultList[].way string 处置方式 -judgeResultList[].toRole string 受让角色 -judgeResultList[].priceType string 金额类型 -judgeResultList[].priceUnit string 金额单位 -judgeResultList[].currency string 币种 -relationCaseList[] string 案件关联 -relationCaseList[].caseCode string 案号 -relationCaseList[].court string 法院 -relationCaseList[].realationCaseCode string 关联案号 -relationCaseList[].relationCaseId string 关联文书ID -areaName string 区域名称 -litigant string 当事人 -accuser string 原告 -defender string 被告 -others string 其他 -courtName string 法院 -caseCode string 案号 -caseStat string 案件状态 -openTime string 立案时间 -trialTime string 开庭时间 -closeTime string 结案时间 -program string 审判程序 -reason string 案由 -caseType string 案件类型 -target string 诉讼标的 diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index 8ff2dd2..1e2780a 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -3,6 +3,7 @@ from fastapi import APIRouter from app.core.dependency import DependPermission from .apis import apis_router +from .app_users import app_users_router from .auditlog import auditlog_router from .base import base_router from .depts import depts_router @@ -18,6 +19,7 @@ from .users import users_router v1_router = APIRouter() v1_router.include_router(base_router, prefix="/base") +v1_router.include_router(app_users_router, prefix="/app-user") # AppUser路由,无需权限依赖 v1_router.include_router(users_router, prefix="/user", dependencies=[DependPermission]) v1_router.include_router(roles_router, prefix="/role", dependencies=[DependPermission]) v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependPermission]) diff --git a/app/api/v1/app_users/__init__.py b/app/api/v1/app_users/__init__.py new file mode 100644 index 0000000..a1793b6 --- /dev/null +++ b/app/api/v1/app_users/__init__.py @@ -0,0 +1,5 @@ +from fastapi import APIRouter +from .app_users import router + +app_users_router = APIRouter() +app_users_router.include_router(router, tags=["AppUser认证"]) \ No newline at end of file diff --git a/app/api/v1/app_users/app_users.py b/app/api/v1/app_users/app_users.py new file mode 100644 index 0000000..e59cf6e --- /dev/null +++ b/app/api/v1/app_users/app_users.py @@ -0,0 +1,134 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from app.controllers.app_user import app_user_controller +from app.schemas.app_user import ( + AppUserRegisterSchema, + AppUserLoginSchema, + AppUserJWTOut, + AppUserInfoOut, + AppUserUpdateSchema, + AppUserChangePasswordSchema +) +from app.utils.app_user_jwt import ( + create_app_user_access_token, + get_current_app_user, + ACCESS_TOKEN_EXPIRE_MINUTES +) +from app.models.user import AppUser + +router = APIRouter() + + +@router.post("/register", response_model=dict, summary="用户注册") +async def register( + register_data: AppUserRegisterSchema +): + """ + 用户注册 - 只需要手机号 + 默认密码为手机号后六位 + """ + try: + user = await app_user_controller.register(register_data) + return { + "code": 200, + "message": "注册成功", + "data": { + "user_id": user.id, + "phone": user.phone, + "default_password": register_data.phone[-6:] # 返回默认密码供用户知晓 + } + } + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post("/login", response_model=AppUserJWTOut, summary="用户登录") +async def login( + login_data: AppUserLoginSchema +): + """ + 用户登录 + """ + user = await app_user_controller.authenticate(login_data) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="手机号或密码错误" + ) + + # 更新最后登录时间 + await app_user_controller.update_last_login(user.id) + + # 生成访问令牌 + access_token = create_app_user_access_token(user.id, user.phone) + + return AppUserJWTOut( + access_token=access_token, + token_type="bearer", + expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60 + ) + + +@router.post("/logout", summary="用户登出") +async def logout(current_user: AppUser = Depends(get_current_app_user)): + """ + 用户登出(客户端需要删除本地token) + """ + return {"code": 200, "message": "登出成功"} + + +@router.get("/profile", response_model=AppUserInfoOut, summary="获取用户信息") +async def get_profile(current_user: AppUser = Depends(get_current_app_user)): + """ + 获取当前用户信息 + """ + return current_user + + +@router.put("/profile", response_model=AppUserInfoOut, summary="更新用户信息") +async def update_profile( + update_data: AppUserUpdateSchema, + current_user: AppUser = Depends(get_current_app_user) +): + """ + 更新用户信息 + """ + updated_user = await app_user_controller.update_user_info(current_user.id, update_data) + if not updated_user: + raise HTTPException(status_code=404, detail="用户不存在") + + return updated_user + + +@router.post("/change-password", summary="修改密码") +async def change_password( + password_data: AppUserChangePasswordSchema, + current_user: AppUser = Depends(get_current_app_user) +): + """ + 修改密码 + """ + success = await app_user_controller.change_password( + current_user.id, + password_data.old_password, + password_data.new_password + ) + + if not success: + raise HTTPException(status_code=400, detail="原密码错误") + + return {"code": 200, "message": "密码修改成功"} + + +@router.get("/validate-token", summary="验证token") +async def validate_token(current_user: AppUser = Depends(get_current_app_user)): + """ + 验证token是否有效 + """ + return { + "code": 200, + "message": "token有效", + "data": { + "user_id": current_user.id, + "phone": current_user.phone + } + } \ No newline at end of file diff --git a/app/controllers/app_user.py b/app/controllers/app_user.py new file mode 100644 index 0000000..5ffd1ff --- /dev/null +++ b/app/controllers/app_user.py @@ -0,0 +1,124 @@ +from app.models.user import AppUser +from app.schemas.app_user import AppUserRegisterSchema, AppUserLoginSchema, AppUserUpdateSchema +from app.utils.password import get_password_hash, verify_password +from app.core.crud import CRUDBase +from fastapi.exceptions import HTTPException +from datetime import datetime +from typing import Optional + + +class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSchema]): + """AppUser控制器""" + + def __init__(self): + super().__init__(model=AppUser) + + async def register(self, register_data: AppUserRegisterSchema) -> AppUser: + """ + 用户注册 - 只需要手机号,默认使用手机号后六位作为密码 + """ + # 检查手机号是否已存在 + existing_user = await self.model.filter(phone=register_data.phone).first() + if existing_user: + raise HTTPException(status_code=400, detail="手机号已存在") + + # 生成默认密码:手机号后六位 + default_password = register_data.phone[-6:] + hashed_password = get_password_hash(default_password) + + # 创建新用户 + new_user = self.model( + phone=register_data.phone, + password=hashed_password, + is_active=True + ) + + await new_user.save() + return new_user + + async def authenticate(self, login_data: AppUserLoginSchema) -> Optional[AppUser]: + """ + 用户认证 + """ + user = await self.model.filter( + phone=login_data.phone, is_active=True + ).first() + + if not user: + return None + + if not verify_password(login_data.password, user.password): + return None + + return user + + async def get_user_by_id(self, user_id: int) -> Optional[AppUser]: + """ + 根据ID获取用户 + """ + return await self.model.filter(id=user_id, is_active=True).first() + + async def get_user_by_phone(self, phone: str) -> Optional[AppUser]: + """ + 根据手机号获取用户 + """ + return await self.model.filter(phone=phone, is_active=True).first() + + async def update_last_login(self, user_id: int) -> bool: + """ + 更新最后登录时间 + """ + user = await self.model.filter(id=user_id).first() + if user: + user.last_login = datetime.now() + await user.save() + return True + return False + + async def update_user_info(self, user_id: int, update_data: AppUserUpdateSchema) -> Optional[AppUser]: + """ + 更新用户信息 + """ + user = await self.model.filter(id=user_id).first() + if not user: + return None + + # 更新字段 + update_dict = update_data.model_dump(exclude_unset=True) + for field, value in update_dict.items(): + setattr(user, field, value) + + await user.save() + return user + + async def change_password(self, user_id: int, old_password: str, new_password: str) -> bool: + """ + 修改密码 + """ + user = await self.model.filter(id=user_id).first() + if not user: + return False + + # 验证原密码 + if not verify_password(old_password, user.password): + return False + + # 更新密码 + user.password = get_password_hash(new_password) + await user.save() + return True + + async def deactivate_user(self, user_id: int) -> bool: + """ + 停用用户 + """ + user = await self.model.filter(id=user_id).first() + if user: + user.is_active = False + await user.save() + return True + return False + + +# 创建控制器实例 +app_user_controller = AppUserController() \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py index cbe3e9e..ccc836b 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -4,3 +4,4 @@ from .esg import * from .index import * from .industry import * from .policy import * +from .user import * \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..bee95ad --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,25 @@ +from tortoise import fields + +from app.schemas.menus import MenuType + +from .base import BaseModel, TimestampMixin +from .enums import MethodType + + +class AppUser(BaseModel, TimestampMixin): + username = fields.CharField(max_length=20, unique=True, null=True, description="用户名称", index=True) + alias = fields.CharField(max_length=30, null=True, description="姓名", index=True) + email = fields.CharField(max_length=255, unique=True, null=True, description="邮箱", index=True) + phone = fields.CharField(max_length=20, unique=True, description="手机号", index=True) + password = fields.CharField(max_length=128, description="密码") + company_name = fields.CharField(max_length=100, null=True, description="公司名称", index=True) + company_address = fields.CharField(max_length=255, null=True, description="公司地址") + company_contact = fields.CharField(max_length=50, null=True, description="公司联系人") + company_phone = fields.CharField(max_length=20, null=True, description="公司电话") + company_email = fields.CharField(max_length=100, null=True, description="公司邮箱") + is_active = fields.BooleanField(default=True, description="是否激活", index=True) + last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True) + + class Meta: + table = "app_user" + table_description = "用户表" \ No newline at end of file diff --git a/app/schemas/app_user.py b/app/schemas/app_user.py new file mode 100644 index 0000000..eb0cae0 --- /dev/null +++ b/app/schemas/app_user.py @@ -0,0 +1,69 @@ +from pydantic import BaseModel, Field, validator +from typing import Optional +from datetime import datetime +import re + + +class AppUserRegisterSchema(BaseModel): + """AppUser注册Schema - 只需要手机号""" + phone: str = Field(..., description="手机号") + + @validator('phone') + def validate_phone(cls, v): + if not re.match(r'^1[3-9]\d{9}$', v): + raise ValueError('手机号格式不正确') + return v + + +class AppUserLoginSchema(BaseModel): + """AppUser登录Schema""" + phone: str = Field(..., description="手机号") + password: str = Field(..., description="密码") + + +class AppUserJWTPayload(BaseModel): + """AppUser JWT载荷""" + user_id: int + phone: str + exp: datetime + + +class AppUserJWTOut(BaseModel): + """AppUser JWT输出""" + access_token: str + token_type: str = "bearer" + expires_in: int + + +class AppUserInfoOut(BaseModel): + """AppUser信息输出""" + id: int + phone: str + nickname: Optional[str] = None + avatar: Optional[str] = None + company_name: Optional[str] = None + company_address: Optional[str] = None + company_contact: Optional[str] = None + company_phone: Optional[str] = None + company_email: Optional[str] = None + is_active: bool + last_login: Optional[datetime] = None + created_at: datetime + updated_at: datetime + + +class AppUserUpdateSchema(BaseModel): + """AppUser更新Schema""" + nickname: Optional[str] = Field(None, description="昵称") + avatar: Optional[str] = Field(None, description="头像") + company_name: Optional[str] = Field(None, description="公司名称") + company_address: Optional[str] = Field(None, description="公司地址") + company_contact: Optional[str] = Field(None, description="公司联系人") + company_phone: Optional[str] = Field(None, description="公司电话") + company_email: Optional[str] = Field(None, description="公司邮箱") + + +class AppUserChangePasswordSchema(BaseModel): + """AppUser修改密码Schema""" + old_password: str = Field(..., description="原密码") + new_password: str = Field(..., description="新密码") \ No newline at end of file diff --git a/app/utils/app_user_jwt.py b/app/utils/app_user_jwt.py new file mode 100644 index 0000000..0a622e6 --- /dev/null +++ b/app/utils/app_user_jwt.py @@ -0,0 +1,83 @@ +from datetime import datetime, timedelta +from typing import Optional +import jwt +from fastapi import HTTPException, status, Depends +from fastapi.security import HTTPBearer +from app.controllers.app_user import app_user_controller +from app.schemas.app_user import AppUserJWTPayload +from app.settings import settings + +# JWT配置 +SECRET_KEY = settings.SECRET_KEY +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7天 + +security = HTTPBearer() + + +def create_app_user_access_token(user_id: int, phone: str) -> str: + """ + 为AppUser创建访问令牌 + """ + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode = { + "user_id": user_id, + "phone": phone, + "exp": expire + } + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def verify_app_user_token(token: str) -> Optional[AppUserJWTPayload]: + """ + 验证AppUser令牌 + """ + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: int = payload.get("user_id") + phone: str = payload.get("phone") + exp: datetime = datetime.fromtimestamp(payload.get("exp")) + + if user_id is None or phone is None: + return None + + return AppUserJWTPayload(user_id=user_id, phone=phone, exp=exp) + except jwt.DecodeError: + return None + except jwt.ExpiredSignatureError: + return None + except Exception: + return None + + +def get_current_app_user_id(token: str = Depends(security)) -> int: + """ + 从令牌中获取当前AppUser ID + """ + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无效的认证凭据", + headers={"WWW-Authenticate": "Bearer"}, + ) + + payload = verify_app_user_token(token.credentials) + if payload is None: + raise credentials_exception + + return payload.user_id + + +async def get_current_app_user( + current_user_id: int = Depends(get_current_app_user_id) +): + """ + 获取当前AppUser + """ + user = await app_user_controller.get_user_by_id(current_user_id) + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户不存在或已被停用" + ) + return user \ No newline at end of file