Przeglądaj źródła

daynumber的bug修正

chengjie 1 tydzień temu
rodzic
commit
90cefd8e56

+ 92 - 0
scripts/backfill_2months.mjs

@@ -0,0 +1,92 @@
1
+// 补齐近两个月:有学习记录(Flag=0)但缺 MiaoguoTask 记录的练习日,并对受影响用户完整重算 DayNumber/里程碑/勤勉号。
2
+import moment from 'moment';
3
+import Miaoguo from '../src/model/miaoguo.js';
4
+import constantClass from '../src/util/constant/index.js';
5
+import { stringUtils } from '../src/util/stringClass.js';
6
+
7
+const startDate = new Date(Date.now() - 60 * 86400000).toISOString().slice(0, 10);
8
+const dayKey = (x) => moment(x.CreateTime).format('YYYY-MM-DD');
9
+const score = (x) => (x.IsShare > 0 ? 4 : 0) + (x.MilestoneDayNumber > 0 ? 2 : 0) + (x.AphorismID > 0 ? 1 : 0);
10
+
11
+// 1) 找出缺失的练习日
12
+const missing = await Miaoguo.RunSql([startDate], `
13
+    SELECT mc.UserID, DATE_FORMAT(r.CreateTime,'%Y-%m-%d') d,
14
+           COUNT(DISTINCT r.CardID) CardCount, SUM(r.Duration) Duration
15
+    FROM MiaoguoLearningRecord r
16
+    INNER JOIN MiaoguoCard mc ON mc.MiaoguoCardID = r.CardID
17
+    LEFT JOIN MiaoguoTask t ON t.UserID = mc.UserID AND DATE(t.CreateTime) = DATE(r.CreateTime)
18
+    WHERE r.Flag=0 AND r.CreateTime >= ? AND t.ID IS NULL
19
+    GROUP BY mc.UserID, DATE_FORMAT(r.CreateTime,'%Y-%m-%d')
20
+    ORDER BY mc.UserID, d;`);
21
+
22
+console.log('待补齐的练习日:', missing.length, '涉及用户:', [...new Set(missing.map(r => r.UserID))].length);
23
+if (missing.length === 0) process.exit(0);
24
+
25
+const byUser = new Map();
26
+for (const r of missing) {
27
+    if (!byUser.has(r.UserID)) byUser.set(r.UserID, []);
28
+    byUser.get(r.UserID).push(r);
29
+}
30
+
31
+// 2) 插入缺失行(带存在性保护)
32
+let inserted = 0;
33
+for (const [userID, days] of byUser) {
34
+    for (const d of days) {
35
+        const createTime = d.d + ' 00:00:00';
36
+        await Miaoguo.RunSql([userID, createTime, d.CardCount, d.Duration, userID, createTime],
37
+            "insert into MiaoguoTask (UserID,CreateTime,CardTask,Duration,IsShare,Flag) " +
38
+            "select ?,?,?,?,0,0 from dual where not exists " +
39
+            "(select UserID,CreateTime from MiaoguoTask where UserID=? and CreateTime=?) limit 1;");
40
+        inserted++;
41
+    }
42
+    console.log(`U${userID}: 补插 ${days.length} 天 ->`, days.map(x => x.d).join(','));
43
+}
44
+console.log('补插完成,共', inserted, '天。\n');
45
+
46
+// 3) 对受影响用户完整重算(与夜间重算一致)
47
+const users = [...byUser.keys()].map(Number);
48
+const placeholders = users.map(() => '?').join(',');
49
+await Miaoguo.RunSql(users, `update MiaoguoTask set MilestoneID=0,MilestoneDayNumber=0,HardworkingNumber=0 where UserID in (${placeholders});`);
50
+
51
+const milestone = constantClass.GetMilestone();
52
+for (const userID of users) {
53
+    const tasks = await Miaoguo.RunSql([userID],
54
+        "SELECT * FROM MiaoguoTask where CardTask>0 and UserID=? order by CreateTime, ID desc;");
55
+    if (!tasks || tasks.length === 0) continue;
56
+
57
+    const dayBest = new Map();
58
+    for (const t of tasks) {
59
+        const key = dayKey(t);
60
+        const best = dayBest.get(key);
61
+        if (!best) dayBest.set(key, t);
62
+        else if (score(t) > score(best) || (score(t) === score(best) && t.ID > best.ID))
63
+            dayBest.set(key, t);
64
+    }
65
+    for (const t of tasks) {
66
+        if (dayBest.get(dayKey(t)).ID !== t.ID)
67
+            await Miaoguo.RunSql([t.ID], "delete from MiaoguoTask where ID=?;");
68
+    }
69
+    const dayTasks = [...dayBest.values()].sort((a, b) =>
70
+        a.CreateTime < b.CreateTime ? -1 : a.CreateTime > b.CreateTime ? 1 : a.ID - b.ID);
71
+
72
+    let milestoneIDs = [];
73
+    for (let i = 0; i < 40; i++) milestoneIDs.push(i + 1);
74
+    milestoneIDs = stringUtils.RandomArray(milestoneIDs);
75
+    let milestoneIndex = 0, hardworkingIndex = 0;
76
+    for (let i = 0; i < dayTasks.length; i++) {
77
+        const t = dayTasks[i];
78
+        const update = { ID: t.ID, DayNumber: i + 1 };
79
+        const milestoneItem = milestone.find(item => item.DayNumber == i + 1);
80
+        if (milestoneItem) {
81
+            update.MilestoneDayNumber = milestoneItem.DayNumber;
82
+            update.MilestoneID = milestoneIDs[milestoneIndex++];
83
+        }
84
+        if (t.IsShare > 0 || update.MilestoneDayNumber > 0 || t.AphorismID > 0)
85
+            update.HardworkingNumber = ++hardworkingIndex;
86
+        await Miaoguo.UpdateMiaoguoTask(update);
87
+    }
88
+    console.log(`U${userID}: 重算完成,共 ${dayTasks.length} 天 (1..${dayTasks.length})`);
89
+}
90
+
91
+console.log('\n全部补齐完成。');
92
+process.exit(0);

