@@ -174,6 +174,8 @@ func (s *CourseService) putCourseToDatabase(stuId string, term string, courses [
174174}
175175
176176func (s * CourseService ) putExamToDatabase (stuId string , term string , rawCourses []* jwch.Course ) error {
177+ // 考试通知采用“先抢占全局去重记录,再异步发送,成功后提交快照”的顺序。
178+ // 发送或快照提交失败时释放去重记录,确保后续刷新仍然可以重试。
177179 exams := buildCourseExamInfo (rawCourses )
178180 examInfo , err := utils .JSONEncode (exams )
179181 if err != nil {
@@ -205,21 +207,27 @@ func (s *CourseService) putExamToDatabase(stuId string, term string, rawCourses
205207 }
206208 }
207209 if old .ExamInfoSHA256 == "" {
210+ // 历史数据没有考试快照时只建立基线,不把已有考试信息当作新增变化通知。
208211 return s .updateExamSnapshot (old .Id , examInfo , examInfoSHA256 )
209212 }
210213
211214 changes := buildCourseExamChanges (term , oldExams , exams )
212215 if len (changes ) == 0 {
216+ // 内容没有实际变化时只更新快照,避免重复进入全局去重和推送流程。
213217 return s .updateExamSnapshot (old .Id , examInfo , examInfoSHA256 )
214218 }
215219
216220 claimed := make ([]courseExamChange , 0 , len (changes ))
217221 for _ , change := range changes {
222+ // CreateExamOffering 依赖 exam_hash 唯一索引原子抢占发送资格。
223+ // 返回 nil 表示其他用户已经处理过相同变化,本次不再重复推送。
218224 offering , createErr := s .db .Course .CreateExamOffering (s .ctx , & model.ExamOffering {
219225 ExamHash : change .ExamHash ,
220226 Tag : change .Tag ,
221227 })
222228 if createErr != nil {
229+ // 本批次后续无法继续抢占时,释放已经抢到的去重记录。
230+ // 否则这些考试变化虽然没有完成通知流程,却会被全局去重表永久拦截。
223231 return errors .Join (createErr , s .releaseExamOfferings (claimed ))
224232 }
225233 if offering != nil {
@@ -228,17 +236,26 @@ func (s *CourseService) putExamToDatabase(stuId string, term string, rawCourses
228236 }
229237
230238 if len (claimed ) == 0 {
239+ // 所有变化都已被其他任务去重,本次只同步本地快照,不重复发送通知。
231240 return s .updateExamSnapshot (old .Id , examInfo , examInfoSHA256 )
232241 }
242+ // 去重记录已经抢占成功,但通知和快照更新放入同一个异步任务。
243+ // 只有两步都成功,exam_offerings 记录才会继续保留。
233244 if ! umeng .EnqueueAsync (func () error {
234245 if err := s .sendExamNotifications (claimed ); err != nil {
246+ // 推送失败时释放去重记录;考试快照也不会更新,后续刷新仍能识别到这次变化。
247+ // 这样可以允许后续课程刷新重新抢占并重试通知。
235248 return errors .Join (err , s .releaseExamOfferings (claimed ))
236249 }
237250 if err := s .updateExamSnapshot (old .Id , examInfo , examInfoSHA256 ); err != nil {
251+ // 快照更新失败时释放去重记录,避免通知状态未完整落库却被永久去重。
252+ // 下次刷新会再次发现变化,并重新执行通知流程。
238253 return errors .Join (err , s .releaseExamOfferings (claimed ))
239254 }
240255 return nil
241256 }) {
257+ // 队列已满时任务没有进入异步流程,当前请求不会再发送这些通知。
258+ // 因此需要释放已抢到的去重记录,避免这次未发送的变化无法重试。
242259 return errors .Join (
243260 errno .NewErrNo (errno .InternalQueueErrorCode , "service.putExamToDatabase: exam notification queue is full" ),
244261 s .releaseExamOfferings (claimed ),
@@ -248,6 +265,7 @@ func (s *CourseService) putExamToDatabase(stuId string, term string, rawCourses
248265}
249266
250267func (s * CourseService ) updateExamSnapshot (id int64 , examInfo , examInfoSHA256 string ) error {
268+ // 快照更新是本次考试变化处理的提交步骤;成功后下一次刷新不会再次识别同一变化。
251269 _ , err := s .db .Course .UpdateUserTermCourse (s .ctx , & model.UserCourse {
252270 Id : id ,
253271 ExamInfo : examInfo ,
@@ -257,8 +275,13 @@ func (s *CourseService) updateExamSnapshot(id int64, examInfo, examInfoSHA256 st
257275}
258276
259277func (s * CourseService ) releaseExamOfferings (changes []courseExamChange ) error {
278+ // exam_offerings 既是全局去重记录,也是本次通知流程的占位状态。
279+ // 仅在发送或快照提交失败时调用,释放后下一次刷新可以重新抢占并重试。
260280 var releaseErr error
261281 for _ , change := range changes {
282+ // exam_offerings 记录表示本次变化已经被某个刷新任务抢占。
283+ // 只有通知发送和考试快照更新都成功后才应保留;失败时删除该记录,
284+ // 让后续刷新可以重新抢占,避免考试变化被错误地永久去重。
262285 if err := s .db .Course .DeleteExamOfferingByHash (s .ctx , change .ExamHash ); err != nil {
263286 releaseErr = errors .Join (releaseErr , err )
264287 }
@@ -267,6 +290,7 @@ func (s *CourseService) releaseExamOfferings(changes []courseExamChange) error {
267290}
268291
269292func (s * CourseService ) sendExamNotifications (changes []courseExamChange ) error {
293+ // 同一批变化同时发送 Android 和 iOS;记录首个错误,但继续处理剩余通知。
270294 var firstErr error
271295 for _ , change := range changes {
272296 title := fmt .Sprintf ("%v考试信息更新啦" , change .Exam .Name )
0 commit comments