This commit is contained in:
2026-03-20 21:41:00 +08:00
commit 3d1d4cf506
53 changed files with 7105 additions and 0 deletions

62
internal/dao/mysql.go Normal file
View File

@@ -0,0 +1,62 @@
package dao
import (
"exam_registration/internal/model"
"fmt"
"github.com/spf13/viper"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"time"
)
var DB *gorm.DB
func InitMySQL(config *viper.Viper) error {
host := config.GetString("database.host")
port := config.GetString("database.port")
user := config.GetString("database.user")
password := config.GetString("database.password")
dbname := config.GetString("database.dbname")
charset := config.GetString("database.charset")
maxIdleConns := config.GetInt("database.max_idle_conns")
maxOpenConns := config.GetInt("database.max_open_conns")
connMaxLifetime := config.GetInt("database.conn_max_lifetime")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
user, password, host, port, dbname, charset)
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return fmt.Errorf("failed to connect database: %w", err)
}
// 配置连接池
sqlDB, err := DB.DB()
if err != nil {
return fmt.Errorf("failed to get sql.DB: %w", err)
}
sqlDB.SetMaxIdleConns(maxIdleConns)
sqlDB.SetMaxOpenConns(maxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Second)
// 自动迁移表结构
err = DB.AutoMigrate(
&model.User{},
&model.Exam{},
&model.ExamRegistration{},
&model.ExamNotice{},
&model.ExamScore{},
)
if err != nil {
return fmt.Errorf("failed to auto migrate: %w", err)
}
return nil
}

View File

