feat(api): implement exam and grade synchronization modules
All checks were successful
Build Backend / build (push) Successful in 2m18s
Build Backend / build-docker (push) Successful in 1m20s

Add new functionality for managing and synchronizing academic grades and exam schedules. This includes new models, repositories, services, and HTTP handlers, along with updated gRPC definitions and dependency injection configuration.

Additionally, improve the reliability of unread message count tracking by implementing an atomic Lua script for Redis operations in the conversation cache.
This commit is contained in:
2026-05-12 01:28:18 +08:00
parent 628a6acbe9
commit 2f2bbc646e
20 changed files with 1052 additions and 212 deletions

View File

@@ -40,6 +40,8 @@ func ProvideRouter(
voteHandler *handler.VoteHandler, voteHandler *handler.VoteHandler,
channelHandler *handler.ChannelHandler, channelHandler *handler.ChannelHandler,
scheduleHandler *handler.ScheduleHandler, scheduleHandler *handler.ScheduleHandler,
gradeHandler *handler.GradeHandler,
examHandler *handler.ExamHandler,
roleHandler *handler.RoleHandler, roleHandler *handler.RoleHandler,
adminUserHandler *handler.AdminUserHandler, adminUserHandler *handler.AdminUserHandler,
adminPostHandler *handler.AdminPostHandler, adminPostHandler *handler.AdminPostHandler,
@@ -78,6 +80,8 @@ func ProvideRouter(
VoteHandler: voteHandler, VoteHandler: voteHandler,
ChannelHandler: channelHandler, ChannelHandler: channelHandler,
ScheduleHandler: scheduleHandler, ScheduleHandler: scheduleHandler,
GradeHandler: gradeHandler,
ExamHandler: examHandler,
RoleHandler: roleHandler, RoleHandler: roleHandler,
AdminUserHandler: adminUserHandler, AdminUserHandler: adminUserHandler,
AdminPostHandler: adminPostHandler, AdminPostHandler: adminPostHandler,

View File

@@ -105,6 +105,12 @@ func InitializeApp() (*App, error) {
taskManager := wire.ProvideTaskManager(runnerHub, logger) taskManager := wire.ProvideTaskManager(runnerHub, logger)
scheduleSyncService := wire.ProvideScheduleSyncService(taskManager, scheduleRepository, logger) scheduleSyncService := wire.ProvideScheduleSyncService(taskManager, scheduleRepository, logger)
scheduleHandler := wire.ProvideScheduleHandler(scheduleService, scheduleSyncService) scheduleHandler := wire.ProvideScheduleHandler(scheduleService, scheduleSyncService)
gradeRepository := repository.NewGradeRepository(db)
gradeSyncService := wire.ProvideGradeSyncService(taskManager, gradeRepository, logger)
gradeHandler := handler.NewGradeHandler(gradeSyncService, gradeRepository)
examRepository := repository.NewExamRepository(db)
examSyncService := wire.ProvideExamSyncService(taskManager, examRepository, logger)
examHandler := handler.NewExamHandler(examSyncService, examRepository)
roleRepository := wire.ProvideRoleRepository(db) roleRepository := wire.ProvideRoleRepository(db)
enforcer := wire.ProvideCasbinEnforcer(config, db) enforcer := wire.ProvideCasbinEnforcer(config, db)
casbinService := wire.ProvideCasbinService(enforcer, cache, roleRepository, config) casbinService := wire.ProvideCasbinService(enforcer, cache, roleRepository, config)
@@ -147,7 +153,7 @@ func InitializeApp() (*App, error) {
tradeService := wire.ProvideTradeService(tradeRepository) tradeService := wire.ProvideTradeService(tradeRepository)
tradeHandler := handler.NewTradeHandler(tradeService) tradeHandler := handler.NewTradeHandler(tradeService)
wsHandler := wire.ProvideWSHandler(messagePublisher, chatService, groupService, jwtService, callService, userRepository) wsHandler := wire.ProvideWSHandler(messagePublisher, chatService, groupService, jwtService, callService, userRepository)
router := ProvideRouter(userRepository, userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, adminProfileAuditHandler, setupHandler, tradeHandler, logService, userActivityService, casbinService, wsHandler) router := ProvideRouter(userRepository, userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, gradeHandler, examHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, adminProfileAuditHandler, setupHandler, tradeHandler, logService, userActivityService, casbinService, wsHandler)
hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, channelRepository, cache, client) hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, channelRepository, cache, client)
server := wire.ProvideGRPCServer(config, logger, runnerHub) server := wire.ProvideGRPCServer(config, logger, runnerHub)
runnerRegistry := wire.ProvideRunnerRegistry(config, client) runnerRegistry := wire.ProvideRunnerRegistry(config, client)
@@ -175,6 +181,8 @@ func ProvideRouter(
voteHandler *handler.VoteHandler, voteHandler *handler.VoteHandler,
channelHandler *handler.ChannelHandler, channelHandler *handler.ChannelHandler,
scheduleHandler *handler.ScheduleHandler, scheduleHandler *handler.ScheduleHandler,
gradeHandler *handler.GradeHandler,
examHandler *handler.ExamHandler,
roleHandler *handler.RoleHandler, roleHandler *handler.RoleHandler,
adminUserHandler *handler.AdminUserHandler, adminUserHandler *handler.AdminUserHandler,
adminPostHandler *handler.AdminPostHandler, adminPostHandler *handler.AdminPostHandler,
@@ -213,6 +221,8 @@ func ProvideRouter(
VoteHandler: voteHandler, VoteHandler: voteHandler,
ChannelHandler: channelHandler, ChannelHandler: channelHandler,
ScheduleHandler: scheduleHandler, ScheduleHandler: scheduleHandler,
GradeHandler: gradeHandler,
ExamHandler: examHandler,
RoleHandler: roleHandler, RoleHandler: roleHandler,
AdminUserHandler: adminUserHandler, AdminUserHandler: adminUserHandler,
AdminPostHandler: adminPostHandler, AdminPostHandler: adminPostHandler,

View File

@@ -670,12 +670,14 @@ func (c *ConversationCache) IncrementUnread(ctx context.Context, userID, convID
return nil return nil
} }
// ClearUnread 清零用户在某会话的未读数 // ClearUnread 清零用户在某会话的未读数(原子操作,防止与 IncrementUnread 竞态)
func (c *ConversationCache) ClearUnread(ctx context.Context, userID, convID string) error { func (c *ConversationCache) ClearUnread(ctx context.Context, userID, convID string) error {
hashKey := UnreadHashKey(userID) hashKey := UnreadHashKey(userID)
totalKey := UnreadTotalKey(userID) totalKey := UnreadTotalKey(userID)
// 读取当前未读数 redisCache, ok := c.cache.(*RedisCache)
if !ok {
// miniredis/内存模式:保持原逻辑(非原子,但无并发竞态风险)
currentStr, err := c.cache.HGet(ctx, hashKey, convID) currentStr, err := c.cache.HGet(ctx, hashKey, convID)
if err != nil { if err != nil {
currentStr = "0" currentStr = "0"
@@ -684,17 +686,35 @@ func (c *ConversationCache) ClearUnread(ctx context.Context, userID, convID stri
if v, parseErr := fmt.Sscanf(currentStr, "%d", &current); parseErr != nil || v != 1 { if v, parseErr := fmt.Sscanf(currentStr, "%d", &current); parseErr != nil || v != 1 {
current = 0 current = 0
} }
// 清零该会话未读数
if err := c.cache.HSet(ctx, hashKey, convID, "0"); err != nil { if err := c.cache.HSet(ctx, hashKey, convID, "0"); err != nil {
return fmt.Errorf("hset clear unread failed: %w", err) return fmt.Errorf("hset clear unread failed: %w", err)
} }
// 减少总未读数
if current > 0 { if current > 0 {
c.cache.IncrBySeq(ctx, totalKey, -current) c.cache.IncrBySeq(ctx, totalKey, -current)
} }
return nil
}
rdb := redisCache.GetRedisClient().GetClient()
nHashKey := normalizeKey(hashKey)
nTotalKey := normalizeKey(totalKey)
script := redis.NewScript(`
local current = tonumber(redis.call('HGET', KEYS[1], ARGV[1]))
if current == nil or current <= 0 then
return 0
end
redis.call('HSET', KEYS[1], ARGV[1], '0')
local newTotal = tonumber(redis.call('INCRBY', KEYS[2], -current))
if newTotal < 0 then
redis.call('SET', KEYS[2], '0')
end
return current
`)
_, err := script.Run(ctx, rdb, []string{nHashKey, nTotalKey}, convID).Result()
if err != nil {
return fmt.Errorf("lua clear unread failed: %w", err)
}
return nil return nil
} }

View File

@@ -0,0 +1,78 @@
package handler
import (
"github.com/gin-gonic/gin"
"with_you/internal/pkg/response"
"with_you/internal/repository"
"with_you/internal/service"
)
type ExamHandler struct {
examSyncService service.ExamSyncService
examRepo repository.ExamRepository
}
func NewExamHandler(examSyncService service.ExamSyncService, examRepo repository.ExamRepository) *ExamHandler {
return &ExamHandler{
examSyncService: examSyncService,
examRepo: examRepo,
}
}
type syncExamsRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Semester string `json:"semester"`
}
func (h *ExamHandler) SyncExams(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
var req syncExamsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request: "+err.Error())
return
}
result, err := h.examSyncService.SyncUserExams(c.Request.Context(), userID, &service.SyncExamsRequest{
Username: req.Username,
Password: req.Password,
Semester: req.Semester,
})
if err != nil {
response.HandleError(c, err, "failed to sync exams")
return
}
if !result.Success {
response.Success(c, result)
return
}
response.SuccessWithMessage(c, result.Message, result)
}
func (h *ExamHandler) ListExams(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
}
exams, err := h.examRepo.ListByUserAndSemester(userID, semester)
if err != nil {
response.HandleError(c, err, "failed to list exams")
return
}
response.Success(c, gin.H{"exams": exams})
}

View File

@@ -0,0 +1,83 @@
package handler
import (
"github.com/gin-gonic/gin"
"with_you/internal/pkg/response"
"with_you/internal/repository"
"with_you/internal/service"
)
type GradeHandler struct {
gradeSyncService service.GradeSyncService
gradeRepo repository.GradeRepository
}
func NewGradeHandler(gradeSyncService service.GradeSyncService, gradeRepo repository.GradeRepository) *GradeHandler {
return &GradeHandler{
gradeSyncService: gradeSyncService,
gradeRepo: gradeRepo,
}
}
type syncGradesRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Semester string `json:"semester"`
}
func (h *GradeHandler) SyncGrades(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
var req syncGradesRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "invalid request: "+err.Error())
return
}
result, err := h.gradeSyncService.SyncUserGrades(c.Request.Context(), userID, &service.SyncGradesRequest{
Username: req.Username,
Password: req.Password,
Semester: req.Semester,
})
if err != nil {
response.HandleError(c, err, "failed to sync grades")
return
}
if !result.Success {
response.Success(c, result)
return
}
response.SuccessWithMessage(c, result.Message, result)
}
func (h *GradeHandler) ListGrades(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
}
grades, err := h.gradeRepo.ListByUserAndSemester(userID, semester)
if err != nil {
response.HandleError(c, err, "failed to list grades")
return
}
gpaSummary, _ := h.gradeRepo.GetGpaSummary(userID, semester)
response.Success(c, gin.H{
"grades": grades,
"gpa_summary": gpaSummary,
})
}