+ 27 - 0
scripts/check_658.mjs

@@ -0,0 +1,27 @@
1
+import Miaoguo from '../src/model/miaoguo.js';
2
+
3
+const startDate = new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);
4
+console.log('startDate =', startDate);
5
+
6
+// 重算的真实用户集合(与 updateMiaoguoTaskData 完全一致的 SQL)
7
+const users = await Miaoguo.RunSql([startDate],
8
+    "select distinct mc.UserID from MiaoguoLearningRecord mlr inner join MiaoguoCard mc on mc.MiaoguoCardID=mlr.CardID where mlr.Flag=0 and mlr.CreateTime>=?;");
9
+const ids = users.map(u => Number(u.UserID));
10
+console.log('重算用户集合数量:', ids.length);
11
+console.log('658 是否在重算集合中:', ids.includes(658));
12
+
13
+// 658 最近一个月的学习记录(Flag=0)
14
+const lr = await Miaoguo.RunSql([658, startDate],
15
+    "select count(*) as c, max(mlr.CreateTime) as lastTime from MiaoguoLearningRecord mlr inner join MiaoguoCard mc on mc.MiaoguoCardID=mlr.CardID where mlr.Flag=0 and mc.UserID=? and mlr.CreateTime>=?;");
16
+console.log('658 近30天 Flag=0 学习记录:', JSON.stringify(lr[0]));
17
+
18
+// 658 的 DayNumber=0 记录数
19
+const zero = await Miaoguo.RunSql([658],
20
+    "select count(*) as total, sum(case when DayNumber=0 then 1 else 0 end) as zeroCnt, max(CreateTime) as lastTask from MiaoguoTask where CardTask>0 and UserID=?;");
21
+console.log('658 任务总数/其中Day=0/最后一天:', JSON.stringify(zero[0]));
22
+
23
+// 全库最近30天有任务但 DayNumber=0 的用户规模
24
+const scope = await Miaoguo.RunSql([startDate],
25
+    "select count(distinct UserID) as users from MiaoguoTask where CardTask>0 and CreateTime>=? and DayNumber=0;");
26
+console.log('近30天存在 DayNumber=0 记录的用户数:', JSON.stringify(scope[0]));
27
+process.exit(0);

