feat: 优化静态文件服务和部署配置
主要改进: - 重构静态文件服务: 实现自定义 StaticFileHandler,完善 SPA 路由支持和 Content-Type 处理 - 优化 Docker 构建: 简化前端资源复制逻辑,直接复制整个 dist 目录 - 添加 K8s 部署配置: 包含健康检查探针、资源限制和服务配置 - 前端配置优化: Vite 使用相对路径 base,确保打包后资源路径正确 - 代码清理: 删除已废弃的数据迁移脚本 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
30c5236bea
commit
53d3ebe318
@ -60,11 +60,9 @@ ENV TZ=Asia/Shanghai
|
||||
COPY --from=backend-builder /app/server ./server
|
||||
|
||||
# 从前端构建阶段复制构建产物
|
||||
# 根据 main.go 配置:
|
||||
# - r.NoRoute() 返回 ./web/index.html
|
||||
# - r.Static("/assets", "./web/static") 提供静态资源
|
||||
COPY --from=frontend-builder /app/web/dist/index.html ./web/index.html
|
||||
COPY --from=frontend-builder /app/web/dist/assets ./web/static
|
||||
# 直接将整个 dist 目录复制为 web 目录
|
||||
# dist 目录包含:index.html, assets/, icon.svg 等所有构建产物
|
||||
COPY --from=frontend-builder /app/web/dist ./web
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
@ -4,7 +4,10 @@ import (
|
||||
"ankao/internal/database"
|
||||
"ankao/internal/models"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@ -80,3 +83,70 @@ func GetDailyRanking(c *gin.Context) {
|
||||
"data": rankings,
|
||||
})
|
||||
}
|
||||
|
||||
// StaticFileHandler 静态文件处理器,用于服务前端静态资源
|
||||
// 使用 NoRoute 避免与 API 路由冲突
|
||||
func StaticFileHandler(root string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取请求路径
|
||||
path := r.URL.Path
|
||||
|
||||
// 构建完整文件路径
|
||||
fullPath := filepath.Join(root, path)
|
||||
|
||||
// 检查文件是否存在
|
||||
info, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
// 文件不存在,尝试返回 index.html(SPA 应用)
|
||||
indexPath := filepath.Join(root, "index.html")
|
||||
if _, err := os.Stat(indexPath); err == nil {
|
||||
http.ServeFile(w, r, indexPath)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是目录,尝试返回目录下的 index.html
|
||||
if info.IsDir() {
|
||||
indexPath := filepath.Join(fullPath, "index.html")
|
||||
if _, err := os.Stat(indexPath); err == nil {
|
||||
http.ServeFile(w, r, indexPath)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置正确的 Content-Type
|
||||
setContentType(w, fullPath)
|
||||
|
||||
// 返回文件
|
||||
http.ServeFile(w, r, fullPath)
|
||||
})
|
||||
}
|
||||
|
||||
// setContentType 根据文件扩展名设置正确的 Content-Type
|
||||
func setContentType(w http.ResponseWriter, filePath string) {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
contentTypes := map[string]string{
|
||||
".html": "text/html; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".js": "application/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".svg": "image/svg+xml",
|
||||
".ico": "image/x-icon",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".ttf": "font/ttf",
|
||||
".eot": "application/vnd.ms-fontobject",
|
||||
}
|
||||
|
||||
if contentType, ok := contentTypes[ext]; ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
}
|
||||
|
||||
123
k8s-deployment.yaml
Normal file
123
k8s-deployment.yaml
Normal file
@ -0,0 +1,123 @@
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
name: ankao
|
||||
namespace: default
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ankao
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ankao
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: aliyun
|
||||
containers:
|
||||
- name: ankao
|
||||
image: registry.cn-qingdao.aliyuncs.com/yuchat/ankao:0.0.9
|
||||
env:
|
||||
- name: DB_HOST
|
||||
value: pgsql
|
||||
- name: DB_USERNAME
|
||||
value: postgres
|
||||
- name: DB_PASSWORD
|
||||
value: longqi@1314
|
||||
- name: AI_BASE_URL
|
||||
value: http://new-api
|
||||
- name: AI_API_KEY
|
||||
value: sk-OKBmOpJx855juSOPU14cWG6Iz87tZQuv3Xg9PiaJYXdHoKcN
|
||||
- name: AI_MODEL
|
||||
value: deepseek-v3
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: tcp-8080
|
||||
|
||||
# 存活探针 - 检测容器是否正在运行
|
||||
# 如果失败,Kubernetes 会重启容器
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 30 # 容器启动后等待30秒再开始探测
|
||||
periodSeconds: 10 # 每10秒探测一次
|
||||
timeoutSeconds: 5 # 探测超时时间5秒
|
||||
successThreshold: 1 # 成功1次即认为成功
|
||||
failureThreshold: 3 # 连续失败3次后重启容器
|
||||
|
||||
# 就绪探针 - 检测容器是否准备好接收流量
|
||||
# 如果失败,会从 Service 负载均衡中移除
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 10 # 容器启动后等待10秒再开始探测
|
||||
periodSeconds: 5 # 每5秒探测一次
|
||||
timeoutSeconds: 3 # 探测超时时间3秒
|
||||
successThreshold: 1 # 成功1次即认为就绪
|
||||
failureThreshold: 3 # 连续失败3次后标记为未就绪
|
||||
|
||||
# 启动探针 - 检测容器应用是否已经启动(可选)
|
||||
# 启动探针成功后,存活探针和就绪探针才会接管
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 0 # 立即开始探测
|
||||
periodSeconds: 5 # 每5秒探测一次
|
||||
timeoutSeconds: 3 # 探测超时时间3秒
|
||||
successThreshold: 1 # 成功1次即认为启动完成
|
||||
failureThreshold: 12 # 最多失败12次(60秒)才判定启动失败
|
||||
|
||||
# 资源限制(建议配置)
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
|
||||
volumeMounts:
|
||||
- name: timezone
|
||||
mountPath: /etc/timezone
|
||||
readOnly: true
|
||||
- name: localtime
|
||||
mountPath: /etc/localtime
|
||||
readOnly: true
|
||||
|
||||
volumes:
|
||||
- name: timezone
|
||||
hostPath:
|
||||
path: /etc/timezone
|
||||
type: File
|
||||
- name: localtime
|
||||
hostPath:
|
||||
path: /etc/localtime
|
||||
type: File
|
||||
restartPolicy: Always
|
||||
|
||||
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: ankao
|
||||
namespace: default
|
||||
spec:
|
||||
selector:
|
||||
app: ankao
|
||||
ports:
|
||||
- name: tcp-80
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
type: LoadBalancer
|
||||
ipFamilyPolicy: PreferDualStack
|
||||
ipFamilies:
|
||||
- IPv4
|
||||
- IPv6
|
||||
49
main.go
49
main.go
@ -40,12 +40,12 @@ func main() {
|
||||
auth := api.Group("", middleware.Auth())
|
||||
{
|
||||
// 用户相关API
|
||||
auth.PUT("/user/type", handlers.UpdateUserType) // 更新用户类型
|
||||
auth.PUT("/user/profile", handlers.UpdateProfile) // 更新用户信息
|
||||
auth.PUT("/user/password", handlers.ChangePassword) // 修改密码
|
||||
auth.PUT("/user/type", handlers.UpdateUserType) // 更新用户类型
|
||||
auth.PUT("/user/profile", handlers.UpdateProfile) // 更新用户信息
|
||||
auth.PUT("/user/password", handlers.ChangePassword) // 修改密码
|
||||
|
||||
// 排行榜API
|
||||
auth.GET("/ranking/daily", handlers.GetDailyRanking) // 获取今日排行榜
|
||||
auth.GET("/ranking/daily", handlers.GetDailyRanking) // 获取今日排行榜
|
||||
|
||||
// 练习题相关API(需要登录)
|
||||
auth.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表
|
||||
@ -54,27 +54,27 @@ func main() {
|
||||
|
||||
// 练习题提交(需要登录才能记录错题)
|
||||
auth.POST("/practice/submit", handlers.SubmitPracticeAnswer) // 提交练习答案
|
||||
auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据
|
||||
auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据
|
||||
|
||||
// 错题本相关API
|
||||
auth.GET("/wrong-questions", handlers.GetWrongQuestions) // 获取错题列表(支持筛选和排序)
|
||||
auth.GET("/wrong-questions/stats", handlers.GetWrongQuestionStats) // 获取错题统计(含趋势)
|
||||
auth.GET("/wrong-questions", handlers.GetWrongQuestions) // 获取错题列表(支持筛选和排序)
|
||||
auth.GET("/wrong-questions/stats", handlers.GetWrongQuestionStats) // 获取错题统计(含趋势)
|
||||
auth.GET("/wrong-questions/recommended", handlers.GetRecommendedWrongQuestions) // 获取推荐错题
|
||||
auth.GET("/wrong-questions/:id", handlers.GetWrongQuestionDetail) // 获取错题详情
|
||||
auth.DELETE("/wrong-questions/:id", handlers.DeleteWrongQuestion) // 删除错题
|
||||
auth.DELETE("/wrong-questions", handlers.ClearWrongQuestions) // 清空错题本
|
||||
auth.PUT("/wrong-questions/:id/tags", handlers.UpdateWrongQuestionTags) // 更新错题标签
|
||||
auth.GET("/wrong-questions/:id", handlers.GetWrongQuestionDetail) // 获取错题详情
|
||||
auth.DELETE("/wrong-questions/:id", handlers.DeleteWrongQuestion) // 删除错题
|
||||
auth.DELETE("/wrong-questions", handlers.ClearWrongQuestions) // 清空错题本
|
||||
auth.PUT("/wrong-questions/:id/tags", handlers.UpdateWrongQuestionTags) // 更新错题标签
|
||||
|
||||
// 标签管理API
|
||||
auth.GET("/wrong-question-tags", handlers.GetWrongQuestionTags) // 获取标签列表
|
||||
auth.POST("/wrong-question-tags", handlers.CreateWrongQuestionTag) // 创建标签
|
||||
auth.PUT("/wrong-question-tags/:id", handlers.UpdateWrongQuestionTag) // 更新标签
|
||||
auth.GET("/wrong-question-tags", handlers.GetWrongQuestionTags) // 获取标签列表
|
||||
auth.POST("/wrong-question-tags", handlers.CreateWrongQuestionTag) // 创建标签
|
||||
auth.PUT("/wrong-question-tags/:id", handlers.UpdateWrongQuestionTag) // 更新标签
|
||||
auth.DELETE("/wrong-question-tags/:id", handlers.DeleteWrongQuestionTag) // 删除标签
|
||||
|
||||
// 考试相关API
|
||||
auth.POST("/exam/generate", handlers.GenerateExam) // 生成考试
|
||||
auth.GET("/exam/:id", handlers.GetExam) // 获取考试详情
|
||||
auth.POST("/exam/:id/submit", handlers.SubmitExam) // 提交考试
|
||||
auth.POST("/exam/generate", handlers.GenerateExam) // 生成考试
|
||||
auth.GET("/exam/:id", handlers.GetExam) // 获取考试详情
|
||||
auth.POST("/exam/:id/submit", handlers.SubmitExam) // 提交考试
|
||||
}
|
||||
|
||||
// 题库管理API(需要管理员权限)
|
||||
@ -88,19 +88,14 @@ func main() {
|
||||
// 用户管理API(仅yanlongqi用户可访问)
|
||||
userAdmin := api.Group("", middleware.Auth(), middleware.AdminOnly())
|
||||
{
|
||||
userAdmin.GET("/admin/users", handlers.GetAllUsersWithStats) // 获取所有用户及统计
|
||||
userAdmin.GET("/admin/users/:id", handlers.GetUserDetailStats) // 获取用户详细统计
|
||||
userAdmin.GET("/admin/users", handlers.GetAllUsersWithStats) // 获取所有用户及统计
|
||||
userAdmin.GET("/admin/users/:id", handlers.GetUserDetailStats) // 获取用户详细统计
|
||||
}
|
||||
}
|
||||
|
||||
// 静态文件服务(必须在 API 路由之后)
|
||||
// 提供静态资源(CSS、JS、图片等)
|
||||
r.Static("/assets", "./web/static")
|
||||
|
||||
// SPA 支持:所有非 API 路由都返回 index.html,让前端路由处理
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.File("./web/index.html")
|
||||
})
|
||||
// 静态文件服务(使用 NoRoute 避免路由冲突)
|
||||
// 当没有匹配到任何 API 路由时,尝试提供静态文件
|
||||
r.NoRoute(gin.WrapH(handlers.StaticFileHandler("./web")))
|
||||
|
||||
// 启动服务器
|
||||
port := ":8080"
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ankao/internal/database"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 直接设置数据库配置
|
||||
os.Setenv("DB_HOST", "localhost")
|
||||
os.Setenv("DB_PORT", "5432")
|
||||
os.Setenv("DB_USER", "postgres")
|
||||
os.Setenv("DB_PASSWORD", "root")
|
||||
os.Setenv("DB_NAME", "ankao")
|
||||
|
||||
// 连接数据库
|
||||
if err := database.InitDB(); err != nil {
|
||||
log.Fatalf("数据库连接失败: %v", err)
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
|
||||
log.Println("开始迁移:删除 wrong_questions 表的旧字段...")
|
||||
|
||||
// 删除 wrong_answer 和 correct_answer 字段
|
||||
// 这些字段在新版本中已移至 wrong_question_history 表
|
||||
migrations := []string{
|
||||
"ALTER TABLE wrong_questions DROP COLUMN IF EXISTS wrong_answer",
|
||||
"ALTER TABLE wrong_questions DROP COLUMN IF EXISTS correct_answer",
|
||||
"ALTER TABLE wrong_questions DROP COLUMN IF EXISTS wrong_count", // 也删除旧的 wrong_count 字段(如果存在)
|
||||
}
|
||||
|
||||
for _, migration := range migrations {
|
||||
log.Printf("执行: %s", migration)
|
||||
if err := db.Exec(migration).Error; err != nil {
|
||||
log.Printf("警告: 执行失败 - %v (字段可能已不存在)", err)
|
||||
} else {
|
||||
log.Println("✓ 执行成功")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("迁移完成!")
|
||||
}
|
||||
@ -1,173 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ankao/internal/database"
|
||||
"ankao/internal/models"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 数据迁移脚本:从旧版错题本迁移到新版
|
||||
|
||||
func main() {
|
||||
// 初始化数据库
|
||||
if err := database.InitDB(); err != nil {
|
||||
log.Fatalf("数据库初始化失败: %v", err)
|
||||
}
|
||||
db := database.GetDB()
|
||||
|
||||
fmt.Println("开始错题本数据迁移...")
|
||||
|
||||
// 1. 创建新表
|
||||
fmt.Println("1. 创建新表结构...")
|
||||
if err := db.AutoMigrate(&models.WrongQuestion{}, &models.WrongQuestionHistory{}, &models.WrongQuestionTag{}); err != nil {
|
||||
log.Fatalf("创建新表失败: %v", err)
|
||||
}
|
||||
fmt.Println(" ✓ 新表创建成功")
|
||||
|
||||
// 2. 迁移数据
|
||||
fmt.Println("2. 迁移旧数据到新表...")
|
||||
if err := migrateOldData(db); err != nil {
|
||||
log.Fatalf("数据迁移失败: %v", err)
|
||||
}
|
||||
fmt.Println(" ✓ 数据迁移成功")
|
||||
|
||||
// 3. 统计信息
|
||||
fmt.Println("3. 统计迁移结果...")
|
||||
printStats(db)
|
||||
|
||||
fmt.Println("\n迁移完成!")
|
||||
fmt.Println("\n注意事项:")
|
||||
fmt.Println("- 旧表 'wrong_questions' 已保留,不会删除")
|
||||
fmt.Println("- 新表 'wrong_questions_v2' 包含所有迁移后的数据")
|
||||
fmt.Println("- 如需回滚,可以删除新表并继续使用旧表")
|
||||
}
|
||||
|
||||
func migrateOldData(db *gorm.DB) error {
|
||||
// 查询所有旧错题记录
|
||||
var oldWrongQuestions []models.WrongQuestion
|
||||
if err := db.Find(&oldWrongQuestions).Error; err != nil {
|
||||
return fmt.Errorf("查询旧数据失败: %v", err)
|
||||
}
|
||||
|
||||
if len(oldWrongQuestions) == 0 {
|
||||
fmt.Println(" 没有需要迁移的数据")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf(" 找到 %d 条旧记录,开始迁移...\n", len(oldWrongQuestions))
|
||||
|
||||
// 逐条迁移
|
||||
for i, old := range oldWrongQuestions {
|
||||
if err := migrateOneRecord(db, &old); err != nil {
|
||||
log.Printf(" 警告: 迁移记录 %d 失败: %v", old.ID, err)
|
||||
continue
|
||||
}
|
||||
if (i+1)%100 == 0 {
|
||||
fmt.Printf(" 已迁移: %d/%d\n", i+1, len(oldWrongQuestions))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ 迁移完成: %d/%d\n", len(oldWrongQuestions), len(oldWrongQuestions))
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateOneRecord(db *gorm.DB, old *models.WrongQuestion) error {
|
||||
// 检查是否已存在
|
||||
var existing models.WrongQuestion
|
||||
if err := db.Where("user_id = ? AND question_id = ?", old.UserID, old.QuestionID).First(&existing).Error; err == nil {
|
||||
// 已存在,跳过
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建新记录
|
||||
newRecord := models.WrongQuestion{
|
||||
UserID: old.UserID,
|
||||
QuestionID: old.QuestionID,
|
||||
FirstWrongTime: old.LastWrongTime, // 旧版只有最后错误时间
|
||||
LastWrongTime: old.LastWrongTime,
|
||||
TotalWrongCount: old.WrongCount,
|
||||
MasteryLevel: 0,
|
||||
ConsecutiveCorrect: 0,
|
||||
IsMastered: old.IsMastered,
|
||||
Tags: []string{}, // 默认无标签
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 计算下次复习时间
|
||||
newRecord.CalculateNextReviewTime()
|
||||
|
||||
// 保存新记录
|
||||
if err := db.Create(&newRecord).Error; err != nil {
|
||||
return fmt.Errorf("创建新记录失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建历史记录(基于旧数据)
|
||||
history := models.WrongQuestionHistory{
|
||||
WrongQuestionID: newRecord.ID,
|
||||
UserAnswer: old.WrongAnswer,
|
||||
CorrectAnswer: old.CorrectAnswer,
|
||||
AnsweredAt: old.LastWrongTime,
|
||||
TimeSpent: 0,
|
||||
IsCorrect: false,
|
||||
}
|
||||
|
||||
if err := db.Create(&history).Error; err != nil {
|
||||
log.Printf("警告: 创建历史记录失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printStats(db *gorm.DB) {
|
||||
var oldCount, newCount, historyCount, tagCount int64
|
||||
|
||||
db.Model(&models.WrongQuestion{}).Count(&oldCount)
|
||||
db.Model(&models.WrongQuestion{}).Count(&newCount)
|
||||
db.Model(&models.WrongQuestionHistory{}).Count(&historyCount)
|
||||
db.Model(&models.WrongQuestionTag{}).Count(&tagCount)
|
||||
|
||||
fmt.Printf("\n 旧表记录数: %d\n", oldCount)
|
||||
fmt.Printf(" 新表记录数: %d\n", newCount)
|
||||
fmt.Printf(" 历史记录数: %d\n", historyCount)
|
||||
fmt.Printf(" 标签数: %d\n", tagCount)
|
||||
|
||||
// 统计掌握度分布
|
||||
var masteryStats []struct {
|
||||
MasteryLevel int
|
||||
Count int64
|
||||
}
|
||||
db.Model(&models.WrongQuestion{}).
|
||||
Select("mastery_level, COUNT(*) as count").
|
||||
Group("mastery_level").
|
||||
Scan(&masteryStats)
|
||||
|
||||
if len(masteryStats) > 0 {
|
||||
fmt.Println("\n 掌握度分布:")
|
||||
for _, stat := range masteryStats {
|
||||
fmt.Printf(" - 掌握度 %d%%: %d 题\n", stat.MasteryLevel, stat.Count)
|
||||
}
|
||||
}
|
||||
|
||||
// 统计需要复习的题目
|
||||
var needReview int64
|
||||
now := time.Now()
|
||||
db.Model(&models.WrongQuestion{}).
|
||||
Where("is_mastered = ? AND next_review_time IS NOT NULL AND next_review_time <= ?", false, now).
|
||||
Count(&needReview)
|
||||
fmt.Printf("\n 需要复习的题目: %d\n", needReview)
|
||||
}
|
||||
|
||||
// 辅助函数:解析JSON答案
|
||||
func parseAnswer(answerStr string) interface{} {
|
||||
var answer interface{}
|
||||
if err := json.Unmarshal([]byte(answerStr), &answer); err != nil {
|
||||
return answerStr
|
||||
}
|
||||
return answer
|
||||
}
|
||||
@ -11,7 +11,7 @@ interface QuestionFloatButtonProps {
|
||||
}
|
||||
|
||||
const QuestionFloatButton: React.FC<QuestionFloatButtonProps> = ({
|
||||
currentIndex,
|
||||
currentIndex: _currentIndex, // 保留参数但表示未使用
|
||||
totalQuestions,
|
||||
onClick,
|
||||
correctCount,
|
||||
|
||||
@ -5,6 +5,7 @@ import path from 'path'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: './', // 使用相对路径,确保打包后资源路径正确
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user