Просмотр исходного кода

fix(review): resolve student names for every teacher review row

彭峰 3 часов назад
Родитель
Сommit
fabceefbaa

+ 5 - 1
scripts/cloud/mobile-teaching.js

@@ -20,6 +20,10 @@ async function appLessonReport(input,current) {
   ]);
   return {order:await lessonOrderCapabilities(order,current),feedback:feedback?safe(feedback):null,records,antiForget:reviews,...mobileSource()};
 }
+// Legacy profiles may store an empty realName while the name survives in HoneyName or the task title.
+function reviewStudentNameSql() {
+  return `COALESCE(NULLIF(BTRIM(u."realName"),''),NULLIF(BTRIM(u."nickname"),''),NULLIF(BTRIM(u."legacyUserData"->>'HoneyName'),''),CASE WHEN POSITION('@' IN c."title")>0 THEN NULLIF(BTRIM(SPLIT_PART(c."title",'@',1)),'') END,NULLIF(BTRIM(u."username"),''),'学员 '||m."yhid"::text)`;
+}
 async function appReviewPage(input,current) {
   const uid=await authorizeRequestedUser(current,input.uid),coach=number(current.get('legacyGroupId'))===3,p=mobilePage(input),includeMetadata=input.includeMetadata!==false,today=appOverviewToday();
   const reviewTarget=coach&&number(input.studentId)?await appReviewTarget(current,input.studentId):coach?0:uid,legacyReviewSync=reviewTarget?await syncLegacyReviews(reviewTarget,current,input.refresh===true):undefined;
@@ -40,7 +44,7 @@ async function appReviewPage(input,current) {
   if(input.status==='completed')clauses.push('m."fxzt"=\'1\'');
   if(input.status==='pending')clauses.push('COALESCE(m."fxzt",\'0\')<>\'1\'');
   const where=clauses.join(' AND '),countPromise=Psql.one('SELECT COUNT(*)::int total FROM "MemoryPracticeRecord" m WHERE '+where,args);
-  const rowsPromise=Psql.query('SELECT c.*,row_to_json(m) AS "__addon",n."nodeName" AS "kc_title",d."dqrq" AS "learned_date",d."createdAt" AS "learned_at",r."amount" AS money,r."status" AS "rewardStatus",r."ineligibleReason" AS "ineligibleReason",r."completedAt" AS "completedAt",r."originalAmount" AS "originalAmount",r."correctionReason" AS "correctionReason",r."correctionAmount" AS "correctionAmount",COALESCE(u."realName",u."nickname",u."username") AS "honeyName",(SELECT COUNT(*) FROM "MemoryPracticeRecord" sibling WHERE sibling."company"=m."company" AND sibling."yhid"=m."yhid" AND sibling."xxjlid"=m."xxjlid" AND (sibling."kywsj",sibling."id")<=(m."kywsj",m."id"))::int AS "reviewNo" FROM "MemoryPracticeRecord" m JOIN "CommonModel" c ON c."company"=m."company" AND c."modelId"=60 AND c."itemId"=m."id" LEFT JOIN "Node" n ON n."company"=m."company" AND n."nodeId"::text=m."kcid"::text LEFT JOIN "CommonModel" dc ON dc."company"=m."company" AND dc."modelId"=56 AND dc."generalId"::text=m."xxjlid"::text LEFT JOIN "DailyStudyRecord" d ON d."company"=m."company" AND d."id"=dc."itemId" LEFT JOIN "ReviewRewardIncome" r ON r."company"=m."company" AND r."taskId"=m."objectId" LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+where+(input.scope==='today'?' ORDER BY CASE WHEN m."fxzt"=\'1\' THEN 1 ELSE 0 END,m."kywsj",m."id"':' ORDER BY m."kywsj" DESC,m."id" DESC')+' LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
+  const rowsPromise=Psql.query('SELECT c.*,row_to_json(m) AS "__addon",n."nodeName" AS "kc_title",d."dqrq" AS "learned_date",d."createdAt" AS "learned_at",r."amount" AS money,r."status" AS "rewardStatus",r."ineligibleReason" AS "ineligibleReason",r."completedAt" AS "completedAt",r."originalAmount" AS "originalAmount",r."correctionReason" AS "correctionReason",r."correctionAmount" AS "correctionAmount",'+reviewStudentNameSql()+' AS "honeyName",(SELECT COUNT(*) FROM "MemoryPracticeRecord" sibling WHERE sibling."company"=m."company" AND sibling."yhid"=m."yhid" AND sibling."xxjlid"=m."xxjlid" AND (sibling."kywsj",sibling."id")<=(m."kywsj",m."id"))::int AS "reviewNo" FROM "MemoryPracticeRecord" m JOIN "CommonModel" c ON c."company"=m."company" AND c."modelId"=60 AND c."itemId"=m."id" LEFT JOIN "Node" n ON n."company"=m."company" AND n."nodeId"::text=m."kcid"::text LEFT JOIN "CommonModel" dc ON dc."company"=m."company" AND dc."modelId"=56 AND dc."generalId"::text=m."xxjlid"::text LEFT JOIN "DailyStudyRecord" d ON d."company"=m."company" AND d."id"=dc."itemId" LEFT JOIN "ReviewRewardIncome" r ON r."company"=m."company" AND r."taskId"=m."objectId" LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+where+(input.scope==='today'?' ORDER BY CASE WHEN m."fxzt"=\'1\' THEN 1 ELSE 0 END,m."kywsj",m."id"':' ORDER BY m."kywsj" DESC,m."id" DESC')+' LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
   const versionTarget=coach&&number(input.studentId)?number(input.studentId):uid;
   const [students,summary,dates,count,rows,income,versions]=await Promise.all([studentsPromise,summaryPromise,datesPromise,countPromise,rowsPromise,coach&&input.includeIncome!==false?reviewIncomeTotal(DEFAULT_COMPANY_ID,uid):0,appLiveVersionSnapshot(versionTarget,['review','account'],current)]);
   return {items:rows.map(row=>{const addon=row.__addon;delete row.__addon;return{...legacyAliases({...row,...addon},'CommonModel'),money:number(row.money),rewardStatus:row.rewardStatus||'pending'};}),page:mobilePageMeta(count.total,p),summary:{...summary,todayTotal:number(summary.today),todayCompleted:number(summary.todayCompleted),todayPending:number(summary.today)-number(summary.todayCompleted),overduePending:number(summary.overduePending),income},dates,students,legacyReviewSync,serverToday:today,...mobileSource(),...versions};

+ 2 - 2
scripts/tests/fixtures/teacher-review-schema.sql

@@ -1,9 +1,9 @@
 CREATE TABLE "MemoryPracticeRecord"("objectId" text,company text,id numeric,plid numeric,yhid numeric,xxjlid numeric,kcid numeric,fxzt text,kywrq text,kywsj text);
-  CREATE TABLE "CommonModel"(company text,"modelId" numeric,"generalId" numeric,"itemId" numeric);
+  CREATE TABLE "CommonModel"(company text,"modelId" numeric,"generalId" numeric,"itemId" numeric,title text);
   CREATE TABLE "DailyStudyRecord"(company text,id numeric,learned numeric,xxqs text,dqrq text,"createdAt" timestamptz);
   CREATE TABLE "Node"(company text,"nodeId" numeric,"nodeName" text);
   CREATE TABLE "_User"(company text,"legacyUserId" numeric,"realName" text,nickname text,username text,"legacyUserData" jsonb);
   CREATE TABLE "ReviewRewardIncome"(company text,"taskId" text,amount numeric,status text,"ineligibleReason" text,"completedAt" timestamptz,"originalAmount" numeric,"correctionAmount" numeric,"correctionReason" text);
   INSERT INTO "MemoryPracticeRecord" SELECT 'r'||i,'co',i,30,20,100,1,CASE WHEN i=2 THEN '1' ELSE '0' END,CASE WHEN i<3 THEN '2026-10-14' WHEN i=3 THEN '2026-10-13' ELSE '2026-10-20' END,'2026-10-20 10:00:00' FROM generate_series(1,50) i;
   INSERT INTO "MemoryPracticeRecord" VALUES ('other','co',100,31,21,101,1,'0','2026-10-14','2026-10-14');
-  INSERT INTO "CommonModel" SELECT company,60,id+1000,id FROM "MemoryPracticeRecord";
+  INSERT INTO "CommonModel" (company,"modelId","generalId","itemId") SELECT company,60,id+1000,id FROM "MemoryPracticeRecord";

+ 37 - 0
scripts/tests/review-student-names.test.mjs

@@ -0,0 +1,37 @@
+import {test} from 'node:test';
+import assert from 'node:assert/strict';
+import {readFileSync} from 'node:fs';
+import vm from 'node:vm';
+import {PGlite} from '@electric-sql/pglite';
+const source=readFileSync(new URL('../cloud/mobile-teaching.js',import.meta.url),'utf8');
+test('复习姓名跳过空白,保留真实姓名、昵称、旧姓名及历史任务姓名,无法确认时显示学员编号',async()=>{
+ const expression=vm.runInNewContext(source+';reviewStudentNameSql()'),db=new PGlite();
+ try{
+  for(const [real,nick,legacy,title,login,expected] of [
+   ['可爱同学','昵称','{}','历史名@21天抗遗忘','login','可爱同学'],
+   ['', ' 小林 ', '{}','历史名@21天抗遗忘','login','小林'],
+   [' ','','{"HoneyName":" 小陈 "}','历史名@21天抗遗忘','login','小陈'],
+   [null,null,'{}','谢星星@21天抗遗忘@2024-09-17',null,'谢星星'],
+   ['', '', '{}','课程名称','student-login','student-login'],
+   ['', '', '{}','课程名称',null,'学员 213'],
+   [null,null,'{}','@21天抗遗忘',null,'学员 213']]){
+    const r=await db.query(`SELECT ${expression} AS name FROM (SELECT $1::text "realName",$2::text nickname,$3::jsonb "legacyUserData",$5::text username) u CROSS JOIN (SELECT $4::text title) c CROSS JOIN (SELECT 213 AS yhid) m`,[real,nick,legacy,title,login]);assert.equal(r.rows[0].name,expected);
+   }
+ }finally{await db.close();}
+});
+test('老师复习列表待复习、已复习及翻页均返回姓名,保持工资与老师范围',async()=>{
+ const db=new PGlite();try{
+  await db.exec(readFileSync(new URL('./fixtures/teacher-review-schema.sql',import.meta.url),'utf8'));
+  await db.exec(`INSERT INTO "_User" VALUES('co',20,'','', 'login','{"HoneyName":"小陈"}');
+   UPDATE "CommonModel" SET title='历史学员@21天抗遗忘';
+   INSERT INTO "ReviewRewardIncome"(company,"taskId",amount,status) VALUES('co','r2',0.5,'earned');`);
+  const query=async(s,a)=>(await db.query(s,a)).rows;
+  const ctx=vm.createContext({Date,Promise,DEFAULT_COMPANY_ID:'co',number:(v,f=0)=>Number(v)||f,authorizeRequestedUser:async()=>30,appOverviewToday:()=> '2026-10-14',legacyAliases:r=>r,appLiveVersionSnapshot:async()=>({}),reviewIncomeTotal:async()=>0.5,Psql:{query,one:async(s,a)=>(await query(s,a))[0]}});vm.runInContext(source,ctx);
+  for(const [status,page] of [['pending',1],['pending',2],['completed',1]]){
+   const result=await ctx.appReviewPage({status,page,pageSize:10},{get:()=>3});assert.ok(result.items.length);assert.ok(result.items.every(r=>r.honeyName==='小陈'&&Number(r.plid)===30));assert.equal(result.summary.income,0.5);
+   if(status==='completed'){assert.equal(result.items[0].money,0.5);assert.equal(result.items[0].rewardStatus,'earned');}
+  }
+  await db.exec('DELETE FROM "_User"');
+  const historical=await ctx.appReviewPage({status:'completed'},{get:()=>3});assert.equal(historical.items[0].honeyName,'历史学员');
+ }finally{await db.close();}
+});