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]
|
||||
# 安全获取枚举值(兼容字符串和枚举类型)
|
||||
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(
|
||||
data=TaskResponse(
|
||||
id=task.id,
|
||||
title=task.title,
|
||||
description=task.description,
|
||||
priority=task.priority.value,
|
||||
status=task.status.value,
|
||||
priority=priority_val,
|
||||
status=status_val,
|
||||
creator_id=task.creator_id,
|
||||
deadline=task.deadline,
|
||||
requirements=task.requirements,
|
||||
@@ -58,7 +65,7 @@ async def create_task(
|
||||
updated_at=task.updated_at,
|
||||
courses=courses,
|
||||
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(
|
||||
status: Optional[str] = Query(None, description="任务状态筛选"),
|
||||
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),
|
||||
current_user: User = Depends(require_admin_or_manager)
|
||||
):
|
||||
@@ -77,16 +84,23 @@ async def get_tasks(
|
||||
# 构建响应
|
||||
items = []
|
||||
for task in tasks:
|
||||
# 加载关联数据
|
||||
task_detail = await task_service.get_task_detail(db, task.id)
|
||||
if task_detail:
|
||||
courses = [link.course.name for link in task_detail.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
|
||||
|
||||
# 使用已加载的关联数据(通过 selectinload)
|
||||
courses = [link.course.name for link in task.course_links] if task.course_links else []
|
||||
completed_count = sum(
|
||||
1 for a in task.assignments
|
||||
if (a.status.value if hasattr(a.status, 'value') else a.status) == "completed"
|
||||
) if task.assignments else 0
|
||||
|
||||
items.append(TaskResponse(
|
||||
id=task.id,
|
||||
title=task.title,
|
||||
description=task.description,
|
||||
priority=task.priority.value,
|
||||
status=task.status.value,
|
||||
priority=priority_val,
|
||||
status=status_val,
|
||||
creator_id=task.creator_id,
|
||||
deadline=task.deadline,
|
||||
requirements=task.requirements,
|
||||
@@ -94,8 +108,8 @@ async def get_tasks(
|
||||
created_at=task.created_at,
|
||||
updated_at=task.updated_at,
|
||||
courses=courses,
|
||||
assigned_count=len(task_detail.assignments),
|
||||
completed_count=sum(1 for a in task_detail.assignments if a.status.value == "completed")
|
||||
assigned_count=len(task.assignments) if task.assignments else 0,
|
||||
completed_count=completed_count
|
||||
))
|
||||
|
||||
return ResponseModel(
|
||||
|
||||
@@ -28,10 +28,17 @@ async def get_accessible_team_member_ids(
|
||||
current_user: User,
|
||||
db: AsyncSession
|
||||
) -> List[int]:
|
||||
"""获取用户可访问的团队成员ID列表"""
|
||||
"""获取用户可访问的团队成员ID列表(只返回未删除的用户)"""
|
||||
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)
|
||||
return [row[0] for row in result.all()]
|
||||
else:
|
||||
@@ -44,9 +51,15 @@ async def get_accessible_team_member_ids(
|
||||
if not team_ids:
|
||||
return []
|
||||
|
||||
# 2. 查询这些团队的所有成员
|
||||
stmt = select(UserTeam.user_id).where(
|
||||
UserTeam.team_id.in_(team_ids)
|
||||
# 2. 查询这些团队的所有成员(过滤已删除用户)
|
||||
stmt = select(UserTeam.user_id).join(
|
||||
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()
|
||||
result = await db.execute(stmt)
|
||||
return [row[0] for row in result.all()]
|
||||
|
||||
@@ -42,7 +42,7 @@ class PaginationParams(BaseModel):
|
||||
"""分页参数"""
|
||||
|
||||
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
|
||||
def offset(self) -> int:
|
||||
|
||||
@@ -42,6 +42,7 @@ class DashboardService:
|
||||
Returns:
|
||||
企业级数据概览
|
||||
"""
|
||||
try:
|
||||
today = date.today()
|
||||
week_ago = today - timedelta(days=7)
|
||||
month_ago = today - timedelta(days=30)
|
||||
@@ -55,42 +56,66 @@ class DashboardService:
|
||||
total_users = result.scalar() or 0
|
||||
|
||||
# 2. 今日活跃用户(有经验值记录)
|
||||
try:
|
||||
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
|
||||
except Exception as e:
|
||||
logger.warning(f"获取今日活跃用户失败: {e}")
|
||||
today_active = 0
|
||||
|
||||
# 3. 本周活跃用户
|
||||
try:
|
||||
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
|
||||
except Exception as e:
|
||||
logger.warning(f"获取本周活跃用户失败: {e}")
|
||||
week_active = 0
|
||||
|
||||
# 4. 本月活跃用户
|
||||
try:
|
||||
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
|
||||
except Exception as e:
|
||||
logger.warning(f"获取本月活跃用户失败: {e}")
|
||||
month_active = 0
|
||||
|
||||
# 5. 总学习时长(小时)
|
||||
practice_hours = 0
|
||||
training_hours = 0
|
||||
try:
|
||||
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),
|
||||
@@ -100,24 +125,42 @@ class DashboardService:
|
||||
.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 >= Exam.total_score)
|
||||
.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(func.date(UserLevel.last_login_date) == today)
|
||||
.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 {
|
||||
@@ -137,6 +180,26 @@ class DashboardService:
|
||||
},
|
||||
"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]]:
|
||||
"""
|
||||
@@ -313,6 +376,7 @@ class DashboardService:
|
||||
"""
|
||||
activities = []
|
||||
|
||||
try:
|
||||
# 获取最近的经验值记录
|
||||
result = await self.db.execute(
|
||||
select(ExpHistory, User)
|
||||
@@ -324,13 +388,14 @@ class DashboardService:
|
||||
|
||||
for exp, user in rows:
|
||||
activity_type = "学习"
|
||||
if "考试" in (exp.description or ""):
|
||||
description = exp.description or ""
|
||||
if "考试" in description:
|
||||
activity_type = "考试"
|
||||
elif "签到" in (exp.description or ""):
|
||||
elif "签到" in description:
|
||||
activity_type = "签到"
|
||||
elif "陪练" in (exp.description or ""):
|
||||
elif "陪练" in description:
|
||||
activity_type = "陪练"
|
||||
elif "奖章" in (exp.description or ""):
|
||||
elif "奖章" in description:
|
||||
activity_type = "奖章"
|
||||
|
||||
activities.append({
|
||||
@@ -338,10 +403,13 @@ class DashboardService:
|
||||
"user_id": user.id,
|
||||
"user_name": user.full_name or user.username,
|
||||
"type": activity_type,
|
||||
"description": exp.description,
|
||||
"exp_amount": exp.exp_amount,
|
||||
"description": description,
|
||||
"exp_amount": exp.exp_change, # 修复: exp_change 而非 exp_amount
|
||||
"created_at": exp.created_at.isoformat() if exp.created_at else None,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"获取实时动态失败: {e}")
|
||||
# 返回空列表而不是抛出异常
|
||||
|
||||
return activities
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ class EmployeeSyncService:
|
||||
logger.info(f"创建岗位: {position_name} (ID: {position.id})")
|
||||
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: 员工数据
|
||||
|
||||
Returns:
|
||||
用户对象或None(如果创建失败)
|
||||
(用户对象, 状态): 状态为 'created'/'existing'/'restored'/'skipped'
|
||||
"""
|
||||
phone = employee_data.get('phone')
|
||||
full_name = employee_data.get('full_name')
|
||||
|
||||
if not phone:
|
||||
logger.warning(f"员工 {full_name} 没有手机号,跳过")
|
||||
return None
|
||||
return None, 'skipped'
|
||||
|
||||
# 检查用户是否已存在(通过手机号,包括已软删除的)
|
||||
stmt = select(User).where(User.phone == phone)
|
||||
@@ -270,15 +270,15 @@ class EmployeeSyncService:
|
||||
if dingtalk_id:
|
||||
existing_user.dingtalk_id = dingtalk_id
|
||||
logger.info(f"恢复软删除用户: {phone} ({full_name})")
|
||||
return existing_user
|
||||
return existing_user, 'restored'
|
||||
|
||||
# 如果用户已存在但没有dingtalk_id,则更新
|
||||
dingtalk_id = employee_data.get('dingtalk_id')
|
||||
if dingtalk_id and not existing_user.dingtalk_id:
|
||||
existing_user.dingtalk_id = dingtalk_id
|
||||
logger.info(f"更新用户 {phone} 的钉钉ID: {dingtalk_id}")
|
||||
logger.info(f"用户已存在: {phone} ({full_name})")
|
||||
return existing_user
|
||||
logger.debug(f"用户已存在: {phone} ({full_name})")
|
||||
return existing_user, 'existing'
|
||||
|
||||
# 生成邮箱
|
||||
email = self.generate_email(phone, employee_data.get('email'))
|
||||
@@ -315,7 +315,7 @@ class EmployeeSyncService:
|
||||
await self.db.flush()
|
||||
|
||||
logger.info(f"创建用户: {phone} ({full_name}) - 角色: {role}")
|
||||
return user
|
||||
return user, 'created'
|
||||
|
||||
async def sync_employees(self) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -331,6 +331,9 @@ class EmployeeSyncService:
|
||||
stats = {
|
||||
'total_employees': 0,
|
||||
'users_created': 0,
|
||||
'users_existing': 0,
|
||||
'users_restored': 0,
|
||||
'users_departed': 0,
|
||||
'users_skipped': 0,
|
||||
'teams_created': 0,
|
||||
'positions_created': 0,
|
||||
@@ -351,12 +354,18 @@ class EmployeeSyncService:
|
||||
for employee in employees:
|
||||
try:
|
||||
# 创建用户
|
||||
user = await self.create_user(employee)
|
||||
user, status = await self.create_user(employee)
|
||||
if not user:
|
||||
stats['users_skipped'] += 1
|
||||
continue
|
||||
|
||||
# 根据状态统计
|
||||
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')
|
||||
@@ -400,7 +409,29 @@ class EmployeeSyncService:
|
||||
stats['errors'].append(error_msg)
|
||||
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()
|
||||
logger.info("✅ 数据库事务已提交")
|
||||
|
||||
@@ -414,12 +445,15 @@ class EmployeeSyncService:
|
||||
stats['end_time'] = datetime.now()
|
||||
stats['duration'] = (stats['end_time'] - stats['start_time']).total_seconds()
|
||||
|
||||
# 4. 输出统计信息
|
||||
# 5. 输出统计信息
|
||||
logger.info("=" * 60)
|
||||
logger.info("同步完成统计")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"总员工数: {stats['total_employees']}")
|
||||
logger.info(f"创建用户: {stats['users_created']}")
|
||||
logger.info(f"钉钉在职员工: {stats['total_employees']}")
|
||||
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['duration']:.2f}秒")
|
||||
|
||||
@@ -630,10 +664,12 @@ class EmployeeSyncService:
|
||||
|
||||
try:
|
||||
# 创建用户
|
||||
user = await self.create_user(employee)
|
||||
user, status = await self.create_user(employee)
|
||||
if not user:
|
||||
continue
|
||||
|
||||
# 只有真正创建的才计入新增
|
||||
if status == 'created':
|
||||
stats['added_count'] += 1
|
||||
stats['added_users'].append({
|
||||
'full_name': user.full_name,
|
||||
@@ -684,50 +720,33 @@ class EmployeeSyncService:
|
||||
stats['errors'].append(error_msg)
|
||||
continue
|
||||
|
||||
# 5. 删除离职员工(物理删除)
|
||||
# 先flush之前的新增操作,避免与删除操作冲突
|
||||
# 5. 处理离职员工(软删除)
|
||||
# 先flush之前的新增操作
|
||||
await self.db.flush()
|
||||
|
||||
# 收集需要删除的用户ID
|
||||
users_to_delete = []
|
||||
# 标记离职员工
|
||||
for user in system_users:
|
||||
if user.phone and user.phone in phones_to_delete:
|
||||
# 双重保护:确保不删除admin
|
||||
if user.username == 'admin' or user.role == 'admin':
|
||||
logger.warning(f"⚠️ 跳过删除管理员账户: {user.username}")
|
||||
logger.warning(f"⚠️ 跳过处理管理员账户: {user.username}")
|
||||
continue
|
||||
|
||||
users_to_delete.append({
|
||||
'id': user.id,
|
||||
try:
|
||||
# 软删除:标记为离职
|
||||
user.is_active = False
|
||||
user.is_deleted = True
|
||||
|
||||
stats['deleted_users'].append({
|
||||
'full_name': user.full_name,
|
||||
'phone': user.phone,
|
||||
'username': user.username
|
||||
})
|
||||
|
||||
# 批量删除用户及其关联数据
|
||||
for user_info in users_to_delete:
|
||||
try:
|
||||
user_id = user_info['id']
|
||||
|
||||
# 先清理关联数据(外键约束)
|
||||
await self._cleanup_user_related_data(user_id)
|
||||
|
||||
# 用SQL直接删除用户(避免ORM的级联操作冲突)
|
||||
await self.db.execute(
|
||||
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']})")
|
||||
logger.info(f"🚪 标记离职员工: {user.full_name} ({user.phone})")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"删除员工 {user_info['full_name']} 失败: {str(e)}"
|
||||
error_msg = f"处理离职员工 {user.full_name} 失败: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
stats['errors'].append(error_msg)
|
||||
continue
|
||||
@@ -751,7 +770,7 @@ class EmployeeSyncService:
|
||||
logger.info("增量同步完成统计")
|
||||
logger.info("=" * 60)
|
||||
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['duration']:.2f}秒")
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from sqlalchemy import select, func, and_, case
|
||||
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.course import Course
|
||||
from app.schemas.task import TaskCreate, TaskUpdate, TaskStatsResponse
|
||||
@@ -44,7 +44,14 @@ class TaskService(BaseService[Task]):
|
||||
db.add(assignment)
|
||||
|
||||
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
|
||||
|
||||
async def get_tasks(
|
||||
@@ -62,6 +69,12 @@ class TaskService(BaseService[Task]):
|
||||
|
||||
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)
|
||||
if status:
|
||||
@@ -71,7 +84,7 @@ class TaskService(BaseService[Task]):
|
||||
# 分页
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(stmt)
|
||||
tasks = result.scalars().all()
|
||||
tasks = result.unique().scalars().all()
|
||||
|
||||
return tasks, total
|
||||
|
||||
|
||||
@@ -345,11 +345,20 @@ const triggerSync = async () => {
|
||||
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} 个`
|
||||
)
|
||||
const created = data.users_created || 0
|
||||
const existing = data.users_existing || 0
|
||||
const restored = data.users_restored || 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 {
|
||||
ElMessage.error(response.message || '同步失败')
|
||||
}
|
||||
|
||||
@@ -28,22 +28,22 @@
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="进行中" name="ongoing">
|
||||
<span slot="label">
|
||||
进行中 <el-badge :value="12" class="tab-badge" />
|
||||
进行中 <el-badge :value="taskCounts.ongoing" class="tab-badge" v-if="taskCounts.ongoing > 0" />
|
||||
</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="待开始" name="pending">
|
||||
<span slot="label">
|
||||
待开始 <el-badge :value="5" class="tab-badge" />
|
||||
待开始 <el-badge :value="taskCounts.pending" class="tab-badge" v-if="taskCounts.pending > 0" />
|
||||
</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="已完成" name="completed">
|
||||
<span slot="label">
|
||||
已完成 <el-badge :value="28" class="tab-badge" />
|
||||
已完成 <el-badge :value="taskCounts.completed" class="tab-badge" v-if="taskCounts.completed > 0" />
|
||||
</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="已过期" name="expired">
|
||||
<span slot="label">
|
||||
已过期 <el-badge :value="3" class="tab-badge" />
|
||||
已过期 <el-badge :value="taskCounts.expired" class="tab-badge" v-if="taskCounts.expired > 0" />
|
||||
</span>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -55,7 +55,7 @@
|
||||
<div class="task-title-section">
|
||||
<h3 class="task-title">{{ task.title }}</h3>
|
||||
<el-tag :type="getTaskTagType(task.priority)" size="small">
|
||||
{{ task.priority }}
|
||||
{{ getPriorityLabel(task.priority) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-dropdown trigger="click">
|
||||
@@ -383,6 +383,14 @@ const taskStats = ref([
|
||||
// 任务列表数据
|
||||
const allTasks = ref<Task[]>([])
|
||||
|
||||
// 各状态任务数量
|
||||
const taskCounts = reactive({
|
||||
ongoing: 0,
|
||||
pending: 0,
|
||||
completed: 0,
|
||||
expired: 0
|
||||
})
|
||||
|
||||
// 任务表单
|
||||
const taskForm = reactive({
|
||||
title: '',
|
||||
@@ -417,9 +425,6 @@ const rules = reactive<FormRules>({
|
||||
|
||||
// 根据当前标签页筛选的任务列表
|
||||
const taskList = computed(() => {
|
||||
if (activeTab.value === 'ongoing') {
|
||||
return allTasks.value
|
||||
}
|
||||
return allTasks.value.filter(task => task.status === activeTab.value)
|
||||
})
|
||||
|
||||
@@ -442,15 +447,20 @@ const loadTaskStats = async () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载任务列表
|
||||
* 加载任务列表(加载所有任务,前端筛选)
|
||||
*/
|
||||
const loadTasks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const status = activeTab.value === 'ongoing' ? 'ongoing' : activeTab.value
|
||||
const res = await getTasks({ status })
|
||||
// 不传status参数,获取所有任务
|
||||
const res = await getTasks({ page_size: 500 })
|
||||
if (res.code === 200 && res.data) {
|
||||
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) {
|
||||
console.error('加载任务列表失败:', error)
|
||||
@@ -557,11 +567,18 @@ const handleCreateTask = async () => {
|
||||
createLoading.value = true
|
||||
|
||||
try {
|
||||
// 优先级中文转英文映射
|
||||
const priorityMap: Record<string, string> = {
|
||||
'高': 'high',
|
||||
'中': 'medium',
|
||||
'低': 'low'
|
||||
}
|
||||
|
||||
// 构建请求数据
|
||||
const taskData = {
|
||||
title: taskForm.title,
|
||||
description: taskForm.description,
|
||||
priority: taskForm.priority.toLowerCase(),
|
||||
priority: priorityMap[taskForm.priority] || 'medium',
|
||||
deadline: taskForm.deadline,
|
||||
course_ids: taskForm.courses,
|
||||
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 typeMap: Record<string, string> = {
|
||||
'high': 'danger',
|
||||
'medium': 'warning',
|
||||
'low': 'info',
|
||||
'高': 'danger',
|
||||
'中': 'warning',
|
||||
'低': 'info'
|
||||
|
||||
Reference in New Issue
Block a user