diff --git a/go-backend/backend.md b/go-backend/backend.md index d144136..7b709f0 100644 --- a/go-backend/backend.md +++ b/go-backend/backend.md @@ -179,6 +179,31 @@ Post 时的json样例: } ``` +## 消息 + +| 方法 | 路由 | 功能 | +| ---- | ---- | ---- | +| POST | /api/messages | 添加消息 | +| GET | /api/messages | 获取全部消息 | +| GET | /api/messages/:id | 获取指定 id 的消息 | +| PUT | /api/messages/:id | 更新指定 id 的消息 | +| DELETE |/api/messages/:id | 删除指定 id 的消息 | + +Post 时的json样例: +```json +{ + "openId": "1234567890", + "page": "pages/welcome/welcome", + "vaxName": "新冠疫苗", + "comment": "您的接种时间已到", + "vaxLocation": "本地社区医院", + "vaxNum": 1, + "realTime": true, + "sendTime": "2021-07-01 12:00", + "sent": false +} +``` + # 模型设计 通过以下数据表来支持后端的功能: @@ -311,22 +336,40 @@ type Reply struct { // Message 消息模型 type Message struct { gorm.Model - Content string `json:"content"` // 消息内容,将不同字段拼接成字符串 - UserName string `json:"userName"` - UserID uint `json:"userId"` - SendTime string `json:"sendTime"` // 发送时间,注意用string与前端交互,例如"2021-07-01 12:00" + OpenID string `json:"openId"` + Page string `json:"page"` // 消息跳转页面,例如"pages/welcome/welcome" + VaxName string `json:"vaxName"` + Comment string `json:"comment"` + VaxLocation string `json:"vaxLocation"` + VaxNum int `json:"vaxNum"` + RealTime bool `json:"realTime"` // 是否实时提醒 + SendTime string `json:"sendTime"` // 如果不是实时提醒,则需要设置提醒时间,用字符串存具体时间,例如"2021-07-01 12:00" + Sent bool `json:"sent"` // 是否已发送 } ``` - **接种地点表 (VaccinationLocations)**: +9. **接种地点表 (VaccinationLocations)**: -- Name -- Address -- ContactNumber -- OperatingHours -- PositionX -- PositionY -- OptionalVaccine +```go +// 持有某种疫苗的所有诊所 +type VaccineClinicList struct { + gorm.Model + VaccineName string `json:"vaccineName"` + ClinicList string `json:"clinicList"` + // ClinicName StringList `json:"clinicName"` +} + +// 诊所信息 +type Clinic struct { + gorm.Model + ClinicName string `json:"clinicName"` + VaccineList string `json:"vaccineList"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + PhoneNumber string `json:"phoneNumber"` + Address string `json:"address"` +} +``` ## 项目优化 @@ -486,3 +529,40 @@ JWT 主要包含三个部分,用点(`.`)分隔: - **短期有效性**: 为 JWT 设置合理的过期时间,以减少被盗用的风险。 通过使用 JWT,我们可以实现无状态的身份验证,这意味着服务器不需要存储任何用户的登录信息,从而使应用更加易于扩展。同时,它也为客户端和服务器之间的通信提供了一种安全可靠的方式来验证和传输用户身份信息。 + + +## 消息通知 + +设计一个优雅的系统来处理定时消息发送功能涉及多个方面:数据库设计、消息调度逻辑、错误处理、性能优化等。以下是一个设计方案及其相关思路: + +### 1. **系统目标** +- 实现一个可靠的消息调度系统,能够在预定时间发送消息。 +- 确保系统可扩展性和高效性能。 +- 提供稳健的错误处理和日志记录。 + +### 2. **数据库设计** +- `messages` 表用于存储消息信息。 + - 字段包括:`id`, `open_id`, `page`, `vax_name`, `comment`, `vax_location`, `vax_num`, `real_time`, `send_time`, `sent`, `created_at`, `updated_at`. + - `send_time` 用于确定消息发送时间。 + - `sent` 标记消息是否已发送。 + +### 3. **消息调度逻辑** +- 实现一个消息调度器,定期从数据库中检索未发送的消息,并按`send_time`排序。 +- 对于每个未发送的消息,计算当前时间与`send_time`的差值,若到达或超过发送时间,则触发发送逻辑。 + +### 4. **消息发送机制** +- 消息发送逻辑封装在`SendTemplateMessage`函数中,利用微信小程序的模板消息接口实现。 +- 发送成功后,更新数据库中对应消息的`sent`状态为`true`。 + +### 5. **错误处理和日志记录** +- 在消息发送过程中,任何错误都应被捕获并记录。 +- 提供详细的日志记录,包括消息发送时间、状态和任何失败的原因。 + +#### 6. **性能考虑** +- 消息调度器应避免创建过多goroutine,以减少资源消耗。 +- 数据库查询应优化,避免过大的数据加载和频繁查询。 + +### 7. **安全和隐私** +- 确保处理用户数据时遵循隐私法规。 +- 敏感信息(如用户OpenID)应被适当保护。 + diff --git a/go-backend/cmd/main.go b/go-backend/cmd/main.go index 4e5eb89..6890fe1 100644 --- a/go-backend/cmd/main.go +++ b/go-backend/cmd/main.go @@ -1,6 +1,7 @@ package main import ( + "time" "v-helper/internal/handler" "v-helper/pkg/db" @@ -10,6 +11,13 @@ import ( ) func main() { + // 设置时区为中国时区 + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + panic(err) + } + time.Local = loc + cfg := config.LoadConfig() database := db.Init(cfg) diff --git a/go-backend/internal/handler/handler.go b/go-backend/internal/handler/handler.go index c626c16..76593cd 100644 --- a/go-backend/internal/handler/handler.go +++ b/go-backend/internal/handler/handler.go @@ -1,7 +1,10 @@ package handler import ( + "log" + "net/http" "v-helper/internal/service" + "v-helper/pkg/utils" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -10,56 +13,59 @@ import ( func SetupRoutes(router *gin.Engine, db *gorm.DB) { userService := service.NewUserService(db) userHandler := NewUserHandler(userService) - router.POST("/users", userHandler.HandleCreateUser) - router.GET("/users", userHandler.HandleGetAllUsers) - router.GET("/users/:id", userHandler.HandleGetUserByID) - router.PUT("/users/:id", userHandler.HandleUpdateUserByID) - router.DELETE("/users/:id", userHandler.HandleDeleteUserByID) router.GET("/users/login", userHandler.LogInHandler) - router.GET("/users/:id/following", userHandler.HandleGetUserWithFollowings) - router.GET("/users/addfollowingVaccine/:id", userHandler.HandleAddFollowingVaccine) - router.GET("/users/removefollowingVaccine/:id", userHandler.HandleRemoveFollowingVaccine) - router.GET("/users/addfollowingArticle/:id", userHandler.HandleAddFollowingArticle) - router.GET("/users/removefollowingArticle/:id", userHandler.HandleRemoveFollowingArticle) - router.GET("/users/public/:id", userHandler.HandleGetPublicUserByID) - - profileService := service.NewProfileService(db) - profileHandler := NewProfileHandler(profileService) - router.POST("/profiles", profileHandler.HandleCreateProfile) - router.GET("/profiles", profileHandler.HandleGetAllProfiles) - router.GET("/profiles/:id", profileHandler.HandleGetProfileByID) - router.GET("/profiles/user/:userID", profileHandler.HandleGetProfilesByUserID) - router.PUT("/profiles/:id", profileHandler.HandleUpdateProfileByID) - router.DELETE("/profiles/:id", profileHandler.HandleDeleteProfileByID) - - vaccineService := service.NewVaccineService(db) - vaccineHandler := NewVaccineHandler(vaccineService) - router.POST("/vaccines", vaccineHandler.HandleCreateVaccine) - router.GET("/vaccines", vaccineHandler.HandleGetAllVaccines) - router.GET("/vaccines/:id", vaccineHandler.HandleGetVaccineByID) - router.PUT("/vaccines/:id", vaccineHandler.HandleUpdateVaccineByID) - router.DELETE("/vaccines/:id", vaccineHandler.HandleDeleteVaccineByID) - - vaccinationRecordService := service.NewVaccinationRecordService(db) - vaccinationRecordHandler := NewVaccinationRecordHandler(vaccinationRecordService) - router.POST("/vaccination-records", vaccinationRecordHandler.HandleCreateVaccinationRecord) - router.GET("/vaccination-records", vaccinationRecordHandler.HandleGetAllVaccinationRecords) - router.GET("/vaccination-records/:id", vaccinationRecordHandler.HandleGetVaccinationRecordByID) - router.GET("/vaccination-records/user/:userID", vaccinationRecordHandler.HandleGetVaccinationRecordsByUserID) - router.GET("/vaccination-records/profile/:profileID", vaccinationRecordHandler.HandleGetVaccinationRecordsByProfileID) - router.PUT("/vaccination-records/:id", vaccinationRecordHandler.HandleUpdateVaccinationRecordByID) - router.DELETE("/vaccination-records/:id", vaccinationRecordHandler.HandleDeleteVaccinationRecordByID) - - tempertureRecordService := service.NewTempertureRecordService(db) - tempertureRecordHandler := NewTempertureRecordHandler(tempertureRecordService) - router.POST("/temperature-records", tempertureRecordHandler.HandleCreateTempertureRecord) - router.GET("/temperature-records", tempertureRecordHandler.HandleGetAllTempertureRecords) - router.GET("/temperature-records/:id", tempertureRecordHandler.HandleGetTempertureRecordByID) - router.GET("/temperature-records/user/:userID", tempertureRecordHandler.HandleGetTempertureRecordsByUserID) - router.GET("/temperature-records/profile/:profileID", tempertureRecordHandler.HandleGetTempertureRecordsByProfileID) - router.PUT("/temperature-records/:id", tempertureRecordHandler.HandleUpdateTempertureRecordByID) - router.DELETE("/temperature-records/:id", tempertureRecordHandler.HandleDeleteTempertureRecordByID) + // router.Use(JWTAuthMiddleware()) + { + router.POST("/users", userHandler.HandleCreateUser) + router.GET("/users", userHandler.HandleGetAllUsers) + router.GET("/users/:id", userHandler.HandleGetUserByID) + router.PUT("/users/:id", userHandler.HandleUpdateUserByID) + router.DELETE("/users/:id", userHandler.HandleDeleteUserByID) + router.GET("/users/:id/following", userHandler.HandleGetUserWithFollowings) + router.GET("/users/addfollowingVaccine/:id", userHandler.HandleAddFollowingVaccine) + router.GET("/users/removefollowingVaccine/:id", userHandler.HandleRemoveFollowingVaccine) + router.GET("/users/addfollowingArticle/:id", userHandler.HandleAddFollowingArticle) + router.GET("/users/removefollowingArticle/:id", userHandler.HandleRemoveFollowingArticle) + router.GET("/users/public/:id", userHandler.HandleGetPublicUserByID) + + profileService := service.NewProfileService(db) + profileHandler := NewProfileHandler(profileService) + router.POST("/profiles", profileHandler.HandleCreateProfile) + router.GET("/profiles", profileHandler.HandleGetAllProfiles) + router.GET("/profiles/:id", profileHandler.HandleGetProfileByID) + router.GET("/profiles/user/:userID", profileHandler.HandleGetProfilesByUserID) + router.PUT("/profiles/:id", profileHandler.HandleUpdateProfileByID) + router.DELETE("/profiles/:id", profileHandler.HandleDeleteProfileByID) + + vaccineService := service.NewVaccineService(db) + vaccineHandler := NewVaccineHandler(vaccineService) + router.POST("/vaccines", vaccineHandler.HandleCreateVaccine) + router.GET("/vaccines", vaccineHandler.HandleGetAllVaccines) + router.GET("/vaccines/:id", vaccineHandler.HandleGetVaccineByID) + router.PUT("/vaccines/:id", vaccineHandler.HandleUpdateVaccineByID) + router.DELETE("/vaccines/:id", vaccineHandler.HandleDeleteVaccineByID) + + vaccinationRecordService := service.NewVaccinationRecordService(db) + vaccinationRecordHandler := NewVaccinationRecordHandler(vaccinationRecordService) + router.POST("/vaccination-records", vaccinationRecordHandler.HandleCreateVaccinationRecord) + router.GET("/vaccination-records", vaccinationRecordHandler.HandleGetAllVaccinationRecords) + router.GET("/vaccination-records/:id", vaccinationRecordHandler.HandleGetVaccinationRecordByID) + router.GET("/vaccination-records/user/:userID", vaccinationRecordHandler.HandleGetVaccinationRecordsByUserID) + router.GET("/vaccination-records/profile/:profileID", vaccinationRecordHandler.HandleGetVaccinationRecordsByProfileID) + router.PUT("/vaccination-records/:id", vaccinationRecordHandler.HandleUpdateVaccinationRecordByID) + router.DELETE("/vaccination-records/:id", vaccinationRecordHandler.HandleDeleteVaccinationRecordByID) + + tempertureRecordService := service.NewTempertureRecordService(db) + tempertureRecordHandler := NewTempertureRecordHandler(tempertureRecordService) + router.POST("/temperature-records", tempertureRecordHandler.HandleCreateTempertureRecord) + router.GET("/temperature-records", tempertureRecordHandler.HandleGetAllTempertureRecords) + router.GET("/temperature-records/:id", tempertureRecordHandler.HandleGetTempertureRecordByID) + router.GET("/temperature-records/user/:userID", tempertureRecordHandler.HandleGetTempertureRecordsByUserID) + router.GET("/temperature-records/profile/:profileID", tempertureRecordHandler.HandleGetTempertureRecordsByProfileID) + router.PUT("/temperature-records/:id", tempertureRecordHandler.HandleUpdateTempertureRecordByID) + router.DELETE("/temperature-records/:id", tempertureRecordHandler.HandleDeleteTempertureRecordByID) + } // 帖子 articleService := service.NewArticleService(db) articleHandler := NewArticleHandler(articleService) @@ -82,6 +88,7 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB) { // 通知发送 messageService := service.NewMessageService(db) messageHandler := NewMessageHandler(messageService) + go messageHandler.MessageScheduler() router.POST("/messages", messageHandler.HandleAddMessage) router.GET("/messages", messageHandler.HandleGetAllMessages) router.GET("/messages/:id", messageHandler.HandleGetMessageByID) @@ -102,3 +109,31 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB) { router.GET("/wechat-validation", handleWechatValidation) router.POST("/wechat-validation", SetSubscription) } + +func JWTAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + + if token == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) + c.Abort() + return + } + + log.Println("token:", token) + + ok, err := utils.VerifyToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + c.Abort() + return + } + + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + c.Abort() + return + } + c.Next() + } +} diff --git a/go-backend/internal/handler/message.go b/go-backend/internal/handler/message.go index 6db9b0f..a68a208 100644 --- a/go-backend/internal/handler/message.go +++ b/go-backend/internal/handler/message.go @@ -9,6 +9,7 @@ import ( "sort" "strconv" "strings" + "time" "v-helper/internal/model" "v-helper/internal/service" @@ -201,8 +202,6 @@ func (h *MessageHandler) HandleAddMessage(c *gin.Context) { return } message.Sent = true - } else { - // 非实时提醒 } if err := h.MessageService.CreateMessage(message); err != nil { @@ -268,3 +267,70 @@ func (h *MessageHandler) HandleDeleteMessageByID(c *gin.Context) { log.Println("message deleted successfully: ", messageID) c.JSON(http.StatusOK, gin.H{"message": "message deleted successfully"}) } + +// getMessagesToSend 从数据库获取未发送的消息,并按发送时间排序 +func (h *MessageHandler) getMessagesToSend() []model.Message { + // 实现数据库查询逻辑 + messages, err := h.MessageService.GetAllUnsentMessages() + if err != nil { + log.Println("failed to get messages to send: ", err) + return nil + } + + // 按SendTime排序 + sort.Slice(messages, func(i, j int) bool { + return messages[i].SendTime < messages[j].SendTime + }) + + return messages +} + +// handleMessage 处理消息发送逻辑 +func (h *MessageHandler) handleMessage(messages []model.Message) { + for _, message := range messages { + // 判断是否到达发送时间 + now := time.Now() + sendTime, err := time.ParseInLocation("2006-01-02 15:04", message.SendTime, time.Local) + if err != nil { + log.Println("failed to parse send time: ", err) + return + } + log.Printf("Now:%v, Send Time:%v\n", now, sendTime) + + if now.After(sendTime) { + // 到达发送时间,发送消息 + if err := SendTemplateMessage("", message.OpenID, "", message.Page, message.VaxName, message.Comment, message.VaxLocation, message.VaxNum); err != nil { + log.Println("failed to send template message: ", err) + return + } + message.Sent = true + + // 更新数据库 + if err := h.MessageService.UpdateMessageByID(message); err != nil { + log.Println("failed to update message: ", err) + return + } + + } else { + // 未到达发送时间,跳过 + break + } + + } +} + +// MessageScheduler 负责定时检查并发送消息 +func (h *MessageHandler) MessageScheduler() { + for { + // 从数据库中获取还未发送的消息,按SendTime排序 + messages := h.getMessagesToSend() // 按SendTime排序 + + if len(messages) > 0 { + // 处理最接近当前时间的消息 + h.handleMessage(messages) + } + + // 等待一段时间后再次检查 + time.Sleep(1 * time.Minute) + } +} diff --git a/go-backend/internal/service/message.go b/go-backend/internal/service/message.go index 137b09f..a3bf67c 100644 --- a/go-backend/internal/service/message.go +++ b/go-backend/internal/service/message.go @@ -26,6 +26,15 @@ func (s *MessageService) GetAllMessages() ([]model.Message, error) { return messages, nil } +// 查询所有未发送的消息 +func (s *MessageService) GetAllUnsentMessages() ([]model.Message, error) { + var messages []model.Message + if err := s.db.Where("sent = ?", false).Find(&messages).Error; err != nil { + return nil, err + } + return messages, nil +} + func (s *MessageService) GetMessageByID(id uint) (model.Message, error) { var message model.Message if err := s.db.First(&message, id).Error; err != nil { diff --git a/go-backend/tests/message_test.go b/go-backend/tests/message_test.go index cfde196..2dab324 100644 --- a/go-backend/tests/message_test.go +++ b/go-backend/tests/message_test.go @@ -6,5 +6,5 @@ import ( ) func TestMessage(t *testing.T) { - handler.SendTemplateMessage("", "oYgei62WYUHLbqshmE90303gXFbY", "", "", "", "", "", "", "") + handler.SendTemplateMessage("", "oYgei686TJ13gmm7s8azpU5JTkKI", "", "", "", "", "", 2) }