feat(runner): implement empty classroom sync functionality
Add support for fetching and synchronizing empty classroom data. This includes new protobuf definitions for task payloads and results, database models, repository, service, and HTTP handlers. - Add `TASK_TYPE_GET_EMPTY_CLASSROOM` to `TaskType` enum - Implement `EmptyClassroom` model and auto-migration - Add `EmptyClassroomHandler` with `GET` and `POST /sync` routes - Implement `EmptyClassroomSyncService` and `EmptyClassroomRepository` - Update dependency injection with wire sets
This commit is contained in:
87
internal/handler/empty_classroom_handler.go
Normal file
87
internal/handler/empty_classroom_handler.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"with_you/internal/pkg/response"
|
||||
"with_you/internal/repository"
|
||||
"with_you/internal/service"
|
||||
)
|
||||
|
||||
type EmptyClassroomHandler struct {
|
||||
classroomSyncService service.EmptyClassroomSyncService
|
||||
classroomRepo repository.EmptyClassroomRepository
|
||||
}
|
||||
|
||||
func NewEmptyClassroomHandler(
|
||||
classroomSyncService service.EmptyClassroomSyncService,
|
||||
classroomRepo repository.EmptyClassroomRepository,
|
||||
) *EmptyClassroomHandler {
|
||||
return &EmptyClassroomHandler{
|
||||
classroomSyncService: classroomSyncService,
|
||||
classroomRepo: classroomRepo,
|
||||
}
|
||||
}
|
||||
|
||||
type syncEmptyClassroomsRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Semester string `json:"semester"`
|
||||
WeekStart string `json:"week_start"`
|
||||
WeekEnd string `json:"week_end"`
|
||||
CampusCode string `json:"campus_code"`
|
||||
}
|
||||
|
||||
func (h *EmptyClassroomHandler) SyncEmptyClassrooms(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
var req syncEmptyClassroomsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.classroomSyncService.SyncUserEmptyClassrooms(c.Request.Context(), userID, &service.SyncEmptyClassroomsRequest{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Semester: req.Semester,
|
||||
WeekStart: req.WeekStart,
|
||||
WeekEnd: req.WeekEnd,
|
||||
CampusCode: req.CampusCode,
|
||||
})
|
||||
if err != nil {
|
||||
response.HandleError(c, err, "failed to sync empty classrooms")
|
||||
return
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
response.Success(c, result)
|
||||
return
|
||||
}
|
||||
response.SuccessWithMessage(c, result.Message, result)
|
||||
}
|
||||
|
||||
func (h *EmptyClassroomHandler) ListEmptyClassrooms(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
semester := c.Query("semester")
|
||||
if semester == "" {
|
||||
response.BadRequest(c, "semester is required")
|
||||
return
|
||||
}
|
||||
|
||||
classrooms, err := h.classroomRepo.ListByUserAndSemester(userID, semester)
|
||||
if err != nil {
|
||||
response.HandleError(c, err, "failed to list empty classrooms")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"classrooms": classrooms})
|
||||
}
|
||||
26
internal/model/empty_classroom.go
Normal file
26
internal/model/empty_classroom.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EmptyClassroom struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);index;not null"`
|
||||
Semester string `json:"semester" gorm:"type:varchar(30);not null"`
|
||||
Classroom string `json:"classroom" gorm:"type:varchar(60);not null"`
|
||||
Weekday string `json:"weekday" gorm:"type:varchar(100)"`
|
||||
Periods string `json:"periods" gorm:"type:text"`
|
||||
Weeks string `json:"weeks" gorm:"type:varchar(30)"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (e *EmptyClassroom) BeforeCreate(tx *gorm.DB) error {
|
||||
SetUUIDIfEmpty(&e.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (EmptyClassroom) TableName() string { return "empty_classrooms" }
|
||||
@@ -193,6 +193,7 @@ func autoMigrate(db *gorm.DB) error {
|
||||
&Grade{},
|
||||
&GpaSummary{},
|
||||
&Exam{},
|
||||
&EmptyClassroom{},
|
||||
|
||||
// 用户活跃相关
|
||||
&UserActiveLog{},
|
||||
|
||||
41
internal/repository/empty_classroom_repo.go
Normal file
41
internal/repository/empty_classroom_repo.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"with_you/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EmptyClassroomRepository interface {
|
||||
ListByUserAndSemester(userID, semester string) ([]*model.EmptyClassroom, error)
|
||||
DeleteByUserAndSemester(ctx context.Context, userID, semester string) error
|
||||
BatchCreate(ctx context.Context, classrooms []*model.EmptyClassroom) error
|
||||
}
|
||||
|
||||
type emptyClassroomRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewEmptyClassroomRepository(db *gorm.DB) EmptyClassroomRepository {
|
||||
return &emptyClassroomRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *emptyClassroomRepository) ListByUserAndSemester(userID, semester string) ([]*model.EmptyClassroom, error) {
|
||||
var classrooms []*model.EmptyClassroom
|
||||
err := r.db.Where("user_id = ? AND semester = ?", userID, semester).
|
||||
Order("classroom ASC, weekday ASC").
|
||||
Find(&classrooms).Error
|
||||
return classrooms, err
|
||||
}
|
||||
|
||||
func (r *emptyClassroomRepository) DeleteByUserAndSemester(ctx context.Context, userID, semester string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("user_id = ? AND semester = ?", userID, semester).
|
||||
Delete(&model.EmptyClassroom{}).Error
|
||||
}
|
||||
|
||||
func (r *emptyClassroomRepository) BatchCreate(ctx context.Context, classrooms []*model.EmptyClassroom) error {
|
||||
return r.db.WithContext(ctx).CreateInBatches(classrooms, 100).Error
|
||||
}
|
||||
@@ -28,6 +28,7 @@ type RouterDeps struct {
|
||||
ScheduleHandler *handler.ScheduleHandler
|
||||
GradeHandler *handler.GradeHandler
|
||||
ExamHandler *handler.ExamHandler
|
||||
EmptyClassroomHandler *handler.EmptyClassroomHandler
|
||||
RoleHandler *handler.RoleHandler
|
||||
AdminUserHandler *handler.AdminUserHandler
|
||||
AdminPostHandler *handler.AdminPostHandler
|
||||
@@ -71,6 +72,7 @@ type Router struct {
|
||||
scheduleHandler *handler.ScheduleHandler
|
||||
gradeHandler *handler.GradeHandler
|
||||
examHandler *handler.ExamHandler
|
||||
emptyClassroomHandler *handler.EmptyClassroomHandler
|
||||
roleHandler *handler.RoleHandler
|
||||
adminUserHandler *handler.AdminUserHandler
|
||||
adminPostHandler *handler.AdminPostHandler
|
||||
@@ -117,6 +119,7 @@ func New(deps RouterDeps) *Router {
|
||||
scheduleHandler: deps.ScheduleHandler,
|
||||
gradeHandler: deps.GradeHandler,
|
||||
examHandler: deps.ExamHandler,
|
||||
emptyClassroomHandler: deps.EmptyClassroomHandler,
|
||||
roleHandler: deps.RoleHandler,
|
||||
adminUserHandler: deps.AdminUserHandler,
|
||||
adminPostHandler: deps.AdminPostHandler,
|
||||
@@ -372,6 +375,16 @@ func (r *Router) setupRoutes() {
|
||||
}
|
||||
}
|
||||
|
||||
// 空教室路由
|
||||
if r.emptyClassroomHandler != nil {
|
||||
classrooms := v1.Group("/classrooms")
|
||||
classrooms.Use(authMiddleware)
|
||||
{
|
||||
classrooms.GET("", r.emptyClassroomHandler.ListEmptyClassrooms)
|
||||
classrooms.POST("/sync", r.emptyClassroomHandler.SyncEmptyClassrooms)
|
||||
}
|
||||
}
|
||||
|
||||
// 学习资料路由(公开)
|
||||
if r.materialHandler != nil {
|
||||
materials := v1.Group("/materials")
|
||||
|
||||
135
internal/service/empty_classroom_sync_service.go
Normal file
135
internal/service/empty_classroom_sync_service.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"with_you/internal/grpc/runner"
|
||||
"with_you/internal/model"
|
||||
"with_you/internal/repository"
|
||||
pb "with_you/proto/runner"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SyncEmptyClassroomsRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Semester string `json:"semester"`
|
||||
WeekStart string `json:"week_start"`
|
||||
WeekEnd string `json:"week_end"`
|
||||
CampusCode string `json:"campus_code"`
|
||||
}
|
||||
|
||||
type SyncEmptyClassroomsResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
SyncedCount int `json:"synced_count"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
}
|
||||
|
||||
type EmptyClassroomSyncService interface {
|
||||
SyncUserEmptyClassrooms(ctx context.Context, userID string, req *SyncEmptyClassroomsRequest) (*SyncEmptyClassroomsResult, error)
|
||||
}
|
||||
|
||||
type emptyClassroomSyncService struct {
|
||||
taskManager *runner.TaskManager
|
||||
classroomRepo repository.EmptyClassroomRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewEmptyClassroomSyncService(
|
||||
taskManager *runner.TaskManager,
|
||||
classroomRepo repository.EmptyClassroomRepository,
|
||||
logger *zap.Logger,
|
||||
) EmptyClassroomSyncService {
|
||||
return &emptyClassroomSyncService{
|
||||
taskManager: taskManager,
|
||||
classroomRepo: classroomRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *emptyClassroomSyncService) SyncUserEmptyClassrooms(ctx context.Context, userID string, req *SyncEmptyClassroomsRequest) (*SyncEmptyClassroomsResult, error) {
|
||||
payload := &pb.GetEmptyClassroomPayload{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Semester: req.Semester,
|
||||
WeekStart: req.WeekStart,
|
||||
WeekEnd: req.WeekEnd,
|
||||
CampusCode: req.CampusCode,
|
||||
}
|
||||
|
||||
taskResult, err := s.taskManager.SubmitTaskSync(ctx, pb.TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM, payload)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to submit empty classroom sync task",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to submit task: %w", err)
|
||||
}
|
||||
|
||||
if taskResult.Status != pb.TaskStatus_TASK_STATUS_SUCCESS {
|
||||
return &SyncEmptyClassroomsResult{
|
||||
Success: false,
|
||||
Message: "空教室查询失败",
|
||||
ErrorMessage: taskResult.ErrorMessage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var resultData pb.EmptyClassroomResultData
|
||||
if err := json.Unmarshal(taskResult.Data, &resultData); err != nil {
|
||||
s.logger.Error("failed to parse empty classroom result",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to parse result: %w", err)
|
||||
}
|
||||
|
||||
classrooms := s.convertClassrooms(userID, &resultData)
|
||||
if err := s.saveClassrooms(ctx, userID, resultData.Semester, classrooms); err != nil {
|
||||
s.logger.Error("failed to save empty classrooms",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to save classrooms: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("empty classrooms sync completed",
|
||||
zap.String("user_id", userID),
|
||||
zap.Int("classroom_count", len(classrooms)))
|
||||
|
||||
return &SyncEmptyClassroomsResult{
|
||||
Success: true,
|
||||
Message: "空教室查询成功",
|
||||
SyncedCount: len(classrooms),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *emptyClassroomSyncService) convertClassrooms(userID string, data *pb.EmptyClassroomResultData) []*model.EmptyClassroom {
|
||||
classrooms := make([]*model.EmptyClassroom, 0, len(data.Classrooms))
|
||||
for _, c := range data.Classrooms {
|
||||
classrooms = append(classrooms, &model.EmptyClassroom{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
Semester: data.Semester,
|
||||
Classroom: c.Classroom,
|
||||
Weekday: c.Weekday,
|
||||
Periods: c.Periods,
|
||||
Weeks: c.Weeks,
|
||||
})
|
||||
}
|
||||
return classrooms
|
||||
}
|
||||
|
||||
func (s *emptyClassroomSyncService) saveClassrooms(ctx context.Context, userID, semester string, classrooms []*model.EmptyClassroom) error {
|
||||
if err := s.classroomRepo.DeleteByUserAndSemester(ctx, userID, semester); err != nil {
|
||||
return fmt.Errorf("failed to delete old classrooms: %w", err)
|
||||
}
|
||||
if len(classrooms) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := s.classroomRepo.BatchCreate(ctx, classrooms); err != nil {
|
||||
return fmt.Errorf("failed to create classrooms: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -37,6 +37,7 @@ var HandlerSet = wire.NewSet(
|
||||
handler.NewTradeHandler,
|
||||
handler.NewGradeHandler,
|
||||
handler.NewExamHandler,
|
||||
handler.NewEmptyClassroomHandler,
|
||||
|
||||
// 需要特殊处理的 Handler
|
||||
ProvideUserHandler,
|
||||
|
||||
@@ -26,6 +26,7 @@ var RepositorySet = wire.NewSet(
|
||||
repository.NewScheduleRepository,
|
||||
repository.NewGradeRepository,
|
||||
repository.NewExamRepository,
|
||||
repository.NewEmptyClassroomRepository,
|
||||
repository.NewMaterialSubjectRepository,
|
||||
repository.NewMaterialFileRepository,
|
||||
repository.NewCallRepository,
|
||||
|
||||
@@ -48,6 +48,7 @@ var ServiceSet = wire.NewSet(
|
||||
ProvideScheduleSyncService,
|
||||
ProvideGradeSyncService,
|
||||
ProvideExamSyncService,
|
||||
ProvideEmptyClassroomSyncService,
|
||||
ProvideGroupService,
|
||||
ProvideUploadService,
|
||||
ProvideUserActivityService,
|
||||
@@ -263,6 +264,15 @@ func ProvideExamSyncService(
|
||||
return service.NewExamSyncService(taskManager, examRepo, logger)
|
||||
}
|
||||
|
||||
// ProvideEmptyClassroomSyncService 提供空教室同步服务
|
||||
func ProvideEmptyClassroomSyncService(
|
||||
taskManager *runner.TaskManager,
|
||||
classroomRepo repository.EmptyClassroomRepository,
|
||||
logger *zap.Logger,
|
||||
) service.EmptyClassroomSyncService {
|
||||
return service.NewEmptyClassroomSyncService(taskManager, classroomRepo, logger)
|
||||
}
|
||||
|
||||
// ProvideGroupService 提供群组服务
|
||||
func ProvideGroupService(
|
||||
groupRepo repository.GroupRepository,
|
||||
|
||||
Reference in New Issue
Block a user