88use App \Entity \QuestionDifficulty ;
99use App \Entity \SolutionEvent ;
1010use App \Entity \SolutionEventStatus ;
11+ use App \Entity \SolutionVideoEvent ;
1112use App \Entity \User ;
1213use App \Repository \HintOpenEventRepository ;
13- use App \Repository \QuestionRepository ;
1414use App \Repository \SolutionEventRepository ;
1515use App \Repository \SolutionVideoEventRepository ;
16+ use Psr \Cache \InvalidArgumentException ;
17+ use Symfony \Contracts \Cache \ItemInterface ;
18+ use Symfony \Contracts \Cache \TagAwareCacheInterface ;
1619
1720final class PointCalculationService
1821{
@@ -37,11 +40,14 @@ final class PointCalculationService
3740 public function __construct (
3841 private readonly SolutionEventRepository $ solutionEventRepository ,
3942 private readonly SolutionVideoEventRepository $ solutionVideoEventRepository ,
40- private readonly QuestionRepository $ questionRepository ,
4143 private readonly HintOpenEventRepository $ hintOpenEventRepository ,
44+ private readonly TagAwareCacheInterface $ cache ,
4245 ) {
4346 }
4447
48+ /**
49+ * @throws InvalidArgumentException
50+ */
4551 public function calculate (User $ user ): int
4652 {
4753 return self ::$ base
@@ -76,40 +82,82 @@ protected function calculateSolutionQuestionPoints(User $user): int
7682 * Calculate the points if the user is the first solver of a question.
7783 *
7884 * 第一位解題成功者加10點。
85+ *
86+ * @throws InvalidArgumentException
7987 */
8088 protected function calculateFirstSolutionPoints (User $ user ): int
8189 {
82- $ questions = $ this ->questionRepository ->findAll ();
83-
90+ // select the question this user has ever solved
91+ $ qb = $ this ->solutionEventRepository ->createQueryBuilder ('e ' )
92+ ->select ('q ' )
93+ ->from (Question::class, 'q ' )
94+ ->where ('e.question = q.id ' )
95+ ->andWhere ('e.status = :status ' )
96+ ->andWhere ('e.submitter = :submitter ' )
97+ ->setParameter ('status ' , SolutionEventStatus::Passed)
98+ ->setParameter ('submitter ' , $ user );
99+
100+ /**
101+ * @var Question[] $questions
102+ */
103+ $ questions = $ qb ->getQuery ()->getResult ();
104+
105+ // check if the user is the first solver of each question
84106 $ points = 0 ;
85107
86- // list the first solver of each question
87108 foreach ($ questions as $ question ) {
88- $ solutionEvent = $ question
89- ->getSolutionEvents ()
90- ->findFirst (fn ($ _ , SolutionEvent $ event ) => SolutionEventStatus::Passed === $ event ->getStatus ());
91-
92- if (!$ solutionEvent || $ solutionEvent ->getSubmitter () !== $ user ) {
93- continue ;
109+ $ firstSolver = $ this ->listFirstSolversOfQuestion ($ question );
110+ if ($ firstSolver && $ firstSolver === $ user ->getId ()) {
111+ $ points += self ::$ firstSolverPoint ;
94112 }
95-
96- $ points += self ::$ firstSolverPoint ;
97113 }
98114
99115 return $ points ;
100116 }
101117
118+ /**
119+ * List and cache the first solvers of each question.
120+ *
121+ * @param Question $question the question to get the first solver
122+ *
123+ * @returns int|null the first solver ID of the question
124+ *
125+ * @throws InvalidArgumentException
126+ */
127+ protected function listFirstSolversOfQuestion (Question $ question ): ?int
128+ {
129+ return $ this ->cache ->get (
130+ "question.q {$ question ->getId ()}.first-solver " ,
131+ function (ItemInterface $ item ) use ($ question ) {
132+ $ item ->tag (['question ' , 'first-solver ' ]);
133+
134+ $ solutionEvent = $ question
135+ ->getSolutionEvents ()
136+ ->findFirst (fn ($ _ , SolutionEvent $ event ) => SolutionEventStatus::Passed === $ event ->getStatus ());
137+
138+ return $ solutionEvent ?->getSubmitter()?->getId();
139+ }
140+ );
141+ }
142+
102143 protected function calculateSolutionVideoPoints (User $ user ): int
103144 {
104- $ solutionVideoEvents = $ this ->solutionVideoEventRepository ->findByUser ($ user );
145+ /**
146+ * @var Question[] $questions
147+ */
148+ $ questions = $ this ->solutionVideoEventRepository ->createQueryBuilder ('sve ' )
149+ ->from (Question::class, 'q ' )
150+ ->select ('q ' )
151+ ->where ('sve.opener = :user ' )
152+ ->andWhere ('sve.question = q.id ' )
153+ ->groupBy ('q.id ' )
154+ ->setParameter ('user ' , $ user )
155+ ->getQuery ()
156+ ->getResult ();
105157
106158 $ questionPointsPair = [];
107- foreach ($ solutionVideoEvents as $ event ) {
108- $ question = $ event ->getQuestion ();
109- if (!$ question ) {
110- continue ;
111- }
112159
160+ foreach ($ questions as $ question ) {
113161 $ questionPointsPair [$ question ->getId ()] = match ($ question ->getDifficulty ()) {
114162 QuestionDifficulty::Easy => self ::$ solutionVideoEventEasy ,
115163 QuestionDifficulty::Medium => self ::$ solutionVideoEventMedium ,
0 commit comments