mobile-teaching.js 9.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. // Shared teaching reads. Business state lives in the new database, not browser caches.
  2. function mobilePage(input) { return { index: Math.max(1, Math.floor(number(input.page || input.cpage, 1))), size: Math.min(100, Math.max(1, Math.floor(number(input.pageSize || input.psize, 20)))) }; }
  3. function mobilePageMeta(total, p) { return { itemCount:Number(total),pageCount:Math.ceil(Number(total)/p.size),pageIndex:p.index,pageSize:p.size }; }
  4. function mobileSource() { return {source:'database',refreshedAt:new Date().toISOString(),complete:true}; }
  5. // Pending migration classification affects scheduling, not a student's access to
  6. // previously assigned content. Explicitly unpublished/recycled content stays hidden.
  7. function mobileReadableBindingWhere(includeHistorical = true) {
  8. const status = includeHistorical
  9. ? `COALESCE(b."enrollmentStatus",'') IN ('active','pending','')`
  10. : `b."enrollmentStatus"='active'`;
  11. return `b."company"=$1 AND b."yhid"::text=$2 AND ${status} AND COALESCE(c."status",99)=99 AND COALESCE(n."zstatus",99)=99`;
  12. }
  13. function mobileReadableBindingFrom() {
  14. return `FROM "CourseBinding" b LEFT JOIN "CommonModel" c ON c."company"=b."company" AND c."modelId"=58 AND c."itemId"=b."id" LEFT JOIN "Node" n ON n."company"=b."company" AND n."nodeId"=CASE WHEN COALESCE(b."kcid"::text,'')~'^[0-9]+(?:[.]0+)?$' THEN b."kcid"::numeric ELSE -1 END`;
  15. }
  16. async function mobileReadableCourseBindings(uid) {
  17. return Psql.query(`SELECT c."generalId",c."title",c."createTime",b.*,b."objectId" AS "bindingObjectId",n."nodeName" AS "courseTitle" ${mobileReadableBindingFrom()} WHERE ${mobileReadableBindingWhere()} ORDER BY b."id" DESC`,[DEFAULT_COMPANY_ID,String(uid)]);
  18. }
  19. async function mobileUnitBindingRows(uid,bindingId,parentId,current) {
  20. const ownStudent = number(current.get('legacyGroupId') || current.get('groupId'))===1 && number(uid)===ownLegacyId(current);
  21. return Psql.query(`SELECT b.* ${mobileReadableBindingFrom()} WHERE ${mobileReadableBindingWhere(ownStudent)} AND (($3<>'' AND (b."id"::text=$3 OR b."objectId"=$3)) OR ($3='' AND b."kcid"::text=$4))`,[DEFAULT_COMPANY_ID,String(uid),bindingId,String(parentId)]);
  22. }
  23. const MOBILE_DISABLED_ACTIONS = new Set(['cart_add','payment_cart','payment_cart_again','payment_success','user_group_usr_supply','user_group_usr_upgrade']);
  24. async function appCreditOverview(input,current) {
  25. const uid=await authorizeRequestedUser(current,input.studentId||input.uid),owner=uid===ownLegacyId(current)?current:await legacyUserObject(uid);
  26. if(!owner)fail(404,'学员不存在');
  27. const data=legacyData(owner),reserved=await Psql.query('SELECT cost->>\'account\' AS account,SUM((cost->>\'amount\')::numeric) AS amount FROM "CourseCreditReservation" r CROSS JOIN LATERAL jsonb_array_elements(r."creditCosts") cost WHERE r."company"=$1 AND r."studentId"=$2 AND r."state"=\'reserved\' GROUP BY cost->>\'account\'',[DEFAULT_COMPANY_ID,uid]);
  28. const labels={Purse:'30 分钟课时',SilverCoin:'60 分钟课时',UserExp:'体验课时',UserPoint:'陪练时长'};
  29. return {studentId:uid,accounts:Object.entries(labels).map(([account,label],i)=>{const balance=number(data[account]),held=number(reserved.find(r=>r.account===account)?.amount);return{account,label,historyType:i+1,balance,reserved:held,available:balance-held,unit:account==='UserPoint'?'小时':'次'};}),...mobileSource(),...await appLiveVersionSnapshot(uid,['account'],current)};
  30. }
  31. async function appLessonReport(input,current) {
  32. const order=await orderDetailRow(input.id||input.orderId,current);if(!order)fail(404,'预约不存在');
  33. const id=String(order.GeneralID||input.id||input.orderId);
  34. const [feedback,records,reviews]=await Promise.all([
  35. Psql.oneOrNone('SELECT * FROM "LessonCompletionReport" WHERE "company"=$1 AND "appointmentGeneralId"::text=$2 ORDER BY "submittedAt" DESC LIMIT 1',[DEFAULT_COMPANY_ID,id]),
  36. Psql.query('SELECT c."generalId",d."con",d."dqrq",d."learned",d."ygg",d."djq" FROM "DailyStudyRecord" d LEFT JOIN "CommonModel" c ON c."company"=d."company" AND c."modelId"=56 AND c."itemId"=d."id" WHERE d."company"=$1 AND d."dsid"::text=$2 AND d."userId"=$3 ORDER BY d."dqrq" DESC',[DEFAULT_COMPANY_ID,id,number(order.szyh)]),
  37. Psql.one('SELECT COUNT(*)::int total,COUNT(*) FILTER(WHERE "fxzt"=\'1\')::int completed FROM "MemoryPracticeRecord" WHERE "company"=$1 AND "orderId"::text=$2 AND "yhid"=$3',[DEFAULT_COMPANY_ID,id,number(order.szyh)])
  38. ]);
  39. return {order,feedback:feedback?safe(feedback):null,records,antiForget:reviews,...mobileSource()};
  40. }
  41. async function appReviewPage(input,current) {
  42. const uid=await authorizeRequestedUser(current,input.uid),coach=number(current.get('legacyGroupId'))===3,p=mobilePage(input);
  43. const reviewTarget=coach&&number(input.studentId)?await appLearningTarget(current,input.studentId):coach?0:uid,legacyReviewSync=reviewTarget?await syncLegacyReviews(reviewTarget,current,input.refresh===true):undefined;
  44. const args=[DEFAULT_COMPANY_ID,String(uid)],clauses=['m."company"=$1','m."'+(coach?'plid':'yhid')+'"::text=$2'];
  45. const add=(sql,value)=>{args.push(value);clauses.push(sql.replace('?', '$'+args.length));};
  46. const students=coach?await Psql.query('SELECT m."yhid"::text AS id,COALESCE(MAX(NULLIF(BTRIM(u."realName"),\'\')),MAX(NULLIF(BTRIM(u."nickname"),\'\')),MAX(NULLIF(BTRIM(u."legacyUserData"->>\'HoneyName\'),\'\')),MAX(NULLIF(BTRIM(u."username"),\'\')),\'学员 \'||m."yhid"::text) AS name,COUNT(*)::int AS "taskCount" FROM "MemoryPracticeRecord" m LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+clauses.join(' AND ')+' GROUP BY m."yhid"',args):[];
  47. if(coach&&number(input.studentId))add('m."yhid"::text=?',String(input.studentId));
  48. const baseWhere=clauses.join(' AND '),baseArgs=[...args];
  49. const dateSql='LEFT(COALESCE(NULLIF(m."kywrq",\'\'),m."kywsj"),10)';
  50. const summary=await Psql.one('SELECT COUNT(*)::int total,COUNT(*) FILTER(WHERE m."fxzt"=\'1\')::int completed,COUNT(*) FILTER(WHERE COALESCE(m."fxzt",\'0\')<>\'1\' AND '+dateSql+'<=$'+(args.length+1)+')::int due,COUNT(*) FILTER(WHERE '+dateSql+'=$'+(args.length+1)+')::int today FROM "MemoryPracticeRecord" m WHERE '+baseWhere,[...args,appOverviewToday()]);
  51. const dates=await Psql.query('SELECT '+dateSql+' AS date,COUNT(*)::int total,COUNT(*) FILTER(WHERE m."fxzt"=\'1\')::int completed FROM "MemoryPracticeRecord" m WHERE '+baseWhere+' GROUP BY '+dateSql,baseArgs);
  52. if(input.date)add(dateSql+'=?',String(input.date));
  53. if(input.month)add('LEFT('+dateSql+',7)=?',String(input.month));
  54. if(input.status==='completed')clauses.push('m."fxzt"=\'1\'');
  55. if(input.status==='pending')clauses.push('COALESCE(m."fxzt",\'0\')<>\'1\'');
  56. const where=clauses.join(' AND '),count=await Psql.one('SELECT COUNT(*)::int total FROM "MemoryPracticeRecord" m WHERE '+where,args);
  57. const rows=await 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",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 "ReviewReward" 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+' ORDER BY m."kywsj" DESC,m."id" DESC LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
  58. const versionTarget=coach&&number(input.studentId)?number(input.studentId):uid;
  59. 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,income:coach?await reviewIncomeTotal(DEFAULT_COMPANY_ID,uid):0},dates,students,legacyReviewSync,...mobileSource(),...await appLiveVersionSnapshot(versionTarget,['review','account'],current)};
  60. }
  61. function mobileCommandKey(input) {const key=String(input.requestKey||'');return /^[A-Za-z0-9:_-]{10,160}$/.test(key)?key:null;}
  62. function mobileObjectData(row) {
  63. const json=row.toJSON(),data={...json,objectId:row.id||Math.random().toString(36).slice(2,12),createdAt:json.createdAt||new Date().toISOString()};
  64. delete data.ACL;delete data.className;
  65. for(const [key,value] of Object.entries(data))if(value&&typeof value==='object'&&value.__type==='Pointer')data[key]=value.className+'$'+value.objectId;else if(value&&typeof value==='object'&&value.__type==='Date')data[key]=value.iso;
  66. if(['CommonModel','DailyStudyRecord','PracticeRecord'].includes(row.className))data.company=DEFAULT_COMPANY_ID;
  67. return data;
  68. }
  69. async function mobileAtomicWrite(input,current,operations,scope='') {
  70. const key=mobileCommandKey(input)||('legacy:'+randomObjectId());
  71. const semantic={...input};delete semantic.requestKey;delete semantic.token;
  72. const row=await Psql.one('SELECT xs_mobile_teaching_write($1,$2,$3,md5($4),$5::jsonb) AS result',[DEFAULT_COMPANY_ID,scope||String(ownLegacyId(current)),String(current.id)+':'+key,JSON.stringify(semantic),JSON.stringify(operations)]);
  73. return row.result;
  74. }
  75. // Empty profile fields must not hide the student's legacy display name.
  76. function companionStudentNameSql() {
  77. return `COALESCE(NULLIF(BTRIM(u."realName"),''),NULLIF(BTRIM(u."nickname"),''),NULLIF(BTRIM(u."legacyUserData"->>'HoneyName'),''),NULLIF(BTRIM(SPLIT_PART(REPLACE(c."title",'(','('),'(',1)),''),NULLIF(BTRIM(u."username"),''),'学员 '||a."szyh")`;
  78. }