feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

311
backend/app/api/v1/scrm.py Normal file
View File

@@ -0,0 +1,311 @@
"""
SCRM 系统对接 API 路由
提供给 SCRM 系统调用的数据查询接口
认证方式Bearer Token (SCRM_API_KEY)
"""
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_db, verify_scrm_api_key
from app.services.scrm_service import SCRMService
from app.schemas.scrm import (
EmployeePositionResponse,
EmployeePositionData,
PositionCoursesResponse,
PositionCoursesData,
KnowledgePointSearchRequest,
KnowledgePointSearchResponse,
KnowledgePointSearchData,
KnowledgePointDetailResponse,
KnowledgePointDetailData,
SCRMErrorResponse,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/scrm", tags=["scrm"])
# ==================== 1. 获取员工岗位 ====================
@router.get(
"/employees/{userid}/position",
response_model=EmployeePositionResponse,
summary="获取员工岗位通过userid",
description="根据企微 userid 查询员工在考陪练系统中的岗位信息",
responses={
200: {"model": EmployeePositionResponse, "description": "成功"},
401: {"model": SCRMErrorResponse, "description": "认证失败"},
404: {"model": SCRMErrorResponse, "description": "员工不存在"},
}
)
async def get_employee_position_by_userid(
userid: str,
_: bool = Depends(verify_scrm_api_key),
db: AsyncSession = Depends(get_db)
):
"""
获取员工岗位通过企微userid
- **userid**: 企微员工 userid
"""
service = SCRMService(db)
result = await service.get_employee_position(userid=userid)
if result is None:
raise HTTPException(
status_code=404,
detail={
"code": 404,
"message": "员工不存在",
"data": None
}
)
# 检查是否有多个匹配结果
if result.get("multiple_matches"):
return {
"code": 0,
"message": f"找到 {result['count']} 个匹配的员工,请确认",
"data": result
}
return EmployeePositionResponse(
code=0,
message="success",
data=EmployeePositionData(**result)
)
@router.get(
"/employees/search/by-name",
summary="获取员工岗位(通过姓名搜索)",
description="根据员工姓名查询员工在考陪练系统中的岗位信息,支持精确匹配和模糊匹配",
responses={
200: {"description": "成功"},
401: {"model": SCRMErrorResponse, "description": "认证失败"},
404: {"model": SCRMErrorResponse, "description": "员工不存在"},
}
)
async def get_employee_position_by_name(
name: str = Query(..., description="员工姓名,支持精确匹配和模糊匹配"),
_: bool = Depends(verify_scrm_api_key),
db: AsyncSession = Depends(get_db)
):
"""
获取员工岗位(通过姓名搜索)
- **name**: 员工姓名(必填),优先精确匹配,无结果时模糊匹配
注意:如果有多个同名员工,会返回员工列表供确认
"""
service = SCRMService(db)
result = await service.get_employee_position(name=name)
if result is None:
raise HTTPException(
status_code=404,
detail={
"code": 404,
"message": f"未找到姓名包含 '{name}' 的员工",
"data": None
}
)
# 检查是否有多个匹配结果
if result.get("multiple_matches"):
return {
"code": 0,
"message": f"找到 {result['count']} 个匹配的员工,请确认后使用 employee_id 精确查询",
"data": result
}
return EmployeePositionResponse(
code=0,
message="success",
data=EmployeePositionData(**result)
)
@router.get(
"/employees/by-id/{employee_id}/position",
response_model=EmployeePositionResponse,
summary="获取员工岗位通过员工ID",
description="根据员工ID精确查询员工岗位信息用于多个同名员工时的精确查询",
responses={
200: {"model": EmployeePositionResponse, "description": "成功"},
401: {"model": SCRMErrorResponse, "description": "认证失败"},
404: {"model": SCRMErrorResponse, "description": "员工不存在"},
}
)
async def get_employee_position_by_id(
employee_id: int,
_: bool = Depends(verify_scrm_api_key),
db: AsyncSession = Depends(get_db)
):
"""
获取员工岗位通过员工ID精确查询
- **employee_id**: 员工ID考陪练系统用户ID
适用场景:通过姓名搜索返回多个匹配结果后,使用此接口精确查询
"""
service = SCRMService(db)
result = await service.get_employee_position_by_id(employee_id)
if result is None:
raise HTTPException(
status_code=404,
detail={
"code": 404,
"message": "员工不存在",
"data": None
}
)
return EmployeePositionResponse(
code=0,
message="success",
data=EmployeePositionData(**result)
)
# ==================== 2. 获取岗位课程列表 ====================
@router.get(
"/positions/{position_id}/courses",
response_model=PositionCoursesResponse,
summary="获取岗位课程列表",
description="获取指定岗位的必修/选修课程列表",
responses={
200: {"model": PositionCoursesResponse, "description": "成功"},
401: {"model": SCRMErrorResponse, "description": "认证失败"},
404: {"model": SCRMErrorResponse, "description": "岗位不存在"},
}
)
async def get_position_courses(
position_id: int,
course_type: Optional[str] = Query(
default="all",
description="课程类型required/optional/all",
regex="^(required|optional|all)$"
),
_: bool = Depends(verify_scrm_api_key),
db: AsyncSession = Depends(get_db)
):
"""
获取岗位课程列表
- **position_id**: 岗位ID
- **course_type**: 课程类型筛选required/optional/all默认 all
"""
service = SCRMService(db)
result = await service.get_position_courses(position_id, course_type)
if result is None:
raise HTTPException(
status_code=404,
detail={
"code": 40002,
"message": "position_id 不存在",
"data": None
}
)
return PositionCoursesResponse(
code=0,
message="success",
data=PositionCoursesData(**result)
)
# ==================== 3. 搜索知识点 ====================
@router.post(
"/knowledge-points/search",
response_model=KnowledgePointSearchResponse,
summary="搜索知识点",
description="根据关键词和岗位搜索匹配的知识点",
responses={
200: {"model": KnowledgePointSearchResponse, "description": "成功"},
401: {"model": SCRMErrorResponse, "description": "认证失败"},
400: {"model": SCRMErrorResponse, "description": "请求参数错误"},
}
)
async def search_knowledge_points(
request: KnowledgePointSearchRequest,
_: bool = Depends(verify_scrm_api_key),
db: AsyncSession = Depends(get_db)
):
"""
搜索知识点
- **keywords**: 搜索关键词列表(必填)
- **position_id**: 岗位ID用于优先排序可选
- **course_ids**: 限定课程范围(可选)
- **knowledge_type**: 知识点类型筛选(可选)
- **limit**: 返回数量默认10最大100
"""
service = SCRMService(db)
result = await service.search_knowledge_points(
keywords=request.keywords,
position_id=request.position_id,
course_ids=request.course_ids,
knowledge_type=request.knowledge_type,
limit=request.limit
)
return KnowledgePointSearchResponse(
code=0,
message="success",
data=KnowledgePointSearchData(**result)
)
# ==================== 4. 获取知识点详情 ====================
@router.get(
"/knowledge-points/{knowledge_point_id}",
response_model=KnowledgePointDetailResponse,
summary="获取知识点详情",
description="获取知识点的完整信息",
responses={
200: {"model": KnowledgePointDetailResponse, "description": "成功"},
401: {"model": SCRMErrorResponse, "description": "认证失败"},
404: {"model": SCRMErrorResponse, "description": "知识点不存在"},
}
)
async def get_knowledge_point_detail(
knowledge_point_id: int,
_: bool = Depends(verify_scrm_api_key),
db: AsyncSession = Depends(get_db)
):
"""
获取知识点详情
- **knowledge_point_id**: 知识点ID
"""
service = SCRMService(db)
result = await service.get_knowledge_point_detail(knowledge_point_id)
if result is None:
raise HTTPException(
status_code=404,
detail={
"code": 40003,
"message": "knowledge_point_id 不存在",
"data": None
}
)
return KnowledgePointDetailResponse(
code=0,
message="success",
data=KnowledgePointDetailData(**result)
)