Compare commits

...

10 Commits

Author SHA1 Message Date
yuliang_guo
cf71fabef0 fix: 修复企业看板API 500错误
All checks were successful
continuous-integration/drone/push Build is passing
- 修复 get_realtime_activities() 中字段名错误 (exp_amount -> exp_change)
- 添加 get_enterprise_overview() 的异常处理,防止单个查询失败导致整体失败
- 满分人数查询添加 NULL 值检查
2026-02-02 12:57:31 +08:00
yuliang_guo
99c4ac5473 fix: 团队统计只计算未删除的活跃用户
All checks were successful
continuous-integration/drone/push Build is passing
- get_accessible_team_member_ids 增加 is_deleted=False 和 is_active=True 过滤
- 避免统计已离职/删除的用户导致数量不准
2026-01-31 19:01:01 +08:00
yuliang_guo
616bb7185e fix: 任务中心标签页显示真实任务数量
Some checks failed
continuous-integration/drone/push Build is failing
- 移除硬编码的任务数量(12/5/28/3)
- 加载所有任务后统计各状态数量
- 后端任务API page_size限制调整为500
2026-01-31 18:48:26 +08:00
yuliang_guo
e942a9de2c fix: 任务列表优先级显示英文转中文
Some checks failed
continuous-integration/drone/push Build is failing
API返回英文优先级(high/medium/low),前端显示转换为中文(高/中/低)
2026-01-31 18:42:14 +08:00
yuliang_guo
586c51955e fix: 增加分页参数page_size上限到500
All checks were successful
continuous-integration/drone/push Build is passing
任务中心需要加载全部用户列表,将限制从100提升到500
2026-01-31 18:38:25 +08:00
yuliang_guo
ebf196686f fix: 修复任务API枚举值访问错误
All checks were successful
continuous-integration/drone/push Build is passing
- 使用 hasattr 检查是否为枚举类型,兼容字符串和枚举
- 移除 get_tasks 中多余的 get_task_detail 调用,使用已加载的关联数据
2026-01-31 18:35:17 +08:00
yuliang_guo
fc9775e61f fix: 修复任务服务SQLAlchemy异步加载错误
All checks were successful
continuous-integration/drone/push Build is passing
- create_task 和 get_tasks 现在使用 selectinload 预加载关联关系
- 避免懒加载导致的 MissingGreenlet 错误
2026-01-31 18:31:07 +08:00
yuliang_guo
eca0ed8c9d fix: 修复创建任务时优先级转换错误
Some checks failed
continuous-integration/drone/push Build is failing
前端优先级使用中文(高/中/低),需要转换为英文(high/medium/low)
.toLowerCase() 无法转换中文字符
2026-01-31 18:19:19 +08:00
yuliang_guo
506e9ea2e2 feat: 员工同步增加离职处理功能
Some checks failed
continuous-integration/drone/push Build is failing
- 全量同步 (sync_employees) 现在会检测并软删除离职员工
- 增量同步改为软删除而非物理删除,更安全
- 离职处理:设置 is_active=False, is_deleted=True
- 前端显示离职处理数量统计
2026-01-31 18:10:45 +08:00
yuliang_guo
e5dd6f3acb fix: 修复员工同步统计错误
Some checks failed
continuous-integration/drone/push Build is failing
- create_user 现在返回 (user, status) 元组,区分 created/existing/restored/skipped
- sync_employees 正确统计新增、已存在、恢复、跳过的用户数
- 前端显示更准确的同步结果信息
2026-01-31 18:04:27 +08:00
8 changed files with 405 additions and 233 deletions

View File

@@ -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(

View File

@@ -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()]

View File

@@ -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:

View File

