learning-report-query.test.mjs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import { after, before, test } from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { readFileSync } from 'node:fs';
  4. import vm from 'node:vm';
  5. import { PGlite } from '@electric-sql/pglite';
  6. const db = new PGlite();
  7. const source = readFileSync(new URL('../deploy-admin-functions.mjs', import.meta.url), 'utf8');
  8. const gateway = {};
  9. vm.createContext(gateway);
  10. vm.runInContext(source.match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1], gateway);
  11. gateway.companyIdOf = () => 'co';
  12. before(async () => {
  13. await db.exec(`
  14. CREATE TABLE "DailyStudyRecord"("objectId" text,"company" text,"id" int,"userId" int,"pl" text,"dsid" text,"dqrq" text,"xxqs" text,"createdAt" timestamptz);
  15. CREATE TABLE "CommonModel"("objectId" text,"company" text,"modelId" int,"itemId" int,"generalId" int,"createTime" timestamptz,"createdAt" timestamptz);
  16. CREATE TABLE "_User"("objectId" text,"company" text,"legacyUserId" int,"legacyUserData" jsonb,"realName" text,"nickname" text,"username" text,"mobile" text,"identityType" text,"isDeleted" boolean);
  17. CREATE TABLE "CourseAppointment"("objectId" text,"company" text,"id" int,"kcid" text,"dszt" text,"courseCategoryKey" text,"courseCategoryName" text,szyh text);
  18. CREATE TABLE "Node"("company" text,"nodeId" int,"nodeName" text);
  19. INSERT INTO "DailyStudyRecord" VALUES
  20. ('compact','co',1,20,'30','','20261004','[]','2026-10-04T00:00:00Z'),
  21. ('dashed','co',2,20,'30','','2026-10-04','[]','2026-10-04T00:00:00Z'),
  22. ('slash','co',3,20,'30','','2026/10/03','[]','2026-10-03T00:00:00Z'),
  23. ('fallback','co',4,20,'30','',NULL,'[]','2026-09-30T16:01:00Z'),
  24. ('history','co',5,20,'30','','20260930','[]','2026-09-30T00:00:00Z'),
  25. ('foreign','other',6,20,'30','','20261004','[]','2026-10-04T00:00:00Z');
  26. `);
  27. });
  28. after(() => db.close());
  29. async function rows(filters) {
  30. const query = gateway.learningReportQueryParts({}, { page: 1, pageSize: 20, filters });
  31. return (await db.query('SELECT d."objectId"' + query.from + query.where + ' ORDER BY ' + query.studyDateSql + ' DESC,d."objectId"', query.values)).rows.map(row => row.objectId);
  32. }
  33. test('月份筛选兼容三种日期格式,并按北京时间补齐缺失学习日期', async () => {
  34. assert.deepEqual(await rows({ month: '2026-10' }), ['compact','dashed','slash','fallback']);
  35. });
  36. test('全部月份包含历史记录,保持帐套隔离', async () => {
  37. assert.deepEqual(await rows({ month: '' }), ['compact','dashed','slash','fallback','history']);
  38. });
  39. test('日期范围包含首尾当天,兼容不同保存格式', async () => {
  40. assert.deepEqual(await rows({ dateFrom: '2026-10-03', dateTo: '2026-10-04' }), ['compact','dashed','slash']);
  41. assert.deepEqual(await rows({ month: '2026-09' }), ['history']);
  42. });
  43. test('报表展示与筛选使用同一个学习日期,斜线日期与 UTC 时间不会少一天', () => {
  44. for(const raw of ['20261004','2026-10-04','2026/10/04'])assert.equal(gateway.learningReportProjection({dqrq:raw}).studyDate,'2026-10-04');
  45. assert.equal(gateway.learningReportProjection({recordCreatedAt:'2026-09-30T16:01:00Z'}).studyDate,'2026-10-01');
  46. });
  47. test('门店报表按当前会员归属过滤后再计数,无法通过过滤器读取其他门店',async()=>{
  48. await db.exec(`INSERT INTO "_User" ("objectId",company,"legacyUserId","legacyUserData","identityType") VALUES ('own','co',20,'{"ParentUserID":57}','member'),('other','co',21,'{"ParentUserID":73}','member');
  49. INSERT INTO "DailyStudyRecord" ("objectId",company,"userId",dqrq) VALUES ('other-store','co',21,'20261004');`);
  50. const query=gateway.learningReportQueryParts({storeId:57},{page:1,pageSize:20,filters:{month:'2026-10',storeId:73}});
  51. const count=(await db.query('SELECT COUNT(*)::int total'+query.from+query.where,query.values)).rows[0].total;
  52. assert.equal(count,4);
  53. const foreign=gateway.learningReportQueryParts({storeId:73},{page:1,pageSize:20,filters:{month:'2026-10'}});
  54. assert.deepEqual((await db.query('SELECT d."objectId"'+foreign.from+foreign.where,foreign.values)).rows.map(r=>r.objectId),['other-store']);
  55. });
  56. test('无教材的语法写作课按预约分类显示并支持课程名称搜索',async()=>{
  57. await db.exec(`INSERT INTO "CourseAppointment" VALUES
  58. ('writing','co',201,'0','30','high_writing','高中语法写作课','59'),
  59. ('word','co',202,'13','30','word','单词课','59'),
  60. ('legacy-writing','co',203,'398','30','high_writing','高中语法写作课','59'),
  61. ('legacy-unclassified','co',204,'398','30',NULL,NULL,'59'),
  62. ('unrelated-appointment','co',205,'398','30',NULL,NULL,'60');
  63. INSERT INTO "CommonModel" VALUES
  64. ('writing-main','co',54,201,201,NULL,NULL),
  65. ('word-main','co',54,202,202,NULL,NULL),
  66. ('legacy-writing-main','co',54,203,203,NULL,NULL),
  67. ('legacy-unclassified-main','co',54,204,204,NULL,NULL),
  68. ('unrelated-main','co',54,205,205,NULL,NULL);
  69. INSERT INTO "Node" VALUES ('co',13,'人教版词表'),('co',398,'阅读写作');
  70. INSERT INTO "DailyStudyRecord" VALUES
  71. ('writing-report','co',21,59,'88','201','20261111','[]',NULL),
  72. ('word-report','co',22,59,'88','202','20261111','[]',NULL),
  73. ('legacy-writing-report','co',23,59,'88','203','20261111','[]',NULL),
  74. ('legacy-unclassified-report','co',24,59,'88','204','20261111','[]',NULL),
  75. ('unrelated-report','co',25,59,'88','205','20261111','[]',NULL);`);
  76. const q=gateway.learningReportQueryParts({}, {filters:{dateFrom:'2026-11-11',dateTo:'2026-11-11',studentId:59}});
  77. const sql='SELECT d."objectId",'+gateway.learningReportCourseTitleSql()+' AS title'+q.from+q.where+' ORDER BY d.id';
  78. assert.deepEqual((await db.query(sql,q.values)).rows.map(row=>row.title),['高中语法写作课','人教版词表','阅读写作','阅读写作','阅读写作']);
  79. assert.deepEqual(await rows({courseSearch:'高中语法写作课'}),['writing-report']);
  80. assert.deepEqual(await rows({courseSearch:'人教版'}),['word-report']);
  81. });
  82. test('写作课不进入单词明细缺失或单词报表筛选', async () => {
  83. const dates = { dateFrom: '2026-11-11', dateTo: '2026-11-11' };
  84. assert.deepEqual(await rows({ ...dates, status: 'writing' }), [
  85. 'legacy-unclassified-report', 'legacy-writing-report', 'writing-report',
  86. ]);
  87. assert.deepEqual(await rows({ ...dates, status: 'missing' }), ['unrelated-report', 'word-report']);
  88. assert.deepEqual(await rows({ ...dates, status: 'available' }), []);
  89. });
  90. test('写作报表的词汇掌握率不适用,普通单词报表仍计算掌握率', () => {
  91. const wordData = { xxqs: '[{"Title":"apple","check":1}]', appointmentObjectId: 'word', appointmentStatus: '30' };
  92. const writing = gateway.learningReportProjection({ ...wordData, isWriting: true });
  93. assert.equal(writing.status, 'writing');
  94. assert.equal(writing.masteryRate, null);
  95. assert.equal(writing.canExport, false);
  96. assert.equal(writing.learnedCount, 0);
  97. const word = gateway.learningReportProjection(wordData);
  98. assert.equal(word.status, 'available');
  99. assert.equal(word.masteryRate, 100);
  100. assert.equal(word.canExport, true);
  101. });