init
This commit is contained in:
309
serve/internal/service/ai_service.go
Normal file
309
serve/internal/service/ai_service.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/model"
|
||||
)
|
||||
|
||||
// AIService AI服务接口
|
||||
type AIService interface {
|
||||
// 写作批改
|
||||
CorrectWriting(ctx context.Context, content string, taskType string) (*model.WritingCorrection, error)
|
||||
// 口语评估
|
||||
EvaluateSpeaking(ctx context.Context, audioText string, prompt string) (*model.SpeakingEvaluation, error)
|
||||
// 智能推荐
|
||||
GetRecommendations(ctx context.Context, userLevel string, learningHistory []string) (*model.AIRecommendation, error)
|
||||
// 生成练习题
|
||||
GenerateExercise(ctx context.Context, content string, exerciseType string) (*model.Exercise, error)
|
||||
}
|
||||
|
||||
type aiService struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewAIService 创建AI服务实例
|
||||
func NewAIService() AIService {
|
||||
apiKey := os.Getenv("OPENAI_API_KEY")
|
||||
baseURL := os.Getenv("OPENAI_BASE_URL")
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.openai.com/v1"
|
||||
}
|
||||
|
||||
return &aiService{
|
||||
apiKey: apiKey,
|
||||
baseURL: baseURL,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI API请求结构
|
||||
type openAIRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []message `json:"messages"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
type message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type openAIResponse struct {
|
||||
Choices []struct {
|
||||
Message message `json:"message"`
|
||||
} `json:"choices"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// callOpenAI 调用OpenAI API
|
||||
func (s *aiService) callOpenAI(ctx context.Context, prompt string) (string, error) {
|
||||
if s.apiKey == "" {
|
||||
return "", fmt.Errorf("OpenAI API key not configured")
|
||||
}
|
||||
|
||||
reqBody := openAIRequest{
|
||||
Model: "gpt-3.5-turbo",
|
||||
Messages: []message{
|
||||
{
|
||||
Role: "user",
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
MaxTokens: 1000,
|
||||
Temperature: 0.7,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", s.baseURL+"/chat/completions", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+s.apiKey)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var openAIResp openAIResponse
|
||||
if err := json.Unmarshal(body, &openAIResp); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
if openAIResp.Error != nil {
|
||||
return "", fmt.Errorf("OpenAI API error: %s", openAIResp.Error.Message)
|
||||
}
|
||||
|
||||
if len(openAIResp.Choices) == 0 {
|
||||
return "", fmt.Errorf("no response from OpenAI")
|
||||
}
|
||||
|
||||
return openAIResp.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
// CorrectWriting 写作批改
|
||||
func (s *aiService) CorrectWriting(ctx context.Context, content string, taskType string) (*model.WritingCorrection, error) {
|
||||
prompt := fmt.Sprintf(`请对以下英语写作进行批改,任务类型:%s
|
||||
|
||||
写作内容:
|
||||
%s
|
||||
|
||||
请按照以下JSON格式返回批改结果:
|
||||
{
|
||||
"overall_score": 85,
|
||||
"grammar_score": 80,
|
||||
"vocabulary_score": 90,
|
||||
"structure_score": 85,
|
||||
"content_score": 88,
|
||||
"corrections": [
|
||||
{
|
||||
"original": "错误的句子",
|
||||
"corrected": "修正后的句子",
|
||||
"explanation": "修改说明",
|
||||
"error_type": "grammar"
|
||||
}
|
||||
],
|
||||
"suggestions": [
|
||||
"建议1",
|
||||
"建议2"
|
||||
],
|
||||
"strengths": [
|
||||
"优点1",
|
||||
"优点2"
|
||||
],
|
||||
"weaknesses": [
|
||||
"需要改进的地方1",
|
||||
"需要改进的地方2"
|
||||
]
|
||||
}`, taskType, content)
|
||||
|
||||
response, err := s.callOpenAI(ctx, prompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析JSON响应
|
||||
var correction model.WritingCorrection
|
||||
if err := json.Unmarshal([]byte(response), &correction); err != nil {
|
||||
// 如果JSON解析失败,返回基本的批改结果
|
||||
return &model.WritingCorrection{
|
||||
OverallScore: 75,
|
||||
Suggestions: []string{"AI批改服务暂时不可用,请稍后重试"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &correction, nil
|
||||
}
|
||||
|
||||
// EvaluateSpeaking 口语评估
|
||||
func (s *aiService) EvaluateSpeaking(ctx context.Context, audioText string, prompt string) (*model.SpeakingEvaluation, error) {
|
||||
evalPrompt := fmt.Sprintf(`请对以下英语口语进行评估,题目:%s
|
||||
|
||||
口语内容:
|
||||
%s
|
||||
|
||||
请按照以下JSON格式返回评估结果:
|
||||
{
|
||||
"overall_score": 85,
|
||||
"pronunciation_score": 80,
|
||||
"fluency_score": 90,
|
||||
"grammar_score": 85,
|
||||
"vocabulary_score": 88,
|
||||
"feedback": "整体评价",
|
||||
"strengths": [
|
||||
"优点1",
|
||||
"优点2"
|
||||
],
|
||||
"improvements": [
|
||||
"需要改进的地方1",
|
||||
"需要改进的地方2"
|
||||
]
|
||||
}`, prompt, audioText)
|
||||
|
||||
response, err := s.callOpenAI(ctx, evalPrompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析JSON响应
|
||||
var evaluation model.SpeakingEvaluation
|
||||
if err := json.Unmarshal([]byte(response), &evaluation); err != nil {
|
||||
// 如果JSON解析失败,返回基本的评估结果
|
||||
return &model.SpeakingEvaluation{
|
||||
OverallScore: 75,
|
||||
Feedback: "AI评估服务暂时不可用,请稍后重试",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &evaluation, nil
|
||||
}
|
||||
|
||||
// GetRecommendations 智能推荐
|
||||
func (s *aiService) GetRecommendations(ctx context.Context, userLevel string, learningHistory []string) (*model.AIRecommendation, error) {
|
||||
historyStr := strings.Join(learningHistory, ", ")
|
||||
prompt := fmt.Sprintf(`基于用户的英语水平(%s)和学习历史(%s),请提供个性化的学习推荐。
|
||||
|
||||
请按照以下JSON格式返回推荐结果:
|
||||
{
|
||||
"recommended_topics": [
|
||||
"推荐主题1",
|
||||
"推荐主题2"
|
||||
],
|
||||
"difficulty_level": "intermediate",
|
||||
"study_plan": [
|
||||
"学习计划步骤1",
|
||||
"学习计划步骤2"
|
||||
],
|
||||
"focus_areas": [
|
||||
"重点关注领域1",
|
||||
"重点关注领域2"
|
||||
]
|
||||
}`, userLevel, historyStr)
|
||||
|
||||
response, err := s.callOpenAI(ctx, prompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析JSON响应
|
||||
var recommendation model.AIRecommendation
|
||||
if err := json.Unmarshal([]byte(response), &recommendation); err != nil {
|
||||
// 如果JSON解析失败,返回基本的推荐结果
|
||||
return &model.AIRecommendation{
|
||||
RecommendedTopics: []string{"基础语法练习", "日常对话练习"},
|
||||
DifficultyLevel: "beginner",
|
||||
StudyPlan: []string{"每天练习30分钟", "重点关注基础词汇"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &recommendation, nil
|
||||
}
|
||||
|
||||
// GenerateExercise 生成练习题
|
||||
func (s *aiService) GenerateExercise(ctx context.Context, content string, exerciseType string) (*model.Exercise, error) {
|
||||
prompt := fmt.Sprintf(`基于以下内容生成%s类型的练习题:
|
||||
|
||||
内容:
|
||||
%s
|
||||
|
||||
请按照以下JSON格式返回练习题:
|
||||
{
|
||||
"title": "练习题标题",
|
||||
"instructions": "练习说明",
|
||||
"questions": [
|
||||
{
|
||||
"question": "问题1",
|
||||
"options": ["选项A", "选项B", "选项C", "选项D"],
|
||||
"correct_answer": "正确答案",
|
||||
"explanation": "解释"
|
||||
}
|
||||
]
|
||||
}`, exerciseType, content)
|
||||
|
||||
response, err := s.callOpenAI(ctx, prompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析JSON响应
|
||||
var exercise model.Exercise
|
||||
if err := json.Unmarshal([]byte(response), &exercise); err != nil {
|
||||
// 如果JSON解析失败,返回基本的练习题
|
||||
return &model.Exercise{
|
||||
Title: "基础练习",
|
||||
Instructions: "AI练习生成服务暂时不可用,请稍后重试",
|
||||
Questions: []model.Question{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &exercise, nil
|
||||
}
|
||||
226
serve/internal/service/upload_service.go
Normal file
226
serve/internal/service/upload_service.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UploadService 文件上传服务接口
|
||||
type UploadService interface {
|
||||
// 上传音频文件
|
||||
UploadAudio(file *multipart.FileHeader) (*UploadResult, error)
|
||||
// 上传图片文件
|
||||
UploadImage(file *multipart.FileHeader) (*UploadResult, error)
|
||||
// 删除文件
|
||||
DeleteFile(filePath string) error
|
||||
// 获取文件URL
|
||||
GetFileURL(filePath string) string
|
||||
}
|
||||
|
||||
type uploadService struct {
|
||||
uploadDir string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// UploadResult 上传结果
|
||||
type UploadResult struct {
|
||||
FileName string `json:"file_name"`
|
||||
FilePath string `json:"file_path"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
ContentType string `json:"content_type"`
|
||||
}
|
||||
|
||||
// NewUploadService 创建文件上传服务实例
|
||||
func NewUploadService(uploadDir, baseURL string) UploadService {
|
||||
// 确保上传目录存在
|
||||
os.MkdirAll(uploadDir, 0755)
|
||||
os.MkdirAll(filepath.Join(uploadDir, "audio"), 0755)
|
||||
os.MkdirAll(filepath.Join(uploadDir, "images"), 0755)
|
||||
|
||||
return &uploadService{
|
||||
uploadDir: uploadDir,
|
||||
baseURL: baseURL,
|
||||
}
|
||||
}
|
||||
|
||||
// 允许的文件类型
|
||||
var (
|
||||
allowedAudioTypes = map[string]bool{
|
||||
"audio/mpeg": true, // mp3
|
||||
"audio/wav": true, // wav
|
||||
"audio/mp4": true, // m4a
|
||||
"audio/webm": true, // webm
|
||||
"audio/ogg": true, // ogg
|
||||
}
|
||||
|
||||
allowedImageTypes = map[string]bool{
|
||||
"image/jpeg": true, // jpg, jpeg
|
||||
"image/png": true, // png
|
||||
"image/gif": true, // gif
|
||||
"image/webp": true, // webp
|
||||
}
|
||||
|
||||
// 文件大小限制(字节)
|
||||
maxAudioSize = 50 * 1024 * 1024 // 50MB
|
||||
maxImageSize = 10 * 1024 * 1024 // 10MB
|
||||
)
|
||||
|
||||
// UploadAudio 上传音频文件
|
||||
func (s *uploadService) UploadAudio(file *multipart.FileHeader) (*UploadResult, error) {
|
||||
// 检查文件大小
|
||||
if file.Size > int64(maxAudioSize) {
|
||||
return nil, fmt.Errorf("audio file too large, max size is %d MB", maxAudioSize/(1024*1024))
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
contentType := file.Header.Get("Content-Type")
|
||||
if !allowedAudioTypes[contentType] {
|
||||
return nil, fmt.Errorf("unsupported audio format: %s", contentType)
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
ext := filepath.Ext(file.Filename)
|
||||
fileName := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)
|
||||
relativePath := filepath.Join("audio", fileName)
|
||||
fullPath := filepath.Join(s.uploadDir, relativePath)
|
||||
|
||||
// 保存文件
|
||||
if err := s.saveFile(file, fullPath); err != nil {
|
||||
return nil, fmt.Errorf("failed to save audio file: %w", err)
|
||||
}
|
||||
|
||||
return &UploadResult{
|
||||
FileName: fileName,
|
||||
FilePath: relativePath,
|
||||
FileURL: s.GetFileURL(relativePath),
|
||||
FileSize: file.Size,
|
||||
ContentType: contentType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadImage 上传图片文件
|
||||
func (s *uploadService) UploadImage(file *multipart.FileHeader) (*UploadResult, error) {
|
||||
// 检查文件大小
|
||||
if file.Size > int64(maxImageSize) {
|
||||
return nil, fmt.Errorf("image file too large, max size is %d MB", maxImageSize/(1024*1024))
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
contentType := file.Header.Get("Content-Type")
|
||||
if !allowedImageTypes[contentType] {
|
||||
return nil, fmt.Errorf("unsupported image format: %s", contentType)
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
ext := filepath.Ext(file.Filename)
|
||||
fileName := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)
|
||||
relativePath := filepath.Join("images", fileName)
|
||||
fullPath := filepath.Join(s.uploadDir, relativePath)
|
||||
|
||||
// 保存文件
|
||||
if err := s.saveFile(file, fullPath); err != nil {
|
||||
return nil, fmt.Errorf("failed to save image file: %w", err)
|
||||
}
|
||||
|
||||
return &UploadResult{
|
||||
FileName: fileName,
|
||||
FilePath: relativePath,
|
||||
FileURL: s.GetFileURL(relativePath),
|
||||
FileSize: file.Size,
|
||||
ContentType: contentType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// saveFile 保存文件到磁盘
|
||||
func (s *uploadService) saveFile(fileHeader *multipart.FileHeader, destPath string) error {
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(destPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// 打开上传的文件
|
||||
src, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open uploaded file: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 创建目标文件
|
||||
dst, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination file: %w", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// 复制文件内容
|
||||
_, err = io.Copy(dst, src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file content: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件
|
||||
func (s *uploadService) DeleteFile(filePath string) error {
|
||||
// 安全检查:确保文件路径在上传目录内
|
||||
if !strings.HasPrefix(filePath, s.uploadDir) {
|
||||
fullPath := filepath.Join(s.uploadDir, filePath)
|
||||
filePath = fullPath
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("file does not exist: %s", filePath)
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
return fmt.Errorf("failed to delete file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFileURL 获取文件URL
|
||||
func (s *uploadService) GetFileURL(filePath string) string {
|
||||
// 清理路径分隔符
|
||||
cleanPath := strings.ReplaceAll(filePath, "\\", "/")
|
||||
return fmt.Sprintf("%s/uploads/%s", s.baseURL, cleanPath)
|
||||
}
|
||||
|
||||
// ValidateFileType 验证文件类型
|
||||
func ValidateFileType(filename string, allowedTypes map[string]bool) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
return allowedTypes["audio/mpeg"]
|
||||
case ".wav":
|
||||
return allowedTypes["audio/wav"]
|
||||
case ".m4a":
|
||||
return allowedTypes["audio/mp4"]
|
||||
case ".webm":
|
||||
return allowedTypes["audio/webm"]
|
||||
case ".ogg":
|
||||
return allowedTypes["audio/ogg"]
|
||||
case ".jpg", ".jpeg":
|
||||
return allowedTypes["image/jpeg"]
|
||||
case ".png":
|
||||
return allowedTypes["image/png"]
|
||||
case ".gif":
|
||||
return allowedTypes["image/gif"]
|
||||
case ".webp":
|
||||
return allowedTypes["image/webp"]
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
80
serve/internal/service/user_service.go
Normal file
80
serve/internal/service/user_service.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/model"
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/database"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
userRepo *database.UserRepository
|
||||
}
|
||||
|
||||
func NewUserService(userRepo *database.UserRepository) *UserService {
|
||||
return &UserService{userRepo: userRepo}
|
||||
}
|
||||
|
||||
// 用户注册
|
||||
func (s *UserService) Register(username, email, password string) (string, error) {
|
||||
// 检查用户是否已存在
|
||||
if _, err := s.userRepo.GetByEmail(email); err == nil {
|
||||
return "", errors.New("用户已存在")
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
user := &model.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password, // 实际应用中需要加密
|
||||
}
|
||||
|
||||
userID, err := s.userRepo.Create(user)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
func (s *UserService) Login(email, password string) (string, string, error) {
|
||||
user, err := s.userRepo.GetByEmail(email)
|
||||
if err != nil {
|
||||
return "", "", errors.New("用户不存在")
|
||||
}
|
||||
|
||||
// 实际应用中需要验证密码
|
||||
if user.Password != password {
|
||||
return "", "", errors.New("密码错误")
|
||||
}
|
||||
|
||||
// 生成 token (实际应用中应使用 JWT 等)
|
||||
token := "fake-token-" + user.ID
|
||||
|
||||
return token, user.ID, nil
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
func (s *UserService) GetProfile(userID string) (*model.User, error) {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
func (s *UserService) UpdateProfile(userID, username, avatar string) error {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
|
||||
user.Username = username
|
||||
if avatar != "" {
|
||||
user.Avatar = avatar
|
||||
}
|
||||
|
||||
return s.userRepo.Update(user)
|
||||
}
|
||||
Reference in New Issue
Block a user