diff --git a/lifespan.py b/lifespan.py index 5481daf..abc875f 100644 --- a/lifespan.py +++ b/lifespan.py @@ -4,6 +4,20 @@ from fastapi import FastAPI from uvicorn.server import logger +async def test_init(): + from service.sync.department import sync_department, check_department_datebase + from service.sync.employee import sync_department_user, check_employee_datebase + + if not check_department_datebase(): + logger.info("[数据库] 开始同步部门 📦") + await sync_department() + logger.info("[数据库] 同步部门完成 📦") + if not check_employee_datebase(): + logger.info("[数据库] 开始同步员工 📦") + await sync_department_user() + logger.info("[数据库] 同步员工完成 📦") + + def init_database(): from model import create_db_and_tables @@ -49,5 +63,6 @@ async def lifespan(app: FastAPI): import_router(app) init_scheduler(app) await import_mcp_server(app) + await test_init() yield logger.info(f"[生命周期] 应用关闭 🔧✅") diff --git a/model/__init__.py b/model/__init__.py index 32a5c59..61ce5da 100644 --- a/model/__init__.py +++ b/model/__init__.py @@ -1,7 +1,7 @@ from sqlmodel import Session, SQLModel, create_engine from config import Settings -from model.model import Department, Employee, Tenant +from model.model import Department, Employee PGSQL = Settings().PGSQL @@ -18,3 +18,9 @@ def get_engine(): def get_session(): return Session(get_engine()) + + +__all__ = [ + "Department", + "Employee", +] diff --git a/model/model.py b/model/model.py index 2f525b2..4ccf263 100644 --- a/model/model.py +++ b/model/model.py @@ -1,85 +1,92 @@ from datetime import datetime +from typing import Optional -from sqlalchemy import JSON, Column, DateTime, func -from sqlmodel import JSON, Field, SQLModel +from sqlalchemy import Column, DateTime, JSON, func +from sqlmodel import Field, SQLModel -class TenantTimeMixin(SQLModel): - tenant_id: int = Field(index=True, description="租户ID") +class Department(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) - created_at: datetime = Field( - sa_column=Column( - DateTime(timezone=True), - server_default=func.now(), - nullable=False, - ) + wecom_dept_id: str = Field(default=None, index=True) + + dname: str = Field( + max_length=100, + description="部门名称", ) - updated_at: datetime = Field( - sa_column=Column( - DateTime(timezone=True), - server_default=func.now(), - onupdate=func.now(), - nullable=False, - ) + name_en: str = Field( + max_length=100, + description="部门英文名", ) + department_leader: list[str] = Field( + default_factory=list, + sa_column=Column(JSON), + description="部门负责人 user_id 列表", + ) -class Department(TenantTimeMixin, SQLModel, table=True): - id: int | None = Field(default=None, primary_key=True) - - dname: str = Field(max_length=100) - name_en: str = Field(max_length=100) - - department_leader: list[int] = Field(default_factory=list, sa_column=Column(JSON)) - - parent_id: int = Field(default=0, index=True) + parent_id: str = Field(default=0, index=True) order: int = Field(default=0) - -class Employee(TenantTimeMixin, SQLModel, table=True): - id: int | None = Field(default=None, primary_key=True) - - ename: str = Field(max_length=100) - - dept_id: int = Field(foreign_key="department.id", index=True) - - open_userid: str = Field(max_length=100, index=True) - - -class Tenant(SQLModel, table=True): - id: int | None = Field(default=None, primary_key=True) - - # ========== 基础信息 ========== - name: str = Field(max_length=100, index=True, description="租户名称 / 企业名称") - - # ========== 企业微信配置 ========== - wecom_corp_id: str = Field(max_length=64, index=True, description="企业微信 CorpID") - - wecom_corp_secret: str = Field(max_length=128, description="企业微信应用 Secret") - - wecom_agent_id: int = Field(description="企业微信应用 AgentId") - - wecom_token: str = Field(max_length=64, description="企业微信回调 Token") - - wecom_encoding_aes_key: str = Field( - max_length=64, description="企业微信回调 EncodingAESKey" - ) - - # ========== 时间字段 ========== created_at: datetime = Field( + default_factory=datetime.now, sa_column=Column( DateTime(timezone=True), server_default=func.now(), nullable=False, - ) + ), ) updated_at: datetime = Field( + default_factory=datetime.now, sa_column=Column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False, - ) + ), + ) + + +class Employee(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + wecom_user_id: str = Field(default=None, index=True) + + ename: str = Field( + max_length=100, + description="员工姓名", + ) + + dept_ids: list[str] = Field( + default_factory=list, + sa_column=Column(JSON), + description="部门ID", + ) + + open_userid: str | None = Field( + default=None, + max_length=100, + index=True, + description="企业微信 user_id", + ) + + created_at: datetime = Field( + default_factory=datetime.now, + sa_column=Column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + ), + ) + + updated_at: datetime = Field( + default_factory=datetime.now, + sa_column=Column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ), ) diff --git a/scheduler/scheduler.py b/scheduler/scheduler.py index 4539802..8617127 100644 --- a/scheduler/scheduler.py +++ b/scheduler/scheduler.py @@ -1,12 +1,11 @@ from fastscheduler import FastScheduler - -from service import get_wecom +from service.sync.department import sync_department +from service.sync.employee import sync_department_user scheduler = FastScheduler(quiet=True) -@scheduler.every(4).hours +@scheduler.daily.at("04:00") async def background_task(): - wecom = get_wecom() - - await wecom.get_departments() + await sync_department() + await sync_department_user() diff --git a/service/__init__.py b/service/__init__.py index 93f5c45..7464ddb 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -8,7 +8,7 @@ def get_wecom_single() -> Wecom: wecom = Wecom(Settings().WECOM_CORPID, Settings().WECOM_CORPSECRET) WECOM_PROXY = Settings().WECOM_PROXY if WECOM_PROXY and WECOM_PROXY != "": - wecom.BASE_URL = WECOM_PROXY + wecom.BASE_URL = WECOM_PROXY + wecom.BASE_URL return wecom diff --git a/service/sync/__init__.py b/service/sync/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/sync/department.py b/service/sync/department.py new file mode 100644 index 0000000..0be3401 --- /dev/null +++ b/service/sync/department.py @@ -0,0 +1,38 @@ +from service import get_wecom +from model import get_session, Department +from sqlmodel import delete, select + + +async def sync_department(): + wecom = get_wecom() + + department_res = await wecom.get_departments() + + with get_session() as session: + # 删除原来的数据 + stmt = delete(Department) + session.execute(stmt) + session.commit() + + # DepartmentInfoItem(id=302, name='亮剑一部一组-梁鹏涛', name_en=None, department_leader=['LiangPengTao'], parentid=96, order=100000000) + # 插入新的数据 + for index, item in enumerate(department_res): + new_dept = Department( + id=index + 1, + wecom_dept_id=str(item.id), + dname=str(item.name), + name_en=str(item.name_en), + department_leader=item.department_leader or [], + parent_id=str(item.parentid), + order=item.order or 0, + ) + session.add(new_dept) + session.commit() + + +def check_department_datebase(): + with get_session() as session: + has = session.exec(select(Department)).first() + if not has: + return False + return True diff --git a/service/sync/employee.py b/service/sync/employee.py new file mode 100644 index 0000000..33999df --- /dev/null +++ b/service/sync/employee.py @@ -0,0 +1,37 @@ +from service import get_wecom +from model import get_session, Employee +from sqlmodel import delete, select + + +async def sync_department_user(): + wecom = get_wecom() + + dept_res = await wecom.get_departments() + + with get_session() as session: + # 删除原来的数据 + stmt = delete(Employee) + session.execute(stmt) + session.commit() + index_id = 1 + for dept in dept_res: + user_res = await wecom.get_user_in_department(dept.id) + for item in user_res: + new_employee = Employee( + id=index_id, + wecom_user_id=item.userid, + ename=item.name, + dept_ids=[str(i) for i in item.department], + open_userid=item.open_userid, + ) + index_id += 1 + session.add(new_employee) + session.commit() + + +def check_employee_datebase(): + with get_session() as session: + has = session.exec(select(Employee)).first() + if not has: + return False + return True diff --git a/service/wecom/modules/base.py b/service/wecom/modules/base.py index bb03283..2fa5f86 100644 --- a/service/wecom/modules/base.py +++ b/service/wecom/modules/base.py @@ -1,8 +1,8 @@ from datetime import datetime, timedelta -from wecom.exceptions.general import SDKException -from wecom.schemas.token import AccessTokenInfo, AccessTokenParams -from wecom.utils.requests import HttpxRequest +from service.wecom.exceptions.general import SDKException +from service.wecom.schemas.token import AccessTokenInfo, AccessTokenParams +from service.wecom.utils.requests import HttpxRequest BASE_URL: str = "https://qyapi.weixin.qq.com/cgi-bin" diff --git a/service/wecom/modules/department.py b/service/wecom/modules/department.py index 4691e10..cfae85f 100644 --- a/service/wecom/modules/department.py +++ b/service/wecom/modules/department.py @@ -1,13 +1,14 @@ -from wecom.exceptions.general import SDKException -from wecom.modules.base import WecomBaseClient -from wecom.schemas.departments import ( +from service.wecom.exceptions.general import SDKException +from service.wecom.modules.base import WecomBaseClient +from service.wecom.schemas.departments import ( CreateDepartmentInfo, CreateDepartmentParams, DepartmentInfo, + DepartmentInfoItem, UpdateDepartmentInfo, UpdateDepartmentParams, ) -from wecom.utils.requests import HttpxRequest +from service.wecom.utils.requests import HttpxRequest class WecomDepartmentClient(WecomBaseClient): @@ -63,7 +64,7 @@ class WecomDepartmentClient(WecomBaseClient): else: raise SDKException(resp.errcode, resp.errmsg) - async def get_departments(self, id: int = None) -> list[DepartmentInfo]: + async def get_departments(self, id: int = None) -> list[DepartmentInfoItem]: """ 获取部门列表 @param id: 部门id。获取指定部门及其下的子部门。 diff --git a/service/wecom/modules/message.py b/service/wecom/modules/message.py index 670fcc6..582ef47 100644 --- a/service/wecom/modules/message.py +++ b/service/wecom/modules/message.py @@ -1,14 +1,14 @@ from typing import Literal -from wecom.exceptions.general import SDKException -from wecom.modules.base import WecomBaseClient -from wecom.schemas.message import ( +from service.wecom.exceptions.general import SDKException +from service.wecom.modules.base import WecomBaseClient +from service.wecom.schemas.message import ( MessageParams, RecallMessageInfo, RecallMessageParams, SendMessageInfo, ) -from wecom.utils.requests import HttpxRequest +from service.wecom.utils.requests import HttpxRequest class WecomMessageClient(WecomBaseClient): diff --git a/service/wecom/modules/mixin.py b/service/wecom/modules/mixin.py index 1a7bd2c..8679b33 100644 --- a/service/wecom/modules/mixin.py +++ b/service/wecom/modules/mixin.py @@ -1,7 +1,7 @@ -from wecom.modules.base import WecomBaseClient -from wecom.modules.department import WecomDepartmentClient -from wecom.modules.message import WecomMessageClient -from wecom.modules.users import WecomUsersClient +from service.wecom.modules.base import WecomBaseClient +from service.wecom.modules.department import WecomDepartmentClient +from service.wecom.modules.message import WecomMessageClient +from service.wecom.modules.users import WecomUsersClient class Wecom( diff --git a/service/wecom/modules/users.py b/service/wecom/modules/users.py index cc0e341..7f43cd3 100644 --- a/service/wecom/modules/users.py +++ b/service/wecom/modules/users.py @@ -1,8 +1,13 @@ -from wecom.exceptions.general import SDKException -from wecom.modules.base import WecomBaseClient -from wecom.schemas.departments import DepartmentInfo -from wecom.schemas.users import DepartmentUserDetailInfo, DepartmentUserInfo, UserInfo -from wecom.utils.requests import HttpxRequest +from service.wecom.exceptions.general import SDKException +from service.wecom.modules.base import WecomBaseClient +from service.wecom.schemas.departments import DepartmentInfo +from service.wecom.schemas.users import ( + DepartmentUserDetailInfo, + DepartmentUserInfo, + UserInfo, + UserSimpleInfo, +) +from service.wecom.utils.requests import HttpxRequest class WecomUsersClient(WecomBaseClient): @@ -45,7 +50,7 @@ class WecomUsersClient(WecomBaseClient): else: raise SDKException(resp.errcode, resp.errmsg) - async def get_user_in_department(self, department_id: int) -> dict: + async def get_user_in_department(self, department_id: int) -> list[UserSimpleInfo]: """ 读取部门成员简要信息 @param department_id: 获取的部门id @@ -61,7 +66,7 @@ class WecomUsersClient(WecomBaseClient): resp = DepartmentUserInfo(**await HttpxRequest.get(url=url, params=params)) if resp.errcode == 0: - return resp.model_dump(exclude={"errcode", "errmsg"}) + return resp.userlist else: raise SDKException(resp.errcode, resp.errmsg) diff --git a/service/wecom/schemas/departments.py b/service/wecom/schemas/departments.py index ff95335..d2c23f5 100644 --- a/service/wecom/schemas/departments.py +++ b/service/wecom/schemas/departments.py @@ -1,6 +1,6 @@ -from typing import AnyStr, List +from typing import List -from wecom.schemas.base import BaseSchema +from service.wecom.schemas.base import BaseSchema class CreateDepartmentParams(BaseSchema): @@ -26,23 +26,23 @@ class UpdateDepartmentParams(CreateDepartmentParams): ... class UpdateDepartmentInfo(BaseSchema): errcode: int - errmsg: AnyStr + errmsg: str class CreateDepartmentInfo(BaseSchema): errcode: int - errmsg: AnyStr + errmsg: str id: int -class DepartmentInfo(BaseSchema): +class DepartmentInfoItem(BaseSchema): """ 部门单体响应数据 """ id: int - name: AnyStr - name_en: AnyStr | None = None + name: str + name_en: str | None = None department_leader: List[str] | None = None parentid: int | None = None order: int | None = None @@ -54,5 +54,5 @@ class DepartmentInfo(BaseSchema): """ errcode: int - errmsg: AnyStr - department: List[DepartmentInfo] + errmsg: str + department: List[DepartmentInfoItem] diff --git a/service/wecom/schemas/message.py b/service/wecom/schemas/message.py index 6ad8e48..83474e5 100644 --- a/service/wecom/schemas/message.py +++ b/service/wecom/schemas/message.py @@ -1,6 +1,6 @@ -from typing import AnyStr, Literal +from typing import Literal -from wecom.schemas.base import BaseSchema +from service.wecom.schemas.base import BaseSchema class MessageParams(BaseSchema): @@ -24,9 +24,9 @@ class MessageParams(BaseSchema): touser、toparty、totag不能同时为空,后面不再强调 """ - touser: AnyStr | None = None - toparty: AnyStr | None = None - totag: AnyStr | None = None + touser: str | None = None + toparty: str | None = None + totag: str | None = None msgtype: Literal[ "text", "image", "voice", "video", "textcard", "news", "mpnews", "markdown" ] @@ -55,13 +55,13 @@ class SendMessageInfo(BaseSchema): """ errcode: int - errmsg: AnyStr - invaliduser: AnyStr | None = None - invalidparty: AnyStr | None = None - invalidtag: AnyStr | None = None - unlicenseduser: AnyStr | None = None - msgid: AnyStr | None = None - response_code: AnyStr | None = None + errmsg: str + invaliduser: str | None = None + invalidparty: str | None = None + invalidtag: str | None = None + unlicenseduser: str | None = None + msgid: str | None = None + response_code: str | None = None class SendMessageInvalid(BaseSchema): @@ -70,11 +70,11 @@ class SendMessageInvalid(BaseSchema): """ - errmsg: AnyStr - invaliduser: AnyStr | None = None - invalidparty: AnyStr | None = None - invalidtag: AnyStr | None = None - unlicenseduser: AnyStr | None = None + errmsg: str + invaliduser: str | None = None + invalidparty: str | None = None + invalidtag: str | None = None + unlicenseduser: str | None = None class RecallMessageParams(BaseSchema): @@ -82,7 +82,7 @@ class RecallMessageParams(BaseSchema): 撤回消息请求参数 """ - msgid: AnyStr + msgid: str class RecallMessageInfo(BaseSchema): @@ -91,4 +91,4 @@ class RecallMessageInfo(BaseSchema): """ errcode: int - errmsg: AnyStr + errmsg: str diff --git a/service/wecom/schemas/token.py b/service/wecom/schemas/token.py index 4013cdf..752f747 100644 --- a/service/wecom/schemas/token.py +++ b/service/wecom/schemas/token.py @@ -1,4 +1,4 @@ -from wecom.schemas.base import BaseSchema +from service.wecom.schemas.base import BaseSchema class AccessTokenParams(BaseSchema): diff --git a/service/wecom/schemas/users.py b/service/wecom/schemas/users.py index cbab066..847b343 100644 --- a/service/wecom/schemas/users.py +++ b/service/wecom/schemas/users.py @@ -1,6 +1,6 @@ -from typing import AnyStr, List +from typing import List -from wecom.schemas.base import BaseSchema +from service.wecom.schemas.base import BaseSchema class UserInfo(BaseSchema): @@ -9,31 +9,31 @@ class UserInfo(BaseSchema): """ errcode: int - errmsg: AnyStr - userid: AnyStr | None = None - name: AnyStr | None = None + errmsg: str + userid: str | None = None + name: str | None = None department: List[int] | None = None - position: AnyStr | None = None - moblie: AnyStr | None = None + position: str | None = None + moblie: str | None = None gender: int | None = None - email: AnyStr | None = None + email: str | None = None status: int | None = None class UserSimpleInfo(BaseSchema): - userid: AnyStr - name: AnyStr + userid: str + name: str department: List[int] - open_userid: AnyStr | None = None + open_userid: str | None = None class DepartmentUserInfo(BaseSchema): errcode: int - errmsg: AnyStr + errmsg: str userlist: List[UserSimpleInfo] class DepartmentUserDetailInfo(BaseSchema): errcode: int - errmsg: AnyStr + errmsg: str userlist: List[UserInfo]