+ 11 - 0
scripts/diag_milestone.mjs

@@ -0,0 +1,11 @@
1
+import constantClass from '../src/util/constant/index.js';
2
+
3
+const m = constantClass.GetMilestone();
4
+console.log('里程碑条目数:', m.length);
5
+console.log('各 DayNumber:', m.map(x => x.DayNumber).join(','));
6
+
7
+// 检查 milestoneIDs 只有 40 个,但命中里程碑超过 40 次就会越界
8
+const maxHits = m.length;
9
+console.log('若用户命中里程碑次数 > 40,则 milestoneIDs 越界 -> undefined');
10
+console.log('是否可能越界:', maxHits > 40);
11
+process.exit(0);

+ 16 - 0
scripts/dump_658.mjs

@@ -0,0 +1,16 @@
1
+import Miaoguo from '../src/model/miaoguo.js';
2
+
3
+const r = await Miaoguo.RunSql([658],
4
+    'SELECT ID,CreateTime,DayNumber,CardTask,IsShare,HardworkingNumber,MilestoneDayNumber,AphorismID FROM MiaoguoTask where UserID=658 and ID>384493 order by ID;');
5
+console.log('UserID=658, ID>384493 共', r.length, '条:');
6
+for (const x of r)
7
+    console.log([x.ID, x.CreateTime, 'Day=' + x.DayNumber, 'Card=' + x.CardTask, 'Share=' + x.IsShare, 'HW=' + x.HardworkingNumber, 'Mil=' + x.MilestoneDayNumber, 'Aph=' + x.AphorismID].join(' | '));
8
+
9
+const g = await Miaoguo.RunSql([658],
10
+    'SELECT COUNT(*) as rows, COUNT(DISTINCT DATE(CreateTime)) as days, MAX(DayNumber) as maxDay, MIN(DayNumber) as minDay FROM MiaoguoTask where CardTask>0 and UserID=658;');
11
+console.log('\n汇总:', JSON.stringify(g[0]));
12
+
13
+const dup = await Miaoguo.RunSql([658],
14
+    "SELECT DATE(CreateTime) d, COUNT(*) c FROM MiaoguoTask WHERE CardTask>0 AND UserID=658 GROUP BY DATE(CreateTime) HAVING c>1 ORDER BY d DESC LIMIT 20;");
15
+console.log('同一天多条:', JSON.stringify(dup));
16
+process.exit(0);

+ 71 - 0
scripts/fix_all_broken.mjs

