| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778 |
- // Appointment reads and commands share the same new-database identity.
- function assertLessonTenant(current) {
- const company=current.get('company');
- if ((company && (company.id || company.objectId)) !== DEFAULT_COMPANY_ID) fail(403, '无权访问该帐套');
- }
- function lessonFeedbackInput(input) {
- const raw = parseObject(input.feedback, '完课反馈'), feedback = {};
- for (const key of ['contentSummary','reviewSummary','trainingSummary','homework','notes','coachComment']) {
- const value = String(raw[key] || '').trim();
- if (value.length > 1000) fail(400, '课堂反馈每项最多1000字');
- feedback[key] = value;
- }
- for (const key of ['reviewStatus','trainingStatus']) {
- feedback[key] = String(raw[key] || '').trim();
- if (feedback[key].length > 200) fail(400, '复习情况和训练状态每项最多200字');
- }
- feedback.noHomework = raw.noHomework === true;
- if (!feedback.contentSummary || !feedback.trainingSummary
- || !feedback.noHomework && !feedback.homework) fail(400, '请完整填写学习内容、进度说明和作业');
- return feedback;
- }
- async function appLessonComplete(input, current) {
- assertLessonTenant(current);
- const id = String(input.orderId || '').trim(), key = String(input.requestKey || '');
- if (!id || key.length < 10 || key.length > 160 || !input.expectedUpdatedAt || Number.isNaN(Date.parse(input.expectedUpdatedAt))) fail(400, '缺少预约编号、请求编号或详情版本,请刷新后重试');
- const feedback = lessonFeedbackInput(input);
- try {
- return (await Psql.one('SELECT xs_lesson_complete($1,$2,$3,$4,$5::timestamptz,$6,$7::jsonb) AS result',
- [DEFAULT_COMPANY_ID,id,current.id,ownLegacyId(current),input.expectedUpdatedAt,key,JSON.stringify(feedback)])).result;
- } catch (error) {
- const message = String(error.message || error);
- if (message.includes('历史凭据')) await Psql.none('INSERT INTO "LessonWorkflowIssue" (company,"appointmentGeneralId",message) VALUES($1,$2,$3) ON CONFLICT(company,"appointmentGeneralId") DO UPDATE SET message=EXCLUDED.message,"updatedAt"=now()', [DEFAULT_COMPANY_ID,id,message]);
- fail(409, message);
- }
- }
- async function appLessonReview(input, current) {
- assertLessonTenant(current);
- const score = Number(input.score), comment = String(input.comment || '').trim(), id = String(input.orderId || '');
- if (!id || !Number.isInteger(score) || score < 1 || score > 5 || comment.length > 200) fail(400, '评分应为1至5分,评价最多200字');
- try { return (await Psql.one('SELECT xs_lesson_review($1,$2,$3,$4,$5,$6) AS result', [DEFAULT_COMPANY_ID,id,current.id,ownLegacyId(current),score,comment])).result; }
- catch (error) { fail(409, String(error.message || error)); }
- }
- async function lessonOrderCapabilities(order, current) {
- assertLessonTenant(current);
- const id = String(order.GeneralID), uid = ownLegacyId(current), status = number(order.dszt);
- const stateSql='SELECT c."objectId",a."sourceKey",a.kcid,a."updatedAt"::text AS "updatedAt",xs_appointment_valid_at(a.bxrq,a.yysj) AS "validToday",a."creditHoldStatus",a."creditHoldReason",a."bindingId",a."creditCostsSnapshot",a."kssj",a."jssj" 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';
- let state = await Psql.oneOrNone(stateSql, [DEFAULT_COMPANY_ID,id]);
- if (!state) fail(404, '预约不存在');
- let preparationReason='';
- if(!order.cancelled&&status===0&&state.validToday&&/^legacy[:\-]/.test(state.sourceKey||'')&&(uid===number(order.pl)||uid===number(order.szyh))&&(!state.bindingId||!state.creditCostsSnapshot?.length||state.creditHoldStatus!=='ready')){
- const prepared=await prepareLegacyAppointment({...order,objectId:state.objectId},current);
- preparationReason=prepared.blockedReason||'';
- if(!preparationReason)state=await Psql.oneOrNone(stateSql,[DEFAULT_COMPANY_ID,id]);
- }
- const lesson = await Psql.oneOrNone('SELECT pf,pjnr,pldp FROM "LessonRecord" WHERE company=$1 AND yyds::text=$2 ORDER BY "createdAt" LIMIT 1',[DEFAULT_COMPANY_ID,id]);
- const review = await Psql.oneOrNone('SELECT score,comment,"submittedAt" FROM "LessonReview" WHERE company=$1 AND "appointmentGeneralId"=$2',[DEFAULT_COMPANY_ID,id]);
- const teacher = uid === number(order.pl), student = uid === number(order.szyh), actions = [];
- let blockedReason = '';
- if (order.cancelled) blockedReason = '预约已取消';
- else if (teacher) {
- if (status === 0 && !state.validToday) blockedReason = '课程仅在预约当天可开始,其他日期请联系管理员改期';
- else if (status === 0 && (preparationReason||!state.bindingId || !Array.isArray(state.creditCostsSnapshot) || !state.creditCostsSnapshot.length || state.creditHoldStatus && state.creditHoldStatus !== 'ready')) blockedReason = preparationReason||state.creditHoldReason||'开课记录或课时凭据尚未核实,请联系管理员';
- else if (status === 0) actions.push('start');
- if (status === 10) actions.push('continue','complete');
- if ([11,20].includes(status)) actions.push('complete');
- }
- const reviewed = Boolean(review || number(lesson && lesson.pf) > 0);
- if (!order.cancelled && student && [30,40].includes(status) && !reviewed) actions.push('review');
- return {...order,...state,serverNow:new Date().toISOString(),allowedActions:actions,blockedReason,reviewStatus:reviewed?'submitted':'pending',studentReview:review || (reviewed ? {score:number(lesson.pf),comment:lesson.pjnr,submittedAt:null} : null)};
- }
- async function databaseAppointmentVersions(input,current) {
- assertLessonTenant(current);
- const coach = number(current.get('legacyGroupId')) === 3;
- const uid = ownLegacyId(current), selected = coach && number(input.studentId) ? await appLearningTarget(current,input.studentId) : 0;
- const row=await Psql.one('SELECT MAX(GREATEST(a."updatedAt",c."updatedAt"))::text AS changed_at,COUNT(*)::int AS total FROM "CourseAppointment" a LEFT JOIN "CommonModel" c ON c.company=a.company AND c."modelId"=54 AND c."itemId"=a.id WHERE a.company=$1 AND a."'+(coach?'pl':'szyh')+'"::text=$2 AND ($3::numeric=0 OR a.szyh::text=$3::text)',[DEFAULT_COMPANY_ID,String(uid),selected]);
- const value=String(row.changed_at||'')+':'+row.total;
- return {version:'appointments='+value,scopeVersions:{appointments:value},refreshedAt:new Date().toISOString(),syncStatus:{state:'current'},source:'database'};
- }
|