|
|
@@ -2741,8 +2741,30 @@ async function findAddon(className, company, id) { const fields=await fieldsOf(c
|
|
|
function durationMinutes(type) { return number(type)===2||number(type)===3?60:30; }
|
|
|
function durationHours(type) { return durationMinutes(type)/60; }
|
|
|
function appointmentDate(value,date,time) { const raw=text(value); if(raw) { const normalized=raw.includes('T')?raw:raw.replace(' ','T')+(raw.includes('+')||raw.endsWith('Z')?'':'+08:00'); const parsed=new Date(normalized); if(!Number.isNaN(parsed.getTime())) return parsed; } const fallback=new Date(text(date)+'T'+(text(time)||'00:00')+':00+08:00'); return fallback; }
|
|
|
-function appointmentProjection(row) { const start=appointmentDate(row.yysj,row.bxrq,row.sdsd),type=number(row.dslx),end=new Date(start.getTime()+durationMinutes(type)*60000),student=legacyOf({legacyUserData:row.studentLegacy}),coach=legacyOf({legacyUserData:row.coachLegacy}),cancelled=number(row.commonStatus)===-2,coachId=number(row.pl||row.fxpl),coachObjectId=text(row.coachObjectId),startsAt=Number.isNaN(start.getTime())?'':start.toISOString(),endsAt=Number.isNaN(end.getTime())?'':end.toISOString(); return { targetId:text(row.targetId),generalId:number(row.generalId),studentObjectId:text(row.studentObjectId),studentId:number(row.szyh),studentName:text(row.studentRealName||row.studentNickname||student.HoneyName||row.studentUsername)||'用户 '+number(row.szyh),coachObjectId,coachId,coachName:text(row.coachRealName||row.coachNickname||coach.HoneyName||row.coachUsername||row.plxm)||(coachId?'历史陪练 '+coachId:'未分配'),courseId:number(row.kcid),courseName:text(row.courseTitle)||'课程 '+number(row.kcid),classType:type,classTypeLabel:classType(type),deliveryMethod:text(row.jffs)||'未设置',startsAt,endsAt,statusCode:number(row.dszt),statusLabel:cancelled?'已取消':statusLabel(row.dszt),cancelled,cancellationReason:text(row.cancellationReason),coachAccountMissing:Boolean(coachId&&!coachObjectId),availabilityWarning:row.availabilityConfigured===false,updatedAt:dateIso(row.commonUpdatedAt||row.updatedAt) }; }
|
|
|
+function appointmentProjection(row) { const start=appointmentDate(row.yysj,row.bxrq,row.sdsd),type=number(row.dslx),end=new Date(start.getTime()+durationMinutes(type)*60000),student=legacyOf({legacyUserData:row.studentLegacy}),coach=legacyOf({legacyUserData:row.coachLegacy}),cancelled=number(row.commonStatus)===-2,coachId=number(row.pl||row.fxpl),coachObjectId=text(row.coachObjectId),startsAt=Number.isNaN(start.getTime())?'':start.toISOString(),endsAt=Number.isNaN(end.getTime())?'':end.toISOString(),source=text(row.source)||'parse'; return { targetId:text(row.targetId),generalId:number(row.generalId),studentObjectId:text(row.studentObjectId),studentId:number(row.szyh),studentName:text(row.studentRealName||row.studentNickname||student.HoneyName||row.studentUsername)||'用户 '+number(row.szyh),coachObjectId,coachId,coachName:text(row.coachRealName||row.coachNickname||coach.HoneyName||row.coachUsername||row.plxm)||(coachId?'历史陪练 '+coachId:'未分配'),courseId:number(row.kcid),courseName:text(row.courseTitle)||'课程 '+number(row.kcid),classType:type,classTypeLabel:classType(type),deliveryMethod:text(row.jffs)||'未设置',startsAt,endsAt,statusCode:number(row.dszt),statusLabel:cancelled?'已取消':statusLabel(row.dszt),cancelled,cancellationReason:text(row.cancellationReason),coachAccountMissing:source==='legacy-live'?false:Boolean(coachId&&!coachObjectId),availabilityWarning:row.availabilityConfigured===false,source,sourceLabel:source==='legacy-live'?'实时业务系统':'迁移数据库',readOnly:source==='legacy-live',updatedAt:dateIso(row.commonUpdatedAt||row.updatedAt||row.CreateTime) }; }
|
|
|
async function allAppointmentRows(context,input) { const companyId=companyIdOf(context,input); const raw=await Psql.query('SELECT c."objectId" AS "targetId",c."generalId",c."status" AS "commonStatus",c."updatedAt" AS "commonUpdatedAt",a.*,s."objectId" AS "studentObjectId",s."realName" AS "studentRealName",s."nickname" AS "studentNickname",s."username" AS "studentUsername",s."legacyUserData" AS "studentLegacy",p."objectId" AS "coachObjectId",p."realName" AS "coachRealName",p."nickname" AS "coachNickname",p."username" AS "coachUsername",p."legacyUserData" AS "coachLegacy",n."nodeName" AS "courseTitle" FROM "CommonModel" c JOIN "CourseAppointment" a ON a."company"=c."company" AND CAST(a."id" AS text)=CAST(c."itemId" AS text) LEFT JOIN "_User" s ON s."company"=c."company" AND COALESCE(CAST(s."legacyUserId" AS text),s."legacyUserData"->>\'UserID\')=CAST(a."szyh" AS text) LEFT JOIN "_User" p ON p."company"=c."company" AND COALESCE(CAST(p."legacyUserId" AS text),p."legacyUserData"->>\'UserID\')=COALESCE(NULLIF(CAST(a."pl" AS text),\'\'),CAST(a."fxpl" AS text)) LEFT JOIN "Node" n ON n."company"=c."company" AND CAST(n."nodeId" AS text)=CAST(a."kcid" AS text) WHERE ($1::text IS NULL OR c."company"=$1) AND CAST(c."modelId" AS text)=\'54\' ORDER BY c."updatedAt" DESC LIMIT 10000',[companyId]); return raw.map(appointmentProjection); }
|
|
|
+function parseLegacyPayload(value) { let parsed=value; if(typeof parsed==='string'){try{parsed=JSON.parse(parsed);}catch(_){return[];}} return Array.isArray(parsed)?parsed:[]; }
|
|
|
+async function legacyScheduleConfig() { const config=await Parse.Config.get({useMasterKey:true}); return { url:text(config.get('legacyScheduleApiUrl')),apiId:text(config.get('legacyScheduleApiId')),apiKey:text(config.get('legacyScheduleApiKey')) }; }
|
|
|
+async function legacyScheduleRequest(config,params) {
|
|
|
+ const query=Object.entries({...params,apiId:config.apiId,apiKey:config.apiKey,action:'content_list'}).map(([key,value])=>encodeURIComponent(key)+'='+encodeURIComponent(String(value))).join('&'),url=config.url+(config.url.includes('?')?'&':'?')+query;
|
|
|
+ let payload;
|
|
|
+ if(typeof fetch==='function'){const response=await fetch(url,{method:'GET',headers:{accept:'application/json'}});if(!response.ok)throw new Error('实时排课接口返回 HTTP '+response.status);payload=await response.json();}
|
|
|
+ else if(Parse.Cloud&&typeof Parse.Cloud.httpRequest==='function'){const response=await Parse.Cloud.httpRequest({url,method:'GET',headers:{accept:'application/json'}});payload=typeof response.data==='object'?response.data:JSON.parse(text(response.text));}
|
|
|
+ else throw new Error('云函数运行环境不支持外部请求');
|
|
|
+ if(number(payload&&payload.retcode)===-1)throw new Error(text(payload.retmsg)||'实时排课接口拒绝请求');
|
|
|
+ return {items:parseLegacyPayload(payload&&payload.result),page:payload&&payload.page&&typeof payload.page==='object'?payload.page:{}};
|
|
|
+}
|
|
|
+async function liveAppointmentRows(input) {
|
|
|
+ const from=text(input.dateFrom||new Date().toISOString().slice(0,10)),config=await legacyScheduleConfig();
|
|
|
+ if(!config.url||!config.apiId||!config.apiKey)return{available:false,message:'实时排课数据源尚未配置',items:[],total:0,allItems:[]};
|
|
|
+ try {
|
|
|
+ const overall=await legacyScheduleRequest(config,{nid:29,dateFilter:'2000-01-01',psize:1,cpage:1}),raw=[],pageSize=1000;let total=number(overall.page.itemCount),pageCount=1;
|
|
|
+ for(let page=1;page<=Math.min(20,pageCount);page++){const result=await legacyScheduleRequest(config,{nid:29,dateFilter:from,psize:pageSize,cpage:page});raw.push(...result.items);total=Math.max(total,number(result.page.itemCount),raw.length);pageCount=Math.max(1,number(result.page.pageCount,Math.ceil(total/pageSize)));if(result.items.length<pageSize&&page>=pageCount)break;}
|
|
|
+ const seen=new Set(),allItems=[];
|
|
|
+ for(const row of raw){const generalId=number(row.GeneralID||row.generalId),key=generalId?'general:'+generalId:[row.yysj,row.szyh,row.pl,row.kcid].map(text).join('|');if(seen.has(key))continue;seen.add(key);allItems.push(appointmentProjection({...row,targetId:'legacy-live:'+generalId,generalId,studentRealName:row.Title,courseTitle:row.Subtitle,commonStatus:99,commonUpdatedAt:row.CreateTime,source:'legacy-live'}));}
|
|
|
+ return{available:true,message:'',items:allItems,total,allItems};
|
|
|
+ } catch(error) { return{available:false,message:'实时排课数据暂时读取失败,当前显示迁移数据库中的备用数据',items:[],total:0,allItems:[]}; }
|
|
|
+}
|
|
|
async function newClassObjects(className,context,input,limit=1000) { try { const fields=await fieldsOf(className).catch(()=>({})); if(!Object.keys(fields).length)return[]; const query=new Parse.Query(className); const company=companyPointer(context,input); if(company&&fields.company) query.equalTo('company',company); query.limit(limit); query.descending(fields.createdAt?'createdAt':'objectId'); return await query.find({useMasterKey:true}); } catch(error) { if(error.code===100||error.code===103||/class|schema|column .* does not exist/i.test(text(error.message))) return []; throw error; } }
|
|
|
async function availabilityRows(context,input,coachId=0) { const rows=await newClassObjects('CoachAvailability',context,input,1000); return rows.filter((row)=>!coachId||number(row.get('coachId'))===coachId).map((row)=>({objectId:row.id,coachId:number(row.get('coachId')),ruleType:text(row.get('ruleType'))||'weekly',weekday:number(row.get('weekday')),date:text(row.get('date')),startTime:text(row.get('startTime')),endTime:text(row.get('endTime')),available:row.get('available')!==false,reason:text(row.get('reason')),updatedAt:dateIso(row.updatedAt)})); }
|
|
|
function defaultPayRates() { return [{objectId:'default-1',classType:1,classTypeLabel:classType(1),amount:20,durationHours:.5,effectiveFrom:'1970-01-01',active:true,isDefault:true},{objectId:'default-2',classType:2,classTypeLabel:classType(2),amount:40,durationHours:1,effectiveFrom:'1970-01-01',active:true,isDefault:true},{objectId:'default-3',classType:3,classTypeLabel:classType(3),amount:40,durationHours:1,effectiveFrom:'1970-01-01',active:true,isDefault:true}]; }
|
|
|
@@ -2841,7 +2863,15 @@ async function appointmentCountsForBusinessDate(context,input,date=businessDate(
|
|
|
function appointmentHistory(rows) { const byCoach=new Map(),dates=[];for(const row of rows){const date=row.startsAt?businessDate(row.startsAt):'';if(/^\d{4}-\d{2}-\d{2}$/.test(date))dates.push(date);if(!row.coachId)continue;const current=byCoach.get(row.coachId)||{totalAppointments:0,lastAppointmentAt:''};current.totalAppointments+=1;if(row.startsAt>current.lastAppointmentAt)current.lastAppointmentAt=row.startsAt;byCoach.set(row.coachId,current);}dates.sort();return{byCoach,totalAppointments:rows.length,dataDateFrom:dates[0]||'',dataDateTo:dates[dates.length-1]||''};}
|
|
|
async function professionalCoachRows(context,input) { const page=await coachRows(context,input),all=await allAppointmentRows(context,input),counts=await appointmentCountsForBusinessDate(context,input),history=appointmentHistory(all);return{...page,items:page.items.map((row)=>({...row,todayAppointments:counts.byCoach.get(row.userId)||0,...(history.byCoach.get(row.userId)||{totalAppointments:0,lastAppointmentAt:''})}))}; }
|
|
|
async function professionalCoachDetail(context,input,objectId) { const detail=await coachDetailData(context,input,objectId),all=await allAppointmentRows(context,input),counts=await appointmentCountsForBusinessDate(context,input),history=appointmentHistory(all),coachHistory=history.byCoach.get(detail.coach.userId)||{totalAppointments:0,lastAppointmentAt:''};return{...detail,coach:{...detail.coach,todayAppointments:counts.byCoach.get(detail.coach.userId)||0,...coachHistory},appointments:all.filter((row)=>row.coachId===detail.coach.userId).slice(0,500)}; }
|
|
|
-async function professionalScheduleDataLocal(context,input) { const data=await professionalScheduleData(context,input),all=await allAppointmentRows(context,input),counts=await appointmentCountsForBusinessDate(context,input),history=appointmentHistory(all),coachId=number((input.filters||{}).coachId),appointments=all.filter((row)=>{const date=row.startsAt?businessDate(row.startsAt):'';return date>=data.dateFrom&&date<=data.dateTo&&(!coachId||row.coachId===coachId);}),unassigned=appointments.filter((row)=>!row.coachId||row.coachAccountMissing);return{...data,totalAppointments:history.totalAppointments,dataDateFrom:history.dataDateFrom,dataDateTo:history.dataDateTo,appointments,unassigned,coaches:data.coaches.map((row)=>({...row,todayAppointments:counts.byCoach.get(row.userId)||0,...(history.byCoach.get(row.userId)||{totalAppointments:0,lastAppointmentAt:''})}))}; }
|
|
|
+async function professionalScheduleDataLocal(context,input) {
|
|
|
+ const data=await professionalScheduleData(context,input),stored=await allAppointmentRows(context,input),live=await liveAppointmentRows(input),coachId=number((input.filters||{}).coachId),memberMap=new Map(data.members.map((row)=>[row.userId,row])),baseCoachMap=new Map(data.coaches.map((row)=>[row.userId,row]));
|
|
|
+ const hydratedLive=live.allItems.map((row)=>{const member=memberMap.get(row.studentId),coach=baseCoachMap.get(row.coachId);return{...row,studentObjectId:member?member.objectId:'',studentName:member?member.displayName:row.studentName,coachObjectId:coach?coach.objectId:'',coachName:coach?coach.displayName:row.coachName};});
|
|
|
+ const merged=new Map();for(const row of stored)merged.set(row.generalId?'general:'+row.generalId:'target:'+row.targetId,row);for(const row of hydratedLive)merged.set(row.generalId?'general:'+row.generalId:'target:'+row.targetId,row);const all=Array.from(merged.values()),history=appointmentHistory(all),appointments=all.filter((row)=>{const date=row.startsAt?businessDate(row.startsAt):'';return date>=data.dateFrom&&date<=data.dateTo&&(!coachId||row.coachId===coachId);}).sort((a,b)=>a.startsAt.localeCompare(b.startsAt)),rangeCounts=new Map();for(const row of appointments)if(row.coachId)rangeCounts.set(row.coachId,(rangeCounts.get(row.coachId)||0)+1);
|
|
|
+ const coaches=data.coaches.map((row)=>({...row,todayAppointments:rangeCounts.get(row.userId)||0,...(history.byCoach.get(row.userId)||{totalAppointments:0,lastAppointmentAt:''})})),knownCoachIds=new Set(coaches.map((row)=>row.userId));
|
|
|
+ for(const row of hydratedLive){if(!row.coachId||knownCoachIds.has(row.coachId))continue;knownCoachIds.add(row.coachId);const coachHistory=history.byCoach.get(row.coachId)||{totalAppointments:0,lastAppointmentAt:''};coaches.push({objectId:'legacy-live-coach:'+row.coachId,userId:row.coachId,displayName:row.coachName,mobile:'',mobileMasked:'',storeName:'实时业务系统',status:'active',statusLabel:'实时账号',todayAppointments:rangeCounts.get(row.coachId)||0,pendingAppointments:0,...coachHistory,monthLessons:0,monthHours:0,estimatedPay:0,averageScore:0,lastLessonAt:'',availabilityConfigured:false,updatedAt:''});
|
|
|
+ }
|
|
|
+ const unassigned=appointments.filter((row)=>!row.coachId);return{...data,refreshedAt:new Date().toISOString(),totalAppointments:Math.max(history.totalAppointments,live.total),dataDateFrom:history.dataDateFrom,dataDateTo:history.dataDateTo,appointments,unassigned,coaches:coaches.sort((a,b)=>a.displayName.localeCompare(b.displayName,'zh-CN')),liveSourceAvailable:live.available,liveSourceMessage:live.message,liveSourceCount:hydratedLive.length};
|
|
|
+}
|
|
|
async function professionalDashboard(context,input) { const data=await dashboard(context,input),date=businessDate(),counts=await appointmentCountsForBusinessDate(context,input),metrics=data.metrics.map((row)=>row.key==='today'?{...row,value:counts.active.length}:row),alerts=data.alerts.map((row)=>row.key==='unassigned'?{...row,value:counts.active.filter((item)=>!item.coachId).length}:row);return{...data,date,metrics,alerts}; }
|
|
|
async function handler(request,response) {
|
|
|
try {
|