package service import ( "fmt" "gorm.io/gorm" "psi/database" "psi/models" "psi/utils" "time" ) type StatistTaskService struct{} // GenerateDailyStat 生成每日统计数据(定时任务调用) // GenerateDailyStat 生成每日统计数据(定时任务调用) // GenerateDailyStat 生成每日统计数据(定时任务调用) func (s *StatistTaskService) GenerateDailyStat(db ...*gorm.DB) error { now := time.Now() yesterday := now.AddDate(0, 0, -1) statDateStr := yesterday.Format("20060102") var statDate int64 if _, err := fmt.Sscanf(statDateStr, "%d", &statDate); err != nil { return fmt.Errorf("日期格式化失败: %v", err) } yesterdayStart := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, yesterday.Location()).Unix() yesterdayEnd := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 23, 59, 59, 0, yesterday.Location()).Unix() mainDB := database.DB if len(db) > 0 && db[0] != nil { mainDB = db[0] } var aboutIDs []int64 if err := mainDB.Model(&models.Employee{}).Where("deleted_at = ?", 0).Distinct().Pluck("about_id", &aboutIDs).Error; err != nil { return fmt.Errorf("查询租户列表失败: %v", err) } if len(aboutIDs) == 0 { utils.InfoLog("work", map[string]interface{}{ "source": "定时任务-生成每日统计", "stat_date": statDate, "message": "没有找到任何租户", }) return nil } for _, aboutID := range aboutIDs { if aboutID == 0 { continue } tenantDB, err := database.GetTenantDB(aboutID) if err != nil { utils.ErrorLog("work", map[string]interface{}{ "source": "定时任务-生成每日统计", "about_id": aboutID, "error": fmt.Sprintf("获取租户数据库失败: %v", err), }) continue } tx := tenantDB.Begin() func() { defer func() { if r := recover(); r != nil { tx.Rollback() } }() _, err := s.generateDashboardDailyStat(tx, statDate, yesterdayStart, yesterdayEnd) if err != nil { tx.Rollback() utils.ErrorLog("work", map[string]interface{}{ "source": "定时任务-生成每日统计", "about_id": aboutID, "stat_date": statDate, "error": fmt.Sprintf("生成全局统计失败: %v", err), }) return } if err := s.generateUserDailyStat(tx, mainDB, aboutID, statDate, yesterdayStart, yesterdayEnd); err != nil { tx.Rollback() utils.ErrorLog("work", map[string]interface{}{ "source": "定时任务-生成每日统计", "about_id": aboutID, "stat_date": statDate, "error": fmt.Sprintf("生成用户统计失败: %v", err), }) return } if err := tx.Commit().Error; err != nil { utils.ErrorLog("work", map[string]interface{}{ "source": "定时任务-生成每日统计", "about_id": aboutID, "stat_date": statDate, "error": fmt.Sprintf("提交事务失败: %v", err), }) return } utils.InfoLog("work", map[string]interface{}{ "source": "定时任务-生成每日统计", "about_id": aboutID, "stat_date": statDate, "message": fmt.Sprintf("成功生成%s的统计数据", yesterday.Format("2006-01-02")), }) }() } return nil } // generateDashboardDailyStat 生成全局每日统计 func (s *StatistTaskService) generateDashboardDailyStat(tx *gorm.DB, statDate int64, startDate, endDate int64) (*models.DashboardDailyStat, error) { var existingStat models.DashboardDailyStat err := tx.Where("stat_date = ? AND is_del = ?", statDate, 0).First(&existingStat).Error if err == nil { if err := tx.Model(&models.DashboardDailyStat{}).Where("id = ?", existingStat.ID).Update("is_del", 1).Error; err != nil { return nil, err } } // statist.stat_date 存储 YYYYMMDD 格式 int64(如 20260626),非 Unix 时间戳 // statDate 已经是 YYYYMMDD 格式,直接用于查询 // 昨天的 YYYYMMDD = statDate;前天需从 startDate(Unix时间戳)反算 yesterdayTime := time.Unix(startDate, 0) dayBeforeTime := yesterdayTime.AddDate(0, 0, -1) dayBeforeStatDateStr := fmt.Sprintf("%04d%02d%02d", dayBeforeTime.Year(), dayBeforeTime.Month(), dayBeforeTime.Day()) var dayBeforeStatDate int64 fmt.Sscanf(dayBeforeStatDateStr, "%d", &dayBeforeStatDate) // 前天的 Unix 时间戳范围(用于 sales_order 查询,sales_order.created_at 存储 Unix 时间戳) dayBeforeStart := time.Date(dayBeforeTime.Year(), dayBeforeTime.Month(), dayBeforeTime.Day(), 0, 0, 0, 0, dayBeforeTime.Location()).Unix() dayBeforeEnd := time.Date(dayBeforeTime.Year(), dayBeforeTime.Month(), dayBeforeTime.Day(), 23, 59, 59, 0, dayBeforeTime.Location()).Unix() var totalReceivingNum, totalOutboundNum int64 tx.Model(&models.Statist{}). Where("stat_date <= ? AND is_del = ?", statDate, 0). Select("COALESCE(SUM(receiving_num), 0), COALESCE(SUM(outbound_num), 0)"). Row().Scan(&totalReceivingNum, &totalOutboundNum) var todayInbound, todayOutbound int64 tx.Model(&models.Statist{}). Where("stat_date = ? AND is_del = ?", statDate, 0). Select("COALESCE(SUM(receiving_num), 0), COALESCE(SUM(outbound_num), 0)"). Row().Scan(&todayInbound, &todayOutbound) var yesterdayInbound, yesterdayOutbound int64 tx.Model(&models.Statist{}). Where("stat_date = ? AND is_del = ?", dayBeforeStatDate, 0). Select("COALESCE(SUM(receiving_num), 0), COALESCE(SUM(outbound_num), 0)"). Row().Scan(&yesterdayInbound, &yesterdayOutbound) var totalSalesCount int64 tx.Model(&models.SalesOrder{}). Where("created_at <= ? AND is_del = ?", endDate, 0). Count(&totalSalesCount) var todaySalesCount int64 tx.Model(&models.SalesOrder{}). Where("created_at >= ? AND created_at <= ? AND is_del = ?", startDate, endDate, 0). Count(&todaySalesCount) var yesterdaySalesCount int64 tx.Model(&models.SalesOrder{}). Where("created_at >= ? AND created_at <= ? AND is_del = ?", dayBeforeStart, dayBeforeEnd, 0). Count(&yesterdaySalesCount) var productTotal int64 tx.Model(&models.Product{}). Where("is_del = ?", 0). Count(&productTotal) var inventoryTotal int64 tx.Model(&models.Inventory{}). Where("is_del = ?", 0). Select("COALESCE(SUM(quantity), 0)"). Row().Scan(&inventoryTotal) now := time.Now().Unix() dashboardStat := &models.DashboardDailyStat{ StatDate: statDate, TotalReceivingNum: totalReceivingNum, TotalOutboundNum: totalOutboundNum, TotalSalesCount: totalSalesCount, ProductTotal: productTotal, InventoryTotal: inventoryTotal, TodayInbound: todayInbound, TodayOutbound: todayOutbound, YesterdayInbound: yesterdayInbound, YesterdayOutbound: yesterdayOutbound, TodaySalesCount: todaySalesCount, YesterdaySalesCount: yesterdaySalesCount, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(dashboardStat).Error; err != nil { return nil, err } return dashboardStat, nil } // generateUserDailyStat 生成用户每日统计 // mainDB: 主库连接(查询 employee 表),tx: 租户事务(查询 statist/sales_order,写入 user_daily_stat) func (s *StatistTaskService) generateUserDailyStat(tx *gorm.DB, mainDB *gorm.DB, aboutID int64, statDate int64, startDate, endDate int64) error { // 1. 从主库查询该租户下的所有员工 type EmployeeBrief struct { UserID int64 `gorm:"column:user_id"` UserName string `gorm:"column:user_name"` } var employees []EmployeeBrief if err := mainDB.Model(&models.Employee{}). Where("about_id = ? AND deleted_at = ?", aboutID, 0). Select("id as user_id, username as user_name"). Find(&employees).Error; err != nil { return fmt.Errorf("查询员工列表失败: %v", err) } if len(employees) == 0 { return nil } // 2. 提取员工ID列表 userIDs := make([]int64, len(employees)) for i, emp := range employees { userIDs[i] = emp.UserID } // 3. 批量查询租户库的入库/出库统计 type UserReceivingStat struct { CreateBy int64 `gorm:"column:create_by"` ReceivingNum int64 `gorm:"column:receiving_num"` OutboundNum int64 `gorm:"column:outbound_num"` } var receivingStats []UserReceivingStat tx.Model(&models.Statist{}). Where("create_by IN ? AND stat_date = ? AND is_del = ?", userIDs, statDate, 0). Select("create_by, COALESCE(SUM(receiving_num), 0) as receiving_num, COALESCE(SUM(outbound_num), 0) as outbound_num"). Group("create_by"). Find(&receivingStats) receivingMap := make(map[int64]UserReceivingStat, len(receivingStats)) for _, rs := range receivingStats { receivingMap[rs.CreateBy] = rs } // 4. 写入 user_daily_stat(注:SalesCount=0,sales_order 表无 create_by 字段无法归因到员工) now := time.Now().Unix() for _, emp := range employees { // 软删除已有记录 tx.Model(&models.UserDailyStat{}). Where("user_id = ? AND stat_date = ? AND is_del = ?", emp.UserID, statDate, 0). Update("is_del", 1) receiving := receivingMap[emp.UserID] newStat := &models.UserDailyStat{ UserID: emp.UserID, UserName: emp.UserName, StatDate: statDate, ReceivingNum: receiving.ReceivingNum, OutboundNum: receiving.OutboundNum, SalesCount: 0, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(newStat).Error; err != nil { utils.ErrorLog("work", map[string]interface{}{ "source": "生成用户统计", "user_id": emp.UserID, "error": fmt.Sprintf("创建用户统计记录失败: %v", err), }) } } return nil }