增加定时同步组织架构

This commit is contained in:
2026-01-15 19:24:52 +08:00
parent 4a51ec89cc
commit 723c7817b6
17 changed files with 240 additions and 132 deletions

View File

@@ -4,6 +4,20 @@ from fastapi import FastAPI
from uvicorn.server import logger 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(): def init_database():
from model import create_db_and_tables from model import create_db_and_tables
@@ -49,5 +63,6 @@ async def lifespan(app: FastAPI):
import_router(app) import_router(app)
init_scheduler(app) init_scheduler(app)
await import_mcp_server(app) await import_mcp_server(app)
await test_init()
yield yield
logger.info(f"[生命周期] 应用关闭 🔧✅") logger.info(f"[生命周期] 应用关闭 🔧✅")

View File

@@ -1,7 +1,7 @@
from sqlmodel import Session, SQLModel, create_engine from sqlmodel import Session, SQLModel, create_engine
from config import Settings from config import Settings
from model.model import Department, Employee, Tenant from model.model import Department, Employee
PGSQL = Settings().PGSQL PGSQL = Settings().PGSQL
@@ -18,3 +18,9 @@ def get_engine():
def get_session(): def get_session():
return Session(get_engine()) return Session(get_engine())
__all__ = [
"Department",
"Employee",
]

View File

