mobile-teaching.js 8.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  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. const MOBILE_DISABLED_ACTIONS = new Set(['cart_add','payment_cart','payment_cart_again','payment_success','user_group_usr_supply','user_group_usr_upgrade']);
  6. async function appCreditOverview(input,current) {
  7. const uid=await authorizeRequestedUser(current,input.studentId||input.uid),owner=uid===ownLegacyId(current)?current:await legacyUserObject(uid);
  8. if(!owner)fail(404,'学员不存在');
  9. 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]);
  10. const labels={Purse:'30 分钟课时',SilverCoin:'60 分钟课时',UserExp:'体验课时',UserPoint:'陪练时长'};
  11. 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)};
  12. }
  13. async function appLessonReport(input,current) {
  14. const order=await orderDetailRow(input.id||input.orderId,current);if(!order)fail(404,'预约不存在');
  15. const id=String(order.GeneralID||input.id||input.orderId);
  16. const [feedback,records,reviews]=await Promise.all([
  17. Psql.oneOrNone('SELECT * FROM "LessonCompletionReport" WHERE "company"=$1 AND "appointmentGeneralId"::text=$2 ORDER BY "submittedAt" DESC LIMIT 1',[DEFAULT_COMPANY_ID,id]),
  18. 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)]),
  19. 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)])
  20. ]);
  21. return {order,feedback:feedback?safe(feedback):null,records,antiForget:reviews,...mobileSource()};
  22. }
  23. async function appReviewPage(input,current) {
  24. const uid=await authorizeRequestedUser(current,input.uid),coach=number(current.get('legacyGroupId'))===3,p=mobilePage(input);
  25. const args=[DEFAULT_COMPANY_ID,String(uid)],clauses=['m."company"=$1','m."'+(coach?'plid':'yhid')+'"::text=$2'];
  26. const add=(sql,value)=>{args.push(value);clauses.push(sql.replace('?', '$'+args.length));};
  27. 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):[];
  28. if(coach&&number(input.studentId))add('m."yhid"::text=?',String(input.studentId));
  29. const baseWhere=clauses.join(' AND '),baseArgs=[...args];
  30. const dateSql='LEFT(COALESCE(NULLIF(m."kywrq",\'\'),m."kywsj"),10)';
  31. 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()]);
  32. 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);
  33. if(input.date)add(dateSql+'=?',String(input.date));
  34. if(input.month)add('LEFT('+dateSql+',7)=?',String(input.month));
  35. if(input.status==='completed')clauses.push('m."fxzt"=\'1\'');
  36. if(input.status==='pending')clauses.push('COALESCE(m."fxzt",\'0\')<>\'1\'');
  37. const where=clauses.join(' AND '),count=await Psql.one('SELECT COUNT(*)::int total FROM "MemoryPracticeRecord" m WHERE '+where,args);
  38. 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]);
  39. const versionTarget=coach&&number(input.studentId)?number(input.studentId):uid;
  40. 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,...mobileSource(),...await appLiveVersionSnapshot(versionTarget,['review','account'],current)};
  41. }
  42. function mobileCommandKey(input) {const key=String(input.requestKey||'');return /^[A-Za-z0-9:_-]{10,160}$/.test(key)?key:null;}
  43. function mobileObjectData(row) {
  44. const json=row.toJSON(),data={...json,objectId:row.id||Math.random().toString(36).slice(2,12),createdAt:json.createdAt||new Date().toISOString()};
  45. delete data.ACL;delete data.className;
  46. 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;
  47. if(['CommonModel','DailyStudyRecord','PracticeRecord'].includes(row.className))data.company=DEFAULT_COMPANY_ID;
  48. return data;
  49. }
  50. async function mobileAtomicWrite(input,current,operations,scope='') {
  51. const key=mobileCommandKey(input)||('legacy:'+randomObjectId());
  52. const semantic={...input};delete semantic.requestKey;delete semantic.token;
  53. 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)]);
  54. return row.result;
  55. }
  56. // Empty profile fields must not hide the student's legacy display name.
  57. function companionStudentNameSql() {
  58. 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")`;
  59. }