@@ -0,0 +1,71 @@
1
+// 修复全库所有 DayNumber 异常用户:去重(每天保留最完整一条) + 按天连续重编号。
2
+import moment from 'moment';
3
+import Miaoguo from '../src/model/miaoguo.js';
4
+import constantClass from '../src/util/constant/index.js';
5
+import { stringUtils } from '../src/util/stringClass.js';
6
+
7
+const dayKey = (x) => moment(x.CreateTime).format('YYYY-MM-DD');
8
+const score = (x) => (x.IsShare > 0 ? 4 : 0) + (x.MilestoneDayNumber > 0 ? 2 : 0) + (x.AphorismID > 0 ? 1 : 0);
9
+
10
+async function fixUser(userID) {
11
+    const tasks = await Miaoguo.RunSql([userID],
12
+        "SELECT * FROM MiaoguoTask where CardTask>0 and UserID=? order by CreateTime, ID desc;");
13
+    if (!tasks || tasks.length === 0) return `User ${userID}: 无记录`;
14
+
15
+    const dayBest = new Map();
16
+    for (const t of tasks) {
17
+        const key = dayKey(t);
18
+        const best = dayBest.get(key);
19
+        if (!best) dayBest.set(key, t);
20
+        else if (score(t) > score(best) || (score(t) === score(best) && t.ID > best.ID))
21
+            dayBest.set(key, t);
22
+    }
23
+
24
+    let deleted = 0;
25
+    for (const t of tasks) {
26
+        if (dayBest.get(dayKey(t)).ID !== t.ID) {
27
+            await Miaoguo.RunSql([t.ID], "delete from MiaoguoTask where ID=?;");
28
+            deleted++;
29
+        }
30
+    }
31
+
32
+    const dayTasks = [...dayBest.values()].sort((a, b) =>
33
+        a.CreateTime < b.CreateTime ? -1 : a.CreateTime > b.CreateTime ? 1 : a.ID - b.ID);
34
+
35
+    const milestone = constantClass.GetMilestone();
36
+    let milestoneIDs = [];
37
+    for (let i = 0; i < 40; i++) milestoneIDs.push(i + 1);
38
+    milestoneIDs = stringUtils.RandomArray(milestoneIDs);
39
+    let milestoneIndex = 0, hardworkingIndex = 0;
40
+    for (let i = 0; i < dayTasks.length; i++) {
41
+        const t = dayTasks[i];
42
+        const update = { ID: t.ID, DayNumber: i + 1 };
43
+        const milestoneItem = milestone.find(item => item.DayNumber == i + 1);
44
+        if (milestoneItem) {
45
+            update.MilestoneDayNumber = milestoneItem.DayNumber;
46
+            update.MilestoneID = milestoneIDs[milestoneIndex++];
47
+        }
48
+        if (t.IsShare > 0 || update.MilestoneDayNumber > 0 || t.AphorismID > 0)
49
+            update.HardworkingNumber = ++hardworkingIndex;
50
+        await Miaoguo.UpdateMiaoguoTask(update);
51
+    }
52
+    return `User ${userID}: 删除重复 ${deleted} 条,重编号 ${dayTasks.length} 天 (1..${dayTasks.length})`;
53
+}
54
+
55
+const rows = await Miaoguo.RunSql([], `
56
+    SELECT UserID,
57
+           COUNT(DISTINCT DATE(CreateTime)) as days,
58
+           MAX(DayNumber) as maxDay,
59
+           SUM(CASE WHEN DayNumber=0 THEN 1 ELSE 0 END) as zeroCnt
60
+    FROM MiaoguoTask
61
+    WHERE CardTask>0
62
+    GROUP BY UserID
63
+    HAVING zeroCnt>0 OR maxDay<>days
64
+    ORDER BY UserID;`);
65
+
66
+console.log('待修复用户数:', rows.length);
67
+for (const r of rows)
68
+    console.log(await fixUser(r.UserID));
69
+
70
+console.log('\n全部完成。');
71
+process.exit(0);

+ 52 - 0
scripts/scan_2months.mjs

