// Learning content is chosen from the student's enrollments. Billing stays on the appointment. async function appLessonCourses(input,current) { assertLessonTenant(current); const order=await orderDetailRow(input.orderId||input.id,current,false); if(!order)fail(404,'预约不存在'); if(number(current.get('legacyGroupId'))!==3||ownLegacyId(current)!==number(order.pl))fail(403,'仅预约老师可以选择学员课程'); if(order.cancelled)fail(409,'预约已取消'); const rows=await Psql.query('SELECT b.*,c."generalId",n."nodeName" AS "courseName" 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.yhid=$2 AND b."enrollmentStatus"=\'active\' AND c.status=99 AND COALESCE(n.zstatus,99)=99 ORDER BY b.id DESC,b."objectId"',[DEFAULT_COMPANY_ID,String(order.szyh)]); return {items:rows.map(b=>({bindingId:b.objectId,GeneralID:b.generalId,kcid:b.kcid,Title:b.courseName,categoryKey:b.categoryKey,categoryName:b.categoryName,syjd:b.syjd,scheduled:b.objectId===order.bindingId})),studentId:number(order.szyh),scheduledBindingId:order.bindingId,source:'database'}; } async function lessonStudyBinding(addon,current,uid,order,words=[]) { assertLessonTenant(current); if(order){ if(number(order.szyh)!==uid||order.cancelled)fail(403,'学习记录与预约学员不匹配'); if(ownLegacyId(current)!==uid&&(number(current.get('legacyGroupId'))!==3||ownLegacyId(current)!==number(order.pl)))fail(403,'仅预约师生可以保存本节课学习记录'); if(number(order.dszt)!==10)fail(409,'请在课程进行中保存学习记录'); } const id=String(addon.bindingId||order?.bindingId||''); if(!id)return null; const binding=await Psql.oneOrNone('SELECT b."objectId",b.kcid 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.yhid=$2 AND b."objectId"=$3 AND b."enrollmentStatus"=\'active\' AND c.status=99 AND COALESCE(n.zstatus,99)=99',[DEFAULT_COMPANY_ID,String(uid),id]); if(!binding)fail(409,'所选课程尚未开通或已停用,请刷新课程列表'); if(words.length){ const ids=[...new Set(words.map(w=>String(w.GeneralID)))]; const found=await Psql.one('WITH RECURSIVE nodes AS (SELECT "nodeId" FROM "Node" WHERE company=$1 AND "nodeId"::text=$2 AND COALESCE(zstatus,99)=99 UNION SELECT n."nodeId" FROM "Node" n JOIN nodes p ON n."parentId"=p."nodeId" WHERE n.company=$1 AND COALESCE(n.zstatus,99)=99) SELECT count(DISTINCT c."generalId")::int total FROM "CommonModel" c JOIN nodes n ON n."nodeId"=c."nodeId" WHERE c.company=$1 AND c."modelId"=52 AND c.status=99 AND c."generalId"::text=ANY($3::text[])',[DEFAULT_COMPANY_ID,String(binding.kcid),ids]); if(number(found.total)!==ids.length)fail(409,'学习单词与所选课程不匹配,请重新进入课程'); } return binding; } async function startLessonWithContent(current,appointmentId,bindingId,expected) { // Selection and assignment are checked under the same lock as enrollment recycling and scheduling. try{return await Psql.tx(async t=>{ await t.one('SELECT pg_advisory_xact_lock(hashtext($1))',['xs-credit:'+DEFAULT_COMPANY_ID]); const a=await t.one('SELECT a.szyh,a."updatedAt"::text AS version 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."objectId"=$2',[DEFAULT_COMPANY_ID,appointmentId]); if(expected){const v=await t.one('SELECT $1::timestamptz=$2::timestamptz same',[a.version,String(expected)]);if(!v.same)throw Error('预约已更新,请刷新详情后重试');} const b=await t.one('SELECT b."objectId",b.kcid 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.yhid=$2 AND b."objectId"=$3 AND b."enrollmentStatus"=\'active\' AND c.status=99 AND COALESCE(n.zstatus,99)=99',[DEFAULT_COMPANY_ID,String(a.szyh),String(bindingId)]); await t.one('SELECT xs_course_size($1,$2)',[DEFAULT_COMPANY_ID,String(b.kcid)]); await t.one('SELECT xs_start_appointment_for_actor($1,$2,$3,$4)',[DEFAULT_COMPANY_ID,appointmentId,current.id,ownLegacyId(current)]); return {bindingId:b.objectId,courseId:b.kcid}; });}catch(e){fail(409,String(e.message||e));} }