Compare commits
3 Commits
18d6d5aff3
...
6b7b828854
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b7b828854 | ||
|
|
940777a86e | ||
|
|
41a2f7944a |
@@ -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})
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -168,22 +168,41 @@ 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]
|
||||||
await db.execute(
|
feature_group = default_row[1]
|
||||||
text("""
|
description = default_row[2]
|
||||||
INSERT INTO feature_switches (tenant_id, feature_code, feature_name, feature_group, is_enabled, description)
|
elif feature_code in FEATURE_DEFAULTS:
|
||||||
VALUES (:tenant_id, :feature_code, :feature_name, :feature_group, :is_enabled, :description)
|
feature_name, feature_group, description = FEATURE_DEFAULTS[feature_code]
|
||||||
"""),
|
else:
|
||||||
{
|
# 未知功能码,使用默认值
|
||||||
"tenant_id": tenant_id,
|
feature_name = feature_code
|
||||||
"feature_code": feature_code,
|
feature_group = 'system'
|
||||||
"feature_name": default_row[0],
|
description = f'{feature_code} 功能开关'
|
||||||
"feature_group": default_row[1],
|
|
||||||
"is_enabled": 1 if is_enabled else 0,
|
# 插入租户级配置
|
||||||
"description": default_row[2]
|
await db.execute(
|
||||||
}
|
text("""
|
||||||
)
|
INSERT INTO feature_switches (tenant_id, feature_code, feature_name, feature_group, is_enabled, description)
|
||||||
|
VALUES (:tenant_id, :feature_code, :feature_name, :feature_group, :is_enabled, :description)
|
||||||
|
"""),
|
||||||
|
{
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"feature_code": feature_code,
|
||||||
|
"feature_name": feature_name,
|
||||||
|
"feature_group": feature_group,
|
||||||
|
"is_enabled": 1 if is_enabled else 0,
|
||||||
|
"description": description
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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("定时任务调度器已停止")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user