|
@@ -1,6 +1,12 @@
|
|
|
// Appended to the operations gateway; all writes remain behind its authentication.
|
|
// Appended to the operations gateway; all writes remain behind its authentication.
|
|
|
-async function enrollmentSql(sql, args) {
|
|
|
|
|
- try { return await Psql.one(sql,args); }
|
|
|
|
|
|
|
+async function enrollmentSql(sql, args, scope=null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if(scope?.context.storeId)return await Psql.tx(async tx=>{
|
|
|
|
|
+ await tx.one('SELECT xs_store_scope_guard($1,$2,$3,$4,$5)',[pointerId(scope.context.company),scope.context.current.id,scope.studentId,scope.coachId||0,scope.targetId||'']);
|
|
|
|
|
+ return tx.one(sql,args);
|
|
|
|
|
+ });
|
|
|
|
|
+ return await Psql.one(sql,args);
|
|
|
|
|
+ }
|
|
|
catch(error) { if(/does not exist/i.test(text(error.message))) fail(503,'用户开课数据迁移尚未完成'); fail(409,text(error.message).replace(/^.*ERROR:\s*/,'')); }
|
|
catch(error) { if(/does not exist/i.test(text(error.message))) fail(503,'用户开课数据迁移尚未完成'); fail(409,text(error.message).replace(/^.*ERROR:\s*/,'')); }
|
|
|
}
|
|
}
|
|
|
async function creditAccounts(context,input,store=false) {
|
|
async function creditAccounts(context,input,store=false) {
|
|
@@ -11,14 +17,14 @@ async function creditAccounts(context,input,store=false) {
|
|
|
const transactions=await Psql.query('SELECT t."objectId",t."kind",t."account",t."amount",t."storeId",t."studentId",t."sourceTransactionId",t."operatorId",t."reason",t."payMethod",t."balances",t."createdAt",t."operationId",t."amount"-COALESCE((SELECT SUM(r."amount") FROM "CreditTransaction" r WHERE r."company"=t."company" AND r."sourceTransactionId"=t."objectId" AND r."kind"=\'refund\'),0) AS "unreturned" FROM "CreditTransaction" t WHERE t."company"=$1 AND '+(store?'t."storeId"':'t."studentId"')+'=$2 ORDER BY t."createdAt" DESC',[co,uid]);
|
|
const transactions=await Psql.query('SELECT t."objectId",t."kind",t."account",t."amount",t."storeId",t."studentId",t."sourceTransactionId",t."operatorId",t."reason",t."payMethod",t."balances",t."createdAt",t."operationId",t."amount"-COALESCE((SELECT SUM(r."amount") FROM "CreditTransaction" r WHERE r."company"=t."company" AND r."sourceTransactionId"=t."objectId" AND r."kind"=\'refund\'),0) AS "unreturned" FROM "CreditTransaction" t WHERE t."company"=$1 AND '+(store?'t."storeId"':'t."studentId"')+'=$2 ORDER BY t."createdAt" DESC',[co,uid]);
|
|
|
const parent=store?null:await userByLegacyId(context,input,number(legacy.ParentUserID),2);
|
|
const parent=store?null:await userByLegacyId(context,input,number(legacy.ParentUserID),2);
|
|
|
const trial=store?null:await Psql.oneOrNone('SELECT status,evidence FROM "TrialEligibility" WHERE company=$1 AND "studentId"=$2',[co,uid]);
|
|
const trial=store?null:await Psql.oneOrNone('SELECT status,evidence FROM "TrialEligibility" WHERE company=$1 AND "studentId"=$2',[co,uid]);
|
|
|
- return{trialStatus:trial?.status||'pending',accounts,transactions:transactions.filter(t=>store||!t.operationId||t.account!=='UserPoint'||!transactions.some(o=>o.operationId===t.operationId&&o.account!=='UserPoint')),storeId:store?uid:number(legacy.ParentUserID),storeName:store?displayName(target.toJSON()):parent?displayName(parent):'',canPost:store?context.isSuperAdmin:roleOf(context)==='ops-manager'};
|
|
|
|
|
|
|
+ return{trialStatus:trial?.status||'pending',accounts,transactions:transactions.filter(t=>(!context.storeId||number(t.storeId)===context.storeId)&&(store||!t.operationId||t.account!=='UserPoint'||!transactions.some(o=>o.operationId===t.operationId&&o.account!=='UserPoint'))),storeId:store?uid:number(legacy.ParentUserID),storeName:store?displayName(target.toJSON()):parent?displayName(parent):'',canPost:store?context.isSuperAdmin:roleOf(context)==='ops-manager'};
|
|
|
}
|
|
}
|
|
|
async function postCredits(context,input,payload,store=false) {
|
|
async function postCredits(context,input,payload,store=false) {
|
|
|
if(store)return storeCreditPost(context,input,payload);
|
|
if(store)return storeCreditPost(context,input,payload);
|
|
|
requireRole(context,['ops-manager']);
|
|
requireRole(context,['ops-manager']);
|
|
|
const target=store?await scopedStoreObject(context,input):await memberTarget(context,input,text(input.targetId));
|
|
const target=store?await scopedStoreObject(context,input):await memberTarget(context,input,text(input.targetId));
|
|
|
const account=text(payload.account)||({1:'Purse',2:'SilverCoin',3:'UserExp',4:'UserPoint'})[number(payload.type)],kind=store?'fund':payload.direction==='deduct'?'refund':'transfer';
|
|
const account=text(payload.account)||({1:'Purse',2:'SilverCoin',3:'UserExp',4:'UserPoint'})[number(payload.type)],kind=store?'fund':payload.direction==='deduct'?'refund':'transfer';
|
|
|
- if(!store){const result=await enrollmentSql('SELECT xs_credit_operation($1,$2,$3,$4::jsonb,$5,$6,$7) AS result',[pointerId(target.get('company')),context.current.id,target.id,JSON.stringify({...payload,account}),reasonOf(input),text(input.idempotencyKey),text(input.expectedUpdatedAt)]);return result.result;}
|
|
|
|
|
|
|
+ if(!store){const result=await enrollmentSql('SELECT xs_credit_operation($1,$2,$3,$4::jsonb,$5,$6,$7) AS result',[pointerId(target.get('company')),context.current.id,target.id,JSON.stringify({...payload,account}),reasonOf(input),text(input.idempotencyKey),text(input.expectedUpdatedAt)],{context,studentId:number(target.get('legacyUserId'))});return result.result;}
|
|
|
const result=await enrollmentSql('SELECT xs_credit_post($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) AS result',[pointerId(target.get('company')),context.current.id,kind,target.id,account,number(payload.amount),text(payload.sourceTransactionId),reasonOf(input),text(payload.payMethod),text(input.idempotencyKey)]);
|
|
const result=await enrollmentSql('SELECT xs_credit_post($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) AS result',[pointerId(target.get('company')),context.current.id,kind,target.id,account,number(payload.amount),text(payload.sourceTransactionId),reasonOf(input),text(payload.payMethod),text(input.idempotencyKey)]);
|
|
|
const clean={...result.result};delete clean.request;return clean;
|
|
const clean={...result.result};delete clean.request;return clean;
|
|
|
}
|
|
}
|
|
@@ -52,10 +58,10 @@ async function saveEnrollment(context,input,payload) {
|
|
|
if(!options.eligible)fail(409,options.reason);
|
|
if(!options.eligible)fail(409,options.reason);
|
|
|
const selected=ids.map(id=>options.items.find(row=>row.courseId===id));if(selected.some(row=>!row||!row.eligible))fail(409,'课程内容为空、已停用或与分类学阶不符');
|
|
const selected=ids.map(id=>options.items.find(row=>row.courseId===id));if(selected.some(row=>!row||!row.eligible))fail(409,'课程内容为空、已停用或与分类学阶不符');
|
|
|
const categoryName=options.categories.find(row=>row.key===category).name;
|
|
const categoryName=options.categories.find(row=>row.key===category).name;
|
|
|
- return(await enrollmentSql('SELECT xs_enroll($1,$2,$3,$4,$5::jsonb,$6::jsonb,$7,$8) AS result',[companyIdOf(context,input),context.current.id,uid,category,JSON.stringify(selected.map(row=>({...row,categoryName}))),JSON.stringify(options.rules.map(rule=>rule.creditCosts)),reason,text(input.idempotencyKey)])).result;
|
|
|
|
|
|
|
+ return(await enrollmentSql('SELECT xs_enroll($1,$2,$3,$4,$5::jsonb,$6::jsonb,$7,$8) AS result',[companyIdOf(context,input),context.current.id,uid,category,JSON.stringify(selected.map(row=>({...row,categoryName}))),JSON.stringify(options.rules.map(rule=>rule.creditCosts)),reason,text(input.idempotencyKey)],{context,studentId:uid})).result;
|
|
|
}
|
|
}
|
|
|
async function enrollmentBindings(context,input,uid=0) {
|
|
async function enrollmentBindings(context,input,uid=0) {
|
|
|
- const rows=await Psql.query('SELECT b.*,c."objectId" AS "targetId",c."status",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"::text WHERE b."company"=$1 AND ($2::numeric=0 OR b."yhid"::text=$2::text) AND c."status"=99 AND b."enrollmentStatus"=\'active\' AND COALESCE(n."zstatus",99)=99',[companyIdOf(context,input),uid]);
|
|
|
|
|
|
|
+ const rows=await Psql.query('SELECT b.*,c."objectId" AS "targetId",c."status",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"::text WHERE b."company"=$1 AND ($2::numeric=0 OR b."yhid"::text=$2::text) AND c."status"=99 AND b."enrollmentStatus"=\'active\' AND COALESCE(n."zstatus",99)=99 AND ($3::text=\'\' OR EXISTS(SELECT 1 FROM \"_User\" s WHERE s.company=b.company AND s.\"legacyUserId\"::text=b.yhid::text AND s.\"identityType\"=\'member\' AND s.\"legacyUserData\"->>\'ParentUserID\'=$3 AND COALESCE(s.\"isDeleted\",false)=false))',[companyIdOf(context,input),uid,context.storeId?String(context.storeId):'']);
|
|
|
return rows.map(row=>({bindingId:text(row.objectId),targetId:text(row.targetId),studentId:number(row.yhid),courseId:number(row.kcid),courseName:text(row.courseName),categoryKey:text(row.categoryKey),categoryName:TEACHING_CATEGORY_LABELS.get(text(row.categoryKey))||text(row.categoryName),learnedCount:number(row.yxx),wordCount:number(row.cksl)}));
|
|
return rows.map(row=>({bindingId:text(row.objectId),targetId:text(row.targetId),studentId:number(row.yhid),courseId:number(row.kcid),courseName:text(row.courseName),categoryKey:text(row.categoryKey),categoryName:TEACHING_CATEGORY_LABELS.get(text(row.categoryKey))||text(row.categoryName),learnedCount:number(row.yxx),wordCount:number(row.cksl)}));
|
|
|
}
|
|
}
|
|
|
async function enrollmentTeachingContext(context,input,payload,date) {
|
|
async function enrollmentTeachingContext(context,input,payload,date) {
|
|
@@ -75,7 +81,7 @@ async function toggleEnrollment(context,input,restore) {
|
|
|
if(number(target.get('modelId'))!==58)fail(400,'目标不是开课记录');
|
|
if(number(target.get('modelId'))!==58)fail(400,'目标不是开课记录');
|
|
|
const b=await findAddon('CourseBinding',target.get('company'),target.get('itemId'));if(!b)fail(404,'开课记录不存在');
|
|
const b=await findAddon('CourseBinding',target.get('company'),target.get('itemId'));if(!b)fail(404,'开课记录不存在');
|
|
|
let costs=[];if(restore){const opt=await enrollmentOptions(context,{...input,studentId:b.get('yhid'),categoryKey:b.get('categoryKey')});if(!opt.items.some(row=>row.courseId===number(b.get('kcid'))&&row.eligible))fail(409,opt.reason||'课程内容不可用');costs=opt.rules.map(rule=>rule.creditCosts);}
|
|
let costs=[];if(restore){const opt=await enrollmentOptions(context,{...input,studentId:b.get('yhid'),categoryKey:b.get('categoryKey')});if(!opt.items.some(row=>row.courseId===number(b.get('kcid'))&&row.eligible))fail(409,opt.reason||'课程内容不可用');costs=opt.rules.map(rule=>rule.creditCosts);}
|
|
|
- return(await enrollmentSql('SELECT xs_enrollment_status($1,$2,$3,$4,$5,$6::jsonb) AS result',[co,b.id,restore,context.current.id,reason,JSON.stringify(costs)])).result;
|
|
|
|
|
|
|
+ return(await enrollmentSql('SELECT xs_enrollment_status($1,$2,$3,$4,$5,$6::jsonb) AS result',[co,b.id,restore,context.current.id,reason,JSON.stringify(costs)],{context,studentId:number(b.get('yhid')),targetId:target.id})).result;
|
|
|
}
|
|
}
|
|
|
async function saveEnrollmentSchedule(context,input,payload,editing=false) {
|
|
async function saveEnrollmentSchedule(context,input,payload,editing=false) {
|
|
|
requireRole(context,['ops-manager','ops-staff']);const reason=reasonOf(input),co=companyIdOf(context,input),idem=text(input.idempotencyKey),editId=editing?text(input.targetId):'',req={actor:context.current.id,operation:editing?'update':'create',targetId:editId,payload,reason};
|
|
requireRole(context,['ops-manager','ops-staff']);const reason=reasonOf(input),co=companyIdOf(context,input),idem=text(input.idempotencyKey),editId=editing?text(input.targetId):'',req={actor:context.current.id,operation:editing?'update':'create',targetId:editId,payload,reason};
|
|
@@ -86,10 +92,10 @@ async function saveEnrollmentSchedule(context,input,payload,editing=false) {
|
|
|
if(check.conflictCount&&!skip)fail(409,'排课存在时间冲突');if(!selected.length)fail(409,'没有可保存的排课');
|
|
if(check.conflictCount&&!skip)fail(409,'排课存在时间冲突');if(!selected.length)fail(409,'没有可保存的排课');
|
|
|
const coach=number(payload.coachId)?await userByLegacyId(context,input,payload.coachId,3):null,student=await userByLegacyId(context,input,payload.studentId,1);
|
|
const coach=number(payload.coachId)?await userByLegacyId(context,input,payload.coachId,3):null,student=await userByLegacyId(context,input,payload.studentId,1);
|
|
|
const entries=selected.map(item=>{const start=new Date(item.startsAt),date=businessDate(start),time=start.toLocaleTimeString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false,hour:'2-digit',minute:'2-digit'});return{common:{title:displayName(student)+'('+check.courseCategoryName+')',subtitle:check.courseName},addon:{szyh:String(payload.studentId),pl:coach?String(payload.coachId):'',fxpl:coach?String(payload.coachId):'',plxm:coach?displayName(coach):'',kcid:String(payload.courseId),yykcid:String(payload.courseId),dslx:String(legacyClassTypeFor(check.courseCategoryKey,check.durationMinutes)),jffs:check.deliveryLabel,bindingId:check.bindingId,courseCategoryKey:check.courseCategoryKey,courseCategoryName:check.courseCategoryName,durationMinutes:check.durationMinutes,deliveryMode:check.deliveryMode,teachingRuleId:item.teachingRule.objectId,teacherPaySnapshot:item.teachingRule.teacherPay,creditCostsSnapshot:item.teachingRule.creditCosts,bxrq:date,sdsd:time,yysj:date+' '+time,dszt:'0'}};});
|
|
const entries=selected.map(item=>{const start=new Date(item.startsAt),date=businessDate(start),time=start.toLocaleTimeString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false,hour:'2-digit',minute:'2-digit'});return{common:{title:displayName(student)+'('+check.courseCategoryName+')',subtitle:check.courseName},addon:{szyh:String(payload.studentId),pl:coach?String(payload.coachId):'',fxpl:coach?String(payload.coachId):'',plxm:coach?displayName(coach):'',kcid:String(payload.courseId),yykcid:String(payload.courseId),dslx:String(legacyClassTypeFor(check.courseCategoryKey,check.durationMinutes)),jffs:check.deliveryLabel,bindingId:check.bindingId,courseCategoryKey:check.courseCategoryKey,courseCategoryName:check.courseCategoryName,durationMinutes:check.durationMinutes,deliveryMode:check.deliveryMode,teachingRuleId:item.teachingRule.objectId,teacherPaySnapshot:item.teachingRule.teacherPay,creditCostsSnapshot:item.teachingRule.creditCosts,bxrq:date,sdsd:time,yysj:date+' '+time,dszt:'0'}};});
|
|
|
- const result=(await enrollmentSql('SELECT xs_schedule_save($1,$2,$3::jsonb,$4::jsonb,$5,$6,$7) AS result',[co,context.current.id,JSON.stringify(req),JSON.stringify(entries),idem,editId,text(input.expectedUpdatedAt)])).result;return{...result,skipped:check.candidates.length-selected.length,warnings:check.warnings};
|
|
|
|
|
|
|
+ const result=(await enrollmentSql('SELECT xs_schedule_save($1,$2,$3::jsonb,$4::jsonb,$5,$6,$7) AS result',[co,context.current.id,JSON.stringify(req),JSON.stringify(entries),idem,editId,text(input.expectedUpdatedAt)],{context,studentId:number(payload.studentId),coachId:number(payload.coachId),targetId:editId})).result;return{...result,skipped:check.candidates.length-selected.length,warnings:check.warnings};
|
|
|
}
|
|
}
|
|
|
async function cancelEnrollmentSchedule(context,input,restore){
|
|
async function cancelEnrollmentSchedule(context,input,restore){
|
|
|
requireRole(context,['ops-manager','ops-staff']);const reason=reasonOf(input),pair=await appointmentPair(context,input,text(input.targetId));assertVersion(pair.common,input.expectedUpdatedAt);
|
|
requireRole(context,['ops-manager','ops-staff']);const reason=reasonOf(input),pair=await appointmentPair(context,input,text(input.targetId));assertVersion(pair.common,input.expectedUpdatedAt);
|
|
|
|
|
|
|
|
- return(await enrollmentSql('SELECT xs_schedule_cancel($1,$2,$3,$4,$5,$6) AS result',[pointerId(pair.common.get('company')),pair.common.id,restore,context.current.id,reason,text(input.expectedUpdatedAt)])).result;
|
|
|
|
|
|
|
+ return(await enrollmentSql('SELECT xs_schedule_cancel($1,$2,$3,$4,$5,$6) AS result',[pointerId(pair.common.get('company')),pair.common.id,restore,context.current.id,reason,text(input.expectedUpdatedAt)],{context,studentId:number(pair.addon.get('szyh')),targetId:pair.common.id})).result;
|
|
|
}
|
|
}
|