| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 |
- // Attachments are uploaded by the assigned teacher and linked to one appointment.
- const LESSON_ATTACHMENT_TYPES = {
- jpg:['image/jpeg','/9j/'],jpeg:['image/jpeg','/9j/'],png:['image/png','iVBOR'],webp:['image/webp','UklGR'],gif:['image/gif','R0lG'],
- pdf:['application/pdf','JVBERi0'],doc:['application/msword','0M8R4KGxGuE'],
- docx:['application/vnd.openxmlformats-officedocument.wordprocessingml.document','UEsDB'],
- xls:['application/vnd.ms-excel','0M8R4KGxGuE'],xlsx:['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','UEsDB'],
- ppt:['application/vnd.ms-powerpoint','0M8R4KGxGuE'],pptx:['application/vnd.openxmlformats-officedocument.presentationml.presentation','UEsDB'],txt:['text/plain',''],
- };
- async function lessonAttachmentOrder(input,current,requireEditable=true) {
- assertLessonTenant(current);
- const order=await orderDetailRow(String(input.orderId||''),current,false);
- if(!order)fail(404,'预约不存在');
- if(number(current.get('legacyGroupId'))!==3||ownLegacyId(current)!==number(order.pl))fail(403,'仅本节课老师可以上传和保存课堂附件');
- if(!['primary_writing','middle_writing','high_writing'].includes(String(order.courseCategoryKey)))fail(403,'课堂附件仅用于语法写作课');
- if(requireEditable){
- const capabilities=await lessonOrderCapabilities(order,current);
- if(!capabilities.allowedActions.some(action=>['complete','editFeedback'].includes(action)))fail(403,'当前课程不可编辑课堂反馈');
- }
- return order;
- }
- async function uploadLessonAttachment(input,current) {
- const order=await lessonAttachmentOrder(input,current);
- const name=String(input.fileName||'').trim(),extension=name.split('.').pop().toLowerCase(),rule=LESSON_ATTACHMENT_TYPES[extension];
- const contentType=String(input.contentType||'').toLowerCase(),base64=String(input.base64||'').replace(/^data:[^;]+;base64,/i,'');
- if(!name||name.length>180||/[\x00-\x1f/\\]/.test(name)||!rule||contentType!==rule[0]||base64.length>6990508||!base64||!/^[A-Za-z0-9+/]+={0,2}$/.test(base64))fail(400,'请选择5MB以内的照片或常用文档');
- let binary;try{binary=atob(base64);}catch(_){fail(400,'附件编码无效');}
- if(!binary.length||binary.length>5*1024*1024||!base64.startsWith(rule[1])||contentType==='text/plain'&&binary.includes('\x00'))fail(400,'附件内容与文件类型不符或超过5MB');
- const extraTypes=Object.fromEntries(Object.values(LESSON_ATTACHMENT_TYPES).map(type=>[type[0],type[1]]));
- const url=await uploadAppMedia({...input,base64},extraTypes);
- if(!/^https?:\/\//i.test(url))fail(502,'上传未返回有效附件地址');
- const attachment={id:randomObjectId(),name,url,contentType,size:binary.length};
- await Psql.none('INSERT INTO "LessonFeedbackAttachment" ("objectId",company,"appointmentGeneralId","coachId","fileName","publicUrl","contentType",size,"createdAt") VALUES($1,$2,$3,$4,$5,$6,$7,$8,now())',
- [attachment.id,DEFAULT_COMPANY_ID,String(order.GeneralID),ownLegacyId(current),name,url,contentType,attachment.size]);
- return attachment;
- }
- async function resolveLessonFeedbackAttachments(input,current,feedback) {
- // The SQL command checks edit locks after replaying successful requests. Resolve
- // ownership here without rejecting an already successful retry after review.
- const order=await lessonAttachmentOrder(input,current,false),ids=feedback.attachments.map(file=>file.id);
- const rows=await Psql.query('SELECT "objectId" AS id,"fileName" AS name,"publicUrl" AS url,"contentType",size::int AS size FROM "LessonFeedbackAttachment" WHERE company=$1 AND "appointmentGeneralId"=$2 AND "coachId"=$3 AND "objectId"=ANY($4::text[])',
- [DEFAULT_COMPANY_ID,String(order.GeneralID),ownLegacyId(current),ids]);
- const byId=new Map(rows.map(row=>[row.id,row]));
- if(byId.size!==ids.length)fail(403,'附件不属于本节课或上传尚未完成,请重新选择');
- feedback.attachments=ids.map(id=>byId.get(id));
- }
|