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
|
|||
|
|
}
|