This commit is contained in:
root
2026-07-04 07:42:19 +00:00
parent 68bb3c9167
commit 73d84169d6
6 changed files with 361 additions and 29 deletions
+65 -24
View File
@@ -56,14 +56,15 @@ type Question struct {
type DescriptiveGradingRecord struct {
ID int `json:"id"`
QuestionID int `json:"questionId"`
Source string `json:"source"`
UserAnswer string `json:"userAnswer"`
Score int `json:"score"`
Evaluation string `json:"evaluation"`
Username string `json:"username"`
CreatedAt string `json:"createdAt"`
ID int `json:"id"`
QuestionID int `json:"questionId"`
Source string `json:"source"`
UserAnswer string `json:"userAnswer"`
Score int `json:"score"`
Evaluation string `json:"evaluation"`
Username string `json:"username"`
CreatedAt string `json:"createdAt"`
OcrImageUrl string `json:"ocrImageUrl"`
}
@@ -227,6 +228,9 @@ func NewQuizService() *QuizService {
log.Fatalf("Failed to create DescriptiveGrading table: %v", err)
}
// Alter DescriptiveGrading to add ocr_image_url if it doesn't exist
_, _ = db.Exec(`ALTER TABLE DescriptiveGrading ADD COLUMN ocr_image_url TEXT DEFAULT ''`)
// Create GeminiKeys table if it doesn't exist
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS GeminiKeys (
@@ -838,7 +842,7 @@ func (s *QuizService) attachQuestionAssets(parts []genai.Part, source string, id
func (s *QuizService) GetDescriptiveGradingHistory(username string, source string, questionID int) []DescriptiveGradingRecord {
query := `
SELECT id, question_id, source, user_answer, score, evaluation, username, strftime('%Y-%m-%d %H:%M:%S', created_at, 'localtime') as created_at
SELECT id, question_id, source, user_answer, score, evaluation, username, strftime('%Y-%m-%d %H:%M:%S', created_at, 'localtime') as created_at, COALESCE(ocr_image_url, '') as ocr_image_url
FROM DescriptiveGrading
WHERE username = ? AND source = ? AND question_id = ?
ORDER BY created_at DESC
@@ -853,7 +857,7 @@ func (s *QuizService) GetDescriptiveGradingHistory(username string, source strin
var records []DescriptiveGradingRecord
for rows.Next() {
var r DescriptiveGradingRecord
err := rows.Scan(&r.ID, &r.QuestionID, &r.Source, &r.UserAnswer, &r.Score, &r.Evaluation, &r.Username, &r.CreatedAt)
err := rows.Scan(&r.ID, &r.QuestionID, &r.Source, &r.UserAnswer, &r.Score, &r.Evaluation, &r.Username, &r.CreatedAt, &r.OcrImageUrl)
if err != nil {
log.Printf("Scan error in GetDescriptiveGradingHistory: %v", err)
continue
@@ -863,7 +867,7 @@ func (s *QuizService) GetDescriptiveGradingHistory(username string, source strin
return records
}
func (s *QuizService) GradeDescriptiveAnswer(username string, source string, questionID int, userAnswer string) (*DescriptiveGradingRecord, error) {
func (s *QuizService) GradeDescriptiveAnswer(username string, source string, questionID int, userAnswer string, ocrImageBase64 string) (*DescriptiveGradingRecord, error) {
var qText, passage, modelAnswer string
queryQ := `SELECT question_text, passage, explanation FROM QuizQuestions WHERE source = ? AND id = ?`
err := s.db.QueryRow(queryQ, source, questionID).Scan(&qText, &passage, &modelAnswer)
@@ -879,6 +883,37 @@ func (s *QuizService) GradeDescriptiveAnswer(username string, source string, que
}
}
var ocrImageUrl string
if ocrImageBase64 != "" {
base64Data := ocrImageBase64
ext := "png"
if idx := strings.Index(base64Data, ";base64,"); idx != -1 {
header := base64Data[:idx]
if strings.Contains(header, "image/jpeg") || strings.Contains(header, "image/jpg") {
ext = "jpg"
} else if strings.Contains(header, "image/webp") {
ext = "webp"
}
base64Data = base64Data[idx+8:]
}
dec, err := base64.StdEncoding.DecodeString(base64Data)
if err == nil {
os.MkdirAll("assets/ocr_uploads", 0755)
safeSource := sanitizeFilename(source)
filename := fmt.Sprintf("ocr_%s_%s_%d_%d.%s", username, safeSource, questionID, time.Now().UnixNano(), ext)
localPath := filepath.Join("assets", "ocr_uploads", filename)
err = os.WriteFile(localPath, dec, 0644)
if err == nil {
ocrImageUrl = "/assets/ocr_uploads/" + filename
} else {
log.Printf("Failed to write OCR image file: %v", err)
}
} else {
log.Printf("Failed to decode base64 OCR image: %v", err)
}
}
// Start background goroutine for Gemini grading
go func() {
ctx := context.Background()
@@ -950,8 +985,8 @@ func (s *QuizService) GradeDescriptiveAnswer(username string, source string, que
}
stmt, err := s.db.Prepare(`
INSERT INTO DescriptiveGrading (source, question_id, user_answer, score, evaluation, username)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO DescriptiveGrading (source, question_id, user_answer, score, evaluation, username, ocr_image_url)
VALUES (?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
log.Printf("Failed to prepare INSERT statement: %v", err)
@@ -964,7 +999,7 @@ func (s *QuizService) GradeDescriptiveAnswer(username string, source string, que
}
defer stmt.Close()
res, err := stmt.Exec(source, questionID, userAnswer, result.Score, result.Evaluation, username)
res, err := stmt.Exec(source, questionID, userAnswer, result.Score, result.Evaluation, username, ocrImageUrl)
if err != nil {
log.Printf("Failed to execute INSERT statement: %v", err)
s.emitEvent("ai:grading:done", map[string]interface{}{
@@ -988,10 +1023,10 @@ func (s *QuizService) GradeDescriptiveAnswer(username string, source string, que
var record DescriptiveGradingRecord
err = s.db.QueryRow(`
SELECT id, question_id, source, user_answer, score, evaluation, username, strftime('%Y-%m-%d %H:%M:%S', created_at, 'localtime')
SELECT id, question_id, source, user_answer, score, evaluation, username, strftime('%Y-%m-%d %H:%M:%S', created_at, 'localtime'), COALESCE(ocr_image_url, '') as ocr_image_url
FROM DescriptiveGrading
WHERE id = ?
`, lastID).Scan(&record.ID, &record.QuestionID, &record.Source, &record.UserAnswer, &record.Score, &record.Evaluation, &record.Username, &record.CreatedAt)
`, lastID).Scan(&record.ID, &record.QuestionID, &record.Source, &record.UserAnswer, &record.Score, &record.Evaluation, &record.Username, &record.CreatedAt, &record.OcrImageUrl)
if err != nil {
log.Printf("Failed to scan grading record: %v", err)
s.emitEvent("ai:grading:done", map[string]interface{}{
@@ -1013,17 +1048,23 @@ func (s *QuizService) GradeDescriptiveAnswer(username string, source string, que
// Return a temporary processing record immediately
return &DescriptiveGradingRecord{
ID: -1,
QuestionID: questionID,
Source: source,
UserAnswer: userAnswer,
Score: -1, // -1 means in progress
Evaluation: "채점 진행 중...",
Username: username,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
ID: -1,
QuestionID: questionID,
Source: source,
UserAnswer: userAnswer,
Score: -1, // -1 means in progress
Evaluation: "채점 진행 중...",
Username: username,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
OcrImageUrl: ocrImageUrl,
}, nil
}
func sanitizeFilename(s string) string {
r := strings.NewReplacer("/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_", " ", "_")
return r.Replace(s)
}
// Login authenticates a user
func (s *QuizService) Login(username, password string) (*User, error) {
var u User