@@ -0,0 +1,52 @@
1
+import Miaoguo from '../src/model/miaoguo.js';
2
+
3
+const startDate = new Date(Date.now() - 60 * 86400000).toISOString().slice(0, 10);
4
+console.log('统计区间: >=', startDate);
5
+
6
+// 1) DayNumber 异常(全时段,限定近两个月有任务的用户)
7
+const bad = await Miaoguo.RunSql([startDate], `
8
+    SELECT UserID, COUNT(DISTINCT DATE(CreateTime)) days, MAX(DayNumber) maxDay,
9
+           SUM(CASE WHEN DayNumber=0 THEN 1 ELSE 0 END) zeroCnt
10
+    FROM MiaoguoTask
11
+    WHERE CardTask>0 AND UserID IN (SELECT DISTINCT UserID FROM MiaoguoTask WHERE CardTask>0 AND CreateTime>=?)
12
+    GROUP BY UserID
13
+    HAVING zeroCnt>0 OR maxDay<>days
14
+    ORDER BY UserID;`);
15
+console.log('\n[1] DayNumber 异常用户数:', bad.length);
16
+if (bad.length) console.log('    ', bad.map(r => `U${r.UserID}(z=${r.zeroCnt},max=${r.maxDay},days=${r.days})`).join(', '));
17
+
18
+// 2) 近两个月:有学习记录但没有 MiaoguoTask 记录的天(需要补齐)
19
+const missing = await Miaoguo.RunSql([startDate], `
20
+    SELECT mc.UserID, DATE_FORMAT(r.CreateTime,'%Y-%m-%d') d,
21
+           COUNT(DISTINCT r.CardID) CardCount, SUM(r.Duration) Duration
22
+    FROM MiaoguoLearningRecord r
23
+    INNER JOIN MiaoguoCard mc ON mc.MiaoguoCardID = r.CardID
24
+    LEFT JOIN MiaoguoTask t ON t.UserID = mc.UserID AND DATE(t.CreateTime) = DATE(r.CreateTime)
25
+    WHERE r.Flag=0 AND r.CreateTime >= ? AND t.ID IS NULL
26
+    GROUP BY mc.UserID, DATE_FORMAT(r.CreateTime,'%Y-%m-%d')
27
+    ORDER BY mc.UserID, d;`);
28
+const mUsers = [...new Set(missing.map(r => r.UserID))];
29
+console.log('\n[2] 缺 MiaoguoTask 记录: 受影响用户数:', mUsers.length, ' 缺失天数合计:', missing.length);
30
+console.log('    用户:', mUsers.join(','));
31
+if (missing.length) {
32
+    console.log('    明细(前30):');
33
+    for (const r of missing.slice(0, 30))
34
+        console.log(`      U${r.UserID} ${r.d} Card=${r.CardCount} Dur=${r.Duration}`);
35
+}
36
+
37
+// 3) 近两个月同一天多条 MiaoguoTask 记录
38
+const dup = await Miaoguo.RunSql([startDate], `
39
+    SELECT UserID, DATE_FORMAT(CreateTime,'%Y-%m-%d') d, COUNT(*) c
40
+    FROM MiaoguoTask
41
+    WHERE CardTask>0 AND CreateTime>=?
42
+    GROUP BY UserID, DATE_FORMAT(CreateTime,'%Y-%m-%d')
43
+    HAVING c>1
44
+    ORDER BY UserID, d;`);
45
+console.log('\n[3] 同一天多条记录: 天数:', dup.length);
46
+if (dup.length) {
47
+    const dUsers = [...new Set(dup.map(r => r.UserID))];
48
+    console.log('    用户:', dUsers.join(','));
49
+    console.log('    明细:', dup.map(r => `U${r.UserID}@${r.d}x${r.c}`).join(', '));
50
+}
51
+
52
+process.exit(0);

+ 25 - 0
scripts/scan_all_broken.mjs

@@ -0,0 +1,25 @@
1
+import Miaoguo from '../src/model/miaoguo.js';
2
+
3
+// 找出所有 DayNumber 异常的用户:
4
+// 1) 存在 DayNumber=0 的行
5
+// 2) DayNumber 最大值 != 实际不同天数(历史膨胀)
6
+const rows = await Miaoguo.RunSql([], `
7
+    SELECT UserID,
8
+           COUNT(DISTINCT DATE(CreateTime)) as days,
9
+           MAX(DayNumber) as maxDay,
10
+           MIN(DayNumber) as minDay,
11
+           SUM(CASE WHEN DayNumber=0 THEN 1 ELSE 0 END) as zeroCnt
12
+    FROM MiaoguoTask
13
+    WHERE CardTask>0
14
+    GROUP BY UserID
15
+    HAVING zeroCnt>0 OR maxDay<>days
16
+    ORDER BY UserID;`);
17
+
18
+console.log('异常用户数:', rows.length);
19
+console.log('有 DayNumber=0 的用户数:', rows.filter(r => r.zeroCnt > 0).length);
20
+console.log('DayNumber 膨胀(maxDay<>days)的用户数:', rows.filter(r => r.maxDay !== r.days).length);
21
+console.log('用户列表:', rows.map(r => r.UserID).join(','));
22
+console.log('明细(前20):');
23
+for (const r of rows.slice(0, 20))
24
+    console.log(`  UserID=${r.UserID} days=${r.days} maxDay=${r.maxDay} zeroCnt=${r.zeroCnt}`);
25
+process.exit(0);

+ 28 - 11
src/api/miaoguo/miaoguoController.js

