|
|
@@ -2687,6 +2687,12 @@ function appOverviewCourseRow(source) {
|
|
|
function appOverviewStudyRow(row) {
|
|
|
if(!row)return null;return{objectId:String(row.objectId||''),GeneralID:number(row.generalId||row.id),ID:number(row.generalId||row.id),Title:String(row.title||row.con||'学习记录'),CreateTime:firstValue(row.createTime,row.createdAt),pl:row.pl,con:row.con,djq:row.djq,ygg:row.ygg,dqrq:row.dqrq,dsid:row.dsid,fxrl:row.fxrl,xxqs:row.xxqs,szmdid:row.szmdid,UserID:row.userId,userId:row.userId,learned:row.learned};
|
|
|
}
|
|
|
+async function appOverviewCompletedReadingMap(records,targetId) {
|
|
|
+ const studyIds=[...new Set(records.map((row)=>String(row&&row.GeneralID||'').trim()).filter(Boolean))];if(!studyIds.length)return new Map();
|
|
|
+ const learner=await legacyUserObject(targetId);if(!learner)return new Map();const ownerIds=[learner.id,'_User$'+learner.id];
|
|
|
+ const rows=await Psql.query('SELECT DISTINCT ON (a."createOptions"#>>\'{params,sourceStudyId}\') a."createOptions"#>>\'{params,sourceStudyId}\' AS "sourceStudyId",a."objectId" AS "readingArticleId",a."title" AS "readingTitle",completed."completedAt" AS "readingCompletedAt" FROM "SurveyItem" a JOIN LATERAL (SELECT COALESCE(l."createdAt",l."updatedAt") AS "completedAt" FROM "SurveyLog" l WHERE CAST(l."surveyItem" AS text)=ANY(ARRAY[CAST(a."objectId" AS text),\'SurveyItem$\'||CAST(a."objectId" AS text)]) AND CAST(l."user" AS text)=ANY($1::text[]) AND COALESCE(l."isDeleted",FALSE)=FALSE ORDER BY COALESCE(l."createdAt",l."updatedAt") DESC,l."objectId" DESC LIMIT 1) completed ON TRUE WHERE CAST(a."user" AS text)=ANY($1::text[]) AND a."type"=\'reading\' AND COALESCE(a."isDeleted",FALSE)=FALSE AND a."createOptions"#>>\'{params,sourceStudyId}\'=ANY($2::text[]) ORDER BY a."createOptions"#>>\'{params,sourceStudyId}\',completed."completedAt" DESC,COALESCE(a."createdAt",a."updatedAt") DESC',[ownerIds,studyIds]);
|
|
|
+ const map=new Map();for(const row of rows)map.set(String(row.sourceStudyId),{readingArticleId:String(row.readingArticleId||''),readingTitle:String(row.readingTitle||''),readingCompletedAt:row.readingCompletedAt});return map;
|
|
|
+}
|
|
|
async function appLearningTarget(current,requestedId) {
|
|
|
const ownId=ownLegacyId(current),targetId=number(requestedId||ownId);if(!targetId)fail(400,'缺少学员 ID');if(isAdminUser(current)||targetId===ownId)return targetId;
|
|
|
if(number(current.get('legacyGroupId')||current.get('groupId'))!==3)fail(403,'仅关联老师可以查看学员学习记录');
|
|
|
@@ -2706,11 +2712,11 @@ async function appLearningOverview(input,current) {
|
|
|
Psql.query('SELECT c."objectId",c."generalId",c."title",c."createTime",c."createdAt",b.*,b."objectId" AS "bindingObjectId",n."nodeName" AS "courseTitle" FROM "CourseBinding" b LEFT JOIN "CommonModel" c ON c."company"=$1 AND c."modelId"=58 AND c."itemId"=b."id" LEFT JOIN "Node" n ON n."company"=$1 AND n."nodeId"=CASE WHEN COALESCE(b."kcid",\'\')~\'^\\d+(?:\\.0+)?$\' THEN b."kcid"::numeric ELSE -1 END 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',[DEFAULT_COMPANY_ID,String(targetId)]),
|
|
|
Psql.oneOrNone('SELECT c."objectId",c."generalId",c."title",c."createTime",c."createdAt",d."id",d."pl",d."con",d."djq",d."ygg",d."dqrq",d."dsid",d."fxrl",d."xxqs",d."szmdid",d."userId",d."learned" FROM "DailyStudyRecord" d LEFT JOIN "CommonModel" c ON c."company"=$1 AND c."modelId"=56 AND c."itemId"=d."id" WHERE d."company"=$1 AND d."userId"=$2 AND COALESCE(NULLIF(d."xxqs",\'\'),\'[]\')<>\'[]\' ORDER BY COALESCE(c."createTime",c."createdAt",d."updatedAt",d."createdAt") DESC LIMIT 1',[DEFAULT_COMPANY_ID,targetId])
|
|
|
]);
|
|
|
- const normalizedRecords=records.map(appOverviewStudyRow);
|
|
|
+ const normalizedRecords=records.map(appOverviewStudyRow),normalizedLatestReading=appOverviewStudyRow(latestReading),completedReadingMap=await appOverviewCompletedReadingMap(normalizedLatestReading?[...normalizedRecords,normalizedLatestReading]:normalizedRecords,targetId),attachCompletedReading=(row)=>{if(!row)return row;const reading=completedReadingMap.get(String(row.GeneralID));return reading?{...row,...reading}:row;};
|
|
|
const recordDateCounts={},scheduleDateCounts={},learnedDates=new Set();for(const row of dateRows){const date=appOverviewDateKey(row.date);if(date){recordDateCounts[date]=number(row.count);learnedDates.add(date);}}
|
|
|
const today=appOverviewToday();for(const row of scheduleGroups){const date=appOverviewDateKey(row.bxrq||row.yysj);if(!date||learnedDates.has(date))continue;const count=number(row.count),status=number(row.dszt),value=scheduleDateCounts[date]||{planned:0,missed:0};if(status<30&&date<today)value.missed+=count;else value.planned+=count;scheduleDateCounts[date]=value;}
|
|
|
const total=await Psql.one('SELECT COUNT(*)::int total FROM "DailyStudyRecord" d WHERE '+recordWhere.join(' AND '),recordValues);
|
|
|
- return {page:mobilePageMeta(total.total,paging),...mobileSource(),records:normalizedRecords,latestReadingRecord:appOverviewStudyRow(latestReading),recordDateCounts,scheduleDateCounts,schedules:schedules.map(appOverviewScheduleRow),courses:courses.map(appOverviewCourseRow),refreshedAt:new Date().toISOString()};
|
|
|
+ return {page:mobilePageMeta(total.total,paging),...mobileSource(),records:normalizedRecords.map(attachCompletedReading),latestReadingRecord:attachCompletedReading(normalizedLatestReading),recordDateCounts,scheduleDateCounts,schedules:schedules.map(appOverviewScheduleRow),courses:courses.map(appOverviewCourseRow),refreshedAt:new Date().toISOString()};
|
|
|
}
|
|
|
async function appCompanionOverview(input,current) {
|
|
|
const uid=await authorizeRequestedUser(current,input.uid),coach=number(current.get('legacyGroupId'))===3,history=input.history===true,p=mobilePage(input);
|
|
|
@@ -3772,6 +3778,23 @@ async function ensureOperationsUserSchema() {
|
|
|
console.log('updated _User schema (' + Object.keys(missingFields).join(', ') + ')');
|
|
|
}
|
|
|
|
|
|
+async function ensureReadingLinkSchema() {
|
|
|
+ const desiredFields = {
|
|
|
+ operatorId: { type: 'String', required: false },
|
|
|
+ learnerLegacyId: { type: 'Number', required: false },
|
|
|
+ sourceStudyId: { type: 'String', required: false },
|
|
|
+ };
|
|
|
+ for (const className of ['SurveyItem', 'SurveyLog']) {
|
|
|
+ const schema = await parseRequest(`/schemas/${className}`);
|
|
|
+ const missingFields = Object.fromEntries(
|
|
|
+ Object.entries(desiredFields).filter(([field]) => !(schema.fields || {})[field]),
|
|
|
+ );
|
|
|
+ if (!Object.keys(missingFields).length) continue;
|
|
|
+ await parseRequest(`/schemas/${className}`, { method: 'PUT', body: JSON.stringify({ fields: missingFields }) });
|
|
|
+ console.log('updated ' + className + ' reading link schema (' + Object.keys(missingFields).join(', ') + ')');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
async function ensurePayrollSchemas() {
|
|
|
const schemas = {
|
|
|
CourseAppointment: {
|
|
|
@@ -3884,6 +3907,7 @@ if (validateOnly) {
|
|
|
await ensureOperationsUserSchema();
|
|
|
await ensurePayrollSchemas();
|
|
|
}
|
|
|
+ if (selectedDefinitions.some((definition) => definition.name === 'xiaoshu.app.gateway')) await ensureReadingLinkSchema();
|
|
|
for (const definition of selectedDefinitions) {
|
|
|
const result = await upsert(definition);
|
|
|
console.log(`${result.action.padEnd(7)} ${definition.path} (${result.objectId})`);
|