lesson-unit-progress.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839
  1. // Explicit teacher acknowledgement of an entire unit. This is progress only,
  2. // never a classroom study batch, review plan, credit deduction or pay event.
  3. async function completeLessonUnit(input,current) {
  4. assertLessonTenant(current);
  5. const gid=String(input.orderId||''),bindingId=String(input.bindingId||''),nodeId=String(input.nodeId||'');
  6. if(number(current.get('legacyGroupId'))!==3)fail(403,'仅预约老师可以标记学完');
  7. if(!gid||!bindingId||!/^\d+$/.test(nodeId))fail(400,'缺少预约、开课或单元');
  8. const key=mobileCommandKey(input);if(!key)fail(400,'缺少操作标识,请刷新重试');
  9. try{return await Psql.tx(async t=>{
  10. await t.one('SELECT pg_advisory_xact_lock(hashtext($1))',['xs-credit:'+DEFAULT_COMPANY_ID]);
  11. const a=await t.oneOrNone('SELECT a.* FROM "CourseAppointment" a JOIN "CommonModel" c ON c.company=a.company AND c."modelId"=54 AND c."itemId"=a.id WHERE c.company=$1 AND c."generalId"::text=$2 AND c.status<>-2 FOR UPDATE OF a',[DEFAULT_COMPANY_ID,gid]);
  12. if(!a||number(a.pl)!==ownLegacyId(current))fail(403,'仅预约老师可以标记该学员进度');
  13. if(![0,10].includes(number(a.dszt)))fail(409,'预约已结束,请刷新详情');
  14. const b=await t.oneOrNone('SELECT b.* FROM "CourseBinding" b JOIN "CommonModel" c ON c.company=b.company AND c."modelId"=58 AND c."itemId"=b.id JOIN "Node" n ON n.company=b.company AND n."nodeId"::text=b.kcid WHERE b.company=$1 AND b."objectId"=$2 AND b.yhid=$3 AND b."enrollmentStatus"=\'active\' AND c.status=99 AND COALESCE(n.zstatus,99)=99 FOR UPDATE OF b',[DEFAULT_COMPANY_ID,bindingId,String(a.szyh)]);
  15. if(!b)fail(403,'该课程尚未分配给当前学员');
  16. const nodes=await t.any('SELECT "nodeId"::text id,"nodeName" name FROM "Node" WHERE company=$1 AND "parentId"::text=$2',[DEFAULT_COMPANY_ID,String(b.kcid)]);
  17. const unit=(nodes.length?nodes:courseAssignedUnits(b.syjd)).find(n=>n.id===nodeId);
  18. if(!unit)fail(403,'单元不属于所选课程');
  19. const words=await t.any('SELECT "generalId" FROM "CommonModel" WHERE company=$1 AND "modelId"=52 AND "nodeId"::text=$2 ORDER BY "generalId"',[DEFAULT_COMPANY_ID,nodeId]);
  20. if(!words.length)fail(409,'该单元暂无单词');
  21. if(words.length>10000)fail(409,'单元词量异常,请联系管理员');
  22. // Same learner lock as e_add_words: do not overwrite a concurrent practice count.
  23. await t.one('SELECT pg_advisory_xact_lock(hashtextextended($1,0))',[DEFAULT_COMPANY_ID+':'+a.szyh]);
  24. const rows=await t.any('SELECT scid,xxcs FROM "PracticeRecord" WHERE company=$1 AND yhid::text=$2 AND scid::text=ANY($3::text[])',[DEFAULT_COMPANY_ID,String(a.szyh),words.map(w=>String(w.generalId))]);
  25. const learned=new Set(rows.filter(r=>number(r.xxcs)>0).map(r=>String(r.scid)));
  26. const missing=words.filter(w=>!learned.has(String(w.generalId)));
  27. for(let offset=0;offset<missing.length;offset+=100){
  28. const operations=missing.slice(offset,offset+100).map(w=>({table:'PracticeRecord',match:{company:DEFAULT_COMPANY_ID,yhid:number(a.szyh),scid:number(w.generalId)},data:{objectId:randomObjectId(),createdAt:new Date().toISOString(),company:DEFAULT_COMPANY_ID,sourceKey:'teacher:unit:'+a.szyh+':'+w.generalId,yhid:number(a.szyh),scid:number(w.generalId),kcid:String(b.kcid),xxcs:1}}));
  29. await t.one('SELECT xs_mobile_teaching_write($1,$2,$3,md5($4),$5::jsonb) result',[DEFAULT_COMPANY_ID,String(a.szyh),current.id+':unit:'+key+':'+offset,JSON.stringify({gid,bindingId,nodeId,words:operations.map(o=>o.data.scid)}),JSON.stringify(operations)]);
  30. }
  31. let progress;try{progress=JSON.parse(b.syjd||'{}');}catch(_){progress={};}
  32. if(!progress||typeof progress!=='object'||Array.isArray(progress))progress={};
  33. progress[nodeId]=nodeId+'@'+unit.name+'@1';
  34. await t.none('UPDATE "CourseBinding" SET syjd=$3,"updatedAt"=now() WHERE company=$1 AND "objectId"=$2',[DEFAULT_COMPANY_ID,bindingId,JSON.stringify(progress)]);
  35. // Stable audit key makes repeated clicks visible without manufacturing learning events.
  36. await t.none('INSERT INTO "MobileTeachingCommand" (company,"actorId","requestKey",fingerprint,result) VALUES($1,$2,$3,$4,$5::jsonb) ON CONFLICT DO NOTHING',[DEFAULT_COMPANY_ID,current.id,'unit-complete:'+key,JSON.stringify({gid,bindingId,nodeId}),JSON.stringify({orderId:gid,bindingId,nodeId,teacherId:ownLegacyId(current),studentId:number(a.szyh),total:words.length,changed:missing.length})]);
  37. return {nodeId,total:words.length,learned:words.length,percent:100,syjd:JSON.stringify(progress)};
  38. });}catch(e){if(e.status||e.statusCode)throw e;fail(409,String(e.message||e));}
  39. }