init
This commit is contained in:
432
serve/internal/services/reading_service.go
Normal file
432
serve/internal/services/reading_service.go
Normal file
@@ -0,0 +1,432 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user