feat(runner): implement empty classroom sync functionality
Some checks failed
Build Backend / build (push) Successful in 2m22s
Build Backend / build-docker (push) Failing after 3m3s

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:
2026-05-31 21:29:45 +08:00
parent 2084473fb5
commit 9356cb6876
13 changed files with 3007 additions and 48 deletions

View 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})
}

View 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" }

View File

@@ -193,6 +193,7 @@ func autoMigrate(db *gorm.DB) error {
&Grade{},
&GpaSummary{},
&Exam{},
&EmptyClassroom{},
// 用户活跃相关
&UserActiveLog{},

View 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
}

View File

@@ -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")

View 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
}

View File

@@ -37,6 +37,7 @@ var HandlerSet = wire.NewSet(
handler.NewTradeHandler,
handler.NewGradeHandler,
handler.NewExamHandler,
handler.NewEmptyClassroomHandler,
// 需要特殊处理的 Handler
ProvideUserHandler,

View File

@@ -26,6 +26,7 @@ var RepositorySet = wire.NewSet(
repository.NewScheduleRepository,
repository.NewGradeRepository,
repository.NewExamRepository,
repository.NewEmptyClassroomRepository,
repository.NewMaterialSubjectRepository,
repository.NewMaterialFileRepository,
repository.NewCallRepository,

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v7.34.1
// source: proto/runner/runner.proto
package runner
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
RunnerHub_Connect_FullMethodName = "/runner.RunnerHub/Connect"
)
// RunnerHubClient is the client API for RunnerHub service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// RunnerHub 服务 - 由 Backend 服务器实现
// Runner 作为客户端主动连接,建立双向流通信
type RunnerHubClient interface {
// Connect 建立双向流连接
// Runner 通过此连接注册、接收任务、返回结果
Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamMessage, StreamMessage], error)
}
type runnerHubClient struct {
cc grpc.ClientConnInterface
}
func NewRunnerHubClient(cc grpc.ClientConnInterface) RunnerHubClient {
return &runnerHubClient{cc}
}
func (c *runnerHubClient) Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamMessage, StreamMessage], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &RunnerHub_ServiceDesc.Streams[0], RunnerHub_Connect_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[StreamMessage, StreamMessage]{ClientStream: stream}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type RunnerHub_ConnectClient = grpc.BidiStreamingClient[StreamMessage, StreamMessage]
// RunnerHubServer is the server API for RunnerHub service.
// All implementations must embed UnimplementedRunnerHubServer
// for forward compatibility.
//
// RunnerHub 服务 - 由 Backend 服务器实现
// Runner 作为客户端主动连接,建立双向流通信
type RunnerHubServer interface {
// Connect 建立双向流连接
// Runner 通过此连接注册、接收任务、返回结果
Connect(grpc.BidiStreamingServer[StreamMessage, StreamMessage]) error
mustEmbedUnimplementedRunnerHubServer()
}
// UnimplementedRunnerHubServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedRunnerHubServer struct{}
func (UnimplementedRunnerHubServer) Connect(grpc.BidiStreamingServer[StreamMessage, StreamMessage]) error {
return status.Error(codes.Unimplemented, "method Connect not implemented")
}
func (UnimplementedRunnerHubServer) mustEmbedUnimplementedRunnerHubServer() {}
func (UnimplementedRunnerHubServer) testEmbeddedByValue() {}
// UnsafeRunnerHubServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to RunnerHubServer will
// result in compilation errors.
type UnsafeRunnerHubServer interface {
mustEmbedUnimplementedRunnerHubServer()
}
func RegisterRunnerHubServer(s grpc.ServiceRegistrar, srv RunnerHubServer) {
// If the following call panics, it indicates UnimplementedRunnerHubServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&RunnerHub_ServiceDesc, srv)
}
func _RunnerHub_Connect_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(RunnerHubServer).Connect(&grpc.GenericServerStream[StreamMessage, StreamMessage]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type RunnerHub_ConnectServer = grpc.BidiStreamingServer[StreamMessage, StreamMessage]
// RunnerHub_ServiceDesc is the grpc.ServiceDesc for RunnerHub service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var RunnerHub_ServiceDesc = grpc.ServiceDesc{
ServiceName: "runner.RunnerHub",
HandlerType: (*RunnerHubServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "Connect",
Handler: _RunnerHub_Connect_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "proto/runner/runner.proto",
}

View File

@@ -31,6 +31,7 @@ const (
TaskType_TASK_TYPE_GET_GRADES TaskType = 3 // 获取成绩
TaskType_TASK_TYPE_GET_EXAMS TaskType = 4 // 获取考试安排
TaskType_TASK_TYPE_GET_USER_INFO TaskType = 5 // 获取用户信息
TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM TaskType = 6 // 获取空教室
)
// Enum value maps for TaskType.
@@ -42,6 +43,7 @@ var (
3: "TASK_TYPE_GET_GRADES",
4: "TASK_TYPE_GET_EXAMS",
5: "TASK_TYPE_GET_USER_INFO",
6: "TASK_TYPE_GET_EMPTY_CLASSROOM",
}
TaskType_value = map[string]int32{
"TASK_TYPE_UNKNOWN": 0,
@@ -50,6 +52,7 @@ var (
"TASK_TYPE_GET_GRADES": 3,
"TASK_TYPE_GET_EXAMS": 4,
"TASK_TYPE_GET_USER_INFO": 5,
"TASK_TYPE_GET_EMPTY_CLASSROOM": 6,
}
)
@@ -1711,6 +1714,237 @@ func (x *UserInfoResultData) GetGrade() string {
return ""
}
// GetEmptyClassroomPayload 获取空教室任务载荷
type GetEmptyClassroomPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` // 学号
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // 密码
Semester string `protobuf:"bytes,3,opt,name=semester,proto3" json:"semester,omitempty"` // 学期
WeekStart string `protobuf:"bytes,4,opt,name=week_start,json=weekStart,proto3" json:"week_start,omitempty"` // 起始周次
WeekEnd string `protobuf:"bytes,5,opt,name=week_end,json=weekEnd,proto3" json:"week_end,omitempty"` // 结束周次
CampusCode string `protobuf:"bytes,6,opt,name=campus_code,json=campusCode,proto3" json:"campus_code,omitempty"` // 校区代码
BuildingCode string `protobuf:"bytes,7,opt,name=building_code,json=buildingCode,proto3" json:"building_code,omitempty"` // 楼号代码(可选)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetEmptyClassroomPayload) Reset() {
*x = GetEmptyClassroomPayload{}
mi := &file_proto_runner_runner_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetEmptyClassroomPayload) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetEmptyClassroomPayload) ProtoMessage() {}
func (x *GetEmptyClassroomPayload) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetEmptyClassroomPayload.ProtoReflect.Descriptor instead.
func (*GetEmptyClassroomPayload) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{21}
}
func (x *GetEmptyClassroomPayload) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *GetEmptyClassroomPayload) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
func (x *GetEmptyClassroomPayload) GetSemester() string {
if x != nil {
return x.Semester
}
return ""
}
func (x *GetEmptyClassroomPayload) GetWeekStart() string {
if x != nil {
return x.WeekStart
}
return ""
}
func (x *GetEmptyClassroomPayload) GetWeekEnd() string {
if x != nil {
return x.WeekEnd
}
return ""
}
func (x *GetEmptyClassroomPayload) GetCampusCode() string {
if x != nil {
return x.CampusCode
}
return ""
}
func (x *GetEmptyClassroomPayload) GetBuildingCode() string {
if x != nil {
return x.BuildingCode
}
return ""
}
// EmptyClassroomResultData 空教室结果数据
type EmptyClassroomResultData struct {
state protoimpl.MessageState `protogen:"open.v1"`
Semester string `protobuf:"bytes,1,opt,name=semester,proto3" json:"semester,omitempty"` // 学期
StudentId string `protobuf:"bytes,2,opt,name=student_id,json=studentId,proto3" json:"student_id,omitempty"` // 学号
Classrooms []*EmptyClassroomEntry `protobuf:"bytes,3,rep,name=classrooms,proto3" json:"classrooms,omitempty"` // 空教室列表
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EmptyClassroomResultData) Reset() {
*x = EmptyClassroomResultData{}
mi := &file_proto_runner_runner_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EmptyClassroomResultData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EmptyClassroomResultData) ProtoMessage() {}
func (x *EmptyClassroomResultData) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EmptyClassroomResultData.ProtoReflect.Descriptor instead.
func (*EmptyClassroomResultData) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{22}
}
func (x *EmptyClassroomResultData) GetSemester() string {
if x != nil {
return x.Semester
}
return ""
}
func (x *EmptyClassroomResultData) GetStudentId() string {
if x != nil {
return x.StudentId
}
return ""
}
func (x *EmptyClassroomResultData) GetClassrooms() []*EmptyClassroomEntry {
if x != nil {
return x.Classrooms
}
return nil
}
// EmptyClassroomEntry 空教室条目
type EmptyClassroomEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
Classroom string `protobuf:"bytes,1,opt,name=classroom,proto3" json:"classroom,omitempty"` // 教室名称
Weekday string `protobuf:"bytes,2,opt,name=weekday,proto3" json:"weekday,omitempty"` // 星期几
Periods string `protobuf:"bytes,3,opt,name=periods,proto3" json:"periods,omitempty"` // 可用节次
Term string `protobuf:"bytes,4,opt,name=term,proto3" json:"term,omitempty"` // 学期描述
Weeks string `protobuf:"bytes,5,opt,name=weeks,proto3" json:"weeks,omitempty"` // 周次范围
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EmptyClassroomEntry) Reset() {
*x = EmptyClassroomEntry{}
mi := &file_proto_runner_runner_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EmptyClassroomEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EmptyClassroomEntry) ProtoMessage() {}
func (x *EmptyClassroomEntry) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EmptyClassroomEntry.ProtoReflect.Descriptor instead.
func (*EmptyClassroomEntry) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{23}
}
func (x *EmptyClassroomEntry) GetClassroom() string {
if x != nil {
return x.Classroom
}
return ""
}
func (x *EmptyClassroomEntry) GetWeekday() string {
if x != nil {
return x.Weekday
}
return ""
}
func (x *EmptyClassroomEntry) GetPeriods() string {
if x != nil {
return x.Periods
}
return ""
}
func (x *EmptyClassroomEntry) GetTerm() string {
if x != nil {
return x.Term
}
return ""
}
func (x *EmptyClassroomEntry) GetWeeks() string {
if x != nil {
return x.Weeks
}
return ""
}
var File_proto_runner_runner_proto protoreflect.FileDescriptor
const file_proto_runner_runner_proto_rawDesc = "" +
@@ -1849,14 +2083,38 @@ const file_proto_runner_runner_proto_rawDesc = "" +
"\x05major\x18\x05 \x01(\tR\x05major\x12\x1d\n" +
"\n" +
"class_name\x18\x06 \x01(\tR\tclassName\x12\x14\n" +
"\x05grade\x18\a \x01(\tR\x05grade*\xa2\x01\n" +
"\x05grade\x18\a \x01(\tR\x05grade\"\xee\x01\n" +
"\x18GetEmptyClassroomPayload\x12\x1a\n" +
"\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" +
"\bpassword\x18\x02 \x01(\tR\bpassword\x12\x1a\n" +
"\bsemester\x18\x03 \x01(\tR\bsemester\x12\x1d\n" +
"\n" +
"week_start\x18\x04 \x01(\tR\tweekStart\x12\x19\n" +
"\bweek_end\x18\x05 \x01(\tR\aweekEnd\x12\x1f\n" +
"\vcampus_code\x18\x06 \x01(\tR\n" +
"campusCode\x12#\n" +
"\rbuilding_code\x18\a \x01(\tR\fbuildingCode\"\x92\x01\n" +
"\x18EmptyClassroomResultData\x12\x1a\n" +
"\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" +
"\n" +
"student_id\x18\x02 \x01(\tR\tstudentId\x12;\n" +
"\n" +
"classrooms\x18\x03 \x03(\v2\x1b.runner.EmptyClassroomEntryR\n" +
"classrooms\"\x91\x01\n" +
"\x13EmptyClassroomEntry\x12\x1c\n" +
"\tclassroom\x18\x01 \x01(\tR\tclassroom\x12\x18\n" +
"\aweekday\x18\x02 \x01(\tR\aweekday\x12\x18\n" +
"\aperiods\x18\x03 \x01(\tR\aperiods\x12\x12\n" +
"\x04term\x18\x04 \x01(\tR\x04term\x12\x14\n" +
"\x05weeks\x18\x05 \x01(\tR\x05weeks*\xc5\x01\n" +
"\bTaskType\x12\x15\n" +
"\x11TASK_TYPE_UNKNOWN\x10\x00\x12\x13\n" +
"\x0fTASK_TYPE_LOGIN\x10\x01\x12\x1a\n" +
"\x16TASK_TYPE_GET_SCHEDULE\x10\x02\x12\x18\n" +
"\x14TASK_TYPE_GET_GRADES\x10\x03\x12\x17\n" +
"\x13TASK_TYPE_GET_EXAMS\x10\x04\x12\x1b\n" +
"\x17TASK_TYPE_GET_USER_INFO\x10\x05*\x8a\x01\n" +
"\x17TASK_TYPE_GET_USER_INFO\x10\x05\x12!\n" +
"\x1dTASK_TYPE_GET_EMPTY_CLASSROOM\x10\x06*\x8a\x01\n" +
"\n" +
"TaskStatus\x12\x17\n" +
"\x13TASK_STATUS_UNKNOWN\x10\x00\x12\x17\n" +
@@ -1880,7 +2138,7 @@ func file_proto_runner_runner_proto_rawDescGZIP() []byte {
}
var file_proto_runner_runner_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
var file_proto_runner_runner_proto_goTypes = []any{
(TaskType)(0), // 0: runner.TaskType
(TaskStatus)(0), // 1: runner.TaskStatus
@@ -1905,7 +2163,10 @@ var file_proto_runner_runner_proto_goTypes = []any{
(*ExamsResultData)(nil), // 20: runner.ExamsResultData
(*Exam)(nil), // 21: runner.Exam
(*UserInfoResultData)(nil), // 22: runner.UserInfoResultData
nil, // 23: runner.RegisterRequest.CapabilitiesEntry
(*GetEmptyClassroomPayload)(nil), // 23: runner.GetEmptyClassroomPayload
(*EmptyClassroomResultData)(nil), // 24: runner.EmptyClassroomResultData
(*EmptyClassroomEntry)(nil), // 25: runner.EmptyClassroomEntry
nil, // 26: runner.RegisterRequest.CapabilitiesEntry
}
var file_proto_runner_runner_proto_depIdxs = []int32{
3, // 0: runner.StreamMessage.register:type_name -> runner.RegisterRequest
@@ -1915,7 +2176,7 @@ var file_proto_runner_runner_proto_depIdxs = []int32{
7, // 4: runner.StreamMessage.task:type_name -> runner.Task
9, // 5: runner.StreamMessage.result:type_name -> runner.TaskResult
8, // 6: runner.StreamMessage.task_cancel:type_name -> runner.TaskCancel
23, // 7: runner.RegisterRequest.capabilities:type_name -> runner.RegisterRequest.CapabilitiesEntry
26, // 7: runner.RegisterRequest.capabilities:type_name -> runner.RegisterRequest.CapabilitiesEntry
0, // 8: runner.Task.type:type_name -> runner.TaskType
1, // 9: runner.TaskResult.status:type_name -> runner.TaskStatus
15, // 10: runner.ScheduleResultData.courses:type_name -> runner.Course
@@ -1923,13 +2184,14 @@ var file_proto_runner_runner_proto_depIdxs = []int32{
19, // 12: runner.GradesResultData.grades:type_name -> runner.Grade
17, // 13: runner.GradesResultData.gpa_summary:type_name -> runner.GpaSummary
21, // 14: runner.ExamsResultData.exams:type_name -> runner.Exam
2, // 15: runner.RunnerHub.Connect:input_type -> runner.StreamMessage
2, // 16: runner.RunnerHub.Connect:output_type -> runner.StreamMessage
16, // [16:17] is the sub-list for method output_type
15, // [15:16] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension type_name
15, // [15:15] is the sub-list for extension extendee
0, // [0:15] is the sub-list for field type_name
25, // 15: runner.EmptyClassroomResultData.classrooms:type_name -> runner.EmptyClassroomEntry
2, // 16: runner.RunnerHub.Connect:input_type -> runner.StreamMessage
2, // 17: runner.RunnerHub.Connect:output_type -> runner.StreamMessage
17, // [17:18] is the sub-list for method output_type
16, // [16:17] is the sub-list for method input_type
16, // [16:16] is the sub-list for extension type_name
16, // [16:16] is the sub-list for extension extendee
0, // [0:16] is the sub-list for field type_name
}
func init() { file_proto_runner_runner_proto_init() }
@@ -1952,7 +2214,7 @@ func file_proto_runner_runner_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_runner_runner_proto_rawDesc), len(file_proto_runner_runner_proto_rawDesc)),
NumEnums: 2,
NumMessages: 22,
NumMessages: 25,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -81,6 +81,7 @@ enum TaskType {
TASK_TYPE_GET_GRADES = 3; // 获取成绩
TASK_TYPE_GET_EXAMS = 4; // 获取考试安排
TASK_TYPE_GET_USER_INFO = 5; // 获取用户信息
TASK_TYPE_GET_EMPTY_CLASSROOM = 6; // 获取空教室
}
// TaskStatus 任务状态枚举
@@ -236,3 +237,30 @@ message UserInfoResultData {
string class_name = 6; // 班级
string grade = 7; // 年级
}
// GetEmptyClassroomPayload 获取空教室任务载荷
message GetEmptyClassroomPayload {
string username = 1; // 学号
string password = 2; // 密码
string semester = 3; // 学期
string week_start = 4; // 起始周次
string week_end = 5; // 结束周次
string campus_code = 6; // 校区代码
string building_code = 7; // 楼号代码(可选)
}
// EmptyClassroomResultData 空教室结果数据
message EmptyClassroomResultData {
string semester = 1; // 学期
string student_id = 2; // 学号
repeated EmptyClassroomEntry classrooms = 3; // 空教室列表
}
// EmptyClassroomEntry 空教室条目
message EmptyClassroomEntry {
string classroom = 1; // 教室名称
string weekday = 2; // 星期几
string periods = 3; // 可用节次
string term = 4; // 学期描述
string weeks = 5; // 周次范围
}