Compare commits

..

3 Commits

Author SHA1 Message Date
yuliang_guo
6b7b828854 feat: 添加员工同步立即执行按钮
All checks were successful
continuous-integration/drone/push Build is passing
在系统设置页面的员工同步配置中增加"立即同步"按钮,
允许管理员手动触发钉钉员工数据同步
2026-01-31 17:51:41 +08:00
yuliang_guo
940777a86e fix: 修复员工同步功能开关保存失败的问题
Some checks failed
continuous-integration/drone/push Build is failing
当 feature_switches 表中没有默认记录时,set_feature_switch 函数
现在会使用预定义的默认值创建记录,而不是静默失败
2026-01-31 17:46:35 +08:00
yuliang_guo
41a2f7944a fix: 修复flake8 lint检查错误
All checks were successful
continuous-integration/drone/push Build is passing
- 删除废弃的 admin_positions_backup.py 备份文件
- 修复 courses.py 缺失的 select 导入
- 修复 coze_gateway.py 异常变量作用域问题
- 修复 scheduler_service.py 无用的 global 声明
- 添加 TYPE_CHECKING 导入解决模型前向引用警告
2026-01-31 17:43:39 +08:00
8 changed files with 93 additions and 188 deletions

View File

@@ -1,158 +0,0 @@
# 此文件备份了admin.py中的positions相关路由代码
# 这些路由已移至positions.py为避免冲突从admin.py中移除
@router.get("/positions")
async def list_positions(
keyword: Optional[str] = Query(None, description="关键词"),
page: int = Query(1, ge=1),
pageSize: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
_db: AsyncSession = Depends(get_db),
) -> ResponseModel:
"""
获取岗位列表stub 数据)
返回结构兼容前端data.list/total/page/pageSize
"""
not_admin = _ensure_admin(current_user)
if not_admin:
return not_admin
try:
items = _sample_positions()
if keyword:
kw = keyword.lower()
items = [
p for p in items if kw in (p.get("name", "") + p.get("description", "")).lower()
]
total = len(items)
start = (page - 1) * pageSize
end = start + pageSize
page_items = items[start:end]
return ResponseModel(
code=200,
message="获取岗位列表成功",
data={
"list": page_items,
"total": total,
"page": page,
"pageSize": pageSize,
},
)
except Exception as exc:
# 记录错误堆栈由全局异常中间件处理;此处返回统一结构
return ResponseModel(code=500, message=f"服务器错误:{exc}")
@router.get("/positions/tree")
async def get_position_tree(
current_user: User = Depends(get_current_user),
_db: AsyncSession = Depends(get_db),
) -> ResponseModel:
"""
获取岗位树stub 数据)
"""
not_admin = _ensure_admin(current_user)
if not_admin:
return not_admin
try:
items = _sample_positions()
id_to_node: Dict[int, Dict[str, Any]] = {}
for p in items:
node = {**p, "children": []}
id_to_node[p["id"]] = node
roots: List[Dict[str, Any]] = []
for p in items:
parent_id = p.get("parentId")
if parent_id and parent_id in id_to_node:
id_to_node[parent_id]["children"].append(id_to_node[p["id"]])
else:
roots.append(id_to_node[p["id"]])
return ResponseModel(code=200, message="获取岗位树成功", data=roots)
except Exception as exc:
return ResponseModel(code=500, message=f"服务器错误:{exc}")
@router.get("/positions/{position_id}")
async def get_position_detail(
position_id: int,
current_user: User = Depends(get_current_user),
_db: AsyncSession = Depends(get_db),
) -> ResponseModel:
not_admin = _ensure_admin(current_user)
if not_admin:
return not_admin
items = _sample_positions()
for p in items:
if p["id"] == position_id:
return ResponseModel(code=200, message="获取岗位详情成功", data=p)
return ResponseModel(code=404, message="岗位不存在")
@router.get("/positions/{position_id}/check-delete")
async def check_position_delete(
position_id: int,
current_user: User = Depends(get_current_user),
_db: AsyncSession = Depends(get_db),
) -> ResponseModel:
not_admin = _ensure_admin(current_user)
if not_admin:
return not_admin
# stub允许删除非根岗位
deletable = position_id != 1
reason = "根岗位不允许删除" if not deletable else ""
return ResponseModel(code=200, message="检查成功", data={"deletable": deletable, "reason": reason})
@router.post("/positions")
async def create_position(
payload: Dict[str, Any],
current_user: User = Depends(get_current_user),
_db: AsyncSession = Depends(get_db),
) -> ResponseModel:
not_admin = _ensure_admin(current_user)
if not_admin:
return not_admin
# stub直接回显并附带一个伪ID
payload = dict(payload)
payload.setdefault("id", 999)
payload.setdefault("createTime", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
return ResponseModel(code=200, message="创建岗位成功", data=payload)
@router.put("/positions/{position_id}")
async def update_position(
position_id: int,
payload: Dict[str, Any],
current_user: User = Depends(get_current_user),
_db: AsyncSession = Depends(get_db),
) -> ResponseModel:
not_admin = _ensure_admin(current_user)
if not_admin:
return not_admin
# stub直接回显
updated = {"id": position_id, **payload}
return ResponseModel(code=200, message="更新岗位成功", data=updated)
@router.delete("/positions/{position_id}")
async def delete_position(
position_id: int,
current_user: User = Depends(get_current_user),
_db: AsyncSession = Depends(get_db),
) -> ResponseModel:
not_admin = _ensure_admin(current_user)
if not_admin:
return not_admin
# stub直接返回成功
return ResponseModel(code=200, message="删除岗位成功", data={"id": position_id})

View File

@@ -4,6 +4,7 @@
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends, Query, status, BackgroundTasks, Request from fastapi import APIRouter, Depends, Query, status, BackgroundTasks, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_db, get_current_user, require_admin, require_admin_or_manager, User from app.core.deps import get_db, get_current_user, require_admin, require_admin_or_manager, User

View File

@@ -203,25 +203,29 @@ async def send_message(request: SendMessageRequest, user=Depends(get_current_use
}, },
} }
except CozeException as e: except CozeException as coze_err:
logger.error(f"发送消息失败: {e}") logger.error(f"发送消息失败: {coze_err}")
if request.stream: if request.stream:
# 流式响应的错误处理 # 流式响应的错误处理 - 捕获异常信息避免闭包问题
err_code = coze_err.code
err_message = coze_err.message
err_details = coze_err.details
async def error_generator(): async def error_generator():
yield { yield {
"event": "error", "event": "error",
"data": { "data": {
"code": e.code, "code": err_code,
"message": e.message, "message": err_message,
"details": e.details, "details": err_details,
}, },
} }
return EventSourceResponse(error_generator()) return EventSourceResponse(error_generator())
else: else:
raise HTTPException( raise HTTPException(
status_code=e.status_code or 500, status_code=coze_err.status_code or 500,
detail={"code": e.code, "message": e.message, "details": e.details}, detail={"code": err_code, "message": err_message, "details": err_details},
) )
except Exception as e: except Exception as e:
logger.error(f"未知错误: {e}", exc_info=True) logger.error(f"未知错误: {e}", exc_info=True)