@@ -1500,7 +1500,9 @@ export async function GetMiaoguoTaskTime(ctx) {
1500 1500
 
1501 1501
     delete param.CreateTime;
1502 1502
     const dayNumberArr = await miaoguo.GetTaskData(param);
1503
-    let dayNumber = dayNumberArr.length;
1503
+    // 按“不同日期数”统计累计练习天数,避免同一天多条记录把 DayNumber 撑大
1504
+    const dayCount = new Set(dayNumberArr.map(item => item.taskDate)).size;
1505
+    let dayNumber = dayCount;
1504 1506
     if (dayNumber == 0)
1505 1507
         dayNumber = 1;
1506 1508
 
@@ -1516,12 +1518,12 @@ export async function GetMiaoguoTaskTime(ctx) {
1516 1518
             };
1517 1519
             const tasks = await miaoguo.GetUserMiaoguoTask(param);
1518 1520
             if (tasks && tasks.length > 0) {
1519
-                // 当天任务已存在,dayNumberArr 已包含当天记录,直接用累计练习天数
1520
-                obj.DayNumber = dayNumberArr.length;
1521
+                // 当天任务已存在,dayCount 已包含当天记录,直接用累计练习天数
1522
+                obj.DayNumber = dayCount;
1521 1523
                 await miaoguo.UpdateMiaoguoTask(obj);
1522 1524
             } else {
1523
-                // 新增当天任务,DayNumber 为已有累计天数 + 1(注意用原始长度,新用户首日应为 1)
1524
-                obj.DayNumber = dayNumberArr.length + 1;
1525
+                // 新增当天任务,DayNumber 为已有累计天数 + 1(新用户首日应为 1)
1526
+                obj.DayNumber = dayCount + 1;
1525 1527
                 await miaoguo.AddMiaoguoTask(obj);
1526 1528
                 await updateMiaoguoMilestoneInfoByUserID(param.UserID);
1527 1529
             }
@@ -2084,10 +2086,18 @@ export async function GetShareItemInfo(ctx) {
2084 2086
             if (list[0].MilestoneDayNumber > 0)
2085 2087
                 isMilestone = 1;
2086 2088
 
2089
+            // 用“截至该任务日期的不同练习天数”计算 DayNumber,
2090
+            // 避免上一条记录 DayNumber=0 时被算成 1
2091
+            const dayCountArr = await miaoguo.GetUserMiaoguoTaskDayCount({
2092
+                UserID: param.UserID,
2093
+                CreateTime: list[0].CreateTime,
2094
+            });
2095
+            const currentDayNumber = (dayCountArr && dayCountArr[0] && dayCountArr[0].DayNumber) || 1;
2096
+
2087 2097
             if (list[1]) {
2088 2098
                 const param2 = {
2089 2099
                     ID: list[0].ID,
2090
-                    DayNumber: list[1].DayNumber + 1,
2100
+                    DayNumber: currentDayNumber,
2091 2101
                 };
2092 2102
                 if (list[1].HardworkingNumber > 0) {
2093 2103
                     param2.HardworkingNumber = list[1].HardworkingNumber + 1;
@@ -2105,7 +2115,7 @@ export async function GetShareItemInfo(ctx) {
2105 2115
             } else {
2106 2116
                 await miaoguo.UpdateMiaoguoTask({
2107 2117
                     ID: list[0].ID,
2108
-                    DayNumber: 1,
2118
+                    DayNumber: currentDayNumber,
2109 2119
                     HardworkingNumber: 1,
2110 2120
                 });
2111 2121
             }
@@ -3598,26 +3608,30 @@ export async function updateMiaoguoTaskData(ctx) {
3598 3608
         );
3599 3609
         const milestone = constantClass.GetMilestone();
3600 3610
         for (const user of taskUsers) {
3611
+          try {
3601 3612
             const tasks = await miaoguo.RunSql([user.UserID], "SELECT * FROM MiaoguoTask where CardTask>0 and UserID=? order by CreateTime, ID desc;");
3602 3613
             if (!tasks || tasks.length == 0) continue;
3603 3614
 
3604 3615
             // 同一天可能有多条记录(历史并发写入导致)。每天只保留“最完整”的一条,
3605 3616
             // 优先 已分享(IsShare>0) / 有里程碑 / 有格言,其次保留最新 ID;其余重复行删除。
3606 3617
             // 这样 DayNumber 按“天”连续递增,且不会因删除重复行而丢失分享/里程碑/勤勉信息。
3618
+            // 注意:CreateTime 是 Date 对象,不能直接当 Map key(对象引用不相等),必须转成日期字符串
3619
+            const dayKey = (x) => moment(x.CreateTime).format('YYYY-MM-DD');
3607 3620
             const dayBest = new Map();
3608 3621
             for (const t of tasks) {
3609
-                const best = dayBest.get(t.CreateTime);
3622
+                const key = dayKey(t);
3623
+                const best = dayBest.get(key);
3610 3624
                 if (!best) {
3611
-                    dayBest.set(t.CreateTime, t);
3625
+                    dayBest.set(key, t);
3612 3626
                 } else {
3613 3627
                     const score = (x) => (x.IsShare > 0 ? 4 : 0) + (x.MilestoneDayNumber > 0 ? 2 : 0) + (x.AphorismID > 0 ? 1 : 0);
3614 3628
                     if (score(t) > score(best) || (score(t) === score(best) && t.ID > best.ID))
3615
-                        dayBest.set(t.CreateTime, t);
3629
+                        dayBest.set(key, t);
3616 3630
                 }
3617 3631
             }
3618 3632
             // 删除同一天的多余记录(仅保留 dayBest 中的那一条)
3619 3633
             for (const t of tasks) {
3620
-                if (dayBest.get(t.CreateTime).ID !== t.ID)
3634
+                if (dayBest.get(dayKey(t)).ID !== t.ID)
3621 3635
                     await miaoguo.RunSql([t.ID], "delete from MiaoguoTask where ID=?;");
3622 3636
             }
3623 3637
             const dayTasks = [...dayBest.values()].sort((a, b) =>
@@ -3640,6 +3654,9 @@ export async function updateMiaoguoTaskData(ctx) {
3640 3654
                     update.HardworkingNumber = ++hardworkingIndex;
3641 3655
                 await miaoguo.UpdateMiaoguoTask(update);
3642 3656
             }
3657
+          } catch (err) {
3658
+            console.error('updateMiaoguoTaskData 处理用户失败 UserID=' + user.UserID + ':', err);
3659
+          }
3643 3660
         }
3644 3661
     }
3645 3662
 

+ 9 - 2
src/model/miaoguo.js

@@ -411,8 +411,15 @@ class Miaoguo {
411 411
     }
412 412
 
413 413
     static async AddMiaoguoTask(obj) {
414
-        const sql = "insert into MiaoguoTask (UserID,CreateTime,CardTask,Duration) select ?,?,?,? from dual where not exists (select UserID,CreateTime from MiaoguoTask where UserID=? and CreateTime=?) limit 1;";
415
-        return await query(sql, [obj.UserID, obj.CreateTime, obj.CardTask, obj.Duration, obj.UserID, obj.CreateTime]);
414
+        // 必须写入 DayNumber:否则新一天的任务记录会用列默认值 0
415
+        const sql = "insert into MiaoguoTask (UserID,CreateTime,CardTask,Duration,DayNumber) select ?,?,?,?,? from dual where not exists (select UserID,CreateTime from MiaoguoTask where UserID=? and CreateTime=?) limit 1;";
416
+        return await query(sql, [obj.UserID, obj.CreateTime, obj.CardTask, obj.Duration, obj.DayNumber || 0, obj.UserID, obj.CreateTime]);
417
+    }
418
+
419
+    // 截至某一天(含)的累计练习天数,用于计算 DayNumber
420
+    static async GetUserMiaoguoTaskDayCount(obj) {
421
+        const sql = "SELECT COUNT(DISTINCT DATE(CreateTime)) as DayNumber FROM MiaoguoTask WHERE CardTask>0 AND UserID=? AND CreateTime<=?;";
422
+        return await query(sql, [obj.UserID, obj.CreateTime]);
416 423
     }
417 424
 
418 425
     static async GetUserMiaoguoTask(obj) {