|
@@ -2560,10 +2560,8 @@ async function teamUserPage(input, current) {
|
|
|
async function teamStats(input,current) {
|
|
async function teamStats(input,current) {
|
|
|
const parentId = await authorizeTeamUser(current,input.uid); const rows = await Psql.query('SELECT COALESCE("legacyUserData"->>\'VIP\',\'0\') AS vip,COUNT(*)::int AS total FROM "_User" WHERE "company"=$1 AND ("isDeleted" IS NULL OR "isDeleted"=FALSE) AND COALESCE("legacyUserData"->>\'ParentUserID\',\'0\')=$2 GROUP BY COALESCE("legacyUserData"->>\'VIP\',\'0\')', [DEFAULT_COMPANY_ID,String(parentId)]); const result = { childs:0,v0:0,v1:0,v2:0,v3:0,v4:0,v5:0 }; for (const row of rows) { const total = number(row.total); result.childs += total; const key = 'v' + Math.max(0,Math.min(5,number(row.vip))); result[key] += total; } return result;
|
|
const parentId = await authorizeTeamUser(current,input.uid); const rows = await Psql.query('SELECT COALESCE("legacyUserData"->>\'VIP\',\'0\') AS vip,COUNT(*)::int AS total FROM "_User" WHERE "company"=$1 AND ("isDeleted" IS NULL OR "isDeleted"=FALSE) AND COALESCE("legacyUserData"->>\'ParentUserID\',\'0\')=$2 GROUP BY COALESCE("legacyUserData"->>\'VIP\',\'0\')', [DEFAULT_COMPANY_ID,String(parentId)]); const result = { childs:0,v0:0,v1:0,v2:0,v3:0,v4:0,v5:0 }; for (const row of rows) { const total = number(row.total); result.childs += total; const key = 'v' + Math.max(0,Math.min(5,number(row.vip))); result[key] += total; } return result;
|
|
|
}
|
|
}
|
|
|
-async function coachStudentPage(input,current) {
|
|
|
|
|
- const coachId = ownLegacyId(current);
|
|
|
|
|
- if (!coachId) fail(400,'陪练账号缺少旧系统用户 ID');
|
|
|
|
|
- const paging = pageInput(input), scheduledOnly = input.scope === 'current-scheduled';
|
|
|
|
|
|
|
+function coachStudentSource(coachId, scope) {
|
|
|
|
|
+ const scheduledOnly = scope === 'current-scheduled' || scope === 'wordbook';
|
|
|
const values = [DEFAULT_COMPANY_ID,String(coachId)];
|
|
const values = [DEFAULT_COMPANY_ID,String(coachId)];
|
|
|
let base = "FROM \"CommonModel\" c JOIN \"CourseAppointment\" a ON a.\"company\"=$1 AND a.\"id\"=c.\"itemId\"";
|
|
let base = "FROM \"CommonModel\" c JOIN \"CourseAppointment\" a ON a.\"company\"=$1 AND a.\"id\"=c.\"itemId\"";
|
|
|
if (scheduledOnly) base += " JOIN \"_User\" eligible ON eligible.\"company\"=$1 AND CAST(eligible.\"legacyUserId\" AS text)=CAST(a.\"szyh\" AS text)";
|
|
if (scheduledOnly) base += " JOIN \"_User\" eligible ON eligible.\"company\"=$1 AND CAST(eligible.\"legacyUserId\" AS text)=CAST(a.\"szyh\" AS text)";
|
|
@@ -2572,8 +2570,28 @@ async function coachStudentPage(input,current) {
|
|
|
values.push(appOverviewToday().replace(/-/g,''));
|
|
values.push(appOverviewToday().replace(/-/g,''));
|
|
|
base += " AND eligible.\"isDisabled\" IS NOT TRUE AND eligible.\"isDeleted\" IS NOT TRUE\n AND COALESCE(eligible.\"legacyGroupId\"::text,eligible.\"legacyUserData\"->>'GroupID','0')='1'\n AND COALESCE(to_jsonb(a)->>'isDeleted','false') NOT IN ('true','1')\n AND COALESCE(to_jsonb(c)->>'isDeleted','false') NOT IN ('true','1')";
|
|
base += " AND eligible.\"isDisabled\" IS NOT TRUE AND eligible.\"isDeleted\" IS NOT TRUE\n AND COALESCE(eligible.\"legacyGroupId\"::text,eligible.\"legacyUserData\"->>'GroupID','0')='1'\n AND COALESCE(to_jsonb(a)->>'isDeleted','false') NOT IN ('true','1')\n AND COALESCE(to_jsonb(c)->>'isDeleted','false') NOT IN ('true','1')";
|
|
|
// Keep students through their assigned review dates, rather than retaining all past students forever.
|
|
// Keep students through their assigned review dates, rather than retaining all past students forever.
|
|
|
- base += " AND ((COALESCE(NULLIF(a.\"dszt\"::text,''),'0') IN ('0','10','20') AND LEFT(REGEXP_REPLACE(COALESCE(NULLIF(a.\"bxrq\",''),a.\"yysj\",''),'[^0-9]','','g'),8) >= $3) OR a.\"szyh\" IN (SELECT CAST(m.\"yhid\" AS text) FROM \"MemoryPracticeRecord\" m JOIN \"CommonModel\" review ON review.\"company\"=$1 AND review.\"modelId\"=60 AND review.\"itemId\"=m.\"id\" AND review.\"status\"=99 WHERE m.\"company\"=$1 AND m.\"plid\"=$2::numeric AND COALESCE(NULLIF(m.\"fxzt\"::text,''),'0')='0' AND LEFT(REGEXP_REPLACE(COALESCE(NULLIF(m.\"kywrq\",''),m.\"kywsj\",''),'[^0-9]','','g'),8) >= $3 AND COALESCE(to_jsonb(m)->>'isDeleted','false') NOT IN ('true','1') AND COALESCE(to_jsonb(m)->>'rewardDuplicate','false') NOT IN ('true','1') AND COALESCE(to_jsonb(review)->>'isDeleted','false') NOT IN ('true','1')))";
|
|
|
|
|
|
|
+ const activeStatuses = scope === 'wordbook' ? "'0','10','11','20'" : "'0','10','20'";
|
|
|
|
|
+ base += " AND ((COALESCE(NULLIF(a.\"dszt\"::text,''),'0') IN ("+activeStatuses+") AND LEFT(REGEXP_REPLACE(COALESCE(NULLIF(a.\"bxrq\",''),a.\"yysj\",''),'[^0-9]','','g'),8) >= $3) OR a.\"szyh\" IN (SELECT CAST(m.\"yhid\" AS text) FROM \"MemoryPracticeRecord\" m JOIN \"CommonModel\" review ON review.\"company\"=$1 AND review.\"modelId\"=60 AND review.\"itemId\"=m.\"id\" AND review.\"status\"=99 WHERE m.\"company\"=$1 AND m.\"plid\"=$2::numeric AND COALESCE(NULLIF(m.\"fxzt\"::text,''),'0')='0' AND LEFT(REGEXP_REPLACE(COALESCE(NULLIF(m.\"kywrq\",''),m.\"kywsj\",''),'[^0-9]','','g'),8) >= $3 AND COALESCE(to_jsonb(m)->>'isDeleted','false') NOT IN ('true','1') AND COALESCE(to_jsonb(m)->>'rewardDuplicate','false') NOT IN ('true','1') AND COALESCE(to_jsonb(review)->>'isDeleted','false') NOT IN ('true','1')))";
|
|
|
}
|
|
}
|
|
|
|
|
+ return { base, values, scheduledOnly };
|
|
|
|
|
+}
|
|
|
|
|
+function isWordbookCoach(current) {
|
|
|
|
|
+ return number(current.get('legacyGroupId') || current.get('groupId')) === 3;
|
|
|
|
|
+}
|
|
|
|
|
+async function coachWordbookTarget(current, requestedId, db = Psql) {
|
|
|
|
|
+ if (!isWordbookCoach(current)) fail(403,'仅陪练老师可以带学生学习生词本');
|
|
|
|
|
+ const uid = number(requestedId), coachId = ownLegacyId(current);
|
|
|
|
|
+ if (!Number.isSafeInteger(uid) || uid <= 0 || !coachId) fail(400,'请选择学员');
|
|
|
|
|
+ const { base, values } = coachStudentSource(coachId, 'wordbook');
|
|
|
|
|
+ const linked = await db.oneOrNone('SELECT 1 ' + base + ' AND a."szyh"=$4 LIMIT 1', values.concat(String(uid)));
|
|
|
|
|
+ if (!linked) fail(403,'该学员不在你的生词本带练名单中');
|
|
|
|
|
+ return uid;
|
|
|
|
|
+}
|
|
|
|
|
+async function coachStudentPage(input,current) {
|
|
|
|
|
+ const coachId = ownLegacyId(current), wordbook = input.scope === 'wordbook';
|
|
|
|
|
+ if (!coachId) fail(400,'陪练账号缺少旧系统用户 ID');
|
|
|
|
|
+ if (wordbook && !isWordbookCoach(current)) fail(403,'仅陪练老师可以查看带练学员名单');
|
|
|
|
|
+ const paging = pageInput(input), {base, values, scheduledOnly} = coachStudentSource(coachId, input.scope);
|
|
|
const countRow = await Psql.one('SELECT COUNT(DISTINCT CAST(a."szyh" AS text))::int AS total ' + base,values);
|
|
const countRow = await Psql.one('SELECT COUNT(DISTINCT CAST(a."szyh" AS text))::int AS total ' + base,values);
|
|
|
// Keep an eligible restored selection on the first page, even with many students.
|
|
// Keep an eligible restored selection on the first page, even with many students.
|
|
|
const preferred = scheduledOnly ? String(number(input.selectedStudentId)) : '';
|
|
const preferred = scheduledOnly ? String(number(input.selectedStudentId)) : '';
|
|
@@ -2582,7 +2600,7 @@ async function coachStudentPage(input,current) {
|
|
|
const rows = await Psql.query('WITH students AS (SELECT CAST(a."szyh" AS text) AS uid,MAX(CAST(a."id" AS text)) AS appointment_id,MAX(c."updatedAt") AS updated_at,(CAST(a."szyh" AS text)=' + preferredParam + ') AS preferred ' + base + ' GROUP BY CAST(a."szyh" AS text) ORDER BY preferred DESC,updated_at DESC,uid LIMIT ' + limitParam + ' OFFSET ' + offsetParam + ') SELECT ' + userSelect('u') + ',students.uid AS "__studentId",students.appointment_id AS "__appointmentId" FROM students LEFT JOIN "_User" u ON u."company"=$1 AND CAST(u."legacyUserId" AS text)=students.uid ORDER BY students.preferred DESC,students.updated_at DESC,students.uid',rowValues);
|
|
const rows = await Psql.query('WITH students AS (SELECT CAST(a."szyh" AS text) AS uid,MAX(CAST(a."id" AS text)) AS appointment_id,MAX(c."updatedAt") AS updated_at,(CAST(a."szyh" AS text)=' + preferredParam + ') AS preferred ' + base + ' GROUP BY CAST(a."szyh" AS text) ORDER BY preferred DESC,updated_at DESC,uid LIMIT ' + limitParam + ' OFFSET ' + offsetParam + ') SELECT ' + userSelect('u') + ',students.uid AS "__studentId",students.appointment_id AS "__appointmentId" FROM students LEFT JOIN "_User" u ON u."company"=$1 AND CAST(u."legacyUserId" AS text)=students.uid ORDER BY students.preferred DESC,students.updated_at DESC,students.uid',rowValues);
|
|
|
const normalized = rows.map((row) => { const value = legacyUserRow(row); const studentId = number(row.__studentId || value.UserID); return { ...value, userId:studentId, UserID:studentId, szyh:studentId, ID:number(row.__appointmentId), honeyname:value.HoneyName, honeyName:value.HoneyName }; });
|
|
const normalized = rows.map((row) => { const value = legacyUserRow(row); const studentId = number(row.__studentId || value.UserID); return { ...value, userId:studentId, UserID:studentId, szyh:studentId, ID:number(row.__appointmentId), honeyname:value.HoneyName, honeyName:value.HoneyName }; });
|
|
|
const total = number(countRow.total);
|
|
const total = number(countRow.total);
|
|
|
- return { rows:normalized,page:{ itemCount:total,pageCount:Math.ceil(total / paging.size),pageIndex:paging.index,pageSize:paging.size } };
|
|
|
|
|
|
|
+ return { rows:wordbook ? normalized.map(({userId, honeyName}) => ({userId, honeyName})) : normalized,page:{ itemCount:total,pageCount:Math.ceil(total / paging.size),pageIndex:paging.index,pageSize:paging.size } };
|
|
|
}
|
|
}
|
|
|
const POINT_HISTORY_CLASSES = { 1:'UserExpDomP',2:'UserSIcon',3:'UserExpHis',4:'UserUserPoint',5:'UserDummyPoint',6:'UserCredit' };
|
|
const POINT_HISTORY_CLASSES = { 1:'UserExpDomP',2:'UserSIcon',3:'UserExpHis',4:'UserUserPoint',5:'UserDummyPoint',6:'UserCredit' };
|
|
|
function legacyPointRow(source) { const row = safe(source); row.ExpHisID = firstValue(row.expHisId,row.objectId); row.UserID = number(row.userId); row.HisTime = firstValue(row.hisTime,row.createdAt); row.Score = number(row.score); row.Detail = String(firstValue(row.detail,row.remark,'') || ''); row.Remark = String(row.remark || ''); row.score_before = number(row.scoreBefore); return row; }
|
|
function legacyPointRow(source) { const row = safe(source); row.ExpHisID = firstValue(row.expHisId,row.objectId); row.UserID = number(row.userId); row.HisTime = firstValue(row.hisTime,row.createdAt); row.Score = number(row.score); row.Detail = String(firstValue(row.detail,row.remark,'') || ''); row.Remark = String(row.remark || ''); row.score_before = number(row.scoreBefore); return row; }
|
|
@@ -2652,7 +2670,7 @@ async function recordDetailData(input, current) {
|
|
|
return { words, addon: order ? [{...order, ...(notesReview ? {reviewContentMode:'notes'} : {})}] : [], record };
|
|
return { words, addon: order ? [{...order, ...(notesReview ? {reviewContentMode:'notes'} : {})}] : [], record };
|
|
|
}
|
|
}
|
|
|
async function newWordPage(input, current) {
|
|
async function newWordPage(input, current) {
|
|
|
- const uid = await authorizeRequestedUser(current, input.uid);
|
|
|
|
|
|
|
+ const uid = await (isWordbookCoach(current) ? coachWordbookTarget(current, input.uid) : authorizeRequestedUser(current, input.uid));
|
|
|
const paging = largePageInput(input);
|
|
const paging = largePageInput(input);
|
|
|
const status = String(input.learningStatus ?? '').trim();
|
|
const status = String(input.learningStatus ?? '').trim();
|
|
|
if (status && status !== 'pending' && status !== 'completed') fail(400, '生词学习状态无效');
|
|
if (status && status !== 'pending' && status !== 'completed') fail(400, '生词学习状态无效');
|
|
@@ -2691,6 +2709,7 @@ async function courseWordPage(input, current) {
|
|
|
}),page:{itemCount:total,pageCount:Math.ceil(total/paging.size),pageIndex:paging.index,pageSize:paging.size,passCounts,bookPassCounts,firstPassProgress:bookTotal?Math.round((bookTotal-bookPassCounts[0])/bookTotal*100):0}};
|
|
}),page:{itemCount:total,pageCount:Math.ceil(total/paging.size),pageIndex:paging.index,pageSize:paging.size,passCounts,bookPassCounts,firstPassProgress:bookTotal?Math.round((bookTotal-bookPassCounts[0])/bookTotal*100):0}};
|
|
|
}
|
|
}
|
|
|
async function updateLearningWords(input, current) {
|
|
async function updateLearningWords(input, current) {
|
|
|
|
|
+ if (input.scope === 'wordbook') return saveCoachWordbookWords(input, current);
|
|
|
const uid = await authorizeRequestedUser(current, input.uid); const wordIds = [...new Set(String(input.wordsId || input.wordIds || '').split(',').map((value) => String(number(value))).filter((value) => value !== '0'))]; if (!wordIds.length) fail(400, '请选择单词'); if (wordIds.length > 100) fail(400, '单次最多操作 100 个单词'); const saveLearning = number(input.save) === 1; const hasNewWordFlag = input.ifnew !== undefined && input.ifnew !== ''; const newWordFlag = hasNewWordFlag ? (number(input.ifnew) === 1 ? '1' : '0') : null; if (!saveLearning && !hasNewWordFlag) fail(400, '缺少学习或生词操作类型'); const kcid = number(input.kcid);
|
|
const uid = await authorizeRequestedUser(current, input.uid); const wordIds = [...new Set(String(input.wordsId || input.wordIds || '').split(',').map((value) => String(number(value))).filter((value) => value !== '0'))]; if (!wordIds.length) fail(400, '请选择单词'); if (wordIds.length > 100) fail(400, '单次最多操作 100 个单词'); const saveLearning = number(input.save) === 1; const hasNewWordFlag = input.ifnew !== undefined && input.ifnew !== ''; const newWordFlag = hasNewWordFlag ? (number(input.ifnew) === 1 ? '1' : '0') : null; if (!saveLearning && !hasNewWordFlag) fail(400, '缺少学习或生词操作类型'); const kcid = number(input.kcid);
|
|
|
const validRows = await Psql.query('SELECT CAST("generalId" AS text) AS id FROM "CommonModel" WHERE "company" = $1 AND CAST("modelId" AS text) = \'52\' AND CAST("generalId" AS text) = ANY($2::text[])', [DEFAULT_COMPANY_ID, wordIds]); const validIds = new Set(validRows.map((row) => String(row.id))); const invalid = wordIds.filter((id) => !validIds.has(id)); if (invalid.length) fail(400, '包含不存在的词库 ID: ' + invalid.join(','));
|
|
const validRows = await Psql.query('SELECT CAST("generalId" AS text) AS id FROM "CommonModel" WHERE "company" = $1 AND CAST("modelId" AS text) = \'52\' AND CAST("generalId" AS text) = ANY($2::text[])', [DEFAULT_COMPANY_ID, wordIds]); const validIds = new Set(validRows.map((row) => String(row.id))); const invalid = wordIds.filter((id) => !validIds.has(id)); if (invalid.length) fail(400, '包含不存在的词库 ID: ' + invalid.join(','));
|
|
|
const existingRows = await Psql.query('SELECT DISTINCT ON (pr.scid) pr."objectId", CAST(pr.scid AS text) AS "scid" FROM "PracticeRecord" pr WHERE pr.company = $1 AND CAST(pr.yhid AS text) = $2 AND CAST(pr.scid AS text) = ANY($3::text[]) ORDER BY pr.scid, COALESCE(pr.xxcs,0) DESC, pr."createdAt", pr."objectId"', [DEFAULT_COMPANY_ID, String(uid), wordIds]); const existing = await Promise.all(existingRows.map((row) => new Parse.Query('PracticeRecord').get(row.objectId, { useMasterKey: true }))); const byWord = new Map(existing.map((item) => [String(item.get('scid')), item])); const changed = []; let created = 0;
|
|
const existingRows = await Psql.query('SELECT DISTINCT ON (pr.scid) pr."objectId", CAST(pr.scid AS text) AS "scid" FROM "PracticeRecord" pr WHERE pr.company = $1 AND CAST(pr.yhid AS text) = $2 AND CAST(pr.scid AS text) = ANY($3::text[]) ORDER BY pr.scid, COALESCE(pr.xxcs,0) DESC, pr."createdAt", pr."objectId"', [DEFAULT_COMPANY_ID, String(uid), wordIds]); const existing = await Promise.all(existingRows.map((row) => new Parse.Query('PracticeRecord').get(row.objectId, { useMasterKey: true }))); const byWord = new Map(existing.map((item) => [String(item.get('scid')), item])); const changed = []; let created = 0;
|
|
@@ -2698,6 +2717,21 @@ async function updateLearningWords(input, current) {
|
|
|
const operations=changed.map(item=>{const data=mobileObjectData(item);if(saveLearning)data.xxcs=1;return{table:'PracticeRecord',data,match:{company:DEFAULT_COMPANY_ID,yhid:uid,scid:number(item.get('scid'))},increment:saveLearning?['xxcs']:[]};});
|
|
const operations=changed.map(item=>{const data=mobileObjectData(item);if(saveLearning)data.xxcs=1;return{table:'PracticeRecord',data,match:{company:DEFAULT_COMPANY_ID,yhid:uid,scid:number(item.get('scid'))},increment:saveLearning?['xxcs']:[]};});
|
|
|
const saved=await mobileAtomicWrite(input,current,operations,String(uid));return{affected:saved.length,created,learnedIncremented:saveLearning?saved.length:0,newWordState:newWordFlag===null?undefined:number(newWordFlag),objectIds:saved.map(item=>item.objectId)};
|
|
const saved=await mobileAtomicWrite(input,current,operations,String(uid));return{affected:saved.length,created,learnedIncremented:saveLearning?saved.length:0,newWordState:newWordFlag===null?undefined:number(newWordFlag),objectIds:saved.map(item=>item.objectId)};
|
|
|
}
|
|
}
|
|
|
|
|
+async function saveCoachWordbookWords(input, current) {
|
|
|
|
|
+ if (!isWordbookCoach(current)) fail(403,'仅陪练老师可以带学生学习生词本');
|
|
|
|
|
+ if (number(input.save) !== 1 || input.ifnew != null) fail(400,'生词本带练仅支持保存学习进度');
|
|
|
|
|
+ const wordIds = [...new Set(String(input.wordsId || '').split(',').map(id => id.trim()).filter(Boolean))];
|
|
|
|
|
+ if (!wordIds.length || wordIds.length > 100 || wordIds.some(id => !/^[1-9][0-9]*$/.test(id))) fail(400,'请选择 1 到 100 个有效生词');
|
|
|
|
|
+ return Psql.tx(async db => {
|
|
|
|
|
+ const uid = await coachWordbookTarget(current, input.uid, db);
|
|
|
|
|
+ await db.query('SELECT pg_advisory_xact_lock(hashtextextended($1,0))', [DEFAULT_COMPANY_ID + ':' + uid]);
|
|
|
|
|
+ const rows = await db.query("WITH candidates AS MATERIALIZED (SELECT p.\"objectId\",p.scid,p.xxcs,p.\"createdAt\" FROM \"PracticeRecord\" p WHERE p.company=$1 AND p.yhid::text=$2 AND p.scid::text=ANY($3::text[]) AND LOWER(p.jrscb::text) IN ('1','true') AND EXISTS (SELECT 1 FROM \"CommonModel\" c WHERE c.company=$1 AND c.\"modelId\"=52 AND c.\"generalId\"::text=p.scid::text) FOR UPDATE) SELECT DISTINCT ON (scid) \"objectId\" FROM candidates ORDER BY scid,COALESCE(xxcs,0) DESC,\"createdAt\",\"objectId\"", [DEFAULT_COMPANY_ID, String(uid), wordIds]);
|
|
|
|
|
+ if (rows.length !== wordIds.length) fail(409,'有单词已不在该学员的生词本中,请返回重新选词');
|
|
|
|
|
+ const operations = rows.map(row => ({table:'PracticeRecord', data:{xxcs:1}, match:{company:DEFAULT_COMPANY_ID, objectId:row.objectId}, increment:['xxcs']}));
|
|
|
|
|
+ const saved = await mobileAtomicWrite(input, current, operations, String(uid), db);
|
|
|
|
|
+ return {affected:saved.length, created:0, learnedIncremented:saved.length, objectIds:saved.map(row => row.objectId)};
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
function parseObject(value, label) { if (value && typeof value === 'object' && !Array.isArray(value)) return value; if (typeof value !== 'string' || !value.trim()) return {}; try { const parsed = JSON.parse(value); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed; } catch (_) {} fail(400, label + '格式错误'); }
|
|
function parseObject(value, label) { if (value && typeof value === 'object' && !Array.isArray(value)) return value; if (typeof value !== 'string' || !value.trim()) return {}; try { const parsed = JSON.parse(value); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed; } catch (_) {} fail(400, label + '格式错误'); }
|
|
|
function cleanText(value, max) { return String(value == null ? '' : value).trim().slice(0, max); }
|
|
function cleanText(value, max) { return String(value == null ? '' : value).trim().slice(0, max); }
|
|
|
async function validateStudyWords(raw) { const words = parseArray(raw); if (words.length > 500) fail(400, '单次学习记录最多 500 个单词'); const normalized = words.map((item) => ({ GeneralID: number(item && (item.GeneralID ?? item.generalId ?? item.id)), Title: cleanText(item && (item.Title ?? item.title), 200), check: number(item && item.check) })).filter((item) => item.GeneralID); if (!normalized.length && words.length) fail(400, '学习记录中的单词 ID 无效'); if (normalized.length) { const ids = [...new Set(normalized.map((item) => String(item.GeneralID)))]; const rows = await Psql.query('SELECT CAST("generalId" AS text) AS id FROM "CommonModel" WHERE "company" = $1 AND CAST("modelId" AS text) = \'52\' AND CAST("generalId" AS text) = ANY($2::text[])', [DEFAULT_COMPANY_ID, ids]); if (new Set(rows.map((row) => String(row.id))).size !== ids.length) fail(400, '学习记录包含不存在的词库 ID'); } return normalized; }
|
|
async function validateStudyWords(raw) { const words = parseArray(raw); if (words.length > 500) fail(400, '单次学习记录最多 500 个单词'); const normalized = words.map((item) => ({ GeneralID: number(item && (item.GeneralID ?? item.generalId ?? item.id)), Title: cleanText(item && (item.Title ?? item.title), 200), check: number(item && item.check) })).filter((item) => item.GeneralID); if (!normalized.length && words.length) fail(400, '学习记录中的单词 ID 无效'); if (normalized.length) { const ids = [...new Set(normalized.map((item) => String(item.GeneralID)))]; const rows = await Psql.query('SELECT CAST("generalId" AS text) AS id FROM "CommonModel" WHERE "company" = $1 AND CAST("modelId" AS text) = \'52\' AND CAST("generalId" AS text) = ANY($2::text[])', [DEFAULT_COMPANY_ID, ids]); if (new Set(rows.map((row) => String(row.id))).size !== ids.length) fail(400, '学习记录包含不存在的词库 ID'); } return normalized; }
|