View File

@@ -168,7 +168,26 @@ async def set_feature_switch(db: AsyncSession, tenant_id: int, feature_code: str
) )
default_row = result.fetchone() default_row = result.fetchone()
# 定义功能开关默认值映射
FEATURE_DEFAULTS = {
'employee_sync': ('员工同步', 'system', '从钉钉同步员工信息'),
'dingtalk_login': ('钉钉免密登录', 'system', '允许通过钉钉免密登录'),
'ai_practice': ('AI 对练', 'feature', 'AI 对练功能'),
'dual_practice': ('双人对练', 'feature', '双人对练功能'),
}
if default_row: if default_row:
feature_name = default_row[0]
feature_group = default_row[1]
description = default_row[2]
elif feature_code in FEATURE_DEFAULTS:
feature_name, feature_group, description = FEATURE_DEFAULTS[feature_code]
else:
# 未知功能码,使用默认值
feature_name = feature_code
feature_group = 'system'
description = f'{feature_code} 功能开关'
# 插入租户级配置 # 插入租户级配置
await db.execute( await db.execute(
text(""" text("""
@@ -178,10 +197,10 @@ async def set_feature_switch(db: AsyncSession, tenant_id: int, feature_code: str
{ {
"tenant_id": tenant_id, "tenant_id": tenant_id,
"feature_code": feature_code, "feature_code": feature_code,
"feature_name": default_row[0], "feature_name": feature_name,
"feature_group": default_row[1], "feature_group": feature_group,
"is_enabled": 1 if is_enabled else 0, "is_enabled": 1 if is_enabled else 0,
"description": default_row[2] "description": description
} }
) )

View File

@@ -2,9 +2,12 @@
课程相关数据库模型 课程相关数据库模型
""" """
from enum import Enum from enum import Enum
from typing import List, Optional from typing import List, Optional, TYPE_CHECKING
from datetime import datetime from datetime import datetime
if TYPE_CHECKING:
from app.models.growth_path import GrowthPathNode
from sqlalchemy import ( from sqlalchemy import (
String, String,
Text, Text,

View File

@@ -2,10 +2,14 @@
成长路径相关数据库模型 成长路径相关数据库模型
""" """
from enum import Enum from enum import Enum
from typing import List, Optional from typing import List, Optional, TYPE_CHECKING
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
if TYPE_CHECKING:
from app.models.course import Course
from app.models.user import User
from sqlalchemy import ( from sqlalchemy import (
String, String,
Text, Text,
@@ -84,7 +88,7 @@ class GrowthPathNode(BaseModel, SoftDeleteMixin):
) )
# 关联关系 # 关联关系
growth_path: Mapped["GrowthPath"] = relationship( growth_path: Mapped["GrowthPath"] = relationship( # noqa: F821
"GrowthPath", back_populates="nodes" "GrowthPath", back_populates="nodes"
) )
course: Mapped["Course"] = relationship("Course") course: Mapped["Course"] = relationship("Course")
@@ -146,7 +150,7 @@ class UserGrowthPathProgress(BaseModel):
# 关联关系 # 关联关系
user: Mapped["User"] = relationship("User") user: Mapped["User"] = relationship("User")
growth_path: Mapped["GrowthPath"] = relationship("GrowthPath") growth_path: Mapped["GrowthPath"] = relationship("GrowthPath") # noqa: F821
class UserNodeCompletion(BaseModel): class UserNodeCompletion(BaseModel):
@@ -203,4 +207,4 @@ class UserNodeCompletion(BaseModel):
node: Mapped["GrowthPathNode"] = relationship( node: Mapped["GrowthPathNode"] = relationship(
"GrowthPathNode", back_populates="user_completions" "GrowthPathNode", back_populates="user_completions"
) )
growth_path: Mapped["GrowthPath"] = relationship("GrowthPath") growth_path: Mapped["GrowthPath"] = relationship("GrowthPath") # noqa: F821

View File

@@ -261,8 +261,6 @@ def start_scheduler():
def stop_scheduler(): def stop_scheduler():
"""停止调度器""" """停止调度器"""
global scheduler
if scheduler and scheduler.running: if scheduler and scheduler.running:
scheduler.shutdown() scheduler.shutdown()
logger.info("定时任务调度器已停止") logger.info("定时任务调度器已停止")

View File

@@ -140,6 +140,14 @@
<el-button @click="testSyncConnection" :loading="syncTesting" :disabled="!syncForm.configured"> <el-button @click="testSyncConnection" :loading="syncTesting" :disabled="!syncForm.configured">
测试连接 测试连接
</el-button> </el-button>
<el-button
type="success"
@click="triggerSync"
:loading="syncing"
:disabled="!syncForm.configured"
>
立即同步
</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
@@ -164,6 +172,7 @@ const dingtalkFormRef = ref<FormInstance>()
const syncLoading = ref(false) const syncLoading = ref(false)
const syncSaving = ref(false) const syncSaving = ref(false)
const syncTesting = ref(false) const syncTesting = ref(false)
const syncing = ref(false)
const syncFormRef = ref<FormInstance>() const syncFormRef = ref<FormInstance>()
// 钉钉配置表单 // 钉钉配置表单
@@ -327,6 +336,31 @@ const testSyncConnection = async () => {
} }
} }
/**
* 立即执行员工同步
*/
const triggerSync = async () => {
syncing.value = true
try {
const response = await request.post('/api/v1/employee-sync/sync')
if (response.success) {
const data = response.data
ElMessage.success(
`同步完成!共处理 ${data.total_employees || 0} 名员工,` +
`创建 ${data.users_created || 0} 个账号,` +
`跳过 ${data.users_skipped || 0}`
)
} else {
ElMessage.error(response.message || '同步失败')
}
} catch (error: any) {
console.error('员工同步失败:', error)
ElMessage.error(error?.response?.data?.detail || '员工同步失败')
} finally {
syncing.value = false
}
}
// 页面加载时获取配置 // 页面加载时获取配置
onMounted(() => { onMounted(() => {
loadDingtalkConfig() loadDingtalkConfig()