@@ -1,85 +1,92 @@
from datetime import datetime from datetime import datetime
from typing import Optional
from sqlalchemy import JSON, Column, DateTime, func from sqlalchemy import Column, DateTime, JSON, func
from sqlmodel import JSON, Field, SQLModel from sqlmodel import Field, SQLModel
class TenantTimeMixin(SQLModel): class Department(SQLModel, table=True):
tenant_id: int = Field(index=True, description="租户ID") id: Optional[int] = Field(default=None, primary_key=True)
created_at: datetime = Field( wecom_dept_id: str = Field(default=None, index=True)
sa_column=Column(
DateTime(timezone=True), dname: str = Field(
server_default=func.now(), max_length=100,
nullable=False, description="部门名称",
)
) )
updated_at: datetime = Field( name_en: str = Field(
sa_column=Column( max_length=100,
DateTime(timezone=True), description="部门英文名",
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
) )
department_leader: list[str] = Field(
default_factory=list,
sa_column=Column(JSON),
description="部门负责人 user_id 列表",
)
class Department(TenantTimeMixin, SQLModel, table=True): parent_id: str = Field(default=0, index=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)
order: int = Field(default=0) 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( created_at: datetime = Field(
default_factory=datetime.now,
sa_column=Column( sa_column=Column(
DateTime(timezone=True), DateTime(timezone=True),
server_default=func.now(), server_default=func.now(),
nullable=False, nullable=False,
) ),
) )
updated_at: datetime = Field( updated_at: datetime = Field(
default_factory=datetime.now,
sa_column=Column( sa_column=Column(
DateTime(timezone=True), DateTime(timezone=True),
server_default=func.now(), server_default=func.now(),
onupdate=func.now(), onupdate=func.now(),
nullable=False, 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,
),
) )

View File

@@ -1,12 +1,11 @@
from fastscheduler import FastScheduler from fastscheduler import FastScheduler
from service.sync.department import sync_department
from service import get_wecom from service.sync.employee import sync_department_user
scheduler = FastScheduler(quiet=True) scheduler = FastScheduler(quiet=True)
@scheduler.every(4).hours @scheduler.daily.at("04:00")
async def background_task(): async def background_task():
wecom = get_wecom() await sync_department()
await sync_department_user()
await wecom.get_departments()

View File

@@ -8,7 +8,7 @@ def get_wecom_single() -> Wecom:
wecom = Wecom(Settings().WECOM_CORPID, Settings().WECOM_CORPSECRET) wecom = Wecom(Settings().WECOM_CORPID, Settings().WECOM_CORPSECRET)
WECOM_PROXY = Settings().WECOM_PROXY WECOM_PROXY = Settings().WECOM_PROXY
if WECOM_PROXY and WECOM_PROXY != "": if WECOM_PROXY and WECOM_PROXY != "":
wecom.BASE_URL = WECOM_PROXY wecom.BASE_URL = WECOM_PROXY + wecom.BASE_URL
return wecom return wecom

0
service/sync/__init__.py Normal file
View File

View File

@@ -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

37
service/sync/employee.py Normal file
View File

@@ -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

View File

@@ -1,8 +1,8 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from wecom.exceptions.general import SDKException from service.wecom.exceptions.general import SDKException
from wecom.schemas.token import AccessTokenInfo, AccessTokenParams from service.wecom.schemas.token import AccessTokenInfo, AccessTokenParams
from wecom.utils.requests import HttpxRequest from service.wecom.utils.requests import HttpxRequest
BASE_URL: str = "https://qyapi.weixin.qq.com/cgi-bin" BASE_URL: str = "https://qyapi.weixin.qq.com/cgi-bin"

View File

@@ -1,13 +1,14 @@
from wecom.exceptions.general import SDKException from service.wecom.exceptions.general import SDKException
from wecom.modules.base import WecomBaseClient from service.wecom.modules.base import WecomBaseClient
from wecom.schemas.departments import ( from service.wecom.schemas.departments import (
CreateDepartmentInfo, CreateDepartmentInfo,
CreateDepartmentParams, CreateDepartmentParams,
DepartmentInfo, DepartmentInfo,
DepartmentInfoItem,
UpdateDepartmentInfo, UpdateDepartmentInfo,
UpdateDepartmentParams, UpdateDepartmentParams,
) )
from wecom.utils.requests import HttpxRequest from service.wecom.utils.requests import HttpxRequest
class WecomDepartmentClient(WecomBaseClient): class WecomDepartmentClient(WecomBaseClient):
@@ -63,7 +64,7 @@ class WecomDepartmentClient(WecomBaseClient):
else: else:
raise SDKException(resp.errcode, resp.errmsg) 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。获取指定部门及其下的子部门。 @param id: 部门id。获取指定部门及其下的子部门。

View File

@@ -1,14 +1,14 @@
from typing import Literal from typing import Literal
from wecom.exceptions.general import SDKException from service.wecom.exceptions.general import SDKException
from wecom.modules.base import WecomBaseClient from service.wecom.modules.base import WecomBaseClient
from wecom.schemas.message import ( from service.wecom.schemas.message import (
MessageParams, MessageParams,
RecallMessageInfo, RecallMessageInfo,
RecallMessageParams, RecallMessageParams,
SendMessageInfo, SendMessageInfo,
) )
from wecom.utils.requests import HttpxRequest from service.wecom.utils.requests import HttpxRequest
class WecomMessageClient(WecomBaseClient): class WecomMessageClient(WecomBaseClient):

View File

@@ -1,7 +1,7 @@
from wecom.modules.base import WecomBaseClient from service.wecom.modules.base import WecomBaseClient
from wecom.modules.department import WecomDepartmentClient from service.wecom.modules.department import WecomDepartmentClient
from wecom.modules.message import WecomMessageClient from service.wecom.modules.message import WecomMessageClient
from wecom.modules.users import WecomUsersClient from service.wecom.modules.users import WecomUsersClient
class Wecom( class Wecom(

View File

@@ -1,8 +1,13 @@
from wecom.exceptions.general import SDKException from service.wecom.exceptions.general import SDKException
from wecom.modules.base import WecomBaseClient from service.wecom.modules.base import WecomBaseClient
from wecom.schemas.departments import DepartmentInfo from service.wecom.schemas.departments import DepartmentInfo
from wecom.schemas.users import DepartmentUserDetailInfo, DepartmentUserInfo, UserInfo from service.wecom.schemas.users import (
from wecom.utils.requests import HttpxRequest DepartmentUserDetailInfo,
DepartmentUserInfo,
UserInfo,
UserSimpleInfo,
)
from service.wecom.utils.requests import HttpxRequest
class WecomUsersClient(WecomBaseClient): class WecomUsersClient(WecomBaseClient):
@@ -45,7 +50,7 @@ class WecomUsersClient(WecomBaseClient):
else: else:
raise SDKException(resp.errcode, resp.errmsg) 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 @param department_id: 获取的部门id
@@ -61,7 +66,7 @@ class WecomUsersClient(WecomBaseClient):
resp = DepartmentUserInfo(**await HttpxRequest.get(url=url, params=params)) resp = DepartmentUserInfo(**await HttpxRequest.get(url=url, params=params))
if resp.errcode == 0: if resp.errcode == 0:
return resp.model_dump(exclude={"errcode", "errmsg"}) return resp.userlist
else: else:
raise SDKException(resp.errcode, resp.errmsg) raise SDKException(resp.errcode, resp.errmsg)

View File

@@ -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): class CreateDepartmentParams(BaseSchema):
@@ -26,23 +26,23 @@ class UpdateDepartmentParams(CreateDepartmentParams): ...
class UpdateDepartmentInfo(BaseSchema): class UpdateDepartmentInfo(BaseSchema):
errcode: int errcode: int
errmsg: AnyStr errmsg: str
class CreateDepartmentInfo(BaseSchema): class CreateDepartmentInfo(BaseSchema):
errcode: int errcode: int
errmsg: AnyStr errmsg: str
id: int id: int
class DepartmentInfo(BaseSchema): class DepartmentInfoItem(BaseSchema):
""" """
部门单体响应数据 部门单体响应数据
""" """
id: int id: int
name: AnyStr name: str
name_en: AnyStr | None = None name_en: str | None = None
department_leader: List[str] | None = None department_leader: List[str] | None = None
parentid: int | None = None parentid: int | None = None
order: int | None = None order: int | None = None
@@ -54,5 +54,5 @@ class DepartmentInfo(BaseSchema):
""" """
errcode: int errcode: int
errmsg: AnyStr errmsg: str
department: List[DepartmentInfo] department: List[DepartmentInfoItem]

View File

@@ -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): class MessageParams(BaseSchema):
@@ -24,9 +24,9 @@ class MessageParams(BaseSchema):
touser、toparty、totag不能同时为空后面不再强调 touser、toparty、totag不能同时为空后面不再强调
""" """
touser: AnyStr | None = None touser: str | None = None
toparty: AnyStr | None = None toparty: str | None = None
totag: AnyStr | None = None totag: str | None = None
msgtype: Literal[ msgtype: Literal[
"text", "image", "voice", "video", "textcard", "news", "mpnews", "markdown" "text", "image", "voice", "video", "textcard", "news", "mpnews", "markdown"
] ]
@@ -55,13 +55,13 @@ class SendMessageInfo(BaseSchema):
""" """
errcode: int errcode: int
errmsg: AnyStr errmsg: str
invaliduser: AnyStr | None = None invaliduser: str | None = None
invalidparty: AnyStr | None = None invalidparty: str | None = None
invalidtag: AnyStr | None = None invalidtag: str | None = None
unlicenseduser: AnyStr | None = None unlicenseduser: str | None = None
msgid: AnyStr | None = None msgid: str | None = None
response_code: AnyStr | None = None response_code: str | None = None
class SendMessageInvalid(BaseSchema): class SendMessageInvalid(BaseSchema):
@@ -70,11 +70,11 @@ class SendMessageInvalid(BaseSchema):
""" """
errmsg: AnyStr errmsg: str
invaliduser: AnyStr | None = None invaliduser: str | None = None
invalidparty: AnyStr | None = None invalidparty: str | None = None
invalidtag: AnyStr | None = None invalidtag: str | None = None
unlicenseduser: AnyStr | None = None unlicenseduser: str | None = None
class RecallMessageParams(BaseSchema): class RecallMessageParams(BaseSchema):
@@ -82,7 +82,7 @@ class RecallMessageParams(BaseSchema):
撤回消息请求参数 撤回消息请求参数
""" """
msgid: AnyStr msgid: str
class RecallMessageInfo(BaseSchema): class RecallMessageInfo(BaseSchema):
@@ -91,4 +91,4 @@ class RecallMessageInfo(BaseSchema):
""" """
errcode: int errcode: int
errmsg: AnyStr errmsg: str

View File

@@ -1,4 +1,4 @@
from wecom.schemas.base import BaseSchema from service.wecom.schemas.base import BaseSchema
class AccessTokenParams(BaseSchema): class AccessTokenParams(BaseSchema):

View File

@@ -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): class UserInfo(BaseSchema):
@@ -9,31 +9,31 @@ class UserInfo(BaseSchema):
""" """
errcode: int errcode: int
errmsg: AnyStr errmsg: str
userid: AnyStr | None = None userid: str | None = None
name: AnyStr | None = None name: str | None = None
department: List[int] | None = None department: List[int] | None = None
position: AnyStr | None = None position: str | None = None
moblie: AnyStr | None = None moblie: str | None = None
gender: int | None = None gender: int | None = None
email: AnyStr | None = None email: str | None = None
status: int | None = None status: int | None = None
class UserSimpleInfo(BaseSchema): class UserSimpleInfo(BaseSchema):
userid: AnyStr userid: str
name: AnyStr name: str
department: List[int] department: List[int]
open_userid: AnyStr | None = None open_userid: str | None = None
class DepartmentUserInfo(BaseSchema): class DepartmentUserInfo(BaseSchema):
errcode: int errcode: int
errmsg: AnyStr errmsg: str
userlist: List[UserSimpleInfo] userlist: List[UserSimpleInfo]
class DepartmentUserDetailInfo(BaseSchema): class DepartmentUserDetailInfo(BaseSchema):
errcode: int errcode: int
errmsg: AnyStr errmsg: str
userlist: List[UserInfo] userlist: List[UserInfo]