Browse Source

fix: 修复系统故障文档中的4个bug

- 故障1: 修复户籍省份分布图表重复问题,添加mergeProvinceData函数合并相同省份的数据
- 故障2: 修复人才列表查询总数错误,使用COUNT(DISTINCT)去重后再groupBy
- 故障3: 修复出勤天数固定显示22天的问题,改为动态计算到当前日期的工作日(排除周末)
- 故障4: 修复首页薪资显示错误月份,改为从API获取上月薪资数据

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 1 month ago
parent
commit
6aae3a09d4

+ 45 - 4
allin-packages/disability-module/src/services/disabled-person.service.ts

@@ -568,6 +568,51 @@ export class DisabledPersonService extends GenericCrudService<DisabledPerson> {
       }
     }
 
+    // 在应用 select 和 groupBy 之前获取总数(修复:避免 getCount 返回分组后的记录数)
+    // 需要计算去重后的人员数量,使用子查询
+    const countQueryBuilder = this.dataSource.getRepository(OrderPerson)
+      .createQueryBuilder('op')
+      .innerJoin('op.person', 'person')
+      .innerJoin('op.order', 'order')
+      .where('order.companyId = :companyId', { companyId });
+
+    // 应用相同的筛选条件
+    if (search) {
+      countQueryBuilder.andWhere('(person.name LIKE :search OR person.idCard LIKE :search OR person.disabilityId LIKE :search)', {
+        search: `%${search}%`
+      });
+    }
+
+    if (disabilityType) {
+      countQueryBuilder.andWhere('person.disabilityType = :disabilityType', { disabilityType });
+    }
+
+    if (jobStatus) {
+      // 重新应用工作状态筛选(因为上面的变量在作用域外)
+      const chineseToWorkStatus: Record<string, WorkStatus> = {
+        '在职': WorkStatus.WORKING,
+        '待入职': WorkStatus.PRE_WORKING,
+        '离职': WorkStatus.RESIGNED
+      };
+
+      let workStatusValue: WorkStatus | undefined;
+      if (Object.values(WorkStatus).includes(jobStatus as WorkStatus)) {
+        workStatusValue = jobStatus as WorkStatus;
+      } else if (chineseToWorkStatus[jobStatus]) {
+        workStatusValue = chineseToWorkStatus[jobStatus];
+      }
+
+      if (workStatusValue) {
+        countQueryBuilder.andWhere('op.workStatus = :workStatus', { workStatus: workStatusValue });
+      }
+    }
+
+    // 使用 COUNT(DISTINCT person.id) 获取去重后的人员数量
+    const total = await countQueryBuilder
+      .select('COUNT(DISTINCT person.id)')
+      .getRawOne()
+      .then(result => parseInt(result.count || '0', 10));
+
     // 按人员ID去重(同一人员可能关联多个订单),获取最新订单信息
     queryBuilder.select([
       'person.id as personId',
@@ -589,10 +634,6 @@ export class DisabledPersonService extends GenericCrudService<DisabledPerson> {
     // 按最新入职日期排序
     queryBuilder.orderBy('latestJoinDate', 'DESC');
 
-    // 获取总数
-    const totalQuery = queryBuilder.clone();
-    const total = await totalQuery.getCount();
-
     // 分页
     queryBuilder.offset((pageNum - 1) * limitNum).limit(limitNum);
 

+ 30 - 3
mini-talent/src/pages/index/index.tsx

@@ -1,12 +1,14 @@
 import React, { useEffect } from 'react'
 import { View, Text, ScrollView } from '@tarojs/components'
 import Taro from '@tarojs/taro'
+import { useQuery } from '@tanstack/react-query'
 import { RencaiTabBarLayout } from '../../components/RencaiTabBarLayout'
 import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
 import { useAuth, useRequireAuth } from '../../hooks'
-import { talentDashboardClient } from '../../api'
+import { talentDashboardClient, talentEmploymentClient } from '../../api'
 import type { InferResponseType } from 'hono'
 import { generateMockAttendanceData, getCurrentYearMonth } from '../../utils/mockAttendanceData'
+import type { SalaryRecord } from '../../types/employment'
 
 /**
  * 人才小程序首页/个人主页
@@ -88,6 +90,27 @@ const Dashboard: React.FC = () => {
     return generateMockAttendanceData(year, month)
   })()
 
+  // 获取上个月的薪资记录(用于首页显示)
+  const { data: lastMonthSalaryData } = useQuery({
+    queryKey: ['last-month-salary'],
+    queryFn: async () => {
+      // 计算上个月份
+      const now = new Date()
+      const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)
+      const monthStr = `${lastMonth.getFullYear()}-${String(lastMonth.getMonth() + 1).padStart(2, '0')}`
+
+      const res = await talentEmploymentClient.employment['salary-records'].$get({
+        query: { month: monthStr, take: 1 }
+      })
+      if (!res.ok) {
+        throw new Error('获取薪资记录失败')
+      }
+      const data = await res.json()
+      return (data.data || []) as SalaryRecord[]
+    },
+    staleTime: 10 * 60 * 1000, // 10分钟缓存
+  })
+
   // 模拟打卡数据
   const clockInData: ClockInData = {
     status: '已打卡',
@@ -235,8 +258,12 @@ const Dashboard: React.FC = () => {
                 <Text className="text-white/80 text-xs">异常记录</Text>
               </View>
               <View className="flex flex-col items-center">
-                <Text className="text-white text-2xl font-bold">¥4,800</Text>
-                <Text className="text-white/80 text-xs">本月薪资</Text>
+                <Text className="text-white text-2xl font-bold">
+                  {lastMonthSalaryData && lastMonthSalaryData.length > 0
+                    ? `¥${(lastMonthSalaryData[0].salaryAmount || 0).toLocaleString()}`
+                    : '--'}
+                </Text>
+                <Text className="text-white/80 text-xs">上月薪资</Text>
               </View>
             </View>
           </View>

+ 35 - 3
mini-talent/src/utils/mockAttendanceData.ts

@@ -45,9 +45,38 @@ function isWeekend(date: Date): boolean {
   return day === 0 || day === 6
 }
 
+/**
+ * 计算从月初到指定日期的工作日天数(排除周末)
+ * @param year 年份
+ * @param month 月份 (1-12)
+ * @param day 截止日期,默认为当天
+ * @returns 工作日天数
+ */
+function calculateWorkDaysUpTo(year: number, month: number, day: number): number {
+  const today = new Date()
+  const targetDay = day || today.getDate()
+  const targetMonth = month || today.getMonth() + 1
+  const targetYear = year || today.getFullYear()
+
+  // 如果查询的是当前月份,只计算到今天
+  const endDate = targetYear === today.getFullYear() && targetMonth === today.getMonth() + 1
+    ? Math.min(targetDay, today.getDate())
+    : targetDay
+
+  let workDays = 0
+  for (let d = 1; d <= endDate; d++) {
+    const date = new Date(targetYear, targetMonth - 1, d)
+    if (!isWeekend(date)) {
+      workDays++
+    }
+  }
+
+  return workDays
+}
+
 /**
  * 生成指定月份的模拟考勤数据
- * 使用固定值:22天正常出勤,100%出勤率,无迟到早退
+ * 修复:正常出勤天数改为截止至今天的实际工作日天数
  * @param year 年份
  * @param month 月份 (1-12)
  * @returns 考勤统计数据和打卡记录
@@ -59,6 +88,9 @@ export function generateMockAttendanceData(year: number, month: number): {
   const dates = getDatesInMonth(year, month)
   const records: AttendanceRecord[] = []
 
+  // 计算截止到今天的实际工作日天数
+  const normalDays = calculateWorkDaysUpTo(year, month, new Date().getDate())
+
   // 从后向前生成记录(倒序)
   for (let i = dates.length - 1; i >= 0; i--) {
     const date = dates[i]
@@ -86,10 +118,10 @@ export function generateMockAttendanceData(year: number, month: number): {
     }
   }
 
-  // 固定统计数据:符合业务需求
+  // 统计数据:使用实际计算的工作日天数
   const stats: AttendanceStats = {
     attendanceRate: 100,
-    normalDays: 22,
+    normalDays,
     lateCount: 0,
     earlyLeaveCount: 0,
     absentCount: 0

+ 45 - 10
mini/src/pages/yongren/statistics/index.tsx

@@ -42,26 +42,56 @@ const extractProvinceFromKey = (key: string): string => {
   return parts[0] || key
 }
 
+/**
+ * 合并相同省份的数据,解决"两个山东"的问题
+ * 例如:"山东-济南" 和 "山东-青岛" 合并为 "山东",人数相加
+ */
+const mergeProvinceData = (stats: any[]) => {
+  const provinceMap = new Map<string, number>()
+
+  stats.forEach(item => {
+    // 确定省份名称:优先使用 province,如果是数字则从 key 提取
+    let provinceName: string
+    if (item.province && !isNumericString(item.province)) {
+      provinceName = item.province
+    } else if (item.key) {
+      provinceName = extractProvinceFromKey(item.key)
+    } else {
+      provinceName = item.province || item.key || '未知'
+    }
+
+    // 累加人数(确保取整)
+    const count = Math.round(item.value || 0)
+    provinceMap.set(provinceName, (provinceMap.get(provinceName) || 0) + count)
+  })
+
+  // 转换回数组格式
+  return Array.from(provinceMap.entries()).map(([province, value]) => ({
+    key: province,
+    province,
+    value
+  }))
+}
+
 /**
  * 数据转换工具:将API统计数据转换为柱状图格式
  * 优先使用中文名称的 province,如果是数字ID则从 key 提取
+ * 修复:合并相同省份的数据,人数显示为整数
  */
 const convertToColumnData = (stats: any[]) => {
   console.debug('[convertToColumnData] Input stats:', JSON.stringify(stats))
+
+  // 先合并相同省份的数据
+  const mergedStats = mergeProvinceData(stats)
+
   const result = {
-    categories: stats.map(item => {
-      // 优先使用 province,但如果 province 是纯数字,则从 key 提取省份名称
-      if (item.province && !isNumericString(item.province)) {
-        return item.province
-      }
-      // 如果 province 是数字或不存在,从 key 提取
-      return item.key ? extractProvinceFromKey(item.key) : item.province || item.key
-    }),
+    categories: mergedStats.map(item => item.province),
     series: [{
       name: '人数',
-      data: stats.map(item => item.value || 0)
+      data: mergedStats.map(item => item.value)
     }]
   }
+  console.debug('[convertToColumnData] Merged stats:', JSON.stringify(mergedStats))
   console.debug('[convertToColumnData] Output categories:', result.categories)
   console.debug('[convertToColumnData] Output series:', result.series)
   return result
@@ -69,11 +99,12 @@ const convertToColumnData = (stats: any[]) => {
 
 /**
  * 数据转换工具:将API统计数据转换为饼图格式
+ * 修复:人数显示为整数
  */
 const convertToPieData = (stats: any[]) =>
   stats.map(item => ({
     name: item.key,
-    data: item.value || 0
+    data: Math.round(item.value || 0)
   }))
 
 export interface StatisticsProps {
@@ -385,6 +416,7 @@ const Statistics: React.FC<StatisticsProps> = () => {
                     series={convertToPieData(ageStats)}
                     config={{
                       color: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444'],
+                      dataLabel: true,
                     }}
                   />
                 </View>
@@ -426,6 +458,7 @@ const Statistics: React.FC<StatisticsProps> = () => {
                       series={chartData.series}
                       config={{
                         color: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#ec4899'],
+                        dataLabel: true,
                       }}
                     />
                   </View>
@@ -453,6 +486,7 @@ const Statistics: React.FC<StatisticsProps> = () => {
                       series={convertToPieData(stats)}
                       config={{
                         color: ['#3b82f6', '#f59e0b', '#ef4444', '#10b981'],
+                        dataLabel: true,
                         legend: {
                           show: true,
                           position: "right",
@@ -487,6 +521,7 @@ const Statistics: React.FC<StatisticsProps> = () => {
                       series={chartData.series}
                       config={{
                         color: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444'],
+                        dataLabel: true,
                       }}
                     />
                   </View>