1 Commits

32 changed files with 149 additions and 545 deletions

View File

@@ -9,3 +9,4 @@ class Settings(BaseSettings):
WECOM_CORPSECRET: str = "" WECOM_CORPSECRET: str = ""
WECOM_APP_TOKEN: str = "" WECOM_APP_TOKEN: str = ""
WECOM_APP_ENCODING_AES_KEY: str = "" WECOM_APP_ENCODING_AES_KEY: str = ""

View File

@@ -1,8 +1,7 @@
import http import http
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import ValidationError from pydantic import ValidationError
from fastapi.responses import JSONResponse
from uvicorn.server import logger from uvicorn.server import logger
exceptions = [Exception, HTTPException, ValidationError] exceptions = [Exception, HTTPException, ValidationError]

View File

@@ -1,114 +1,8 @@
import asyncio
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from uvicorn.server import logger from uvicorn.server import logger
class ChainBuilder:
"""支持链式调用的建造者类,支持同步和异步方法"""
def __init__(self):
self._tasks = []
def add(self, func, *args, **kwargs):
"""添加同步或异步任务到链中"""
self._tasks.append((func, args, kwargs))
return self
def adds(self, *funcs_or_tuples):
"""添加一个或多个同步或异步任务到链中
支持多种调用方式:
1. 单个函数: add(func, *args, **kwargs)
2. 多个函数: add((func1, args1, kwargs1), (func2, args2, kwargs2), ...)
3. 混合方式: add(func1, *args1, **kwargs1), (func2, args2, kwargs2), ...
"""
for item in funcs_or_tuples:
if isinstance(item, tuple) and len(item) == 3:
# 如果是三元组 (func, args, kwargs)
func, args, kwargs = item
self._tasks.append((func, args, kwargs))
elif callable(item):
# 如果是单个函数,需要检查后续参数
if (
len(funcs_or_tuples) >= 3
and isinstance(funcs_or_tuples[1], tuple)
and isinstance(funcs_or_tuples[2], dict)
):
# 如果是 add(func, args, kwargs) 格式
func = item
args = funcs_or_tuples[1] if len(funcs_or_tuples) > 1 else ()
kwargs = funcs_or_tuples[2] if len(funcs_or_tuples) > 2 else {}
self._tasks.append((func, args, kwargs))
break # 处理完这个函数后退出循环
else:
# 单个函数没有参数
self._tasks.append((item, (), {}))
else:
raise ValueError(f"不支持的参数类型: {type(item)}")
return self
async def _execute_async(self):
"""异步执行所有任务"""
for func, args, kwargs in self._tasks:
if asyncio.iscoroutinefunction(func):
await func(*args, **kwargs)
else:
# 如果是同步函数,在事件循环中运行
func(*args, **kwargs)
def __call__(self):
"""同步调用接口"""
import asyncio
# 检查是否有异步任务
has_async = any(asyncio.iscoroutinefunction(func) for func, _, _ in self._tasks)
if has_async:
# 如果有异步任务,创建并运行事件循环
loop = asyncio.get_event_loop()
if loop.is_running():
# 如果事件循环已经在运行,创建任务
asyncio.create_task(self._execute_async())
else:
# 否则运行事件循环
loop.run_until_complete(self._execute_async())
else:
# 如果都是同步任务,直接执行
for func, args, kwargs in self._tasks:
func(*args, **kwargs)
return self
async def __aenter__(self):
"""异步上下文管理器入口"""
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口"""
pass
async def execute_async(self):
"""显式异步执行方法"""
await self._execute_async()
return self
async def data_base_init():
from service.sync.department import check_department_datebase, sync_department
from service.sync.employee import check_employee_datebase, sync_department_user
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
@@ -116,51 +10,38 @@ def init_database():
create_db_and_tables() create_db_and_tables()
logger.info("[数据库] 数据库初始化完成 ✅") logger.info("[数据库] 数据库初始化完成 ✅")
def init_scheduler(app : FastAPI): def init_scheduler(app : FastAPI):
from scheduler import init_scheduler_router from scheduler import init_scheduler_router
logger.info("[定时任务] 初始化定时任务 📦") logger.info("[定时任务] 初始化定时任务 📦")
init_scheduler_router(app) init_scheduler_router(app)
logger.info("[定时任务] 定时任务初始化完成 ✅") logger.info("[定时任务] 定时任务初始化完成 ✅")
def active_config(): def active_config():
logger.info("[激活配置] 加载配置 ⚙️") logger.info(f"[激活配置] 加载配置 ⚙️")
from config import Settings # noqa from config import Settings # noqa
def import_router(app: FastAPI): def import_router(app: FastAPI):
logger.info("[导入路由] 开始导入路由 🛣️") logger.info(f"[导入路由] 开始导入路由 🛣️")
from router import router from router import router
app.include_router(router) app.include_router(router)
logger.info("[导入路由] 路由导入完成 ✅") logger.info(f"[导入路由] 路由导入完成 ✅")
async def import_mcp_server(app: FastAPI): async def import_mcp_server(app: FastAPI):
logger.info("[导入MCP] 开始导入MCP 🛣️") logger.info(f"[导入MCP] 开始导入MCP 🛣️")
from mcps import create_mcp_app from mcps import create_mcp_app
app.mount("/app" , await create_mcp_app()) app.mount("/app" , await create_mcp_app())
logger.info("[导入MCP] MCP导入完成 ✅") logger.info(f"[导入MCP] MCP导入完成 ✅")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
logger.info("[生命周期] 应用启动 🚀") logger.info(f"[生命周期] 应用启动 🚀")
builder = ChainBuilder() active_config()
init_database()
# 激活配置 import_router(app)
builder.add(active_config) init_scheduler(app)
# 初始化数据库 await import_mcp_server(app)
builder.add(init_database).add(data_base_init)
# 导入MCP
builder.add(import_mcp_server, app)
# 导入路由
builder.add(import_router, app)
# 初始化定时任务
builder.add(init_scheduler, app)
await builder.execute_async()
yield yield
logger.info("[生命周期] 应用关闭 🔧✅") logger.info(f"[生命周期] 应用关闭 🔧✅")

View File

@@ -4,6 +4,7 @@ from handler.exception import install as exception_install
from lifespan import lifespan from lifespan import lifespan
from mcps import create_mcp_app from mcps import create_mcp_app
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
exception_install(app) exception_install(app)

View File

@@ -1,5 +1,4 @@
from fastmcp import FastMCP from fastmcp import FastMCP
from mcps.test.test import weather_mcp from mcps.test.test import weather_mcp

View File

@@ -1,7 +1,6 @@
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
PGSQL = Settings().PGSQL PGSQL = Settings().PGSQL
@@ -19,8 +18,3 @@ def get_engine():
def get_session(): def get_session():
return Session(get_engine()) return Session(get_engine())
__all__ = [
"Department",
"Employee",
]

View File

@@ -1,92 +1,22 @@
from datetime import datetime from sqlmodel import SQLModel, Field, Column, JSON
from typing import Optional
from sqlalchemy import Column, DateTime, JSON, func
from sqlmodel import Field, SQLModel
class Department(SQLModel, table = True): class Department(SQLModel, table = True):
id: Optional[int] = Field(default=None, primary_key=True) did: int = Field(default=None, primary_key=True)
dname: str = Field(max_length=100)
wecom_dept_id: str = Field(default=None, index=True) name_en: str = Field(max_length=100)
department_leader: list[int] = Field(default=[], sa_column=Column(JSON))
dname: str = Field( parent_id: int = Field(default=0)
max_length=100,
description="部门名称",
)
name_en: str = Field(
max_length=100,
description="部门英文名",
)
department_leader: list[str] = Field(
default_factory=list,
sa_column=Column(JSON),
description="部门负责人 user_id 列表",
)
parent_id: str = Field(default=0, index=True)
order: int = Field(default=0) order: int = Field(default=0)
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): class Employee(SQLModel, table = True):
id: Optional[int] = Field(default=None, primary_key=True) userid: int = Field(default=None, primary_key=True)
ename: str = Field(max_length=100)
dept_id: int = Field(foreign_key='Department.did')
open_userid: str = Field(max_length=100)
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

@@ -25,7 +25,4 @@ dependencies = [
] ]
[dependency-groups] [dependency-groups]
dev = [ dev = []
"isort>=7.0.0",
"ruff>=0.14.11",
]

View File

@@ -1,14 +1,15 @@
import json import json
from fastapi import APIRouter, HTTPException, Request from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import PlainTextResponse from fastapi.responses import PlainTextResponse
from uvicorn.server import logger from uvicorn.server import logger
from utils.wxcom import wxcpt
from utils.wxcom import decrypt_message, get_request_params, wxcpt from utils.wxcom import (
decrypt_message,
get_request_params
)
router = APIRouter() router = APIRouter()
@router.get("/callback") @router.get("/callback")
async def verify_url(msg_signature: str, timestamp: str, nonce: str, echostr: str): async def verify_url(msg_signature: str, timestamp: str, nonce: str, echostr: str):
"""验证URL有效性""" """验证URL有效性"""
@@ -28,21 +29,20 @@ async def verify_url(msg_signature: str, timestamp: str, nonce: str, echostr: st
logger.error(f"验证过程发生错误: {str(e)}") logger.error(f"验证过程发生错误: {str(e)}")
raise HTTPException(status_code=500, detail="服务器内部错误") raise HTTPException(status_code=500, detail="服务器内部错误")
@router.post("/callback") @router.post("/callback")
async def receive_message(request: Request): async def receive_message(request: Request):
"""接收并处理企业微信消息""" """接收并处理企业微信消息"""
try: try:
# 获取请求参数并验证,返回请求体、消息签名、时间戳和随机数 # 获取请求参数并验证,返回请求体、消息签名、时间戳和随机数
body, msg_signature, timestamp, nonce = await get_request_params(request) body, msg_signature, timestamp, nonce = await get_request_params(request)
# 对请求体进行解密,得到解密后的消息字典 # 对请求体进行解密,得到解密后的消息字典
xml_dict : dict= decrypt_message(body, msg_signature, timestamp, nonce) xml_dict : dict= decrypt_message(body, msg_signature, timestamp, nonce)
logger.info( logger.info(f"解密后的消息字典: \n {json.dumps(xml_dict.get("xml") , ensure_ascii=False , indent=2)}")
f"解密后的消息字典: \n {json.dumps(xml_dict.get('xml'), ensure_ascii=False, indent=2)}"
)
# 处理消息 # 处理消息
# subscription(xml_dict) # subscription(xml_dict)
except Exception as e: except Exception as e:
logger.error(f"处理消息时发生错误: {str(e)}") logger.error(f"处理消息时发生错误: {str(e)}")
raise HTTPException(status_code=500, detail="服务器内部错误") raise HTTPException(status_code=500, detail="服务器内部错误")

View File

@@ -1,9 +1,7 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastscheduler.fastapi_integration import create_scheduler_routes from fastscheduler.fastapi_integration import create_scheduler_routes
from scheduler.scheduler import scheduler from scheduler.scheduler import scheduler
def init_scheduler_router(app : FastAPI): def init_scheduler_router(app : FastAPI):
app.include_router(create_scheduler_routes(scheduler)) app.include_router(create_scheduler_routes(scheduler))
scheduler.start() scheduler.start()

View File

@@ -1,11 +1,9 @@
from fastscheduler import FastScheduler from fastscheduler import FastScheduler
from service.sync.department import sync_department
from service.sync.employee import sync_department_user
scheduler = FastScheduler(quiet=True) scheduler = FastScheduler(quiet=True)
@scheduler.daily.at("04:00") @scheduler.every(10).seconds
async def background_task(): def background_task():
await sync_department() print("Background work")
await sync_department_user()

View File

@@ -1,15 +1,15 @@
from config import Settings
from service.wecom import Wecom from service.wecom import Wecom
from config import Settings
from utils.sing import SingletonProvider from utils.sing import SingletonProvider
# 获取单例函数 # 获取单例函数
def get_wecom_single() -> Wecom: 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.BASE_URL = WECOM_PROXY
return wecom return wecom
get_wecom = SingletonProvider(get_wecom_single) get_wecom = SingletonProvider(get_wecom_single)

View File

@@ -1,38 +0,0 @@
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

View File

@@ -1,37 +0,0 @@
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,11 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from service.wecom.exceptions.general import SDKException from wecom.exceptions.general import SDKException
from service.wecom.schemas.token import AccessTokenInfo, AccessTokenParams from wecom.schemas.token import (
from service.wecom.utils.requests import HttpxRequest AccessTokenInfo,
AccessTokenParams,
)
from 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,28 +0,0 @@
from service.wecom.exceptions.general import SDKException
from service.wecom.modules.base import WecomBaseClient
from service.wecom.schemas.card import (
GetCardRecord,
GetCardRecordsRequest,
GetCardRecordsResponse,
)
from service.wecom.utils.requests import HttpxRequest
class WecomCardClient(WecomBaseClient):
async def get_card_records(
self, data: GetCardRecordsRequest
) -> list[GetCardRecord]:
"""
获取打卡记录数据
@param data: 获取打卡记录数据的参数
"""
url = self.BASE_URL + "/checkin/getcheckindata"
params = {"access_token": await self.access_token}
resp = GetCardRecordsResponse(
**await HttpxRequest.post(url=url, params=params, json=data.model_dump())
)
if resp.errcode == 0:
return resp.checkindata
else:
raise SDKException(resp.errcode, resp.errmsg)

View File

@@ -1,17 +1,17 @@
from service.wecom.exceptions.general import SDKException from wecom.exceptions.general import SDKException
from service.wecom.modules.base import WecomBaseClient from wecom.modules.base import WecomBaseClient
from service.wecom.schemas.departments import ( from wecom.schemas.departments import (
CreateDepartmentInfo, CreateDepartmentInfo,
CreateDepartmentParams, CreateDepartmentParams,
DepartmentInfo, DepartmentInfo,
DepartmentInfoItem,
UpdateDepartmentInfo, UpdateDepartmentInfo,
UpdateDepartmentParams, UpdateDepartmentParams,
) )
from service.wecom.utils.requests import HttpxRequest from wecom.utils.requests import HttpxRequest
class WecomDepartmentClient(WecomBaseClient): class WecomDepartmentClient(WecomBaseClient):
async def create_departments(self, data: CreateDepartmentParams) -> int: async def create_departments(self, data: CreateDepartmentParams) -> int:
""" """
创建部门 创建部门
@@ -64,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[DepartmentInfoItem]: async def get_departments(self, id: int = None) -> list[DepartmentInfo]:
""" """
获取部门列表 获取部门列表
@param id: 部门id。获取指定部门及其下的子部门。 @param id: 部门id。获取指定部门及其下的子部门。

View File

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

View File

@@ -1,15 +1,10 @@
from service.wecom.modules.base import WecomBaseClient from wecom.modules.base import WecomBaseClient
from service.wecom.modules.card import WecomCardClient 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(
WecomDepartmentClient, WecomDepartmentClient, WecomUsersClient, WecomMessageClient, WecomBaseClient
WecomUsersClient,
WecomMessageClient,
WecomCardClient,
WecomBaseClient,
): ):
pass pass

View File

@@ -1,13 +1,12 @@
from service.wecom.exceptions.general import SDKException from wecom.exceptions.general import SDKException
from service.wecom.modules.base import WecomBaseClient from wecom.modules.base import WecomBaseClient
from service.wecom.schemas.departments import DepartmentInfo from wecom.schemas.departments import DepartmentInfo
from service.wecom.schemas.users import ( from wecom.schemas.users import (
DepartmentUserDetailInfo, DepartmentUserDetailInfo,
DepartmentUserInfo, DepartmentUserInfo,
UserInfo, UserInfo,
UserSimpleInfo,
) )
from service.wecom.utils.requests import HttpxRequest from wecom.utils.requests import HttpxRequest
class WecomUsersClient(WecomBaseClient): class WecomUsersClient(WecomBaseClient):
@@ -50,7 +49,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) -> list[UserSimpleInfo]: async def get_user_in_department(self, department_id: int) -> dict:
""" """
读取部门成员简要信息 读取部门成员简要信息
@param department_id: 获取的部门id @param department_id: 获取的部门id
@@ -66,7 +65,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.userlist return resp.model_dump(exclude={"errcode", "errmsg"})
else: else:
raise SDKException(resp.errcode, resp.errmsg) raise SDKException(resp.errcode, resp.errmsg)

View File

@@ -1,45 +0,0 @@
import datetime
from enum import Enum
from service.wecom.schemas.base import BaseSchema
class CheckinType(Enum):
"""打卡类型"""
ON_OFF_DUTY = 1 # 上下班打卡
OUTING = 2 # 外出打卡
ALL = 3 # 全部打卡
class GetCardRecordsRequest(BaseSchema):
"""获取打卡记录请求"""
opencheckindatatype: CheckinType
starttime: datetime.datetime
endtime: datetime.datetime
useridlist: list[str]
class GetCardRecord(BaseSchema):
userid: str
groupname: str
checkin_type: str
exception_type: str
checkin_time: datetime.datetime
location_title: str
location_detail: str
wifiname: str
notes: str
wifimac: str
mediaids: list[str]
sch_checkin_time: datetime.datetime
groupid: int
schedule_id: int
timeline_id: int
class GetCardRecordsResponse(BaseSchema):
errcode: int
errmsg: str
checkindata: list[GetCardRecord]

View File

@@ -1,13 +1,13 @@
from typing import List from typing import AnyStr, List
from service.wecom.schemas.base import BaseSchema from wecom.schemas.base import BaseSchema
class CreateDepartmentParams(BaseSchema): class CreateDepartmentParams(BaseSchema):
""" """
创建部门 创建部门
@param name: 部门名称。长度限制为1~32个字节字符不能包括\\:?”<> @param name: 部门名称。长度限制为1~32个字节字符不能包括\:?”<>
@param name_en: 英文名称 @param name_en: 英文名称
@param parentid: 父部门id。根部门id为1 @param parentid: 父部门id。根部门id为1
@param order: 在父部门中的次序值。order值小的排序靠前。 @param order: 在父部门中的次序值。order值小的排序靠前。
@@ -26,23 +26,23 @@ class UpdateDepartmentParams(CreateDepartmentParams): ...
class UpdateDepartmentInfo(BaseSchema): class UpdateDepartmentInfo(BaseSchema):
errcode: int errcode: int
errmsg: str errmsg: AnyStr
class CreateDepartmentInfo(BaseSchema): class CreateDepartmentInfo(BaseSchema):
errcode: int errcode: int
errmsg: str errmsg: AnyStr
id: int id: int
class DepartmentInfoItem(BaseSchema): class DepartmentInfo(BaseSchema):
""" """
部门单体响应数据 部门单体响应数据
""" """
id: int id: int
name: str name: AnyStr
name_en: str | None = None name_en: AnyStr | 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: str errmsg: AnyStr
department: List[DepartmentInfoItem] department: List[DepartmentInfo]

View File

@@ -1,6 +1,6 @@
from typing import Literal from typing import AnyStr, Literal
from service.wecom.schemas.base import BaseSchema from 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: str | None = None touser: AnyStr | None = None
toparty: str | None = None toparty: AnyStr | None = None
totag: str | None = None totag: AnyStr | 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: str errmsg: AnyStr
invaliduser: str | None = None invaliduser: AnyStr | None = None
invalidparty: str | None = None invalidparty: AnyStr | None = None
invalidtag: str | None = None invalidtag: AnyStr | None = None
unlicenseduser: str | None = None unlicenseduser: AnyStr | None = None
msgid: str | None = None msgid: AnyStr | None = None
response_code: str | None = None response_code: AnyStr | None = None
class SendMessageInvalid(BaseSchema): class SendMessageInvalid(BaseSchema):
@@ -70,11 +70,11 @@ class SendMessageInvalid(BaseSchema):
""" """
errmsg: str errmsg: AnyStr
invaliduser: str | None = None invaliduser: AnyStr | None = None
invalidparty: str | None = None invalidparty: AnyStr | None = None
invalidtag: str | None = None invalidtag: AnyStr | None = None
unlicenseduser: str | None = None unlicenseduser: AnyStr | None = None
class RecallMessageParams(BaseSchema): class RecallMessageParams(BaseSchema):
@@ -82,7 +82,7 @@ class RecallMessageParams(BaseSchema):
撤回消息请求参数 撤回消息请求参数
""" """
msgid: str msgid: AnyStr
class RecallMessageInfo(BaseSchema): class RecallMessageInfo(BaseSchema):
@@ -91,4 +91,4 @@ class RecallMessageInfo(BaseSchema):
""" """
errcode: int errcode: int
errmsg: str errmsg: AnyStr

View File

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

View File

@@ -1,6 +1,6 @@
from typing import List from typing import AnyStr, List
from service.wecom.schemas.base import BaseSchema from wecom.schemas.base import BaseSchema
class UserInfo(BaseSchema): class UserInfo(BaseSchema):
@@ -9,31 +9,31 @@ class UserInfo(BaseSchema):
""" """
errcode: int errcode: int
errmsg: str errmsg: AnyStr
userid: str | None = None userid: AnyStr | None = None
name: str | None = None name: AnyStr | None = None
department: List[int] | None = None department: List[int] | None = None
position: str | None = None position: AnyStr | None = None
moblie: str | None = None moblie: AnyStr | None = None
gender: int | None = None gender: int | None = None
email: str | None = None email: AnyStr | None = None
status: int | None = None status: int | None = None
class UserSimpleInfo(BaseSchema): class UserSimpleInfo(BaseSchema):
userid: str userid: AnyStr
name: str name: AnyStr
department: List[int] department: List[int]
open_userid: str | None = None open_userid: AnyStr | None = None
class DepartmentUserInfo(BaseSchema): class DepartmentUserInfo(BaseSchema):
errcode: int errcode: int
errmsg: str errmsg: AnyStr
userlist: List[UserSimpleInfo] userlist: List[UserSimpleInfo]
class DepartmentUserDetailInfo(BaseSchema): class DepartmentUserDetailInfo(BaseSchema):
errcode: int errcode: int
errmsg: str errmsg: AnyStr
userlist: List[UserInfo] userlist: List[UserInfo]

View File

@@ -1,4 +1,9 @@
from .wx_com import wxcpt from .wx_com import wxcpt
from .wx_utils import decrypt_message, extract_message_content, get_request_params from .wx_utils import get_request_params,decrypt_message,extract_message_content
__all__ = ["wxcpt", "get_request_params", "decrypt_message", "extract_message_content"] __all__ = [
"wxcpt",
"get_request_params",
"decrypt_message",
"extract_message_content"
]

View File

@@ -1,5 +1,4 @@
from uvicorn.server import logger from uvicorn.server import logger
from config import Settings from config import Settings
from utils.wxcom.WXBizMsgCrypt3 import WXBizMsgCrypt from utils.wxcom.WXBizMsgCrypt3 import WXBizMsgCrypt
@@ -16,20 +15,20 @@ def get_wxcpt():
required_configs = [ required_configs = [
Settings().WECOM_APP_TOKEN, Settings().WECOM_APP_TOKEN,
Settings().WECOM_APP_ENCODING_AES_KEY, Settings().WECOM_APP_ENCODING_AES_KEY,
Settings().WECOM_CORPID, Settings().WECOM_CORPID
] ]
if not all(required_configs): if not all(required_configs):
raise ValueError("企业微信配置不完整") raise ValueError("企业微信配置不完整")
return WXBizMsgCrypt( return WXBizMsgCrypt(
Settings().WECOM_APP_TOKEN, # 设置的Token Settings().WECOM_APP_TOKEN, # 设置的Token
Settings().WECOM_APP_ENCODING_AES_KEY, # 设置密钥 Settings().WECOM_APP_ENCODING_AES_KEY, # 设置密钥
Settings().WECOM_CORPID, # 企业ID Settings().WECOM_CORPID # 企业ID
) )
except Exception as e: except Exception as e:
logger.error(f"初始化WXBizMsgCrypt失败: {str(e)}") logger.error(f"初始化WXBizMsgCrypt失败: {str(e)}")
raise raise
wxcpt = get_wxcpt() wxcpt = get_wxcpt()

View File

@@ -4,7 +4,6 @@ from typing import Dict, Tuple, Union
import xmltodict import xmltodict
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from uvicorn.server import logger from uvicorn.server import logger
from .wx_com import wxcpt from .wx_com import wxcpt
@@ -24,10 +23,7 @@ async def get_request_params(request: Request) -> Tuple[bytes, str, str, str]:
return body, msg_signature, timestamp, nonce return body, msg_signature, timestamp, nonce
def decrypt_message(body: bytes, msg_signature: str, timestamp: str, nonce: str) -> dict:
def decrypt_message(
body: bytes, msg_signature: str, timestamp: str, nonce: str
) -> dict:
"""解密消息""" """解密消息"""
ret, sMsg = wxcpt.DecryptMsg(body, msg_signature, timestamp, nonce) ret, sMsg = wxcpt.DecryptMsg(body, msg_signature, timestamp, nonce)
if ret != 0: if ret != 0:
@@ -39,7 +35,6 @@ def decrypt_message(
return xml_dict return xml_dict
def extract_message_content( def extract_message_content(
xml_dict: Dict, xml_dict: Dict,
) -> Tuple[str, str, str, str, Union[Dict[str, Union[str, None]], str, None], str, str]: ) -> Tuple[str, str, str, str, Union[Dict[str, Union[str, None]], str, None], str, str]:
@@ -87,6 +82,7 @@ def extract_message_content(
message_data = xml_content.get("Content") message_data = xml_content.get("Content")
logger.info(f"收到未知类型消息: {message_data}") logger.info(f"收到未知类型消息: {message_data}")
return { return {
"ToUserName": to_user_name, "ToUserName": to_user_name,
"FromUserName": from_user_name, "FromUserName": from_user_name,

46
uv.lock generated
View File

@@ -651,15 +651,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
] ]
[[package]]
name = "isort"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" },
]
[[package]] [[package]]
name = "jaraco-classes" name = "jaraco-classes"
version = "3.4.0" version = "3.4.0"
@@ -1668,32 +1659,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
] ]
[[package]]
name = "ruff"
version = "0.14.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" },
{ url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" },
{ url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" },
{ url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" },
{ url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" },
{ url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" },
{ url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" },
{ url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" },
{ url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" },
{ url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" },
{ url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" },
{ url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" },
{ url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" },
{ url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" },
{ url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" },
{ url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" },
{ url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
]
[[package]] [[package]]
name = "secretstorage" name = "secretstorage"
version = "3.5.0" version = "3.5.0"
@@ -2016,12 +1981,6 @@ dependencies = [
{ name = "xmltodict" }, { name = "xmltodict" },
] ]
[package.dev-dependencies]
dev = [
{ name = "isort" },
{ name = "ruff" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "apscheduler", specifier = ">=3.11.0" }, { name = "apscheduler", specifier = ">=3.11.0" },
@@ -2044,10 +2003,7 @@ requires-dist = [
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = []
{ name = "isort", specifier = ">=7.0.0" },
{ name = "ruff", specifier = ">=0.14.11" },
]
[[package]] [[package]]
name = "wrapt" name = "wrapt"