@@ -0,0 +1,100 @@
package handler
import (
"exam_registration/internal/model"
"exam_registration/internal/service"
"exam_registration/pkg/response"
"github.com/gin-gonic/gin"
"strconv"
)
type ExamHandler struct {
examService *service.ExamService
}
func NewExamHandler() *ExamHandler {
return &ExamHandler{
examService: &service.ExamService{},
}
}
// CreateExam 创建考试
func (h *ExamHandler) CreateExam(c *gin.Context) {
var exam model.Exam
if err := c.ShouldBindJSON(&exam); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
userID, _ := c.Get("user_id")
exam.CreatorID = userID.(uint64)
if err := h.examService.CreateExam(&exam); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, exam)
}
// GetExamByID 获取考试详情
func (h *ExamHandler) GetExamByID(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
exam, err := h.examService.GetExamByID(id)
if err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, exam)
}
// GetExamList 获取考试列表
func (h *ExamHandler) GetExamList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
exams, total, err := h.examService.GetExamList(page, pageSize)
if err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, gin.H{
"list": exams,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
// UpdateExam 更新考试
func (h *ExamHandler) UpdateExam(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
if err := h.examService.UpdateExam(id, updates); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// DeleteExam 删除考试
func (h *ExamHandler) DeleteExam(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.examService.DeleteExam(id); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}

View File

@@ -0,0 +1,106 @@
package handler
import (
"exam_registration/internal/model"
"exam_registration/internal/service"
"exam_registration/pkg/response"
"github.com/gin-gonic/gin"
"strconv"
)
type NoticeHandler struct {
noticeService *service.NoticeService
}
func NewNoticeHandler() *NoticeHandler {
return &NoticeHandler{
noticeService: &service.NoticeService{},
}
}
// CreateNotice 创建通知
func (h *NoticeHandler) CreateNotice(c *gin.Context) {
var notice model.ExamNotice
if err := c.ShouldBindJSON(&notice); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
userID, _ := c.Get("user_id")
notice.PublisherID = userID.(uint64)
if err := h.noticeService.CreateNotice(&notice); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, notice)
}
// GetNoticeByID 获取通知详情
func (h *NoticeHandler) GetNoticeByID(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
notice, err := h.noticeService.GetNoticeByID(id)
if err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, notice)
}
// GetNoticeList 获取通知列表
func (h *NoticeHandler) GetNoticeList(c *gin.Context) {
examIDStr := c.Query("exam_id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
var examID int
if examIDStr != "" {
examID, _ = strconv.Atoi(examIDStr)
}
notices, total, err := h.noticeService.GetNoticeList(examID, page, pageSize)
if err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, gin.H{
"list": notices,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
// UpdateNotice 更新通知
func (h *NoticeHandler) UpdateNotice(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
if err := h.noticeService.UpdateNotice(id, updates); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// DeleteNotice 删除通知
func (h *NoticeHandler) DeleteNotice(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.noticeService.DeleteNotice(id); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}

View File

@@ -0,0 +1,119 @@
package handler
import (
"exam_registration/internal/service"
"exam_registration/pkg/response"
"github.com/gin-gonic/gin"
"strconv"
)
type RegistrationHandler struct {
registrationService *service.RegistrationService
}
func NewRegistrationHandler() *RegistrationHandler {
return &RegistrationHandler{
registrationService: &service.RegistrationService{},
}
}
// CreateRegistration 创建报名
func (h *RegistrationHandler) CreateRegistration(c *gin.Context) {
userID, _ := c.Get("user_id")
var req struct {
ExamID uint64 `json:"exam_id" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
if err := h.registrationService.CreateRegistration(userID.(uint64), req.ExamID, req.Remark); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// GetRegistrationList 获取报名列表
func (h *RegistrationHandler) GetRegistrationList(c *gin.Context) {
userIDStr := c.Query("user_id")
examIDStr := c.Query("exam_id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
var userID, examID int
if userIDStr != "" {
userID, _ = strconv.Atoi(userIDStr)
}
if examIDStr != "" {
examID, _ = strconv.Atoi(examIDStr)
}
registrations, total, err := h.registrationService.GetRegistrationList(userID, examID, page, pageSize)
if err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, gin.H{
"list": registrations,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
// AuditRegistration 审核报名
func (h *RegistrationHandler) AuditRegistration(c *gin.Context) {
regID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
Status int `json:"status" binding:"required"` // 1:通过2:拒绝
Comment string `json:"comment"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
if err := h.registrationService.AuditRegistration(regID, req.Status, req.Comment); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// UpdateRegistration 更新报名信息
func (h *RegistrationHandler) UpdateRegistration(c *gin.Context) {
regID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
if err := h.registrationService.UpdateRegistration(regID, updates); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// DeleteRegistration 取消报名
func (h *RegistrationHandler) DeleteRegistration(c *gin.Context) {
regID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.registrationService.DeleteRegistration(regID); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}

View File

@@ -0,0 +1,114 @@
package handler
import (
"exam_registration/internal/model"
"exam_registration/internal/service"
"exam_registration/pkg/response"
"github.com/gin-gonic/gin"
"strconv"
)
type ScoreHandler struct {
scoreService *service.ScoreService
}
func NewScoreHandler() *ScoreHandler {
return &ScoreHandler{
scoreService: &service.ScoreService{},
}
}
// CreateScore 录入成绩
func (h *ScoreHandler) CreateScore(c *gin.Context) {
var score model.ExamScore
if err := c.ShouldBindJSON(&score); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
if err := h.scoreService.CreateScore(&score); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, score)
}
// BatchCreateScores 批量录入成绩
func (h *ScoreHandler) BatchCreateScores(c *gin.Context) {
var scores []model.ExamScore
if err := c.ShouldBindJSON(&scores); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
if err := h.scoreService.BatchCreateScores(scores); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// GetScoreByUserAndExam 查询个人成绩
func (h *ScoreHandler) GetScoreByUserAndExam(c *gin.Context) {
userID, _ := c.Get("user_id")
examID, _ := strconv.ParseUint(c.Param("exam_id"), 10, 64)
score, err := h.scoreService.GetScoreByUserAndExam(userID.(uint64), examID)
if err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, score)
}
// GetScoreList 获取成绩列表(管理员)
func (h *ScoreHandler) GetScoreList(c *gin.Context) {
examIDStr := c.Query("exam_id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
var examID int
if examIDStr != "" {
examID, _ = strconv.Atoi(examIDStr)
}
scores, total, err := h.scoreService.GetScoreList(examID, page, pageSize)
if err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, gin.H{
"list": scores,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
// PublishScore 发布成绩
func (h *ScoreHandler) PublishScore(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.scoreService.PublishScore(id); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// DeleteScore 删除成绩
func (h *ScoreHandler) DeleteScore(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.scoreService.DeleteScore(id); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}

View File

@@ -0,0 +1,87 @@
package handler
import (
"exam_registration/internal/middleware"
"exam_registration/internal/service"
"exam_registration/pkg/response"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler() *UserHandler {
return &UserHandler{
userService: &service.UserService{},
}
}
// Login 用户登录
func (h *UserHandler) Login(c *gin.Context) {
var req service.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
token, err := h.userService.Login(&req)
if err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, gin.H{"token": token})
}
// Register 用户注册
func (h *UserHandler) Register(c *gin.Context) {
var req service.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
if err := h.userService.Register(&req); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// GetUserInfo 获取当前用户信息
func (h *UserHandler) GetUserInfo(c *gin.Context) {
userID, _ := c.Get("user_id")
user, err := h.userService.GetUserByID(userID.(uint64))
if err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, user)
}
// UpdateUserInfo 更新用户信息
func (h *UserHandler) UpdateUserInfo(c *gin.Context) {
userID, _ := c.Get("user_id")
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
response.Error(c, response.BAD_REQUEST, "参数错误")
return
}
// 不允许修改敏感字段
delete(updates, "password")
delete(updates, "role")
delete(updates, "status")
if err := h.userService.UpdateUser(userID.(uint64), updates); err != nil {
response.Error(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}

View File

@@ -0,0 +1,79 @@
package middleware
import (
"exam_registration/pkg/response"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
"net/http"
"strings"
)
type Claims struct {
UserID uint64 `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func JWT() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
response.Error(c, response.UNAUTHORIZED, "未授权")
c.Abort()
return
}
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
response.Error(c, response.UNAUTHORIZED, "请求头中 auth 格式有误")
c.Abort()
return
}
tokenString := parts[1]
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(viper.GetString("jwt.secret")), nil
})
if err != nil || !token.Valid {
response.Error(c, response.UNAUTHORIZED, "token 无效或已过期")
c.Abort()
return
}
claims, ok := token.Claims.(*Claims)
if !ok {
response.Error(c, response.UNAUTHORIZED, "无法获取用户信息")
c.Abort()
return
}
c.Set("claims", claims)
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
}
// Cors 跨域中间件
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}

119
internal/model/models.go Normal file
View File

@@ -0,0 +1,119 @@
package model
import (
"gorm.io/gorm"
"time"
)
// User 用户表
type User struct {
ID uint64 `gorm:"primarykey" json:"id"`
Username string `gorm:"type:varchar(50);uniqueIndex;not null" json:"username"`
Password string `gorm:"type:varchar(100);not null" json:"-"`
Email string `gorm:"type:varchar(100);index" json:"email"`
Phone string `gorm:"type:varchar(20);index" json:"phone"`
RealName string `gorm:"type:varchar(50)" json:"real_name"`
IDCard string `gorm:"type:varchar(20)" json:"id_card"`
Role string `gorm:"type:varchar(20);default:'user'" json:"role"` // admin, user
Status int `gorm:"type:tinyint;default:1;index" json:"status"` // 1:正常0:禁用
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// Exam 考试表
type Exam struct {
ID uint64 `gorm:"primarykey" json:"id"`
Title string `gorm:"type:varchar(200);not null" json:"title"`
Code string `gorm:"type:varchar(50);uniqueIndex;not null" json:"code"`
Description string `gorm:"type:text" json:"description"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
RegistrationStart time.Time `json:"registration_start"`
RegistrationEnd time.Time `json:"registration_end"`
MaxCandidates int `gorm:"default:0" json:"max_candidates"` // 0 表示不限制
ExamFee float64 `gorm:"type:decimal(10,2);default:0" json:"exam_fee"`
ExamLocation string `gorm:"type:varchar(200)" json:"exam_location"`
Subject string `gorm:"type:varchar(100)" json:"subject"`
Status int `gorm:"type:tinyint;default:1;index" json:"status"` // 1:未开始2:进行中3:已结束
CreatorID uint64 `gorm:"index" json:"creator_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// ExamRegistration 报名表
type ExamRegistration struct {
ID uint64 `gorm:"primarykey" json:"id"`
UserID uint64 `gorm:"index;not null" json:"user_id"`
ExamID uint64 `gorm:"index;not null" json:"exam_id"`
Status int `gorm:"type:tinyint;default:0;index" json:"status"` // 0:待审核1:已通过2:已拒绝3:已取消
PaymentStatus int `gorm:"type:tinyint;default:0;index" json:"payment_status"` // 0:未支付1:已支付
PaymentTime *time.Time `json:"payment_time"`
AuditTime *time.Time `json:"audit_time"`
AuditComment string `gorm:"type:varchar(500)" json:"audit_comment"`
TicketNumber string `gorm:"type:varchar(50);uniqueIndex" json:"ticket_number"` // 准考证号
ExamSeat string `gorm:"type:varchar(20)" json:"exam_seat"` // 考场座位
Remark string `gorm:"type:varchar(500)" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Exam Exam `gorm:"foreignKey:ExamID;constraint:OnDelete:CASCADE" json:"exam,omitempty"`
}
// ExamNotice 考试通知
type ExamNotice struct {
ID uint64 `gorm:"primarykey" json:"id"`
ExamID uint64 `gorm:"index;not null" json:"exam_id"`
Title string `gorm:"type:varchar(200);not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Type int `gorm:"type:tinyint;default:1" json:"type"` // 1:普通通知2:重要通知3:紧急通知
PublishTime time.Time `json:"publish_time"`
PublisherID uint64 `gorm:"index" json:"publisher_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Exam Exam `gorm:"foreignKey:ExamID;constraint:OnDelete:CASCADE" json:"exam,omitempty"`
}
// ExamScore 考试成绩
type ExamScore struct {
ID uint64 `gorm:"primarykey" json:"id"`
UserID uint64 `gorm:"index;not null" json:"user_id"`
ExamID uint64 `gorm:"index;not null" json:"exam_id"`
Score float64 `gorm:"type:decimal(5,2)" json:"score"`
TotalScore float64 `gorm:"type:decimal(5,2)" json:"total_score"`
Pass bool `gorm:"default:false" json:"pass"`
Rank int `gorm:"default:0" json:"rank"`
Remark string `gorm:"type:varchar(500)" json:"remark"`
Published bool `gorm:"default:false;index" json:"published"` // 是否已发布
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Exam Exam `gorm:"foreignKey:ExamID;constraint:OnDelete:CASCADE" json:"exam,omitempty"`
}
func (User) TableName() string {
return "user"
}
func (Exam) TableName() string {
return "exam"
}
func (ExamRegistration) TableName() string {
return "exam_registration"
}
func (ExamNotice) TableName() string {
return "exam_notice"
}
func (ExamScore) TableName() string {
return "exam_score"
}

70
internal/routes/routes.go Normal file
View File

@@ -0,0 +1,70 @@
package routes
import (
"exam_registration/internal/handler"
"exam_registration/internal/middleware"
"github.com/gin-gonic/gin"
)
func SetupRouter(r *gin.Engine) {
userHandler := handler.NewUserHandler()
examHandler := handler.NewExamHandler()
registrationHandler := handler.NewRegistrationHandler()
noticeHandler := handler.NewNoticeHandler()
scoreHandler := handler.NewScoreHandler()
// 公开路由
public := r.Group("/api")
{
// 用户认证
public.POST("/login", userHandler.Login)
public.POST("/register", userHandler.Register)
// 考试列表(公开查询)
public.GET("/exams", examHandler.GetExamList)
public.GET("/exams/:id", examHandler.GetExamByID)
// 通知列表(公开查询)
public.GET("/notices", noticeHandler.GetNoticeList)
public.GET("/notices/:id", noticeHandler.GetNoticeByID)
}
// 需要认证的路由
protected := r.Group("/api")
protected.Use(middleware.JWT())
{
// 用户相关
protected.GET("/user/info", userHandler.GetUserInfo)
protected.PUT("/user/info", userHandler.UpdateUserInfo)
// 考试管理(管理员)
admin := protected.Group("")
{
admin.POST("/exams", examHandler.CreateExam)
admin.PUT("/exams/:id", examHandler.UpdateExam)
admin.DELETE("/exams/:id", examHandler.DeleteExam)
}
// 报名管理
protected.POST("/registrations", registrationHandler.CreateRegistration)
protected.GET("/registrations", registrationHandler.GetRegistrationList)
protected.PUT("/registrations/:id", registrationHandler.UpdateRegistration)
protected.DELETE("/registrations/:id", registrationHandler.DeleteRegistration)
// 报名审核(管理员)
protected.PUT("/registrations/:id/audit", registrationHandler.AuditRegistration)
// 通知管理(管理员)
protected.POST("/notices", noticeHandler.CreateNotice)
protected.PUT("/notices/:id", noticeHandler.UpdateNotice)
protected.DELETE("/notices/:id", noticeHandler.DeleteNotice)
// 成绩管理
protected.POST("/scores", scoreHandler.CreateScore)
protected.POST("/scores/batch", scoreHandler.BatchCreateScores)
protected.GET("/scores/exam/:exam_id", scoreHandler.GetScoreByUserAndExam)
protected.GET("/scores", scoreHandler.GetScoreList)
protected.PUT("/scores/:id/publish", scoreHandler.PublishScore)
protected.DELETE("/scores/:id", scoreHandler.DeleteScore)
}
}

View File

@@ -0,0 +1,66 @@
package service
import (
"exam_registration/internal/dao"
"exam_registration/internal/model"
"errors"
"fmt"
"time"
)
type ExamService struct{}
func (s *ExamService) CreateExam(exam *model.Exam) error {
// 验证时间逻辑
if exam.StartTime.Before(time.Now()) {
return errors.New("考试开始时间不能早于当前时间")
}
if exam.EndTime.Before(exam.StartTime) {
return errors.New("考试结束时间不能早于开始时间")
}
if exam.RegistrationEnd.Before(exam.RegistrationStart) {
return errors.New("报名截止时间不能早于开始时间")
}
return dao.DB.Create(exam).Error
}
func (s *ExamService) GetExamByID(id uint64) (*model.Exam, error) {
var exam model.Exam
if err := dao.DB.First(&exam, id).Error; err != nil {
return nil, errors.New("考试不存在")
}
return &exam, nil
}
func (s *ExamService) GetExamList(page, pageSize int) ([]model.Exam, int64, error) {
var exams []model.Exam
var total int64
offset := (page - 1) * pageSize
if err := dao.DB.Model(&model.Exam{}).Count(&total).Error; err != nil {
return nil, 0, err
}
err := dao.DB.Offset(offset).Limit(pageSize).Order("id DESC").Find(&exams).Error
return exams, total, err
}
func (s *ExamService) UpdateExam(id uint64, updates map[string]interface{}) error {
return dao.DB.Model(&model.Exam{}).Where("id = ?", id).Updates(updates).Error
}
func (s *ExamService) DeleteExam(id uint64) error {
return dao.DB.Delete(&model.Exam{}, id).Error
}
func (s *ExamService) UpdateExamStatus(id uint64, status int) error {
// 更新考试状态并同步更新相关报名记录的状态
return dao.DB.Transaction(func(tx *dao.DB) error {
if err := tx.Model(&model.Exam{}).Where("id = ?", id).Update("status", status).Error; err != nil {
return err
}
return nil
})
}

View File

@@ -0,0 +1,50 @@
package service
import (
"exam_registration/internal/dao"
"exam_registration/internal/model"
"errors"
"time"
)
type NoticeService struct{}
func (s *NoticeService) CreateNotice(notice *model.ExamNotice) error {
notice.PublishTime = time.Now()
return dao.DB.Create(notice).Error
}
func (s *NoticeService) GetNoticeByID(id uint64) (*model.ExamNotice, error) {
var notice model.ExamNotice
if err := dao.DB.Preload("Exam").First(&notice, id).Error; err != nil {
return nil, errors.New("通知不存在")
}
return &notice, nil
}
func (s *NoticeService) GetNoticeList(examID, page, pageSize int) ([]model.ExamNotice, int64, error) {
var notices []model.ExamNotice
var total int64
offset := (page - 1) * pageSize
query := dao.DB.Model(&model.ExamNotice{}).Preload("Exam")
if examID > 0 {
query = query.Where("exam_id = ?", examID)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.Offset(offset).Limit(pageSize).Order("id DESC").Find(&notices).Error
return notices, total, err
}
func (s *NoticeService) UpdateNotice(id uint64, updates map[string]interface{}) error {
return dao.DB.Model(&model.ExamNotice{}).Where("id = ?", id).Updates(updates).Error
}
func (s *NoticeService) DeleteNotice(id uint64) error {
return dao.DB.Delete(&model.ExamNotice{}, id).Error
}

View File

@@ -0,0 +1,100 @@
package service
import (
"exam_registration/internal/dao"
"exam_registration/internal/model"
"errors"
"fmt"
"time"
)
type RegistrationService struct{}
func (s *RegistrationService) CreateRegistration(userID, examID uint64, remark string) error {
// 检查考试是否存在
var exam model.Exam
if err := dao.DB.First(&exam, examID).Error; err != nil {
return errors.New("考试不存在")
}
// 检查报名时间
now := time.Now()
if now.Before(exam.RegistrationStart) {
return errors.New("尚未开始报名")
}
if now.After(exam.RegistrationEnd) {
return errors.New("报名已截止")
}
// 检查是否已报名
var existingReg model.ExamRegistration
if err := dao.DB.Where("user_id = ? AND exam_id = ?", userID, examID).First(&existingReg).Error; err == nil {
return errors.New("您已经报过名了")
}
// 检查考试容量
if exam.MaxCandidates > 0 {
var count int64
dao.DB.Model(&model.ExamRegistration{}).Where("exam_id = ? AND status IN (?)", []int{0, 1}).Count(&count)
if count >= int64(exam.MaxCandidates) {
return errors.New("报名人数已满")
}
}
registration := model.ExamRegistration{
UserID: userID,
ExamID: examID,
Status: 0, // 待审核
PaymentStatus: 0, // 未支付
Remark: remark,
}
return dao.DB.Create(&registration).Error
}
func (s *RegistrationService) GetRegistrationList(userID, examID, page, pageSize int) ([]model.ExamRegistration, int64, error) {
var registrations []model.ExamRegistration
var total int64
offset := (page - 1) * pageSize
query := dao.DB.Model(&model.ExamRegistration{}).Preload("User").Preload("Exam")
if userID > 0 {
query = query.Where("user_id = ?", userID)
}
if examID > 0 {
query = query.Where("exam_id = ?", examID)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.Offset(offset).Limit(pageSize).Order("id DESC").Find(&registrations).Error
return registrations, total, err
}
func (s *RegistrationService) AuditRegistration(regID uint64, status int, comment string) error {
now := time.Now()
updates := map[string]interface{}{
"status": status,
"audit_time": now,
"audit_comment": comment,
}
if status == 1 { // 审核通过,生成准考证号
ticketNumber := fmt.Sprintf("TKT%d%d", regID, now.Unix())
updates["ticket_number"] = ticketNumber
// TODO: 编排考场座位
}
return dao.DB.Model(&model.ExamRegistration{}).Where("id = ?", regID).Updates(updates).Error
}
func (s *RegistrationService) UpdateRegistration(regID uint64, updates map[string]interface{}) error {
return dao.DB.Model(&model.ExamRegistration{}).Where("id = ?", regID).Updates(updates).Error
}
func (s *RegistrationService) DeleteRegistration(regID uint64) error {
return dao.DB.Delete(&model.ExamRegistration{}, regID).Error
}

View File

@@ -0,0 +1,86 @@
package service
import (
"exam_registration/internal/dao"
"exam_registration/internal/model"
"errors"
)
type ScoreService struct{}
func (s *ScoreService) CreateScore(score *model.ExamScore) error {
// 检查是否已存在成绩
var existingScore model.ExamScore
if err := dao.DB.Where("user_id = ? AND exam_id = ?", score.UserID, score.ExamID).First(&existingScore).Error; err == nil {
return errors.New("该用户该考试的成绩已存在")
}
// 判断是否及格(假设 60 分及格)
score.Pass = score.Score >= 60
return dao.DB.Create(score).Error
}
func (s *ScoreService) BatchCreateScores(scores []model.ExamScore) error {
return dao.DB.Transaction(func(tx *dao.DB) error {
for i := range scores {
scores[i].Pass = scores[i].Score >= 60
// 检查是否已存在
var existing model.ExamScore
if err := tx.Where("user_id = ? AND exam_id = ?", scores[i].UserID, scores[i].ExamID).First(&existing).Error; err == nil {
continue // 跳过已存在的记录
}
if err := tx.Create(&scores[i]).Error; err != nil {
return err
}
}
return nil
})
}
func (s *ScoreService) GetScoreByUserAndExam(userID, examID uint64) (*model.ExamScore, error) {
var score model.ExamScore
if err := dao.DB.Preload("User").Preload("Exam").Where("user_id = ? AND exam_id = ?", userID, examID).First(&score).Error; err != nil {
return nil, errors.New("成绩不存在")
}
return &score, nil
}
func (s *ScoreService) GetScoreList(examID, page, pageSize int) ([]model.ExamScore, int64, error) {
var scores []model.ExamScore
var total int64
offset := (page - 1) * pageSize
query := dao.DB.Model(&model.ExamScore{}).Preload("User").Preload("Exam")
if examID > 0 {
query = query.Where("exam_id = ?", examID)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.Offset(offset).Limit(pageSize).Order("score DESC").Find(&scores).Error
// 计算排名
for i := range scores {
scores[i].Rank = offset + i + 1
}
return scores, total, err
}
func (s *ScoreService) UpdateScore(id uint64, updates map[string]interface{}) error {
return dao.DB.Model(&model.ExamScore{}).Where("id = ?", id).Updates(updates).Error
}
func (s *ScoreService) PublishScore(id uint64) error {
return dao.DB.Model(&model.ExamScore{}).Where("id = ?", id).Update("published", true).Error
}
func (s *ScoreService) DeleteScore(id uint64) error {
return dao.DB.Delete(&model.ExamScore{}, id).Error
}

View File

@@ -0,0 +1,91 @@
package service
import (
"exam_registration/internal/dao"
"exam_registration/internal/model"
"errors"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
"golang.org/x/crypto/bcrypt"
"time"
)
type UserService struct{}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email"`
Phone string `json:"phone"`
RealName string `json:"real_name"`
IDCard string `json:"id_card"`
}
func (s *UserService) Login(req *LoginRequest) (string, error) {
var user model.User
if err := dao.DB.Where("username = ?", req.Username).First(&user).Error; err != nil {
return "", errors.New("用户名或密码错误")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return "", errors.New("用户名或密码错误")
}
if user.Status != 1 {
return "", errors.New("账号已被禁用")
}
// 生成 JWT token
claims := jwt.MapClaims{
"user_id": user.ID,
"username": user.Username,
"role": user.Role,
"exp": time.Now().Add(time.Duration(viper.GetInt("jwt.expire")) * time.Second).Unix(),
"issued_at": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(viper.GetString("jwt.secret")))
}
func (s *UserService) Register(req *RegisterRequest) error {
var existingUser model.User
if err := dao.DB.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
return errors.New("用户名已存在")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return errors.New("密码加密失败")
}
user := model.User{
Username: req.Username,
Password: string(hashedPassword),
Email: req.Email,
Phone: req.Phone,
RealName: req.RealName,
IDCard: req.IDCard,
Role: "user",
Status: 1,
}
return dao.DB.Create(&user).Error
}
func (s *UserService) GetUserByID(userID uint64) (*model.User, error) {
var user model.User
if err := dao.DB.First(&user, userID).Error; err != nil {
return nil, errors.New("用户不存在")
}
return &user, nil
}
func (s *UserService) UpdateUser(userID uint64, updates map[string]interface{}) error {
return dao.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error
}