فهرست منبع

fix(api): allow coaches to open linked study records

彭峰 2 هفته پیش
والد
کامیت
e5de58d357
2فایلهای تغییر یافته به همراه6 افزوده شده و 1 حذف شده
  1. 4 1
      scripts/deploy-admin-functions.mjs
  2. 2 0
      scripts/smoke-admin-functions.mjs

+ 4 - 1
scripts/deploy-admin-functions.mjs

@@ -2547,6 +2547,9 @@ async function authorizeBusinessRow(current, row, subjectFields, actorFields = [
   if (isAdminUser(current)) return; const ownId = ownLegacyId(current); const allIds = subjectFields.concat(actorFields).map((name) => number(row[name] ?? row[name.toLowerCase()] ?? row[name.toUpperCase()])).filter(Boolean); if (allIds.includes(ownId)) return;
   const subjectIds = subjectFields.map((name) => number(row[name] ?? row[name.toLowerCase()] ?? row[name.toUpperCase()])).filter(Boolean); if (!subjectIds.length) fail(403, '业务记录缺少可验证的所属用户'); const fields = await fieldsOf('_User'); for (const targetId of [...new Set(subjectIds)]) { const target = await findByLegacyId('_User', fields, targetId, ['legacyUserId','userid','num']); const agent = target && target.get('agent'); if (target && agent && agent.id === current.id) return; } fail(403, '无权查看该业务记录');
 }
+async function authorizeLearningRecordAccess(current, row) {
+  if (isAdminUser(current)) return; const ownId=ownLegacyId(current),ownerId=number(row.userId??row.UserID),coachId=number(row.pl);if([ownerId,coachId].filter(Boolean).includes(ownId))return;if(!ownerId)fail(403,'学习记录缺少可验证的学员');await appLearningTarget(current,ownerId);
+}
 function normalizeWordRow(source) { const addon = source.__addon || {}; const merged = { ...source, ...addon }; delete merged.__addon; return legacyAliases(merged, 'CommonModel'); }
 async function orderDetailRow(inputId, current, authorize = true) {
   const id = String(inputId || ''); if (!id) fail(400, '缺少预约 ID'); const rows = await Psql.query('SELECT c.*, row_to_json(a) AS "__addon", n."nodeName" AS "__courseTitle" FROM "CommonModel" c JOIN "CourseAppointment" a ON a."company" = $1 AND CAST(a."id" AS text) = CAST(c."itemId" AS text) LEFT JOIN "Node" n ON n."company" = $1 AND CAST(n."nodeId" AS text) = CAST(a."kcid" AS text) WHERE c."company" = $1 AND CAST(c."modelId" AS text) = \'54\' AND CAST(c."generalId" AS text) = $2 LIMIT 1', [DEFAULT_COMPANY_ID, id]);
@@ -2555,7 +2558,7 @@ async function orderDetailRow(inputId, current, authorize = true) {
 }
 async function recordDetailData(input, current) {
   const id = String(input.id || input.gid || ''); if (!id) fail(400, '缺少学习记录 ID'); const rows = await Psql.query('SELECT c.*, row_to_json(d) AS "__addon" FROM "CommonModel" c JOIN "DailyStudyRecord" d ON d."company" = $1 AND CAST(d."id" AS text) = CAST(c."itemId" AS text) WHERE c."company" = $1 AND CAST(c."modelId" AS text) = \'56\' AND CAST(c."generalId" AS text) = $2 LIMIT 1', [DEFAULT_COMPANY_ID, id]);
-  if (!rows.length) return null; const source = rows[0]; const recordAddon = source.__addon || {}; delete source.__addon; const record = legacyAliases({ ...source, ...recordAddon }, 'CommonModel'); await authorizeBusinessRow(current, record, ['userId'], ['pl']); const storedWords = parseArray(recordAddon.xxqs); const ids = [...new Set(storedWords.map((item) => String(item && (item.GeneralID ?? item.generalId ?? item.id) || '')).filter(Boolean))]; let wordRows = [];
+  if (!rows.length) return null; const source = rows[0]; const recordAddon = source.__addon || {}; delete source.__addon; const record = legacyAliases({ ...source, ...recordAddon }, 'CommonModel'); await authorizeLearningRecordAccess(current, record); const storedWords = parseArray(recordAddon.xxqs); const ids = [...new Set(storedWords.map((item) => String(item && (item.GeneralID ?? item.generalId ?? item.id) || '')).filter(Boolean))]; let wordRows = [];
   if (ids.length) wordRows = await Psql.query('SELECT c.*, row_to_json(v) AS "__addon" FROM "CommonModel" c LEFT JOIN "VocabularyWord" v ON v."company" = $1 AND CAST(v."id" AS text) = CAST(c."itemId" AS text) WHERE c."company" = $1 AND CAST(c."modelId" AS text) = \'52\' AND CAST(c."generalId" AS text) = ANY($2::text[])', [DEFAULT_COMPANY_ID, ids]);
   const details = new Map(wordRows.map((row) => { const normalized = normalizeWordRow(row); return [String(normalized.GeneralID || normalized.generalId), normalized]; })); const words = storedWords.map((item) => { const raw = item && typeof item === 'object' ? safe(item) : {}; const wordId = String(raw.GeneralID ?? raw.generalId ?? raw.id ?? ''); const detail = details.get(wordId) || legacyAliases({ generalId: wordId, title: raw.Title || raw.title || '' }, 'CommonModel'); return { ...detail, ...raw, GeneralID: number(wordId, wordId), Title: raw.Title || raw.title || detail.Title || detail.title || '', detail: [detail] }; });
   const order = recordAddon.dsid ? await orderDetailRow(recordAddon.dsid, current, false) : null; return { words, addon: order ? [order] : [], record };

+ 2 - 0
scripts/smoke-admin-functions.mjs

@@ -898,6 +898,8 @@ try {
   if (!createdStudy.result?.created || !createdStudy.result?.GeneralID || !createdStudy.result?.recordObjectId) throw new Error('预约学习记录首次双表写入校验失败');
   const studyDetail = await callLegacyFunction(memberLogin.sessionToken, { action: 'e_record_detail', id: createdStudy.result.GeneralID });
   if (studyDetail.result?.[0]?.detail?.[0]?.Title !== 'woman') throw new Error('预约学习记录写后详情校验失败');
+  const coachStudyDetail = await callLegacyFunction(coachLogin.sessionToken, { action: 'e_record_detail', id: createdStudy.result.GeneralID });
+  if (coachStudyDetail.result?.[0]?.detail?.[0]?.Title !== 'woman') throw new Error('陪练老师无法查看关联学员的学习记录');
   studyAddon.xxqs = JSON.stringify([{ GeneralID: 2505, Title: 'woman', check: 1 }]);
   const updatedStudy = await callLegacyFunction(memberLogin.sessionToken, { action: 'stu_record_update_v2', orderId: temporaryAppointmentId, uid: memberLegacyId, inputer: memberUsername, addon: JSON.stringify(studyAddon) });
   if (updatedStudy.result?.created || updatedStudy.result?.GeneralID !== createdStudy.result.GeneralID) throw new Error('预约学习记录幂等更新校验失败');