Compare commits
10 Commits
6b7b828854
...
cf71fabef0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf71fabef0 | ||
|
|
99c4ac5473 | ||
|
|
616bb7185e | ||
|
|
e942a9de2c | ||
|
|
586c51955e | ||
|
|
ebf196686f | ||
|
|
fc9775e61f | ||
|
|
eca0ed8c9d | ||
|
|
506e9ea2e2 | ||
|
|
e5dd6f3acb |
@@ -43,13 +43,20 @@ async def create_task(
|
|||||||
|
|
||||||
# 构建响应
|
# 构建响应
|
||||||
courses = [link.course.name for link in task.course_links]
|
courses = [link.course.name for link in task.course_links]
|
||||||
|
# 安全获取枚举值(兼容字符串和枚举类型)
|
||||||
|
priority_val = task.priority.value if hasattr(task.priority, 'value') else task.priority
|
||||||
|
status_val = task.status.value if hasattr(task.status, 'value') else task.status
|
||||||
|
completed_count = sum(
|
||||||
|
1 for a in task.assignments
|
||||||
|
if (a.status.value if hasattr(a.status, 'value') else a.status) == "completed"
|
||||||
|
)
|
||||||
return ResponseModel(
|
return ResponseModel(
|
||||||
data=TaskResponse(
|
data=TaskResponse(
|
||||||
id=task.id,
|
id=task.id,
|
||||||
title=task.title,
|
title=task.title,
|
||||||
description=task.description,
|
description=task.description,
|
||||||
priority=task.priority.value,
|
priority=priority_val,
|
||||||
status=task.status.value,
|
status=status_val,
|
||||||
creator_id=task.creator_id,
|
creator_id=task.creator_id,
|
||||||
deadline=task.deadline,
|
deadline=task.deadline,
|
||||||
requirements=task.requirements,
|
requirements=task.requirements,
|
||||||
@@ -58,7 +65,7 @@ async def create_task(
|
|||||||
updated_at=task.updated_at,
|
updated_at=task.updated_at,
|
||||||
courses=courses,
|
courses=courses,
|
||||||
assigned_count=len(task.assignments),
|
assigned_count=len(task.assignments),
|
||||||
completed_count=sum(1 for a in task.assignments if a.status.value == "completed")
|
completed_count=completed_count
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,7 +74,7 @@ async def create_task(
|
|||||||
async def get_tasks(
|
async def get_tasks(
|
||||||
status: Optional[str] = Query(None, description="任务状态筛选"),
|
status: Optional[str] = Query(None, description="任务状态筛选"),
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=100),
|
page_size: int = Query(20, ge=1, le=500),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(require_admin_or_manager)
|
current_user: User = Depends(require_admin_or_manager)
|
||||||
):
|
):
|
||||||
@@ -77,26 +84,33 @@ async def get_tasks(
|
|||||||
# 构建响应
|
# 构建响应
|
||||||
items = []
|
items = []
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
# 加载关联数据
|
# 安全获取枚举值
|
||||||
task_detail = await task_service.get_task_detail(db, task.id)
|
priority_val = task.priority.value if hasattr(task.priority, 'value') else task.priority
|
||||||
if task_detail:
|
status_val = task.status.value if hasattr(task.status, 'value') else task.status
|
||||||
courses = [link.course.name for link in task_detail.course_links]
|
|
||||||
items.append(TaskResponse(
|
# 使用已加载的关联数据(通过 selectinload)
|
||||||
id=task.id,
|
courses = [link.course.name for link in task.course_links] if task.course_links else []
|
||||||
title=task.title,
|
completed_count = sum(
|
||||||
description=task.description,
|
1 for a in task.assignments
|
||||||
priority=task.priority.value,
|
if (a.status.value if hasattr(a.status, 'value') else a.status) == "completed"
|
||||||
status=task.status.value,
|
) if task.assignments else 0
|
||||||
creator_id=task.creator_id,
|
|
||||||
deadline=task.deadline,
|
items.append(TaskResponse(
|
||||||
requirements=task.requirements,
|
id=task.id,
|
||||||
progress=task.progress,
|
title=task.title,
|
||||||
created_at=task.created_at,
|
description=task.description,
|
||||||
updated_at=task.updated_at,
|
priority=priority_val,
|
||||||
courses=courses,
|
status=status_val,
|
||||||
assigned_count=len(task_detail.assignments),
|
creator_id=task.creator_id,
|
||||||
completed_count=sum(1 for a in task_detail.assignments if a.status.value == "completed")
|
deadline=task.deadline,
|
||||||
))
|
requirements=task.requirements,
|
||||||
|
progress=task.progress,
|
||||||
|
created_at=task.created_at,
|
||||||
|
updated_at=task.updated_at,
|
||||||
|
courses=courses,
|
||||||
|
assigned_count=len(task.assignments) if task.assignments else 0,
|
||||||
|
completed_count=completed_count
|
||||||
|
))
|
||||||
|
|
||||||
return ResponseModel(
|
return ResponseModel(
|
||||||
data=PaginatedResponse.create(
|
data=PaginatedResponse.create(
|
||||||
|
|||||||
@@ -28,10 +28,17 @@ async def get_accessible_team_member_ids(
|
|||||||
current_user: User,
|
current_user: User,
|
||||||
db: AsyncSession
|
db: AsyncSession
|
||||||
) -> List[int]:
|
) -> List[int]:
|
||||||
"""获取用户可访问的团队成员ID列表"""
|
"""获取用户可访问的团队成员ID列表(只返回未删除的用户)"""
|
||||||
if current_user.role in ['admin', 'manager']:
|
if current_user.role in ['admin', 'manager']:
|
||||||
# 管理员查看所有团队成员
|
# 管理员查看所有团队成员(过滤已删除用户)
|
||||||
stmt = select(UserTeam.user_id).distinct()
|
stmt = select(UserTeam.user_id).join(
|
||||||
|
User, UserTeam.user_id == User.id
|
||||||
|
).where(
|
||||||
|
and_(
|
||||||
|
User.is_deleted == False, # noqa: E712
|
||||||
|
User.is_active == True # noqa: E712
|
||||||
|
)
|
||||||
|
).distinct()
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
return [row[0] for row in result.all()]
|
return [row[0] for row in result.all()]
|
||||||
else:
|
else:
|
||||||
@@ -44,9 +51,15 @@ async def get_accessible_team_member_ids(
|
|||||||
if not team_ids:
|
if not team_ids:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# 2. 查询这些团队的所有成员
|
# 2. 查询这些团队的所有成员(过滤已删除用户)
|
||||||
stmt = select(UserTeam.user_id).where(
|
stmt = select(UserTeam.user_id).join(
|
||||||
UserTeam.team_id.in_(team_ids)
|
User, UserTeam.user_id == User.id
|
||||||
|
).where(
|
||||||
|
and_(
|
||||||
|
UserTeam.team_id.in_(team_ids),
|
||||||
|
User.is_deleted == False, # noqa: E712
|
||||||
|
User.is_active == True # noqa: E712
|
||||||
|
)
|
||||||
).distinct()
|
).distinct()
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
return [row[0] for row in result.all()]
|
return [row[0] for row in result.all()]
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class PaginationParams(BaseModel):
|
|||||||
"""分页参数"""
|
"""分页参数"""
|
||||||
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
page: int = Field(default=1, ge=1, description="页码")
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
page_size: int = Field(default=20, ge=1, le=500, description="每页数量")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def offset(self) -> int:
|
def offset(self) -> int:
|
||||||
|
|||||||
@@ -42,101 +42,164 @@ class DashboardService:
|
|||||||
Returns:
|
Returns:
|
||||||
企业级数据概览
|
企业级数据概览
|
||||||
"""
|
"""
|
||||||
today = date.today()
|
try:
|
||||||
week_ago = today - timedelta(days=7)
|
today = date.today()
|
||||||
month_ago = today - timedelta(days=30)
|
week_ago = today - timedelta(days=7)
|
||||||
|
month_ago = today - timedelta(days=30)
|
||||||
|
|
||||||
# 基础统计
|
# 基础统计
|
||||||
# 1. 总学员数
|
# 1. 总学员数
|
||||||
result = await self.db.execute(
|
result = await self.db.execute(
|
||||||
select(func.count(User.id))
|
select(func.count(User.id))
|
||||||
.where(User.is_deleted == False, User.role == 'trainee')
|
.where(User.is_deleted == False, User.role == 'trainee')
|
||||||
)
|
|
||||||
total_users = result.scalar() or 0
|
|
||||||
|
|
||||||
# 2. 今日活跃用户(有经验值记录)
|
|
||||||
result = await self.db.execute(
|
|
||||||
select(func.count(func.distinct(ExpHistory.user_id)))
|
|
||||||
.where(func.date(ExpHistory.created_at) == today)
|
|
||||||
)
|
|
||||||
today_active = result.scalar() or 0
|
|
||||||
|
|
||||||
# 3. 本周活跃用户
|
|
||||||
result = await self.db.execute(
|
|
||||||
select(func.count(func.distinct(ExpHistory.user_id)))
|
|
||||||
.where(ExpHistory.created_at >= datetime.combine(week_ago, datetime.min.time()))
|
|
||||||
)
|
|
||||||
week_active = result.scalar() or 0
|
|
||||||
|
|
||||||
# 4. 本月活跃用户
|
|
||||||
result = await self.db.execute(
|
|
||||||
select(func.count(func.distinct(ExpHistory.user_id)))
|
|
||||||
.where(ExpHistory.created_at >= datetime.combine(month_ago, datetime.min.time()))
|
|
||||||
)
|
|
||||||
month_active = result.scalar() or 0
|
|
||||||
|
|
||||||
# 5. 总学习时长(小时)
|
|
||||||
result = await self.db.execute(
|
|
||||||
select(func.coalesce(func.sum(PracticeSession.duration_seconds), 0))
|
|
||||||
.where(PracticeSession.status == 'completed')
|
|
||||||
)
|
|
||||||
practice_hours = (result.scalar() or 0) / 3600
|
|
||||||
|
|
||||||
result = await self.db.execute(
|
|
||||||
select(func.coalesce(func.sum(TrainingSession.duration_seconds), 0))
|
|
||||||
.where(TrainingSession.status == 'COMPLETED')
|
|
||||||
)
|
|
||||||
training_hours = (result.scalar() or 0) / 3600
|
|
||||||
|
|
||||||
total_hours = round(practice_hours + training_hours, 1)
|
|
||||||
|
|
||||||
# 6. 考试统计
|
|
||||||
result = await self.db.execute(
|
|
||||||
select(
|
|
||||||
func.count(Exam.id),
|
|
||||||
func.count(case((Exam.is_passed == True, 1))),
|
|
||||||
func.avg(Exam.score)
|
|
||||||
)
|
)
|
||||||
.where(Exam.status == 'submitted')
|
total_users = result.scalar() or 0
|
||||||
)
|
|
||||||
exam_row = result.first()
|
|
||||||
exam_count = exam_row[0] or 0
|
|
||||||
exam_passed = exam_row[1] or 0
|
|
||||||
exam_avg_score = round(exam_row[2] or 0, 1)
|
|
||||||
exam_pass_rate = round(exam_passed / exam_count * 100, 1) if exam_count > 0 else 0
|
|
||||||
|
|
||||||
# 7. 满分人数
|
# 2. 今日活跃用户(有经验值记录)
|
||||||
result = await self.db.execute(
|
try:
|
||||||
select(func.count(func.distinct(Exam.user_id)))
|
result = await self.db.execute(
|
||||||
.where(Exam.status == 'submitted', Exam.score >= Exam.total_score)
|
select(func.count(func.distinct(ExpHistory.user_id)))
|
||||||
)
|
.where(func.date(ExpHistory.created_at) == today)
|
||||||
perfect_users = result.scalar() or 0
|
)
|
||||||
|
today_active = result.scalar() or 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取今日活跃用户失败: {e}")
|
||||||
|
today_active = 0
|
||||||
|
|
||||||
# 8. 签到率(今日签到人数/总用户数)
|
# 3. 本周活跃用户
|
||||||
result = await self.db.execute(
|
try:
|
||||||
select(func.count(UserLevel.id))
|
result = await self.db.execute(
|
||||||
.where(func.date(UserLevel.last_login_date) == today)
|
select(func.count(func.distinct(ExpHistory.user_id)))
|
||||||
)
|
.where(ExpHistory.created_at >= datetime.combine(week_ago, datetime.min.time()))
|
||||||
today_checkin = result.scalar() or 0
|
)
|
||||||
checkin_rate = round(today_checkin / total_users * 100, 1) if total_users > 0 else 0
|
week_active = result.scalar() or 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取本周活跃用户失败: {e}")
|
||||||
|
week_active = 0
|
||||||
|
|
||||||
return {
|
# 4. 本月活跃用户
|
||||||
"overview": {
|
try:
|
||||||
"total_users": total_users,
|
result = await self.db.execute(
|
||||||
"today_active": today_active,
|
select(func.count(func.distinct(ExpHistory.user_id)))
|
||||||
"week_active": week_active,
|
.where(ExpHistory.created_at >= datetime.combine(month_ago, datetime.min.time()))
|
||||||
"month_active": month_active,
|
)
|
||||||
"total_hours": total_hours,
|
month_active = result.scalar() or 0
|
||||||
"checkin_rate": checkin_rate,
|
except Exception as e:
|
||||||
},
|
logger.warning(f"获取本月活跃用户失败: {e}")
|
||||||
"exam": {
|
month_active = 0
|
||||||
"total_count": exam_count,
|
|
||||||
"pass_rate": exam_pass_rate,
|
# 5. 总学习时长(小时)
|
||||||
"avg_score": exam_avg_score,
|
practice_hours = 0
|
||||||
"perfect_users": perfect_users,
|
training_hours = 0
|
||||||
},
|
try:
|
||||||
"updated_at": datetime.now().isoformat()
|
result = await self.db.execute(
|
||||||
}
|
select(func.coalesce(func.sum(PracticeSession.duration_seconds), 0))
|
||||||
|
.where(PracticeSession.status == 'completed')
|
||||||
|
)
|
||||||
|
practice_hours = (result.scalar() or 0) / 3600
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取陪练时长失败: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(func.coalesce(func.sum(TrainingSession.duration_seconds), 0))
|
||||||
|
.where(TrainingSession.status == 'COMPLETED')
|
||||||
|
)
|
||||||
|
training_hours = (result.scalar() or 0) / 3600
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取培训时长失败: {e}")
|
||||||
|
|
||||||
|
total_hours = round(practice_hours + training_hours, 1)
|
||||||
|
|
||||||
|
# 6. 考试统计
|
||||||
|
exam_count = 0
|
||||||
|
exam_passed = 0
|
||||||
|
exam_avg_score = 0
|
||||||
|
try:
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(
|
||||||
|
func.count(Exam.id),
|
||||||
|
func.count(case((Exam.is_passed == True, 1))),
|
||||||
|
func.avg(Exam.score)
|
||||||
|
)
|
||||||
|
.where(Exam.status == 'submitted')
|
||||||
|
)
|
||||||
|
exam_row = result.first()
|
||||||
|
if exam_row:
|
||||||
|
exam_count = exam_row[0] or 0
|
||||||
|
exam_passed = exam_row[1] or 0
|
||||||
|
exam_avg_score = round(exam_row[2] or 0, 1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取考试统计失败: {e}")
|
||||||
|
|
||||||
|
exam_pass_rate = round(exam_passed / exam_count * 100, 1) if exam_count > 0 else 0
|
||||||
|
|
||||||
|
# 7. 满分人数
|
||||||
|
perfect_users = 0
|
||||||
|
try:
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(func.count(func.distinct(Exam.user_id)))
|
||||||
|
.where(
|
||||||
|
Exam.status == 'submitted',
|
||||||
|
Exam.score.isnot(None),
|
||||||
|
Exam.total_score.isnot(None),
|
||||||
|
Exam.score >= Exam.total_score
|
||||||
|
)
|
||||||
|
)
|
||||||
|
perfect_users = result.scalar() or 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取满分人数失败: {e}")
|
||||||
|
|
||||||
|
# 8. 签到率(今日签到人数/总用户数)
|
||||||
|
today_checkin = 0
|
||||||
|
try:
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(func.count(UserLevel.id))
|
||||||
|
.where(UserLevel.last_login_date == today)
|
||||||
|
)
|
||||||
|
today_checkin = result.scalar() or 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取签到率失败: {e}")
|
||||||
|
|
||||||
|
checkin_rate = round(today_checkin / total_users * 100, 1) if total_users > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"overview": {
|
||||||
|
"total_users": total_users,
|
||||||
|
"today_active": today_active,
|
||||||
|
"week_active": week_active,
|
||||||
|
"month_active": month_active,
|
||||||
|
"total_hours": total_hours,
|
||||||
|
"checkin_rate": checkin_rate,
|
||||||
|
},
|
||||||
|
"exam": {
|
||||||
|
"total_count": exam_count,
|
||||||
|
"pass_rate": exam_pass_rate,
|
||||||
|
"avg_score": exam_avg_score,
|
||||||
|
"perfect_users": perfect_users,
|
||||||
|
},
|
||||||
|
"updated_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取企业概览失败: {e}")
|
||||||
|
# 返回默认数据而不是抛出异常
|
||||||
|
return {
|
||||||
|
"overview": {
|
||||||
|
"total_users": 0,
|
||||||
|
"today_active": 0,
|
||||||
|
"week_active": 0,
|
||||||
|
"month_active": 0,
|
||||||
|
"total_hours": 0,
|
||||||
|
"checkin_rate": 0,
|
||||||
|
},
|
||||||
|
"exam": {
|
||||||
|
"total_count": 0,
|
||||||
|
"pass_rate": 0,
|
||||||
|
"avg_score": 0,
|
||||||
|
"perfect_users": 0,
|
||||||
|
},
|
||||||
|
"updated_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
async def get_department_comparison(self) -> List[Dict[str, Any]]:
|
async def get_department_comparison(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -313,35 +376,40 @@ class DashboardService:
|
|||||||
"""
|
"""
|
||||||
activities = []
|
activities = []
|
||||||
|
|
||||||
# 获取最近的经验值记录
|
try:
|
||||||
result = await self.db.execute(
|
# 获取最近的经验值记录
|
||||||
select(ExpHistory, User)
|
result = await self.db.execute(
|
||||||
.join(User, ExpHistory.user_id == User.id)
|
select(ExpHistory, User)
|
||||||
.order_by(ExpHistory.created_at.desc())
|
.join(User, ExpHistory.user_id == User.id)
|
||||||
.limit(limit)
|
.order_by(ExpHistory.created_at.desc())
|
||||||
)
|
.limit(limit)
|
||||||
rows = result.all()
|
)
|
||||||
|
rows = result.all()
|
||||||
|
|
||||||
for exp, user in rows:
|
for exp, user in rows:
|
||||||
activity_type = "学习"
|
activity_type = "学习"
|
||||||
if "考试" in (exp.description or ""):
|
description = exp.description or ""
|
||||||
activity_type = "考试"
|
if "考试" in description:
|
||||||
elif "签到" in (exp.description or ""):
|
activity_type = "考试"
|
||||||
activity_type = "签到"
|
elif "签到" in description:
|
||||||
elif "陪练" in (exp.description or ""):
|
activity_type = "签到"
|
||||||
activity_type = "陪练"
|
elif "陪练" in description:
|
||||||
elif "奖章" in (exp.description or ""):
|
activity_type = "陪练"
|
||||||
activity_type = "奖章"
|
elif "奖章" in description:
|
||||||
|
activity_type = "奖章"
|
||||||
|
|
||||||
activities.append({
|
activities.append({
|
||||||
"id": exp.id,
|
"id": exp.id,
|
||||||
"user_id": user.id,
|
"user_id": user.id,
|
||||||
"user_name": user.full_name or user.username,
|
"user_name": user.full_name or user.username,
|
||||||
"type": activity_type,
|
"type": activity_type,
|
||||||
"description": exp.description,
|
"description": description,
|
||||||
"exp_amount": exp.exp_amount,
|
"exp_amount": exp.exp_change, # 修复: exp_change 而非 exp_amount
|
||||||
"created_at": exp.created_at.isoformat() if exp.created_at else None,
|
"created_at": exp.created_at.isoformat() if exp.created_at else None,
|
||||||
})
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取实时动态失败: {e}")
|
||||||
|
# 返回空列表而不是抛出异常
|
||||||
|
|
||||||
return activities
|
return activities
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ class EmployeeSyncService:
|
|||||||
logger.info(f"创建岗位: {position_name} (ID: {position.id})")
|
logger.info(f"创建岗位: {position_name} (ID: {position.id})")
|
||||||
return position
|
return position
|
||||||
|
|
||||||
async def create_user(self, employee_data: Dict[str, Any]) -> Optional[User]:
|
async def create_user(self, employee_data: Dict[str, Any]) -> Tuple[Optional[User], str]:
|
||||||
"""
|
"""
|
||||||
创建用户
|
创建用户
|
||||||
|
|
||||||
@@ -246,14 +246,14 @@ class EmployeeSyncService:
|
|||||||
employee_data: 员工数据
|
employee_data: 员工数据
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
用户对象或None(如果创建失败)
|
(用户对象, 状态): 状态为 'created'/'existing'/'restored'/'skipped'
|
||||||
"""
|
"""
|
||||||
phone = employee_data.get('phone')
|
phone = employee_data.get('phone')
|
||||||
full_name = employee_data.get('full_name')
|
full_name = employee_data.get('full_name')
|
||||||
|
|
||||||
if not phone:
|
if not phone:
|
||||||
logger.warning(f"员工 {full_name} 没有手机号,跳过")
|
logger.warning(f"员工 {full_name} 没有手机号,跳过")
|
||||||
return None
|
return None, 'skipped'
|
||||||
|
|
||||||
# 检查用户是否已存在(通过手机号,包括已软删除的)
|
# 检查用户是否已存在(通过手机号,包括已软删除的)
|
||||||
stmt = select(User).where(User.phone == phone)
|
stmt = select(User).where(User.phone == phone)
|
||||||
@@ -270,15 +270,15 @@ class EmployeeSyncService:
|
|||||||
if dingtalk_id:
|
if dingtalk_id:
|
||||||
existing_user.dingtalk_id = dingtalk_id
|
existing_user.dingtalk_id = dingtalk_id
|
||||||
logger.info(f"恢复软删除用户: {phone} ({full_name})")
|
logger.info(f"恢复软删除用户: {phone} ({full_name})")
|
||||||
return existing_user
|
return existing_user, 'restored'
|
||||||
|
|
||||||
# 如果用户已存在但没有dingtalk_id,则更新
|
# 如果用户已存在但没有dingtalk_id,则更新
|
||||||
dingtalk_id = employee_data.get('dingtalk_id')
|
dingtalk_id = employee_data.get('dingtalk_id')
|
||||||
if dingtalk_id and not existing_user.dingtalk_id:
|
if dingtalk_id and not existing_user.dingtalk_id:
|
||||||
existing_user.dingtalk_id = dingtalk_id
|
existing_user.dingtalk_id = dingtalk_id
|
||||||
logger.info(f"更新用户 {phone} 的钉钉ID: {dingtalk_id}")
|
logger.info(f"更新用户 {phone} 的钉钉ID: {dingtalk_id}")
|
||||||
logger.info(f"用户已存在: {phone} ({full_name})")
|
logger.debug(f"用户已存在: {phone} ({full_name})")
|
||||||
return existing_user
|
return existing_user, 'existing'
|
||||||
|
|
||||||
# 生成邮箱
|
# 生成邮箱
|
||||||
email = self.generate_email(phone, employee_data.get('email'))
|
email = self.generate_email(phone, employee_data.get('email'))
|
||||||
@@ -315,7 +315,7 @@ class EmployeeSyncService:
|
|||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
|
|
||||||
logger.info(f"创建用户: {phone} ({full_name}) - 角色: {role}")
|
logger.info(f"创建用户: {phone} ({full_name}) - 角色: {role}")
|
||||||
return user
|
return user, 'created'
|
||||||
|
|
||||||
async def sync_employees(self) -> Dict[str, Any]:
|
async def sync_employees(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -331,6 +331,9 @@ class EmployeeSyncService:
|
|||||||
stats = {
|
stats = {
|
||||||
'total_employees': 0,
|
'total_employees': 0,
|
||||||
'users_created': 0,
|
'users_created': 0,
|
||||||
|
'users_existing': 0,
|
||||||
|
'users_restored': 0,
|
||||||
|
'users_departed': 0,
|
||||||
'users_skipped': 0,
|
'users_skipped': 0,
|
||||||
'teams_created': 0,
|
'teams_created': 0,
|
||||||
'positions_created': 0,
|
'positions_created': 0,
|
||||||
@@ -351,12 +354,18 @@ class EmployeeSyncService:
|
|||||||
for employee in employees:
|
for employee in employees:
|
||||||
try:
|
try:
|
||||||
# 创建用户
|
# 创建用户
|
||||||
user = await self.create_user(employee)
|
user, status = await self.create_user(employee)
|
||||||
if not user:
|
if not user:
|
||||||
stats['users_skipped'] += 1
|
stats['users_skipped'] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
stats['users_created'] += 1
|
# 根据状态统计
|
||||||
|
if status == 'created':
|
||||||
|
stats['users_created'] += 1
|
||||||
|
elif status == 'existing':
|
||||||
|
stats['users_existing'] += 1
|
||||||
|
elif status == 'restored':
|
||||||
|
stats['users_restored'] += 1
|
||||||
|
|
||||||
# 创建部门团队
|
# 创建部门团队
|
||||||
department = employee.get('department')
|
department = employee.get('department')
|
||||||
@@ -400,7 +409,29 @@ class EmployeeSyncService:
|
|||||||
stats['errors'].append(error_msg)
|
stats['errors'].append(error_msg)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 3. 提交所有更改
|
# 3. 处理离职员工(软删除)
|
||||||
|
dingtalk_phones = {emp.get('phone') for emp in employees if emp.get('phone')}
|
||||||
|
|
||||||
|
# 获取系统中所有活跃用户(排除 admin)
|
||||||
|
stmt = select(User).where(
|
||||||
|
User.is_deleted == False,
|
||||||
|
User.is_active == True,
|
||||||
|
User.username != 'admin',
|
||||||
|
User.role != 'admin'
|
||||||
|
)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
system_users = result.scalars().all()
|
||||||
|
|
||||||
|
# 找出离职员工(系统有但钉钉没有)
|
||||||
|
for user in system_users:
|
||||||
|
if user.phone and user.phone not in dingtalk_phones:
|
||||||
|
# 软删除:标记为离职
|
||||||
|
user.is_active = False
|
||||||
|
user.is_deleted = True
|
||||||
|
stats['users_departed'] += 1
|
||||||
|
logger.info(f"🚪 标记离职员工: {user.full_name} ({user.phone})")
|
||||||
|
|
||||||
|
# 4. 提交所有更改
|
||||||
await self.db.commit()
|
await self.db.commit()
|
||||||
logger.info("✅ 数据库事务已提交")
|
logger.info("✅ 数据库事务已提交")
|
||||||
|
|
||||||
@@ -414,12 +445,15 @@ class EmployeeSyncService:
|
|||||||
stats['end_time'] = datetime.now()
|
stats['end_time'] = datetime.now()
|
||||||
stats['duration'] = (stats['end_time'] - stats['start_time']).total_seconds()
|
stats['duration'] = (stats['end_time'] - stats['start_time']).total_seconds()
|
||||||
|
|
||||||
# 4. 输出统计信息
|
# 5. 输出统计信息
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
logger.info("同步完成统计")
|
logger.info("同步完成统计")
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
logger.info(f"总员工数: {stats['total_employees']}")
|
logger.info(f"钉钉在职员工: {stats['total_employees']}")
|
||||||
logger.info(f"创建用户: {stats['users_created']}")
|
logger.info(f"新增用户: {stats['users_created']}")
|
||||||
|
logger.info(f"已存在用户: {stats['users_existing']}")
|
||||||
|
logger.info(f"恢复用户: {stats['users_restored']}")
|
||||||
|
logger.info(f"离职处理: {stats['users_departed']}")
|
||||||
logger.info(f"跳过用户: {stats['users_skipped']}")
|
logger.info(f"跳过用户: {stats['users_skipped']}")
|
||||||
logger.info(f"耗时: {stats['duration']:.2f}秒")
|
logger.info(f"耗时: {stats['duration']:.2f}秒")
|
||||||
|
|
||||||
@@ -630,16 +664,18 @@ class EmployeeSyncService:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建用户
|
# 创建用户
|
||||||
user = await self.create_user(employee)
|
user, status = await self.create_user(employee)
|
||||||
if not user:
|
if not user:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
stats['added_count'] += 1
|
# 只有真正创建的才计入新增
|
||||||
stats['added_users'].append({
|
if status == 'created':
|
||||||
'full_name': user.full_name,
|
stats['added_count'] += 1
|
||||||
'phone': user.phone,
|
stats['added_users'].append({
|
||||||
'role': user.role
|
'full_name': user.full_name,
|
||||||
})
|
'phone': user.phone,
|
||||||
|
'role': user.role
|
||||||
|
})
|
||||||
|
|
||||||
# 创建部门团队
|
# 创建部门团队
|
||||||
department = employee.get('department')
|
department = employee.get('department')
|
||||||
@@ -684,53 +720,36 @@ class EmployeeSyncService:
|
|||||||
stats['errors'].append(error_msg)
|
stats['errors'].append(error_msg)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 5. 删除离职员工(物理删除)
|
# 5. 处理离职员工(软删除)
|
||||||
# 先flush之前的新增操作,避免与删除操作冲突
|
# 先flush之前的新增操作
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
|
|
||||||
# 收集需要删除的用户ID
|
# 标记离职员工
|
||||||
users_to_delete = []
|
|
||||||
for user in system_users:
|
for user in system_users:
|
||||||
if user.phone and user.phone in phones_to_delete:
|
if user.phone and user.phone in phones_to_delete:
|
||||||
# 双重保护:确保不删除admin
|
# 双重保护:确保不删除admin
|
||||||
if user.username == 'admin' or user.role == 'admin':
|
if user.username == 'admin' or user.role == 'admin':
|
||||||
logger.warning(f"⚠️ 跳过删除管理员账户: {user.username}")
|
logger.warning(f"⚠️ 跳过处理管理员账户: {user.username}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
users_to_delete.append({
|
try:
|
||||||
'id': user.id,
|
# 软删除:标记为离职
|
||||||
'full_name': user.full_name,
|
user.is_active = False
|
||||||
'phone': user.phone,
|
user.is_deleted = True
|
||||||
'username': user.username
|
|
||||||
})
|
|
||||||
|
|
||||||
# 批量删除用户及其关联数据
|
stats['deleted_users'].append({
|
||||||
for user_info in users_to_delete:
|
'full_name': user.full_name,
|
||||||
try:
|
'phone': user.phone,
|
||||||
user_id = user_info['id']
|
'username': user.username
|
||||||
|
})
|
||||||
|
stats['deleted_count'] += 1
|
||||||
|
logger.info(f"🚪 标记离职员工: {user.full_name} ({user.phone})")
|
||||||
|
|
||||||
# 先清理关联数据(外键约束)
|
except Exception as e:
|
||||||
await self._cleanup_user_related_data(user_id)
|
error_msg = f"处理离职员工 {user.full_name} 失败: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
# 用SQL直接删除用户(避免ORM的级联操作冲突)
|
stats['errors'].append(error_msg)
|
||||||
await self.db.execute(
|
continue
|
||||||
text("DELETE FROM users WHERE id = :user_id"),
|
|
||||||
{"user_id": user_id}
|
|
||||||
)
|
|
||||||
|
|
||||||
stats['deleted_users'].append({
|
|
||||||
'full_name': user_info['full_name'],
|
|
||||||
'phone': user_info['phone'],
|
|
||||||
'username': user_info['username']
|
|
||||||
})
|
|
||||||
stats['deleted_count'] += 1
|
|
||||||
logger.info(f"🗑️ 删除离职员工: {user_info['full_name']} ({user_info['phone']})")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"删除员工 {user_info['full_name']} 失败: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
stats['errors'].append(error_msg)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 6. 提交所有更改
|
# 6. 提交所有更改
|
||||||
await self.db.commit()
|
await self.db.commit()
|
||||||
@@ -751,7 +770,7 @@ class EmployeeSyncService:
|
|||||||
logger.info("增量同步完成统计")
|
logger.info("增量同步完成统计")
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
logger.info(f"新增员工: {stats['added_count']}")
|
logger.info(f"新增员工: {stats['added_count']}")
|
||||||
logger.info(f"删除员工: {stats['deleted_count']}")
|
logger.info(f"离职处理: {stats['deleted_count']}")
|
||||||
logger.info(f"跳过员工: {stats['skipped_count']}")
|
logger.info(f"跳过员工: {stats['skipped_count']}")
|
||||||
logger.info(f"耗时: {stats['duration']:.2f}秒")
|
logger.info(f"耗时: {stats['duration']:.2f}秒")
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import List, Optional
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy import select, func, and_, case
|
from sqlalchemy import select, func, and_, case
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload, selectinload
|
||||||
from app.models.task import Task, TaskCourse, TaskAssignment, TaskStatus, AssignmentStatus
|
from app.models.task import Task, TaskCourse, TaskAssignment, TaskStatus, AssignmentStatus
|
||||||
from app.models.course import Course
|
from app.models.course import Course
|
||||||
from app.schemas.task import TaskCreate, TaskUpdate, TaskStatsResponse
|
from app.schemas.task import TaskCreate, TaskUpdate, TaskStatsResponse
|
||||||
@@ -44,7 +44,14 @@ class TaskService(BaseService[Task]):
|
|||||||
db.add(assignment)
|
db.add(assignment)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(task)
|
|
||||||
|
# 重新查询并加载关联关系(避免懒加载问题)
|
||||||
|
stmt = select(Task).where(Task.id == task.id).options(
|
||||||
|
selectinload(Task.course_links).selectinload(TaskCourse.course),
|
||||||
|
selectinload(Task.assignments)
|
||||||
|
)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
task = result.scalar_one()
|
||||||
return task
|
return task
|
||||||
|
|
||||||
async def get_tasks(
|
async def get_tasks(
|
||||||
@@ -62,6 +69,12 @@ class TaskService(BaseService[Task]):
|
|||||||
|
|
||||||
stmt = stmt.order_by(Task.created_at.desc())
|
stmt = stmt.order_by(Task.created_at.desc())
|
||||||
|
|
||||||
|
# 加载关联关系
|
||||||
|
stmt = stmt.options(
|
||||||
|
selectinload(Task.course_links).selectinload(TaskCourse.course),
|
||||||
|
selectinload(Task.assignments)
|
||||||
|
)
|
||||||
|
|
||||||
# 获取总数
|
# 获取总数
|
||||||
count_stmt = select(func.count()).select_from(Task).where(Task.is_deleted == False)
|
count_stmt = select(func.count()).select_from(Task).where(Task.is_deleted == False)
|
||||||
if status:
|
if status:
|
||||||
@@ -71,7 +84,7 @@ class TaskService(BaseService[Task]):
|
|||||||
# 分页
|
# 分页
|
||||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
tasks = result.scalars().all()
|
tasks = result.unique().scalars().all()
|
||||||
|
|
||||||
return tasks, total
|
return tasks, total
|
||||||
|
|
||||||
|
|||||||
@@ -345,11 +345,20 @@ const triggerSync = async () => {
|
|||||||
const response = await request.post('/api/v1/employee-sync/sync')
|
const response = await request.post('/api/v1/employee-sync/sync')
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
ElMessage.success(
|
const created = data.users_created || 0
|
||||||
`同步完成!共处理 ${data.total_employees || 0} 名员工,` +
|
const existing = data.users_existing || 0
|
||||||
`创建 ${data.users_created || 0} 个账号,` +
|
const restored = data.users_restored || 0
|
||||||
`跳过 ${data.users_skipped || 0} 个`
|
const departed = data.users_departed || 0
|
||||||
)
|
const skipped = data.users_skipped || 0
|
||||||
|
|
||||||
|
let msg = `同步完成!钉钉在职 ${data.total_employees || 0} 人`
|
||||||
|
if (created > 0) msg += `,新增 ${created} 人`
|
||||||
|
if (existing > 0) msg += `,已存在 ${existing} 人`
|
||||||
|
if (restored > 0) msg += `,恢复 ${restored} 人`
|
||||||
|
if (departed > 0) msg += `,离职 ${departed} 人`
|
||||||
|
if (skipped > 0) msg += `,跳过 ${skipped} 人`
|
||||||
|
|
||||||
|
ElMessage.success(msg)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(response.message || '同步失败')
|
ElMessage.error(response.message || '同步失败')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,22 +28,22 @@
|
|||||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||||
<el-tab-pane label="进行中" name="ongoing">
|
<el-tab-pane label="进行中" name="ongoing">
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
进行中 <el-badge :value="12" class="tab-badge" />
|
进行中 <el-badge :value="taskCounts.ongoing" class="tab-badge" v-if="taskCounts.ongoing > 0" />
|
||||||
</span>
|
</span>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="待开始" name="pending">
|
<el-tab-pane label="待开始" name="pending">
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
待开始 <el-badge :value="5" class="tab-badge" />
|
待开始 <el-badge :value="taskCounts.pending" class="tab-badge" v-if="taskCounts.pending > 0" />
|
||||||
</span>
|
</span>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="已完成" name="completed">
|
<el-tab-pane label="已完成" name="completed">
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
已完成 <el-badge :value="28" class="tab-badge" />
|
已完成 <el-badge :value="taskCounts.completed" class="tab-badge" v-if="taskCounts.completed > 0" />
|
||||||
</span>
|
</span>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="已过期" name="expired">
|
<el-tab-pane label="已过期" name="expired">
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
已过期 <el-badge :value="3" class="tab-badge" />
|
已过期 <el-badge :value="taskCounts.expired" class="tab-badge" v-if="taskCounts.expired > 0" />
|
||||||
</span>
|
</span>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<div class="task-title-section">
|
<div class="task-title-section">
|
||||||
<h3 class="task-title">{{ task.title }}</h3>
|
<h3 class="task-title">{{ task.title }}</h3>
|
||||||
<el-tag :type="getTaskTagType(task.priority)" size="small">
|
<el-tag :type="getTaskTagType(task.priority)" size="small">
|
||||||
{{ task.priority }}
|
{{ getPriorityLabel(task.priority) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
@@ -383,6 +383,14 @@ const taskStats = ref([
|
|||||||
// 任务列表数据
|
// 任务列表数据
|
||||||
const allTasks = ref<Task[]>([])
|
const allTasks = ref<Task[]>([])
|
||||||
|
|
||||||
|
// 各状态任务数量
|
||||||
|
const taskCounts = reactive({
|
||||||
|
ongoing: 0,
|
||||||
|
pending: 0,
|
||||||
|
completed: 0,
|
||||||
|
expired: 0
|
||||||
|
})
|
||||||
|
|
||||||
// 任务表单
|
// 任务表单
|
||||||
const taskForm = reactive({
|
const taskForm = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
@@ -417,9 +425,6 @@ const rules = reactive<FormRules>({
|
|||||||
|
|
||||||
// 根据当前标签页筛选的任务列表
|
// 根据当前标签页筛选的任务列表
|
||||||
const taskList = computed(() => {
|
const taskList = computed(() => {
|
||||||
if (activeTab.value === 'ongoing') {
|
|
||||||
return allTasks.value
|
|
||||||
}
|
|
||||||
return allTasks.value.filter(task => task.status === activeTab.value)
|
return allTasks.value.filter(task => task.status === activeTab.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -442,15 +447,20 @@ const loadTaskStats = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载任务列表
|
* 加载任务列表(加载所有任务,前端筛选)
|
||||||
*/
|
*/
|
||||||
const loadTasks = async () => {
|
const loadTasks = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const status = activeTab.value === 'ongoing' ? 'ongoing' : activeTab.value
|
// 不传status参数,获取所有任务
|
||||||
const res = await getTasks({ status })
|
const res = await getTasks({ page_size: 500 })
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
allTasks.value = res.data.items
|
allTasks.value = res.data.items
|
||||||
|
// 统计各状态任务数量
|
||||||
|
taskCounts.ongoing = allTasks.value.filter(t => t.status === 'ongoing').length
|
||||||
|
taskCounts.pending = allTasks.value.filter(t => t.status === 'pending').length
|
||||||
|
taskCounts.completed = allTasks.value.filter(t => t.status === 'completed').length
|
||||||
|
taskCounts.expired = allTasks.value.filter(t => t.status === 'expired').length
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('加载任务列表失败:', error)
|
console.error('加载任务列表失败:', error)
|
||||||
@@ -557,11 +567,18 @@ const handleCreateTask = async () => {
|
|||||||
createLoading.value = true
|
createLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 优先级中文转英文映射
|
||||||
|
const priorityMap: Record<string, string> = {
|
||||||
|
'高': 'high',
|
||||||
|
'中': 'medium',
|
||||||
|
'低': 'low'
|
||||||
|
}
|
||||||
|
|
||||||
// 构建请求数据
|
// 构建请求数据
|
||||||
const taskData = {
|
const taskData = {
|
||||||
title: taskForm.title,
|
title: taskForm.title,
|
||||||
description: taskForm.description,
|
description: taskForm.description,
|
||||||
priority: taskForm.priority.toLowerCase(),
|
priority: priorityMap[taskForm.priority] || 'medium',
|
||||||
deadline: taskForm.deadline,
|
deadline: taskForm.deadline,
|
||||||
course_ids: taskForm.courses,
|
course_ids: taskForm.courses,
|
||||||
user_ids: taskForm.assignType === 'all' ? [] : taskForm.members,
|
user_ids: taskForm.assignType === 'all' ? [] : taskForm.members,
|
||||||
@@ -776,11 +793,30 @@ const deleteTaskItem = async (task: Task) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级英文转中文映射
|
||||||
|
*/
|
||||||
|
const priorityToChineseMap: Record<string, string> = {
|
||||||
|
'high': '高',
|
||||||
|
'medium': '中',
|
||||||
|
'low': '低'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取中文优先级显示
|
||||||
|
*/
|
||||||
|
const getPriorityLabel = (priority: string) => {
|
||||||
|
return priorityToChineseMap[priority] || priority
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取任务标签类型
|
* 获取任务标签类型
|
||||||
*/
|
*/
|
||||||
const getTaskTagType = (priority: string) => {
|
const getTaskTagType = (priority: string) => {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
|
'high': 'danger',
|
||||||
|
'medium': 'warning',
|
||||||
|
'low': 'info',
|
||||||
'高': 'danger',
|
'高': 'danger',
|
||||||
'中': 'warning',
|
'中': 'warning',
|
||||||
'低': 'info'
|
'低': 'info'
|
||||||
|
|||||||
Reference in New Issue
Block a user