432 lines
13 KiB
Go
432 lines
13 KiB
Go
package services
|
||
|
||
import (
|
||
"database/sql"
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||
"github.com/google/uuid"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ReadingService 阅读理解服务
|
||
type ReadingService struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewReadingService 创建阅读理解服务实例
|
||
func NewReadingService(db *gorm.DB) *ReadingService {
|
||
return &ReadingService{db: db}
|
||
}
|
||
|
||
// ===== 阅读材料管理 =====
|
||
|
||
// GetReadingMaterials 获取阅读材料列表
|
||
func (s *ReadingService) GetReadingMaterials(level, category string, page, pageSize int) ([]models.ReadingMaterial, int64, error) {
|
||
var materials []models.ReadingMaterial
|
||
var total int64
|
||
|
||
query := s.db.Model(&models.ReadingMaterial{}).Where("is_active = ?", true)
|
||
|
||
// 按难度级别筛选
|
||
if level != "" {
|
||
query = query.Where("level = ?", level)
|
||
}
|
||
|
||
// 按分类筛选
|
||
if category != "" {
|
||
query = query.Where("category = ?", category)
|
||
}
|
||
|
||
// 获取总数
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
// 分页查询
|
||
offset := (page - 1) * pageSize
|
||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&materials).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
return materials, total, nil
|
||
}
|
||
|
||
// GetReadingMaterial 获取单个阅读材料
|
||
func (s *ReadingService) GetReadingMaterial(id string) (*models.ReadingMaterial, error) {
|
||
var material models.ReadingMaterial
|
||
if err := s.db.Where("id = ? AND is_active = ?", id, true).First(&material).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.New("阅读材料不存在")
|
||
}
|
||
return nil, err
|
||
}
|
||
return &material, nil
|
||
}
|
||
|
||
// CreateReadingMaterial 创建阅读材料
|
||
func (s *ReadingService) CreateReadingMaterial(material *models.ReadingMaterial) error {
|
||
material.ID = uuid.New().String()
|
||
material.CreatedAt = time.Now()
|
||
material.UpdatedAt = time.Now()
|
||
material.IsActive = true
|
||
|
||
return s.db.Create(material).Error
|
||
}
|
||
|
||
// UpdateReadingMaterial 更新阅读材料
|
||
func (s *ReadingService) UpdateReadingMaterial(id string, updates map[string]interface{}) error {
|
||
updates["updated_at"] = time.Now()
|
||
result := s.db.Model(&models.ReadingMaterial{}).Where("id = ?", id).Updates(updates)
|
||
if result.Error != nil {
|
||
return result.Error
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New("阅读材料不存在")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// DeleteReadingMaterial 删除阅读材料(软删除)
|
||
func (s *ReadingService) DeleteReadingMaterial(id string) error {
|
||
result := s.db.Model(&models.ReadingMaterial{}).Where("id = ?", id).Update("is_active", false)
|
||
if result.Error != nil {
|
||
return result.Error
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New("阅读材料不存在")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SearchReadingMaterials 搜索阅读材料
|
||
func (s *ReadingService) SearchReadingMaterials(keyword string, level, category string, page, pageSize int) ([]models.ReadingMaterial, int64, error) {
|
||
var materials []models.ReadingMaterial
|
||
var total int64
|
||
|
||
query := s.db.Model(&models.ReadingMaterial{}).Where("is_active = ?", true)
|
||
|
||
// 关键词搜索
|
||
if keyword != "" {
|
||
query = query.Where("title LIKE ? OR content LIKE ? OR summary LIKE ?",
|
||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||
}
|
||
|
||
// 按难度级别筛选
|
||
if level != "" {
|
||
query = query.Where("level = ?", level)
|
||
}
|
||
|
||
// 按分类筛选
|
||
if category != "" {
|
||
query = query.Where("category = ?", category)
|
||
}
|
||
|
||
// 获取总数
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
// 分页查询
|
||
offset := (page - 1) * pageSize
|
||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&materials).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
return materials, total, nil
|
||
}
|
||
|
||
// ===== 阅读记录管理 =====
|
||
|
||
// CreateReadingRecord 创建阅读记录
|
||
func (s *ReadingService) CreateReadingRecord(record *models.ReadingRecord) error {
|
||
record.ID = uuid.New().String()
|
||
record.StartedAt = time.Now()
|
||
record.CreatedAt = time.Now()
|
||
record.UpdatedAt = time.Now()
|
||
|
||
return s.db.Create(record).Error
|
||
}
|
||
|
||
// UpdateReadingRecord 更新阅读记录
|
||
func (s *ReadingService) UpdateReadingRecord(id string, updates map[string]interface{}) error {
|
||
updates["updated_at"] = time.Now()
|
||
result := s.db.Model(&models.ReadingRecord{}).Where("id = ?", id).Updates(updates)
|
||
if result.Error != nil {
|
||
return result.Error
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New("阅读记录不存在")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetUserReadingRecords 获取用户阅读记录
|
||
func (s *ReadingService) GetUserReadingRecords(userID string, page, pageSize int) ([]models.ReadingRecord, int64, error) {
|
||
var records []models.ReadingRecord
|
||
var total int64
|
||
|
||
query := s.db.Model(&models.ReadingRecord{}).Where("user_id = ?", userID)
|
||
|
||
// 获取总数
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
// 分页查询,预加载材料信息
|
||
offset := (page - 1) * pageSize
|
||
if err := query.Preload("Material").Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
return records, total, nil
|
||
}
|
||
|
||
// GetReadingRecord 获取单个阅读记录
|
||
func (s *ReadingService) GetReadingRecord(id string) (*models.ReadingRecord, error) {
|
||
var record models.ReadingRecord
|
||
if err := s.db.Preload("Material").Where("id = ?", id).First(&record).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.New("阅读记录不存在")
|
||
}
|
||
return nil, err
|
||
}
|
||
return &record, nil
|
||
}
|
||
|
||
// GetReadingProgress 获取用户对特定材料的阅读进度
|
||
func (s *ReadingService) GetReadingProgress(userID, materialID string) (*models.ReadingRecord, error) {
|
||
var record models.ReadingRecord
|
||
if err := s.db.Where("user_id = ? AND material_id = ?", userID, materialID).First(&record).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, nil // 没有阅读记录
|
||
}
|
||
return nil, err
|
||
}
|
||
return &record, nil
|
||
}
|
||
|
||
// ===== 阅读统计 =====
|
||
|
||
// ReadingStats 阅读统计结构
|
||
type ReadingStats struct {
|
||
TotalMaterials int64 `json:"total_materials"` // 总阅读材料数
|
||
CompletedMaterials int64 `json:"completed_materials"` // 已完成材料数
|
||
TotalReadingTime int64 `json:"total_reading_time"` // 总阅读时间(秒)
|
||
AverageScore float64 `json:"average_score"` // 平均理解得分
|
||
AverageSpeed float64 `json:"average_speed"` // 平均阅读速度(词/分钟)
|
||
ContinuousDays int `json:"continuous_days"` // 连续阅读天数
|
||
LevelStats []LevelStat `json:"level_stats"` // 各难度级别统计
|
||
}
|
||
|
||
// LevelStat 难度级别统计
|
||
type LevelStat struct {
|
||
Level string `json:"level"`
|
||
CompletedCount int64 `json:"completed_count"`
|
||
AverageScore float64 `json:"average_score"`
|
||
AverageSpeed float64 `json:"average_speed"`
|
||
}
|
||
|
||
// GetUserReadingStats 获取用户阅读统计
|
||
func (s *ReadingService) GetUserReadingStats(userID string) (*ReadingStats, error) {
|
||
stats := &ReadingStats{}
|
||
|
||
// 获取总阅读材料数
|
||
if err := s.db.Model(&models.ReadingMaterial{}).Where("is_active = ?", true).Count(&stats.TotalMaterials).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 获取已完成材料数
|
||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ? AND completed_at IS NOT NULL", userID).Count(&stats.CompletedMaterials).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 获取总阅读时间
|
||
var totalTime sql.NullInt64
|
||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ?", userID).Select("SUM(reading_time)").Scan(&totalTime).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
if totalTime.Valid {
|
||
stats.TotalReadingTime = totalTime.Int64
|
||
}
|
||
|
||
// 获取平均理解得分
|
||
var avgScore sql.NullFloat64
|
||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ? AND comprehension_score IS NOT NULL", userID).Select("AVG(comprehension_score)").Scan(&avgScore).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
if avgScore.Valid {
|
||
stats.AverageScore = avgScore.Float64
|
||
}
|
||
|
||
// 获取平均阅读速度
|
||
var avgSpeed sql.NullFloat64
|
||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ? AND reading_speed IS NOT NULL", userID).Select("AVG(reading_speed)").Scan(&avgSpeed).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
if avgSpeed.Valid {
|
||
stats.AverageSpeed = avgSpeed.Float64
|
||
}
|
||
|
||
// 计算连续阅读天数
|
||
continuousDays, err := s.calculateContinuousReadingDays(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
stats.ContinuousDays = continuousDays
|
||
|
||
// 获取各难度级别统计
|
||
levelStats, err := s.getLevelStats(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
stats.LevelStats = levelStats
|
||
|
||
return stats, nil
|
||
}
|
||
|
||
// calculateContinuousReadingDays 计算连续阅读天数
|
||
func (s *ReadingService) calculateContinuousReadingDays(userID string) (int, error) {
|
||
// 获取最近的阅读记录日期
|
||
var dates []time.Time
|
||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ?", userID).Select("DATE(created_at) as date").Group("DATE(created_at)").Order("date DESC").Limit(365).Scan(&dates).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
if len(dates) == 0 {
|
||
return 0, nil
|
||
}
|
||
|
||
// 计算连续天数
|
||
continuousDays := 1
|
||
today := time.Now().Truncate(24 * time.Hour)
|
||
lastDate := dates[0].Truncate(24 * time.Hour)
|
||
|
||
// 如果最后一次阅读不是今天或昨天,连续天数为0
|
||
if lastDate.Before(today.AddDate(0, 0, -1)) {
|
||
return 0, nil
|
||
}
|
||
|
||
for i := 1; i < len(dates); i++ {
|
||
currentDate := dates[i].Truncate(24 * time.Hour)
|
||
expectedDate := lastDate.AddDate(0, 0, -1)
|
||
|
||
if currentDate.Equal(expectedDate) {
|
||
continuousDays++
|
||
lastDate = currentDate
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
|
||
return continuousDays, nil
|
||
}
|
||
|
||
// getLevelStats 获取各难度级别统计
|
||
func (s *ReadingService) getLevelStats(userID string) ([]LevelStat, error) {
|
||
var levelStats []LevelStat
|
||
|
||
query := `
|
||
SELECT
|
||
m.level,
|
||
COUNT(r.id) as completed_count,
|
||
AVG(r.comprehension_score) as average_score,
|
||
AVG(r.reading_speed) as average_speed
|
||
FROM ai_reading_records r
|
||
JOIN ai_reading_materials m ON r.material_id = m.id
|
||
WHERE r.user_id = ? AND r.completed_at IS NOT NULL
|
||
GROUP BY m.level
|
||
`
|
||
|
||
if err := s.db.Raw(query, userID).Scan(&levelStats).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return levelStats, nil
|
||
}
|
||
|
||
// GetRecommendedMaterials 获取推荐阅读材料
|
||
func (s *ReadingService) GetRecommendedMaterials(userID string, limit int) ([]models.ReadingMaterial, error) {
|
||
// 获取用户最近的阅读记录,分析偏好
|
||
var userLevel string
|
||
var userCategory string
|
||
|
||
// 获取用户最常阅读的难度级别
|
||
if err := s.db.Raw(`
|
||
SELECT m.level
|
||
FROM ai_reading_records r
|
||
JOIN ai_reading_materials m ON r.material_id = m.id
|
||
WHERE r.user_id = ?
|
||
GROUP BY m.level
|
||
ORDER BY COUNT(*) DESC
|
||
LIMIT 1
|
||
`, userID).Scan(&userLevel).Error; err != nil {
|
||
userLevel = "intermediate" // 默认中级
|
||
}
|
||
|
||
// 获取用户最常阅读的分类
|
||
if err := s.db.Raw(`
|
||
SELECT m.category
|
||
FROM ai_reading_records r
|
||
JOIN ai_reading_materials m ON r.material_id = m.id
|
||
WHERE r.user_id = ?
|
||
GROUP BY m.category
|
||
ORDER BY COUNT(*) DESC
|
||
LIMIT 1
|
||
`, userID).Scan(&userCategory).Error; err != nil {
|
||
userCategory = "" // 不限制分类
|
||
}
|
||
|
||
// 获取用户未读过的材料
|
||
var materials []models.ReadingMaterial
|
||
query := s.db.Model(&models.ReadingMaterial{}).Where(`
|
||
is_active = ? AND id NOT IN (
|
||
SELECT material_id FROM ai_reading_records WHERE user_id = ?
|
||
)
|
||
`, true, userID)
|
||
|
||
// 优先推荐相同难度级别的材料
|
||
if userLevel != "" {
|
||
query = query.Where("level = ?", userLevel)
|
||
}
|
||
|
||
// 如果有偏好分类,优先推荐
|
||
if userCategory != "" {
|
||
query = query.Where("category = ?", userCategory)
|
||
}
|
||
|
||
if err := query.Order("created_at DESC").Limit(limit).Find(&materials).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 如果推荐材料不足,补充其他材料
|
||
if len(materials) < limit {
|
||
var additionalMaterials []models.ReadingMaterial
|
||
remaining := limit - len(materials)
|
||
|
||
// 获取已推荐材料的ID列表
|
||
excludeIDs := make([]string, len(materials))
|
||
for i, m := range materials {
|
||
excludeIDs[i] = m.ID
|
||
}
|
||
|
||
additionalQuery := s.db.Model(&models.ReadingMaterial{}).Where(`
|
||
is_active = ? AND id NOT IN (
|
||
SELECT material_id FROM ai_reading_records WHERE user_id = ?
|
||
)
|
||
`, true, userID)
|
||
|
||
if len(excludeIDs) > 0 {
|
||
additionalQuery = additionalQuery.Where("id NOT IN ?", excludeIDs)
|
||
}
|
||
|
||
if err := additionalQuery.Order("created_at DESC").Limit(remaining).Find(&additionalMaterials).Error; err != nil {
|
||
return materials, nil // 返回已有的推荐
|
||
}
|
||
|
||
materials = append(materials, additionalMaterials...)
|
||
}
|
||
|
||
return materials, nil
|
||
} |