@@ -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. 总学员数 # 基础统计
result = await self.db.execute( # 1. 总学员数
select(func.count(User.id)) result = await self.db.execute(
.where(User.is_deleted == False, User.role == 'trainee') select(func.count(User.id))
) .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() # 2. 今日活跃用户(有经验值记录)
exam_count = exam_row[0] or 0 try:
exam_passed = exam_row[1] or 0 result = await self.db.execute(
exam_avg_score = round(exam_row[2] or 0, 1) select(func.count(func.distinct(ExpHistory.user_id)))
exam_pass_rate = round(exam_passed / exam_count * 100, 1) if exam_count > 0 else 0 .where(func.date(ExpHistory.created_at) == today)
)
# 7. 满分人数 today_active = result.scalar() or 0
result = await self.db.execute( except Exception as e:
select(func.count(func.distinct(Exam.user_id))) logger.warning(f"获取今日活跃用户失败: {e}")
.where(Exam.status == 'submitted', Exam.score >= Exam.total_score) today_active = 0
)
perfect_users = result.scalar() or 0 # 3. 本周活跃用户
try:
# 8. 签到率(今日签到人数/总用户数) result = await self.db.execute(
result = await self.db.execute( select(func.count(func.distinct(ExpHistory.user_id)))
select(func.count(UserLevel.id)) .where(ExpHistory.created_at >= datetime.combine(week_ago, datetime.min.time()))
.where(func.date(UserLevel.last_login_date) == today) )
) week_active = result.scalar() or 0
today_checkin = result.scalar() or 0 except Exception as e:
checkin_rate = round(today_checkin / total_users * 100, 1) if total_users > 0 else 0 logger.warning(f"获取本周活跃用户失败: {e}")
week_active = 0
return {
"overview": { # 4. 本月活跃用户
"total_users": total_users, try:
"today_active": today_active, result = await self.db.execute(
"week_active": week_active, select(func.count(func.distinct(ExpHistory.user_id)))
"month_active": month_active, .where(ExpHistory.created_at >= datetime.combine(month_ago, datetime.min.time()))
"total_hours": total_hours, )
"checkin_rate": checkin_rate, month_active = result.scalar() or 0
}, except Exception as e:
"exam": { logger.warning(f"获取本月活跃用户失败: {e}")
"total_count": exam_count, month_active = 0
"pass_rate": exam_pass_rate,
"avg_score": exam_avg_score, # 5. 总学习时长(小时)
"perfect_users": perfect_users, practice_hours = 0
}, training_hours = 0
"updated_at": datetime.now().isoformat() 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),
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:
activity_type = "学习"
if "考试" in (exp.description or ""):
activity_type = "考试"
elif "签到" in (exp.description or ""):
activity_type = "签到"
elif "陪练" in (exp.description or ""):
activity_type = "陪练"
elif "奖章" in (exp.description or ""):
activity_type = "奖章"
activities.append({ for exp, user in rows:
"id": exp.id, activity_type = "学习"
"user_id": user.id, description = exp.description or ""
"user_name": user.full_name or user.username, if "考试" in description:
"type": activity_type, activity_type = "考试"
"description": exp.description, elif "签到" in description:
"exp_amount": exp.exp_amount, activity_type = "签到"
"created_at": exp.created_at.isoformat() if exp.created_at else None, elif "陪练" in description:
}) activity_type = "陪练"
elif "奖章" in description:
activity_type = "奖章"
activities.append({
"id": exp.id,
"user_id": user.id,
"user_name": user.full_name or user.username,
"type": activity_type,
"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 return activities

View File

@@ -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({
'full_name': user.full_name,
# 批量删除用户及其关联数据 'phone': user.phone,
for user_info in users_to_delete: 'username': user.username
try: })
user_id = user_info['id'] stats['deleted_count'] += 1
logger.info(f"🚪 标记离职员工: {user.full_name} ({user.phone})")
# 先清理关联数据(外键约束)
await self._cleanup_user_related_data(user_id) except Exception as e:
error_msg = f"处理离职员工 {user.full_name} 失败: {str(e)}"
# 用SQL直接删除用户避免ORM的级联操作冲突 logger.error(error_msg)
await self.db.execute( stats['errors'].append(error_msg)
text("DELETE FROM users WHERE id = :user_id"), continue
{"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}")

View File

@@ -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(
@@ -61,6 +68,12 @@ class TaskService(BaseService[Task]):
stmt = stmt.where(Task.status == status) stmt = stmt.where(Task.status == status)
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)
@@ -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

View File

@@ -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 || '同步失败')
} }

View File

@@ -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'