28
internal/model/exam.go Normal file
View File

@@ -0,0 +1,28 @@
package model
import (
"time"
"gorm.io/gorm"
)
type Exam 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"`
CourseName string `json:"course_name" gorm:"type:varchar(120);not null"`
ExamType string `json:"exam_type" gorm:"type:varchar(30)"`
Date string `json:"date" gorm:"type:varchar(20)"`
Time string `json:"time" gorm:"type:varchar(30)"`
Week string `json:"week" gorm:"type:varchar(20)"`
Weekday string `json:"weekday" gorm:"type:varchar(10)"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (e *Exam) BeforeCreate(tx *gorm.DB) error {
SetUUIDIfEmpty(&e.ID)
return nil
}
func (Exam) TableName() string { return "exams" }

51
internal/model/grade.go Normal file
View File

@@ -0,0 +1,51 @@
package model
import (
"time"
"gorm.io/gorm"
)
type Grade struct {
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
UserID string `json:"user_id" gorm:"type:varchar(36);index;not null"`
Term string `json:"term" gorm:"type:varchar(30);not null"`
CourseCode string `json:"course_code" gorm:"type:varchar(30)"`
CourseName string `json:"course_name" gorm:"type:varchar(120);not null"`
Nature string `json:"nature" gorm:"type:varchar(30)"`
Category string `json:"category" gorm:"type:varchar(30)"`
Credit string `json:"credit" gorm:"type:varchar(10)"`
Score string `json:"score" gorm:"type:varchar(20)"`
GradePoint string `json:"grade_point" gorm:"type:varchar(10)"`
IsExam string `json:"is_exam" gorm:"type:varchar(10)"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (g *Grade) BeforeCreate(tx *gorm.DB) error {
SetUUIDIfEmpty(&g.ID)
return nil
}
func (Grade) TableName() string { return "grades" }
type GpaSummary 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"`
AverageGpa string `json:"average_gpa" gorm:"type:varchar(20)"`
Rank string `json:"rank" gorm:"type:varchar(30)"`
ExamTotalGpa string `json:"exam_total_gpa" gorm:"type:varchar(20)"`
ExamTotalCredits string `json:"exam_total_credits" gorm:"type:varchar(20)"`
FailCredits string `json:"fail_credits" gorm:"type:varchar(20)"`
Semesters string `json:"semesters" gorm:"type:varchar(20)"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (g *GpaSummary) BeforeCreate(tx *gorm.DB) error {
SetUUIDIfEmpty(&g.ID)
return nil
}
func (GpaSummary) TableName() string { return "gpa_summaries" }

View File

@@ -154,6 +154,11 @@ func autoMigrate(db *gorm.DB) error {
// 课表 // 课表
&ScheduleCourse{}, &ScheduleCourse{},
// 成绩与考试
&Grade{},
&GpaSummary{},
&Exam{},
// 用户活跃相关 // 用户活跃相关
&UserActiveLog{}, &UserActiveLog{},
&UserActivityStat{}, &UserActivityStat{},

View File

@@ -0,0 +1,41 @@
package repository
import (
"context"
"with_you/internal/model"
"gorm.io/gorm"
)
type ExamRepository interface {
ListByUserAndSemester(userID, semester string) ([]*model.Exam, error)
DeleteByUserAndSemester(ctx context.Context, userID, semester string) error
BatchCreate(ctx context.Context, exams []*model.Exam) error
}
type examRepository struct {
db *gorm.DB
}
func NewExamRepository(db *gorm.DB) ExamRepository {
return &examRepository{db: db}
}
func (r *examRepository) ListByUserAndSemester(userID, semester string) ([]*model.Exam, error) {
var exams []*model.Exam
err := r.db.Where("user_id = ? AND semester = ?", userID, semester).
Order("created_at ASC").
Find(&exams).Error
return exams, err
}
func (r *examRepository) DeleteByUserAndSemester(ctx context.Context, userID, semester string) error {
return r.db.WithContext(ctx).
Where("user_id = ? AND semester = ?", userID, semester).
Delete(&model.Exam{}).Error
}
func (r *examRepository) BatchCreate(ctx context.Context, exams []*model.Exam) error {
return r.db.WithContext(ctx).CreateInBatches(exams, 100).Error
}

View File

@@ -0,0 +1,56 @@
package repository
import (
"context"
"with_you/internal/model"
"gorm.io/gorm"
)
type GradeRepository interface {
ListByUserAndSemester(userID, semester string) ([]*model.Grade, error)
DeleteByUserAndSemester(ctx context.Context, userID, semester string) error
BatchCreate(ctx context.Context, grades []*model.Grade) error
GetGpaSummary(userID, semester string) (*model.GpaSummary, error)
SaveGpaSummary(ctx context.Context, summary *model.GpaSummary) error
}
type gradeRepository struct {
db *gorm.DB
}
func NewGradeRepository(db *gorm.DB) GradeRepository {
return &gradeRepository{db: db}
}
func (r *gradeRepository) ListByUserAndSemester(userID, semester string) ([]*model.Grade, error) {
var grades []*model.Grade
err := r.db.Where("user_id = ? AND term = ?", userID, semester).
Order("created_at ASC").
Find(&grades).Error
return grades, err
}
func (r *gradeRepository) DeleteByUserAndSemester(ctx context.Context, userID, semester string) error {
return r.db.WithContext(ctx).
Where("user_id = ? AND term = ?", userID, semester).
Delete(&model.Grade{}).Error
}
func (r *gradeRepository) BatchCreate(ctx context.Context, grades []*model.Grade) error {
return r.db.WithContext(ctx).CreateInBatches(grades, 100).Error
}
func (r *gradeRepository) GetGpaSummary(userID, semester string) (*model.GpaSummary, error) {
var summary model.GpaSummary
err := r.db.Where("user_id = ? AND semester = ?", userID, semester).First(&summary).Error
if err != nil {
return nil, err
}
return &summary, nil
}
func (r *gradeRepository) SaveGpaSummary(ctx context.Context, summary *model.GpaSummary) error {
return r.db.WithContext(ctx).Save(summary).Error
}

View File

@@ -26,6 +26,8 @@ type RouterDeps struct {
VoteHandler *handler.VoteHandler VoteHandler *handler.VoteHandler
ChannelHandler *handler.ChannelHandler ChannelHandler *handler.ChannelHandler
ScheduleHandler *handler.ScheduleHandler ScheduleHandler *handler.ScheduleHandler
GradeHandler *handler.GradeHandler
ExamHandler *handler.ExamHandler
RoleHandler *handler.RoleHandler RoleHandler *handler.RoleHandler
AdminUserHandler *handler.AdminUserHandler AdminUserHandler *handler.AdminUserHandler
AdminPostHandler *handler.AdminPostHandler AdminPostHandler *handler.AdminPostHandler
@@ -67,6 +69,8 @@ type Router struct {
voteHandler *handler.VoteHandler voteHandler *handler.VoteHandler
channelHandler *handler.ChannelHandler channelHandler *handler.ChannelHandler
scheduleHandler *handler.ScheduleHandler scheduleHandler *handler.ScheduleHandler
gradeHandler *handler.GradeHandler
examHandler *handler.ExamHandler
roleHandler *handler.RoleHandler roleHandler *handler.RoleHandler
adminUserHandler *handler.AdminUserHandler adminUserHandler *handler.AdminUserHandler
adminPostHandler *handler.AdminPostHandler adminPostHandler *handler.AdminPostHandler
@@ -111,6 +115,8 @@ func New(deps RouterDeps) *Router {
voteHandler: deps.VoteHandler, voteHandler: deps.VoteHandler,
channelHandler: deps.ChannelHandler, channelHandler: deps.ChannelHandler,
scheduleHandler: deps.ScheduleHandler, scheduleHandler: deps.ScheduleHandler,
gradeHandler: deps.GradeHandler,
examHandler: deps.ExamHandler,
roleHandler: deps.RoleHandler, roleHandler: deps.RoleHandler,
adminUserHandler: deps.AdminUserHandler, adminUserHandler: deps.AdminUserHandler,
adminPostHandler: deps.AdminPostHandler, adminPostHandler: deps.AdminPostHandler,
@@ -346,6 +352,26 @@ func (r *Router) setupRoutes() {
} }
} }
// 成绩路由
if r.gradeHandler != nil {
grades := v1.Group("/grades")
grades.Use(authMiddleware)
{
grades.GET("", r.gradeHandler.ListGrades)
grades.POST("/sync", r.gradeHandler.SyncGrades)
}
}
// 考试安排路由
if r.examHandler != nil {
exams := v1.Group("/exams")
exams.Use(authMiddleware)
{
exams.GET("", r.examHandler.ListExams)
exams.POST("/sync", r.examHandler.SyncExams)
}
}
// 学习资料路由(公开) // 学习资料路由(公开)
if r.materialHandler != nil { if r.materialHandler != nil {
materials := v1.Group("/materials") materials := v1.Group("/materials")

View File

@@ -387,6 +387,36 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
// 获取会话中的参与者并发送消息 // 获取会话中的参与者并发送消息
participants, err := s.getParticipants(ctx, conversationID) participants, err := s.getParticipants(ctx, conversationID)
if err == nil { if err == nil {
// 先更新未读计数器,再推送 WS 事件,确保事件中的 total_unread 已包含新消息
if s.conversationCache != nil {
for _, p := range participants {
if p.UserID == senderID {
continue
}
if incrErr := s.conversationCache.IncrementUnread(context.Background(), p.UserID, conversationID); incrErr != nil {
zap.L().Warn("increment unread from redis hash failed",
zap.String("userID", p.UserID),
zap.String("convID", conversationID),
zap.Error(incrErr),
)
}
}
keys := make([]string, 0, len(participants)*2)
for _, p := range participants {
keys = append(keys,
cache.UnreadDetailKey(p.UserID, conversationID),
cache.UnreadConversationKey(p.UserID),
)
}
if len(keys) > 0 {
s.cache.DeleteBatch(keys)
}
for _, p := range participants {
s.conversationCache.InvalidateConversationList(p.UserID)
}
}
targetIDs := make([]string, 0, len(participants)) targetIDs := make([]string, 0, len(participants))
for _, p := range participants { for _, p := range participants {
if conv.Type == model.ConversationTypePrivate && p.UserID == senderID { if conv.Type == model.ConversationTypePrivate && p.UserID == senderID {
@@ -422,36 +452,6 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
} }
} }
if s.conversationCache != nil {
// 使用 Redis Hash 预计算未读数
for _, p := range participants {
if p.UserID == senderID {
continue
}
if incrErr := s.conversationCache.IncrementUnread(context.Background(), p.UserID, conversationID); incrErr != nil {
zap.L().Warn("increment unread from redis hash failed",
zap.String("userID", p.UserID),
zap.String("convID", conversationID),
zap.Error(incrErr),
)
}
}
keys := make([]string, 0, len(participants)*2)
for _, p := range participants {
keys = append(keys,
cache.UnreadDetailKey(p.UserID, conversationID),
cache.UnreadConversationKey(p.UserID),
)
}
if len(keys) > 0 {
s.cache.DeleteBatch(keys)
}
for _, p := range participants {
s.conversationCache.InvalidateConversationList(p.UserID)
}
}
if s.pushSvc != nil && len(participants) > 0 { if s.pushSvc != nil && len(participants) > 0 {
sender := ChatMessageSender{ID: senderID} sender := ChatMessageSender{ID: senderID}
if senderUser, sErr := s.userRepo.GetByID(senderID); sErr == nil { if senderUser, sErr := s.userRepo.GetByID(senderID); sErr == nil {
@@ -695,7 +695,8 @@ func (s *chatServiceImpl) GetAllUnreadCount(ctx context.Context, userID string)
maxSeqs[conv.ID] = maxSeq maxSeqs[conv.ID] = maxSeq
} }
} }
if len(maxSeqs) > 0 { // 只有全部会话的 msg_seq 都命中时才用算术结果,避免部分缺失导致低估
if len(maxSeqs) == len(convIDs) {
if total, err := s.conversationCache.ComputeAllUnreadCount(ctx, userID, convIDs, maxSeqs); err == nil { if total, err := s.conversationCache.ComputeAllUnreadCount(ctx, userID, convIDs, maxSeqs); err == nil {
return total, nil return total, nil
} }

View File

@@ -0,0 +1,131 @@
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 SyncExamsRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Semester string `json:"semester"`
}
type SyncExamsResult struct {
Success bool `json:"success"`
Message string `json:"message"`
SyncedCount int `json:"synced_count"`
ErrorMessage string `json:"error_message,omitempty"`
}
type ExamSyncService interface {
SyncUserExams(ctx context.Context, userID string, req *SyncExamsRequest) (*SyncExamsResult, error)
}
type examSyncService struct {
taskManager *runner.TaskManager
examRepo repository.ExamRepository
logger *zap.Logger
}
func NewExamSyncService(
taskManager *runner.TaskManager,
examRepo repository.ExamRepository,
logger *zap.Logger,
) ExamSyncService {
return &examSyncService{
taskManager: taskManager,
examRepo: examRepo,
logger: logger,
}
}
func (s *examSyncService) SyncUserExams(ctx context.Context, userID string, req *SyncExamsRequest) (*SyncExamsResult, error) {
payload := &pb.GetExamsPayload{
Username: req.Username,
Password: req.Password,
Semester: req.Semester,
}
taskResult, err := s.taskManager.SubmitTaskSync(ctx, pb.TaskType_TASK_TYPE_GET_EXAMS, payload)
if err != nil {
s.logger.Error("failed to submit exams 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 &SyncExamsResult{
Success: false,
Message: "考试安排获取失败",
ErrorMessage: taskResult.ErrorMessage,
}, nil
}
var resultData pb.ExamsResultData
if err := json.Unmarshal(taskResult.Data, &resultData); err != nil {
s.logger.Error("failed to parse exams result",
zap.String("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("failed to parse result: %w", err)
}
exams := s.convertExams(userID, &resultData)
if err := s.saveExams(ctx, userID, resultData.Semester, exams); err != nil {
s.logger.Error("failed to save exams",
zap.String("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("failed to save exams: %w", err)
}
s.logger.Info("exams sync completed",
zap.String("user_id", userID),
zap.Int("exam_count", len(exams)))
return &SyncExamsResult{
Success: true,
Message: "考试安排同步成功",
SyncedCount: len(exams),
}, nil
}
func (s *examSyncService) convertExams(userID string, data *pb.ExamsResultData) []*model.Exam {
exams := make([]*model.Exam, 0, len(data.Exams))
for _, e := range data.Exams {
exams = append(exams, &model.Exam{
ID: uuid.New().String(),
UserID: userID,
Semester: data.Semester,
CourseName: e.CourseName,
ExamType: e.ExamType,
Date: e.Date,
Time: e.Time,
Week: e.Week,
Weekday: e.Weekday,
})
}
return exams
}
func (s *examSyncService) saveExams(ctx context.Context, userID, semester string, exams []*model.Exam) error {
if err := s.examRepo.DeleteByUserAndSemester(ctx, userID, semester); err != nil {
return fmt.Errorf("failed to delete old exams: %w", err)
}
if len(exams) == 0 {
return nil
}
if err := s.examRepo.BatchCreate(ctx, exams); err != nil {
return fmt.Errorf("failed to create exams: %w", err)
}
return nil
}

View File

@@ -0,0 +1,156 @@
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 SyncGradesRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Semester string `json:"semester"`
}
type SyncGradesResult struct {
Success bool `json:"success"`
Message string `json:"message"`
SyncedCount int `json:"synced_count"`
ErrorMessage string `json:"error_message,omitempty"`
}
type GradeSyncService interface {
SyncUserGrades(ctx context.Context, userID string, req *SyncGradesRequest) (*SyncGradesResult, error)
}
type gradeSyncService struct {
taskManager *runner.TaskManager
gradeRepo repository.GradeRepository
logger *zap.Logger
}
func NewGradeSyncService(
taskManager *runner.TaskManager,
gradeRepo repository.GradeRepository,
logger *zap.Logger,
) GradeSyncService {
return &gradeSyncService{
taskManager: taskManager,
gradeRepo: gradeRepo,
logger: logger,
}
}
func (s *gradeSyncService) SyncUserGrades(ctx context.Context, userID string, req *SyncGradesRequest) (*SyncGradesResult, error) {
payload := &pb.GetGradesPayload{
Username: req.Username,
Password: req.Password,
Semester: req.Semester,
}
taskResult, err := s.taskManager.SubmitTaskSync(ctx, pb.TaskType_TASK_TYPE_GET_GRADES, payload)
if err != nil {
s.logger.Error("failed to submit grades 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 &SyncGradesResult{
Success: false,
Message: "成绩获取失败",
ErrorMessage: taskResult.ErrorMessage,
}, nil
}
var resultData pb.GradesResultData
if err := json.Unmarshal(taskResult.Data, &resultData); err != nil {
s.logger.Error("failed to parse grades result",
zap.String("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("failed to parse result: %w", err)
}
grades := s.convertGrades(userID, &resultData)
if err := s.saveGrades(ctx, userID, resultData.Semester, grades); err != nil {
s.logger.Error("failed to save grades",
zap.String("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("failed to save grades: %w", err)
}
if resultData.GpaSummary != nil {
summary := s.convertGpaSummary(userID, resultData.Semester, resultData.GpaSummary)
if err := s.gradeRepo.SaveGpaSummary(ctx, summary); err != nil {
s.logger.Warn("failed to save gpa summary",
zap.String("user_id", userID),
zap.Error(err))
}
}
s.logger.Info("grades sync completed",
zap.String("user_id", userID),
zap.Int("grade_count", len(grades)))
return &SyncGradesResult{
Success: true,
Message: "成绩同步成功",
SyncedCount: len(grades),
}, nil
}
func (s *gradeSyncService) convertGrades(userID string, data *pb.GradesResultData) []*model.Grade {
grades := make([]*model.Grade, 0, len(data.Grades))
for _, g := range data.Grades {
grades = append(grades, &model.Grade{
ID: uuid.New().String(),
UserID: userID,
Term: g.Term,
CourseCode: g.CourseCode,
CourseName: g.CourseName,
Nature: g.Nature,
Category: g.Category,
Credit: g.Credit,
Score: g.Score,
GradePoint: g.GradePoint,
IsExam: g.IsExam,
})
}
return grades
}
func (s *gradeSyncService) convertGpaSummary(userID, semester string, gpa *pb.GpaSummary) *model.GpaSummary {
return &model.GpaSummary{
ID: uuid.New().String(),
UserID: userID,
Semester: semester,
AverageGpa: gpa.AverageGpa,
Rank: gpa.Rank,
ExamTotalGpa: gpa.ExamTotalGpa,
ExamTotalCredits: gpa.ExamTotalCredits,
FailCredits: gpa.FailCredits,
Semesters: gpa.Semesters,
}
}
func (s *gradeSyncService) saveGrades(ctx context.Context, userID, semester string, grades []*model.Grade) error {
if err := s.gradeRepo.DeleteByUserAndSemester(ctx, userID, semester); err != nil {
return fmt.Errorf("failed to delete old grades: %w", err)
}
if len(grades) == 0 {
return nil
}
if err := s.gradeRepo.BatchCreate(ctx, grades); err != nil {
return fmt.Errorf("failed to create grades: %w", err)
}
return nil
}

View File

@@ -35,6 +35,8 @@ var HandlerSet = wire.NewSet(
handler.NewAdminProfileAuditHandler, handler.NewAdminProfileAuditHandler,
handler.NewPostHandler, handler.NewPostHandler,
handler.NewTradeHandler, handler.NewTradeHandler,
handler.NewGradeHandler,
handler.NewExamHandler,
// 需要特殊处理的 Handler // 需要特殊处理的 Handler
ProvideUserHandler, ProvideUserHandler,

View File

@@ -24,6 +24,8 @@ var RepositorySet = wire.NewSet(
repository.NewVoteRepository, repository.NewVoteRepository,
repository.NewChannelRepository, repository.NewChannelRepository,
repository.NewScheduleRepository, repository.NewScheduleRepository,
repository.NewGradeRepository,
repository.NewExamRepository,
repository.NewMaterialSubjectRepository, repository.NewMaterialSubjectRepository,
repository.NewMaterialFileRepository, repository.NewMaterialFileRepository,
repository.NewCallRepository, repository.NewCallRepository,

View File

@@ -46,6 +46,8 @@ var ServiceSet = wire.NewSet(
ProvideChatService, ProvideChatService,
ProvideScheduleService, ProvideScheduleService,
ProvideScheduleSyncService, ProvideScheduleSyncService,
ProvideGradeSyncService,
ProvideExamSyncService,
ProvideGroupService, ProvideGroupService,
ProvideUploadService, ProvideUploadService,
ProvideUserActivityService, ProvideUserActivityService,
@@ -230,6 +232,24 @@ func ProvideScheduleSyncService(
return service.NewScheduleSyncService(taskManager, scheduleRepo, logger) return service.NewScheduleSyncService(taskManager, scheduleRepo, logger)
} }
// ProvideGradeSyncService 提供成绩同步服务
func ProvideGradeSyncService(
taskManager *runner.TaskManager,
gradeRepo repository.GradeRepository,
logger *zap.Logger,
) service.GradeSyncService {
return service.NewGradeSyncService(taskManager, gradeRepo, logger)
}
// ProvideExamSyncService 提供考试安排同步服务
func ProvideExamSyncService(
taskManager *runner.TaskManager,
examRepo repository.ExamRepository,
logger *zap.Logger,
) service.ExamSyncService {
return service.NewExamSyncService(taskManager, examRepo, logger)
}
// ProvideGroupService 提供群组服务 // ProvideGroupService 提供群组服务
func ProvideGroupService( func ProvideGroupService(
groupRepo repository.GroupRepository, groupRepo repository.GroupRepository,

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.11
// protoc v6.33.1 // protoc v7.34.1
// source: proto/runner/runner.proto // source: proto/runner/runner.proto
package runner package runner
@@ -1209,19 +1209,105 @@ func (x *ScheduleTime) GetWeeks() []int32 {
return nil return nil
} }
// GpaSummary 学分绩汇总信息
type GpaSummary struct {
state protoimpl.MessageState `protogen:"open.v1"`
AverageGpa string `protobuf:"bytes,1,opt,name=average_gpa,json=averageGpa,proto3" json:"average_gpa,omitempty"` // 平均学分绩
Rank string `protobuf:"bytes,2,opt,name=rank,proto3" json:"rank,omitempty"` // 专业排名
ExamTotalGpa string `protobuf:"bytes,3,opt,name=exam_total_gpa,json=examTotalGpa,proto3" json:"exam_total_gpa,omitempty"` // 考试课学分绩
ExamTotalCredits string `protobuf:"bytes,4,opt,name=exam_total_credits,json=examTotalCredits,proto3" json:"exam_total_credits,omitempty"` // 考试课学分数
FailCredits string `protobuf:"bytes,5,opt,name=fail_credits,json=failCredits,proto3" json:"fail_credits,omitempty"` // 考查课不及格学分
Semesters string `protobuf:"bytes,6,opt,name=semesters,proto3" json:"semesters,omitempty"` // 计算学期数
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GpaSummary) Reset() {
*x = GpaSummary{}
mi := &file_proto_runner_runner_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GpaSummary) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GpaSummary) ProtoMessage() {}
func (x *GpaSummary) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[15]
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 GpaSummary.ProtoReflect.Descriptor instead.
func (*GpaSummary) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{15}
}
func (x *GpaSummary) GetAverageGpa() string {
if x != nil {
return x.AverageGpa
}
return ""
}
func (x *GpaSummary) GetRank() string {
if x != nil {
return x.Rank
}
return ""
}
func (x *GpaSummary) GetExamTotalGpa() string {
if x != nil {
return x.ExamTotalGpa
}
return ""
}
func (x *GpaSummary) GetExamTotalCredits() string {
if x != nil {
return x.ExamTotalCredits
}
return ""
}
func (x *GpaSummary) GetFailCredits() string {
if x != nil {
return x.FailCredits
}
return ""
}
func (x *GpaSummary) GetSemesters() string {
if x != nil {
return x.Semesters
}
return ""
}
// GradesResultData 成绩结果数据 // GradesResultData 成绩结果数据
type GradesResultData struct { type GradesResultData struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Semester string `protobuf:"bytes,1,opt,name=semester,proto3" json:"semester,omitempty"` // 学期 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"` // 学号 StudentId string `protobuf:"bytes,2,opt,name=student_id,json=studentId,proto3" json:"student_id,omitempty"` // 学号
Grades []*Grade `protobuf:"bytes,3,rep,name=grades,proto3" json:"grades,omitempty"` // 成绩列表 Grades []*Grade `protobuf:"bytes,3,rep,name=grades,proto3" json:"grades,omitempty"` // 成绩列表
GpaSummary *GpaSummary `protobuf:"bytes,4,opt,name=gpa_summary,json=gpaSummary,proto3" json:"gpa_summary,omitempty"` // 学分绩汇总 (可选)
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *GradesResultData) Reset() { func (x *GradesResultData) Reset() {
*x = GradesResultData{} *x = GradesResultData{}
mi := &file_proto_runner_runner_proto_msgTypes[15] mi := &file_proto_runner_runner_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1233,7 +1319,7 @@ func (x *GradesResultData) String() string {
func (*GradesResultData) ProtoMessage() {} func (*GradesResultData) ProtoMessage() {}
func (x *GradesResultData) ProtoReflect() protoreflect.Message { func (x *GradesResultData) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[15] mi := &file_proto_runner_runner_proto_msgTypes[16]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1246,7 +1332,7 @@ func (x *GradesResultData) ProtoReflect() protoreflect.Message {
// Deprecated: Use GradesResultData.ProtoReflect.Descriptor instead. // Deprecated: Use GradesResultData.ProtoReflect.Descriptor instead.
func (*GradesResultData) Descriptor() ([]byte, []int) { func (*GradesResultData) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{15} return file_proto_runner_runner_proto_rawDescGZIP(), []int{16}
} }
func (x *GradesResultData) GetSemester() string { func (x *GradesResultData) GetSemester() string {
@@ -1270,21 +1356,32 @@ func (x *GradesResultData) GetGrades() []*Grade {
return nil return nil
} }
func (x *GradesResultData) GetGpaSummary() *GpaSummary {
if x != nil {
return x.GpaSummary
}
return nil
}
// Grade 成绩信息 // Grade 成绩信息
type Grade struct { type Grade struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
CourseCode string `protobuf:"bytes,1,opt,name=course_code,json=courseCode,proto3" json:"course_code,omitempty"` // 课程代码 Term string `protobuf:"bytes,1,opt,name=term,proto3" json:"term,omitempty"` // 学期
CourseName string `protobuf:"bytes,2,opt,name=course_name,json=courseName,proto3" json:"course_name,omitempty"` // 课程名称 CourseCode string `protobuf:"bytes,2,opt,name=course_code,json=courseCode,proto3" json:"course_code,omitempty"` // 课程代码
Credit float32 `protobuf:"fixed32,3,opt,name=credit,proto3" json:"credit,omitempty"` // 学分 CourseName string `protobuf:"bytes,3,opt,name=course_name,json=courseName,proto3" json:"course_name,omitempty"` // 课程名称
Grade string `protobuf:"bytes,4,opt,name=grade,proto3" json:"grade,omitempty"` // 成绩 (可能是等级或分数) Nature string `protobuf:"bytes,4,opt,name=nature,proto3" json:"nature,omitempty"` // 课程性质
GradePoint float32 `protobuf:"fixed32,5,opt,name=grade_point,json=gradePoint,proto3" json:"grade_point,omitempty"` // 绩点 Category string `protobuf:"bytes,5,opt,name=category,proto3" json:"category,omitempty"` // 课程类别
Credit string `protobuf:"bytes,6,opt,name=credit,proto3" json:"credit,omitempty"` // 学分
Score string `protobuf:"bytes,7,opt,name=score,proto3" json:"score,omitempty"` // 成绩 (可能是等级或分数)
GradePoint string `protobuf:"bytes,8,opt,name=grade_point,json=gradePoint,proto3" json:"grade_point,omitempty"` // 绩点
IsExam string `protobuf:"bytes,9,opt,name=is_exam,json=isExam,proto3" json:"is_exam,omitempty"` // 是否考试课
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Grade) Reset() { func (x *Grade) Reset() {
*x = Grade{} *x = Grade{}
mi := &file_proto_runner_runner_proto_msgTypes[16] mi := &file_proto_runner_runner_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1296,7 +1393,7 @@ func (x *Grade) String() string {
func (*Grade) ProtoMessage() {} func (*Grade) ProtoMessage() {}
func (x *Grade) ProtoReflect() protoreflect.Message { func (x *Grade) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[16] mi := &file_proto_runner_runner_proto_msgTypes[17]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1309,7 +1406,14 @@ func (x *Grade) ProtoReflect() protoreflect.Message {
// Deprecated: Use Grade.ProtoReflect.Descriptor instead. // Deprecated: Use Grade.ProtoReflect.Descriptor instead.
func (*Grade) Descriptor() ([]byte, []int) { func (*Grade) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{16} return file_proto_runner_runner_proto_rawDescGZIP(), []int{17}
}
func (x *Grade) GetTerm() string {
if x != nil {
return x.Term
}
return ""
} }
func (x *Grade) GetCourseCode() string { func (x *Grade) GetCourseCode() string {
@@ -1326,25 +1430,46 @@ func (x *Grade) GetCourseName() string {
return "" return ""
} }
func (x *Grade) GetCredit() float32 { func (x *Grade) GetNature() string {
if x != nil { if x != nil {
return x.Credit return x.Nature
}
return 0
}
func (x *Grade) GetGrade() string {
if x != nil {
return x.Grade
} }
return "" return ""
} }
func (x *Grade) GetGradePoint() float32 { func (x *Grade) GetCategory() string {
if x != nil {
return x.Category
}
return ""
}
func (x *Grade) GetCredit() string {
if x != nil {
return x.Credit
}
return ""
}
func (x *Grade) GetScore() string {
if x != nil {
return x.Score
}
return ""
}
func (x *Grade) GetGradePoint() string {
if x != nil { if x != nil {
return x.GradePoint return x.GradePoint
} }
return 0 return ""
}
func (x *Grade) GetIsExam() string {
if x != nil {
return x.IsExam
}
return ""
} }
// ExamsResultData 考试安排结果数据 // ExamsResultData 考试安排结果数据
@@ -1359,7 +1484,7 @@ type ExamsResultData struct {
func (x *ExamsResultData) Reset() { func (x *ExamsResultData) Reset() {
*x = ExamsResultData{} *x = ExamsResultData{}
mi := &file_proto_runner_runner_proto_msgTypes[17] mi := &file_proto_runner_runner_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1371,7 +1496,7 @@ func (x *ExamsResultData) String() string {
func (*ExamsResultData) ProtoMessage() {} func (*ExamsResultData) ProtoMessage() {}
func (x *ExamsResultData) ProtoReflect() protoreflect.Message { func (x *ExamsResultData) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[17] mi := &file_proto_runner_runner_proto_msgTypes[18]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1384,7 +1509,7 @@ func (x *ExamsResultData) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExamsResultData.ProtoReflect.Descriptor instead. // Deprecated: Use ExamsResultData.ProtoReflect.Descriptor instead.
func (*ExamsResultData) Descriptor() ([]byte, []int) { func (*ExamsResultData) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{17} return file_proto_runner_runner_proto_rawDescGZIP(), []int{18}
} }
func (x *ExamsResultData) GetSemester() string { func (x *ExamsResultData) GetSemester() string {
@@ -1411,20 +1536,19 @@ func (x *ExamsResultData) GetExams() []*Exam {
// Exam 考试信息 // Exam 考试信息
type Exam struct { type Exam struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
CourseCode string `protobuf:"bytes,1,opt,name=course_code,json=courseCode,proto3" json:"course_code,omitempty"` // 课程代码 CourseName string `protobuf:"bytes,1,opt,name=course_name,json=courseName,proto3" json:"course_name,omitempty"` // 课程名称
CourseName string `protobuf:"bytes,2,opt,name=course_name,json=courseName,proto3" json:"course_name,omitempty"` // 课程名称 ExamType string `protobuf:"bytes,2,opt,name=exam_type,json=examType,proto3" json:"exam_type,omitempty"` // 考试类型 (期末/期中/补考)
ExamType string `protobuf:"bytes,3,opt,name=exam_type,json=examType,proto3" json:"exam_type,omitempty"` // 考试类型 (期末/期中/补考) Date string `protobuf:"bytes,3,opt,name=date,proto3" json:"date,omitempty"` // 考试日期
Date string `protobuf:"bytes,4,opt,name=date,proto3" json:"date,omitempty"` // 考试日期 (YYYY-MM-DD) Time string `protobuf:"bytes,4,opt,name=time,proto3" json:"time,omitempty"` // 考试时间
Time string `protobuf:"bytes,5,opt,name=time,proto3" json:"time,omitempty"` // 考试时间 (HH:MM-HH:MM) Week string `protobuf:"bytes,5,opt,name=week,proto3" json:"week,omitempty"` // 考试周次
Room string `protobuf:"bytes,6,opt,name=room,proto3" json:"room,omitempty"` // 考场 Weekday string `protobuf:"bytes,6,opt,name=weekday,proto3" json:"weekday,omitempty"` // 星期几
Seat string `protobuf:"bytes,7,opt,name=seat,proto3" json:"seat,omitempty"` // 座位号
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Exam) Reset() { func (x *Exam) Reset() {
*x = Exam{} *x = Exam{}
mi := &file_proto_runner_runner_proto_msgTypes[18] mi := &file_proto_runner_runner_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1436,7 +1560,7 @@ func (x *Exam) String() string {
func (*Exam) ProtoMessage() {} func (*Exam) ProtoMessage() {}
func (x *Exam) ProtoReflect() protoreflect.Message { func (x *Exam) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[18] mi := &file_proto_runner_runner_proto_msgTypes[19]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1449,14 +1573,7 @@ func (x *Exam) ProtoReflect() protoreflect.Message {
// Deprecated: Use Exam.ProtoReflect.Descriptor instead. // Deprecated: Use Exam.ProtoReflect.Descriptor instead.
func (*Exam) Descriptor() ([]byte, []int) { func (*Exam) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{18} return file_proto_runner_runner_proto_rawDescGZIP(), []int{19}
}
func (x *Exam) GetCourseCode() string {
if x != nil {
return x.CourseCode
}
return ""
} }
func (x *Exam) GetCourseName() string { func (x *Exam) GetCourseName() string {
@@ -1487,16 +1604,16 @@ func (x *Exam) GetTime() string {
return "" return ""
} }
func (x *Exam) GetRoom() string { func (x *Exam) GetWeek() string {
if x != nil { if x != nil {
return x.Room return x.Week
} }
return "" return ""
} }
func (x *Exam) GetSeat() string { func (x *Exam) GetWeekday() string {
if x != nil { if x != nil {
return x.Seat return x.Weekday
} }
return "" return ""
} }
@@ -1517,7 +1634,7 @@ type UserInfoResultData struct {
func (x *UserInfoResultData) Reset() { func (x *UserInfoResultData) Reset() {
*x = UserInfoResultData{} *x = UserInfoResultData{}
mi := &file_proto_runner_runner_proto_msgTypes[19] mi := &file_proto_runner_runner_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1529,7 +1646,7 @@ func (x *UserInfoResultData) String() string {
func (*UserInfoResultData) ProtoMessage() {} func (*UserInfoResultData) ProtoMessage() {}
func (x *UserInfoResultData) ProtoReflect() protoreflect.Message { func (x *UserInfoResultData) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[19] mi := &file_proto_runner_runner_proto_msgTypes[20]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1542,7 +1659,7 @@ func (x *UserInfoResultData) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserInfoResultData.ProtoReflect.Descriptor instead. // Deprecated: Use UserInfoResultData.ProtoReflect.Descriptor instead.
func (*UserInfoResultData) Descriptor() ([]byte, []int) { func (*UserInfoResultData) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{19} return file_proto_runner_runner_proto_rawDescGZIP(), []int{20}
} }
func (x *UserInfoResultData) GetStudentId() string { func (x *UserInfoResultData) GetStudentId() string {
@@ -1680,36 +1797,49 @@ const file_proto_runner_runner_proto_rawDesc = "" +
"\fScheduleTime\x12\x10\n" + "\fScheduleTime\x12\x10\n" +
"\x03day\x18\x01 \x01(\tR\x03day\x12\x1a\n" + "\x03day\x18\x01 \x01(\tR\x03day\x12\x1a\n" +
"\bsections\x18\x02 \x03(\x05R\bsections\x12\x14\n" + "\bsections\x18\x02 \x03(\x05R\bsections\x12\x14\n" +
"\x05weeks\x18\x03 \x03(\x05R\x05weeks\"t\n" + "\x05weeks\x18\x03 \x03(\x05R\x05weeks\"\xd6\x01\n" +
"\n" +
"GpaSummary\x12\x1f\n" +
"\vaverage_gpa\x18\x01 \x01(\tR\n" +
"averageGpa\x12\x12\n" +
"\x04rank\x18\x02 \x01(\tR\x04rank\x12$\n" +
"\x0eexam_total_gpa\x18\x03 \x01(\tR\fexamTotalGpa\x12,\n" +
"\x12exam_total_credits\x18\x04 \x01(\tR\x10examTotalCredits\x12!\n" +
"\ffail_credits\x18\x05 \x01(\tR\vfailCredits\x12\x1c\n" +
"\tsemesters\x18\x06 \x01(\tR\tsemesters\"\xa9\x01\n" +
"\x10GradesResultData\x12\x1a\n" + "\x10GradesResultData\x12\x1a\n" +
"\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" + "\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" +
"\n" + "\n" +
"student_id\x18\x02 \x01(\tR\tstudentId\x12%\n" + "student_id\x18\x02 \x01(\tR\tstudentId\x12%\n" +
"\x06grades\x18\x03 \x03(\v2\r.runner.GradeR\x06grades\"\x98\x01\n" + "\x06grades\x18\x03 \x03(\v2\r.runner.GradeR\x06grades\x123\n" +
"\x05Grade\x12\x1f\n" + "\vgpa_summary\x18\x04 \x01(\v2\x12.runner.GpaSummaryR\n" +
"\vcourse_code\x18\x01 \x01(\tR\n" + "gpaSummary\"\xf9\x01\n" +
"\x05Grade\x12\x12\n" +
"\x04term\x18\x01 \x01(\tR\x04term\x12\x1f\n" +
"\vcourse_code\x18\x02 \x01(\tR\n" +
"courseCode\x12\x1f\n" + "courseCode\x12\x1f\n" +
"\vcourse_name\x18\x02 \x01(\tR\n" + "\vcourse_name\x18\x03 \x01(\tR\n" +
"courseName\x12\x16\n" + "courseName\x12\x16\n" +
"\x06credit\x18\x03 \x01(\x02R\x06credit\x12\x14\n" + "\x06nature\x18\x04 \x01(\tR\x06nature\x12\x1a\n" +
"\x05grade\x18\x04 \x01(\tR\x05grade\x12\x1f\n" + "\bcategory\x18\x05 \x01(\tR\bcategory\x12\x16\n" +
"\vgrade_point\x18\x05 \x01(\x02R\n" + "\x06credit\x18\x06 \x01(\tR\x06credit\x12\x14\n" +
"gradePoint\"p\n" + "\x05score\x18\a \x01(\tR\x05score\x12\x1f\n" +
"\vgrade_point\x18\b \x01(\tR\n" +
"gradePoint\x12\x17\n" +
"\ais_exam\x18\t \x01(\tR\x06isExam\"p\n" +
"\x0fExamsResultData\x12\x1a\n" + "\x0fExamsResultData\x12\x1a\n" +
"\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" + "\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" +
"\n" + "\n" +
"student_id\x18\x02 \x01(\tR\tstudentId\x12\"\n" + "student_id\x18\x02 \x01(\tR\tstudentId\x12\"\n" +
"\x05exams\x18\x03 \x03(\v2\f.runner.ExamR\x05exams\"\xb5\x01\n" + "\x05exams\x18\x03 \x03(\v2\f.runner.ExamR\x05exams\"\x9a\x01\n" +
"\x04Exam\x12\x1f\n" + "\x04Exam\x12\x1f\n" +
"\vcourse_code\x18\x01 \x01(\tR\n" + "\vcourse_name\x18\x01 \x01(\tR\n" +
"courseCode\x12\x1f\n" +
"\vcourse_name\x18\x02 \x01(\tR\n" +
"courseName\x12\x1b\n" + "courseName\x12\x1b\n" +
"\texam_type\x18\x03 \x01(\tR\bexamType\x12\x12\n" + "\texam_type\x18\x02 \x01(\tR\bexamType\x12\x12\n" +
"\x04date\x18\x04 \x01(\tR\x04date\x12\x12\n" + "\x04date\x18\x03 \x01(\tR\x04date\x12\x12\n" +
"\x04time\x18\x05 \x01(\tR\x04time\x12\x12\n" + "\x04time\x18\x04 \x01(\tR\x04time\x12\x12\n" +
"\x04room\x18\x06 \x01(\tR\x04room\x12\x12\n" + "\x04week\x18\x05 \x01(\tR\x04week\x12\x18\n" +
"\x04seat\x18\a \x01(\tR\x04seat\"\xc4\x01\n" + "\aweekday\x18\x06 \x01(\tR\aweekday\"\xc4\x01\n" +
"\x12UserInfoResultData\x12\x1d\n" + "\x12UserInfoResultData\x12\x1d\n" +
"\n" + "\n" +
"student_id\x18\x01 \x01(\tR\tstudentId\x12\x12\n" + "student_id\x18\x01 \x01(\tR\tstudentId\x12\x12\n" +
@@ -1735,7 +1865,7 @@ const file_proto_runner_runner_proto_rawDesc = "" +
"\x13TASK_STATUS_TIMEOUT\x10\x03\x12\x19\n" + "\x13TASK_STATUS_TIMEOUT\x10\x03\x12\x19\n" +
"\x15TASK_STATUS_CANCELLED\x10\x042H\n" + "\x15TASK_STATUS_CANCELLED\x10\x042H\n" +
"\tRunnerHub\x12;\n" + "\tRunnerHub\x12;\n" +
"\aConnect\x12\x15.runner.StreamMessage\x1a\x15.runner.StreamMessage(\x010\x01B Z\x1ecarrot_bbs/proto/runner;runnerb\x06proto3" "\aConnect\x12\x15.runner.StreamMessage\x1a\x15.runner.StreamMessage(\x010\x01B(Z&schedule_converter/proto/runner;runnerb\x06proto3"
var ( var (
file_proto_runner_runner_proto_rawDescOnce sync.Once file_proto_runner_runner_proto_rawDescOnce sync.Once
@@ -1750,7 +1880,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_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
var file_proto_runner_runner_proto_goTypes = []any{ var file_proto_runner_runner_proto_goTypes = []any{
(TaskType)(0), // 0: runner.TaskType (TaskType)(0), // 0: runner.TaskType
(TaskStatus)(0), // 1: runner.TaskStatus (TaskStatus)(0), // 1: runner.TaskStatus
@@ -1769,12 +1899,13 @@ var file_proto_runner_runner_proto_goTypes = []any{
(*ScheduleResultData)(nil), // 14: runner.ScheduleResultData (*ScheduleResultData)(nil), // 14: runner.ScheduleResultData
(*Course)(nil), // 15: runner.Course (*Course)(nil), // 15: runner.Course
(*ScheduleTime)(nil), // 16: runner.ScheduleTime (*ScheduleTime)(nil), // 16: runner.ScheduleTime
(*GradesResultData)(nil), // 17: runner.GradesResultData (*GpaSummary)(nil), // 17: runner.GpaSummary
(*Grade)(nil), // 18: runner.Grade (*GradesResultData)(nil), // 18: runner.GradesResultData
(*ExamsResultData)(nil), // 19: runner.ExamsResultData (*Grade)(nil), // 19: runner.Grade
(*Exam)(nil), // 20: runner.Exam (*ExamsResultData)(nil), // 20: runner.ExamsResultData
(*UserInfoResultData)(nil), // 21: runner.UserInfoResultData (*Exam)(nil), // 21: runner.Exam
nil, // 22: runner.RegisterRequest.CapabilitiesEntry (*UserInfoResultData)(nil), // 22: runner.UserInfoResultData
nil, // 23: runner.RegisterRequest.CapabilitiesEntry
} }
var file_proto_runner_runner_proto_depIdxs = []int32{ var file_proto_runner_runner_proto_depIdxs = []int32{
3, // 0: runner.StreamMessage.register:type_name -> runner.RegisterRequest 3, // 0: runner.StreamMessage.register:type_name -> runner.RegisterRequest
@@ -1784,20 +1915,21 @@ var file_proto_runner_runner_proto_depIdxs = []int32{
7, // 4: runner.StreamMessage.task:type_name -> runner.Task 7, // 4: runner.StreamMessage.task:type_name -> runner.Task
9, // 5: runner.StreamMessage.result:type_name -> runner.TaskResult 9, // 5: runner.StreamMessage.result:type_name -> runner.TaskResult
8, // 6: runner.StreamMessage.task_cancel:type_name -> runner.TaskCancel 8, // 6: runner.StreamMessage.task_cancel:type_name -> runner.TaskCancel
22, // 7: runner.RegisterRequest.capabilities:type_name -> runner.RegisterRequest.CapabilitiesEntry 23, // 7: runner.RegisterRequest.capabilities:type_name -> runner.RegisterRequest.CapabilitiesEntry
0, // 8: runner.Task.type:type_name -> runner.TaskType 0, // 8: runner.Task.type:type_name -> runner.TaskType
1, // 9: runner.TaskResult.status:type_name -> runner.TaskStatus 1, // 9: runner.TaskResult.status:type_name -> runner.TaskStatus
15, // 10: runner.ScheduleResultData.courses:type_name -> runner.Course 15, // 10: runner.ScheduleResultData.courses:type_name -> runner.Course
16, // 11: runner.Course.schedule:type_name -> runner.ScheduleTime 16, // 11: runner.Course.schedule:type_name -> runner.ScheduleTime
18, // 12: runner.GradesResultData.grades:type_name -> runner.Grade 19, // 12: runner.GradesResultData.grades:type_name -> runner.Grade
20, // 13: runner.ExamsResultData.exams:type_name -> runner.Exam 17, // 13: runner.GradesResultData.gpa_summary:type_name -> runner.GpaSummary
2, // 14: runner.RunnerHub.Connect:input_type -> runner.StreamMessage 21, // 14: runner.ExamsResultData.exams:type_name -> runner.Exam
2, // 15: runner.RunnerHub.Connect:output_type -> runner.StreamMessage 2, // 15: runner.RunnerHub.Connect:input_type -> runner.StreamMessage
15, // [15:16] is the sub-list for method output_type 2, // 16: runner.RunnerHub.Connect:output_type -> runner.StreamMessage
14, // [14:15] is the sub-list for method input_type 16, // [16:17] is the sub-list for method output_type
14, // [14:14] is the sub-list for extension type_name 15, // [15:16] is the sub-list for method input_type
14, // [14:14] is the sub-list for extension extendee 15, // [15:15] is the sub-list for extension type_name
0, // [0:14] is the sub-list for field type_name 15, // [15:15] is the sub-list for extension extendee
0, // [0:15] is the sub-list for field type_name
} }
func init() { file_proto_runner_runner_proto_init() } func init() { file_proto_runner_runner_proto_init() }
@@ -1820,7 +1952,7 @@ func file_proto_runner_runner_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_runner_runner_proto_rawDesc), len(file_proto_runner_runner_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_runner_runner_proto_rawDesc), len(file_proto_runner_runner_proto_rawDesc)),
NumEnums: 2, NumEnums: 2,
NumMessages: 21, NumMessages: 22,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -2,7 +2,7 @@ syntax = "proto3";
package runner; package runner;
option go_package = "carrot_bbs/proto/runner;runner"; option go_package = "schedule_converter/proto/runner;runner";
// ============================================================ // ============================================================
// 服务定义 // 服务定义
@@ -178,20 +178,35 @@ message ScheduleTime {
repeated int32 weeks = 3; // 周次,如 [1, 2, 3, 4, 5] repeated int32 weeks = 3; // 周次,如 [1, 2, 3, 4, 5]
} }
// GpaSummary 学分绩汇总信息
message GpaSummary {
string average_gpa = 1; // 平均学分绩
string rank = 2; // 专业排名
string exam_total_gpa = 3; // 考试课学分绩
string exam_total_credits = 4; // 考试课学分数
string fail_credits = 5; // 考查课不及格学分
string semesters = 6; // 计算学期数
}
// GradesResultData 成绩结果数据 // GradesResultData 成绩结果数据
message GradesResultData { message GradesResultData {
string semester = 1; // 学期 string semester = 1; // 学期
string student_id = 2; // 学号 string student_id = 2; // 学号
repeated Grade grades = 3; // 成绩列表 repeated Grade grades = 3; // 成绩列表
GpaSummary gpa_summary = 4; // 学分绩汇总 (可选)
} }
// Grade 成绩信息 // Grade 成绩信息
message Grade { message Grade {
string course_code = 1; // 课程代码 string term = 1; // 学期
string course_name = 2; // 课程名称 string course_code = 2; // 课程代码
float credit = 3; // 学分 string course_name = 3; // 课程名称
string grade = 4; // 成绩 (可能是等级或分数) string nature = 4; // 课程性质
float grade_point = 5; // 绩点 string category = 5; // 课程类别
string credit = 6; // 学分
string score = 7; // 成绩 (可能是等级或分数)
string grade_point = 8; // 绩点
string is_exam = 9; // 是否考试课
} }
// ExamsResultData 考试安排结果数据 // ExamsResultData 考试安排结果数据
@@ -203,13 +218,12 @@ message ExamsResultData {
// Exam 考试信息 // Exam 考试信息
message Exam { message Exam {
string course_code = 1; // 课程代码 string course_name = 1; // 课程名称
string course_name = 2; // 课程名称 string exam_type = 2; // 考试类型 (期末/期中/补考)
string exam_type = 3; // 考试类型 (期末/期中/补考) string date = 3; // 考试日期
string date = 4; // 考试日期 (YYYY-MM-DD) string time = 4; // 考试时间
string time = 5; // 考试时间 (HH:MM-HH:MM) string week = 5; // 考试周次
string room = 6; // 考场 string weekday = 6; // 星期几
string seat = 7; // 座位号
} }
// UserInfoResultData 用户信息结果数据 // UserInfoResultData 用户信息结果数据

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.3.0 // - protoc-gen-go-grpc v1.6.1
// - protoc v6.33.1 // - protoc v7.34.1
// source: proto/runner/runner.proto // source: proto/runner/runner.proto
package runner package runner
@@ -15,8 +15,8 @@ import (
// This is a compile-time assertion to ensure that this generated file // This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against. // is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later. // Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion7 const _ = grpc.SupportPackageIsVersion9
const ( const (
RunnerHub_Connect_FullMethodName = "/runner.RunnerHub/Connect" RunnerHub_Connect_FullMethodName = "/runner.RunnerHub/Connect"
@@ -25,10 +25,13 @@ const (
// RunnerHubClient is the client API for RunnerHub service. // 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. // 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 { type RunnerHubClient interface {
// Connect 建立双向流连接 // Connect 建立双向流连接
// Runner 通过此连接注册、接收任务、返回结果 // Runner 通过此连接注册、接收任务、返回结果
Connect(ctx context.Context, opts ...grpc.CallOption) (RunnerHub_ConnectClient, error) Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamMessage, StreamMessage], error)
} }
type runnerHubClient struct { type runnerHubClient struct {
@@ -39,55 +42,44 @@ func NewRunnerHubClient(cc grpc.ClientConnInterface) RunnerHubClient {
return &runnerHubClient{cc} return &runnerHubClient{cc}
} }
func (c *runnerHubClient) Connect(ctx context.Context, opts ...grpc.CallOption) (RunnerHub_ConnectClient, error) { func (c *runnerHubClient) Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamMessage, StreamMessage], error) {
stream, err := c.cc.NewStream(ctx, &RunnerHub_ServiceDesc.Streams[0], RunnerHub_Connect_FullMethodName, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &RunnerHub_ServiceDesc.Streams[0], RunnerHub_Connect_FullMethodName, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
x := &runnerHubConnectClient{stream} x := &grpc.GenericClientStream[StreamMessage, StreamMessage]{ClientStream: stream}
return x, nil return x, nil
} }
type RunnerHub_ConnectClient interface { // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
Send(*StreamMessage) error type RunnerHub_ConnectClient = grpc.BidiStreamingClient[StreamMessage, StreamMessage]
Recv() (*StreamMessage, error)
grpc.ClientStream
}
type runnerHubConnectClient struct {
grpc.ClientStream
}
func (x *runnerHubConnectClient) Send(m *StreamMessage) error {
return x.ClientStream.SendMsg(m)
}
func (x *runnerHubConnectClient) Recv() (*StreamMessage, error) {
m := new(StreamMessage)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// RunnerHubServer is the server API for RunnerHub service. // RunnerHubServer is the server API for RunnerHub service.
// All implementations must embed UnimplementedRunnerHubServer // All implementations must embed UnimplementedRunnerHubServer
// for forward compatibility // for forward compatibility.
//
// RunnerHub 服务 - 由 Backend 服务器实现
// Runner 作为客户端主动连接,建立双向流通信
type RunnerHubServer interface { type RunnerHubServer interface {
// Connect 建立双向流连接 // Connect 建立双向流连接
// Runner 通过此连接注册、接收任务、返回结果 // Runner 通过此连接注册、接收任务、返回结果
Connect(RunnerHub_ConnectServer) error Connect(grpc.BidiStreamingServer[StreamMessage, StreamMessage]) error
mustEmbedUnimplementedRunnerHubServer() mustEmbedUnimplementedRunnerHubServer()
} }
// UnimplementedRunnerHubServer must be embedded to have forward compatible implementations. // UnimplementedRunnerHubServer must be embedded to have
type UnimplementedRunnerHubServer struct { // 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(RunnerHub_ConnectServer) error { func (UnimplementedRunnerHubServer) Connect(grpc.BidiStreamingServer[StreamMessage, StreamMessage]) error {
return status.Errorf(codes.Unimplemented, "method Connect not implemented") return status.Error(codes.Unimplemented, "method Connect not implemented")
} }
func (UnimplementedRunnerHubServer) mustEmbedUnimplementedRunnerHubServer() {} func (UnimplementedRunnerHubServer) mustEmbedUnimplementedRunnerHubServer() {}
func (UnimplementedRunnerHubServer) testEmbeddedByValue() {}
// UnsafeRunnerHubServer may be embedded to opt out of forward compatibility for this service. // 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 // Use of this interface is not recommended, as added methods to RunnerHubServer will
@@ -97,34 +89,22 @@ type UnsafeRunnerHubServer interface {
} }
func RegisterRunnerHubServer(s grpc.ServiceRegistrar, srv RunnerHubServer) { 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) s.RegisterService(&RunnerHub_ServiceDesc, srv)
} }
func _RunnerHub_Connect_Handler(srv interface{}, stream grpc.ServerStream) error { func _RunnerHub_Connect_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(RunnerHubServer).Connect(&runnerHubConnectServer{stream}) return srv.(RunnerHubServer).Connect(&grpc.GenericServerStream[StreamMessage, StreamMessage]{ServerStream: stream})
} }
type RunnerHub_ConnectServer interface { // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
Send(*StreamMessage) error type RunnerHub_ConnectServer = grpc.BidiStreamingServer[StreamMessage, StreamMessage]
Recv() (*StreamMessage, error)
grpc.ServerStream
}
type runnerHubConnectServer struct {
grpc.ServerStream
}
func (x *runnerHubConnectServer) Send(m *StreamMessage) error {
return x.ServerStream.SendMsg(m)
}
func (x *runnerHubConnectServer) Recv() (*StreamMessage, error) {
m := new(StreamMessage)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// RunnerHub_ServiceDesc is the grpc.ServiceDesc for RunnerHub service. // RunnerHub_ServiceDesc is the grpc.ServiceDesc for RunnerHub service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,