|
|
@@ -31,7 +31,8 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
Guestcate: new Set(['cateid','parentId','gtype','sourceKey','orderId']),
|
|
|
GuestBar: new Set(['id','cateId','pid','replyId','replyUserId','cuser','cuname','cdate','status','sourceKey','orderFlag','postFlag']),
|
|
|
ExamClass: new Set(['cId','cClassid','cOrderBy','cClassType','sourceKey']),
|
|
|
- ExamPoint: new Set(['id','tid','orderBy','addUser','addTime','sourceKey'])
|
|
|
+ ExamPoint: new Set(['id','tid','orderBy','addUser','addTime','sourceKey']),
|
|
|
+ QuestionsKnowledge: new Set(['kId','kClassId','kOrderBy','pid','cuser','cdate','sourceKey'])
|
|
|
};
|
|
|
|
|
|
function inputOf(request) {
|
|
|
@@ -784,6 +785,66 @@ async function handler(request, response) {
|
|
|
await target.fetch({ useMasterKey: true }); await audit({ ...context, company }, objectId ? 'update-exam-point' : 'create-exam-point', 'ExamPoint', target.id);
|
|
|
return response.json({ success: true, data: serializeObject(target) });
|
|
|
}
|
|
|
+ if (operation === 'saveKnowledge') {
|
|
|
+ const objectId = String(input.objectId || '');
|
|
|
+ const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
+ const knowledgeName = String(payload.kName || '').trim();
|
|
|
+ const classId = Number(input.classId == null ? payload.kClassId || 0 : input.classId);
|
|
|
+ const parentId = Number(input.parentId == null ? payload.pid || 0 : input.parentId);
|
|
|
+ const orderId = Number(input.orderId == null ? payload.kOrderBy || 0 : input.orderId);
|
|
|
+ const status = Number(payload.status == null ? 1 : payload.status);
|
|
|
+ const grade = Number(payload.grade || 0);
|
|
|
+ const isSys = Number(payload.isSys || 0);
|
|
|
+ if (!knowledgeName || knowledgeName.length > 200) fail(400, '知识点名称长度应为 1 至 200 位');
|
|
|
+ if (!Number.isInteger(classId) || classId < 1) fail(400, '请选择有效试题分类');
|
|
|
+ if (!Number.isInteger(parentId) || parentId < 0) fail(400, '父知识点编号无效');
|
|
|
+ if (!Number.isInteger(orderId) || orderId < 0) fail(400, '知识点排序必须是非负整数');
|
|
|
+ if (![0,1].includes(status) || !Number.isInteger(grade) || grade < 0 || ![0,1].includes(isSys)) fail(400, '知识点状态、年级或系统标记无效');
|
|
|
+ let companyId = pointerId(context.company);
|
|
|
+ if (context.isSuperAdmin && input.companyId) companyId = String(input.companyId);
|
|
|
+ if (!companyId) fail(400, '知识点必须指定帐套');
|
|
|
+ const company = await new Parse.Query('Company').get(companyId, { useMasterKey: true });
|
|
|
+ const subject = await Psql.oneOrNone('SELECT "cId" FROM "ExamClass" WHERE "company"=$1 AND "cId"=$2 LIMIT 1', [companyId, classId]);
|
|
|
+ if (!subject) fail(404, '试题分类不存在或不属于当前帐套');
|
|
|
+ const fields = await schemaFor('QuestionsKnowledge');
|
|
|
+ let target;
|
|
|
+ let currentId = 0;
|
|
|
+ if (objectId) {
|
|
|
+ const query = new Parse.Query('QuestionsKnowledge'); applyTenant(query, fields, context, input.companyId); target = await query.get(objectId, { useMasterKey: true });
|
|
|
+ companyId = pointerId(target.get('company')) || companyId; currentId = Number(target.get('kId')) || 0;
|
|
|
+ } else target = new Parse.Object('QuestionsKnowledge');
|
|
|
+ if (parentId > 0) {
|
|
|
+ const parent = await Psql.oneOrNone('SELECT "kId" FROM "QuestionsKnowledge" WHERE "company"=$1 AND "kId"=$2 AND "kClassId"=$3 LIMIT 1', [companyId, parentId, classId]);
|
|
|
+ if (!parent) fail(404, '父知识点不存在、科目不一致或不属于当前帐套');
|
|
|
+ if (currentId) {
|
|
|
+ const cycle = await Psql.oneOrNone('WITH RECURSIVE chain AS (SELECT "kId","pid",ARRAY["kId"::text] AS path FROM "QuestionsKnowledge" WHERE "company"=$1 AND "kId"=$2 UNION ALL SELECT k."kId",k."pid",chain.path||k."kId"::text FROM "QuestionsKnowledge" k JOIN chain ON k."kId"=chain."pid" WHERE k."company"=$1 AND NOT k."kId"::text=ANY(chain.path)) SELECT 1 AS found FROM chain WHERE "kId"=$3 LIMIT 1', [companyId, parentId, currentId]);
|
|
|
+ if (cycle) fail(409, '不能把知识点移动到自身或其下级知识点');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const duplicate = await Psql.oneOrNone('SELECT "objectId" FROM "QuestionsKnowledge" WHERE "company"=$1 AND "objectId"<>$2 AND "kClassId"=$3 AND LOWER(TRIM(COALESCE("kName",\'\')))=LOWER($4) LIMIT 1', [companyId, objectId, classId, knowledgeName]);
|
|
|
+ if (duplicate) fail(409, '同一试题分类中的知识点名称不能重复');
|
|
|
+ target.set('kName', knowledgeName); target.set('kClassId', classId); target.set('pid', parentId); target.set('status', status); target.set('grade', grade); target.set('isSys', isSys); target.set('company', company);
|
|
|
+ if (objectId) target.set('kOrderBy', orderId);
|
|
|
+ else { target.set('sourceKey', 'cloud:admin-knowledge:' + companyId + ':' + Date.now() + ':' + Math.random().toString(36).slice(2,10)); target.set('cuser', Number(context.current.get('legacyUserId')) || 0); target.set('cdate', new Date()); }
|
|
|
+ try { await target.save(null, { useMasterKey: true }); }
|
|
|
+ catch (error) { fail(422, '知识点初始写入失败: ' + String(error && error.message || error)); }
|
|
|
+ try {
|
|
|
+ if (!objectId) {
|
|
|
+ const rows = await Psql.query('WITH lock_row AS MATERIALIZED (SELECT pg_advisory_xact_lock(hashtext(\'xiaoshu-knowledge-id:\'||$1))), next_id AS MATERIALIZED (SELECT COALESCE(MAX("kId"),0)+1 AS id FROM "QuestionsKnowledge",lock_row WHERE "company"=$1 AND COALESCE("kId",0)>0), next_order AS MATERIALIZED (SELECT COALESCE(MAX("kOrderBy"),0)+1 AS id FROM "QuestionsKnowledge",lock_row WHERE "company"=$1 AND "kClassId"=$3 AND "pid"=$4 AND "objectId"<>$2) UPDATE "QuestionsKnowledge" SET "kId"=next_id.id,"kOrderBy"=CASE WHEN $5>0 THEN $5 ELSE next_order.id END,"sourceKey"=\'[["k_id",\'||next_id.id::text||\']]\',"updatedAt"=NOW() FROM next_id,next_order WHERE "objectId"=$2 RETURNING next_id.id', [companyId, target.id, classId, parentId, orderId]);
|
|
|
+ currentId = Number(rows[0] && rows[0].id) || 0;
|
|
|
+ if (!currentId) throw new Error('无法分配知识点编号');
|
|
|
+ }
|
|
|
+ } catch (error) { if (!objectId) await target.destroy({ useMasterKey: true }).catch(() => undefined); fail(422, '知识点结构写入失败: ' + String(error && error.message || error)); }
|
|
|
+ await target.fetch({ useMasterKey: true }); await audit({ ...context, company }, objectId ? 'update-knowledge' : 'create-knowledge', 'QuestionsKnowledge', target.id);
|
|
|
+ return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'knowledgeChildren') {
|
|
|
+ const parentId = Number(input.parentId || 0); const classId = Number(input.classId || 0);
|
|
|
+ if (!Number.isInteger(parentId) || parentId < 0 || !Number.isInteger(classId) || classId < 1) fail(400, '知识点父级或试题分类编号无效');
|
|
|
+ const fields = await schemaFor('QuestionsKnowledge'); const query = new Parse.Query('QuestionsKnowledge'); applyTenant(query, fields, context, input.companyId); query.equalTo('kClassId', classId); query.equalTo('pid', parentId); query.ascending('kOrderBy'); query.limit(1000);
|
|
|
+ const results = await query.find({ useMasterKey: true });
|
|
|
+ return response.json({ success: true, data: { classId, parentId, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
if (operation === 'barBatch') {
|
|
|
const action = String(input.action || '');
|
|
|
const actions = ['audit','unaudit','recycle','recover','move','top-global','top-section','top-undo','elite','elite-undo','bottom','bottom-undo'];
|
|
|
@@ -869,7 +930,7 @@ async function handler(request, response) {
|
|
|
const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20));
|
|
|
let query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId);
|
|
|
const search = String(input.search || '').trim();
|
|
|
- if (search) { const searchField = ['name','title','nodeName','specName','modelName','fieldName','fieldAlias','groupName','cClassName','testPoint','username','realName','mobile','sourceKey'].find((name) => fields[name] && fields[name].type === 'String'); if (searchField) query.matches(searchField, escapeRegex(search), 'i'); }
|
|
|
+ if (search) { const searchField = ['name','title','nodeName','specName','modelName','fieldName','fieldAlias','groupName','cClassName','testPoint','kName','username','realName','mobile','sourceKey'].find((name) => fields[name] && fields[name].type === 'String'); if (searchField) query.matches(searchField, escapeRegex(search), 'i'); }
|
|
|
const sort = fields[input.sort] ? String(input.sort) : fields.updatedAt ? 'updatedAt' : 'createdAt'; if (input.order === 'asc') query.ascending(sort); else query.descending(sort);
|
|
|
const total = await query.count({ useMasterKey: true }); query.skip((page - 1) * pageSize); query.limit(pageSize);
|
|
|
const results = (await query.find({ useMasterKey: true })).filter(isVisible);
|
|
|
@@ -891,6 +952,7 @@ async function handler(request, response) {
|
|
|
if (className === 'Guestcate') fail(400, '留言/贴吧分类必须走专用保存流程');
|
|
|
if (className === 'ExamClass') fail(400, '试题分类必须走专用保存流程');
|
|
|
if (className === 'ExamPoint') fail(400, '考点必须走专用保存流程');
|
|
|
+ if (className === 'QuestionsKnowledge') fail(400, '知识点必须走专用保存流程');
|
|
|
if (className === 'CommonModel' && !objectId) fail(400, '内容新增必须同时写入模型附表,不能走通用保存');
|
|
|
let object;
|
|
|
if (objectId) { const query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId); object = await query.get(objectId, { useMasterKey: true }); } else object = new Parse.Object(className);
|
|
|
@@ -906,6 +968,7 @@ async function handler(request, response) {
|
|
|
if (className === 'Guestcate') { const cateid = Number(object.get('cateid')) || 0; const companyId = pointerId(object.get('company')); const refs = await Psql.one('SELECT (SELECT COUNT(*)::int FROM "Guestcate" WHERE "company"=$1 AND "parentId"=$2) AS children,(SELECT COUNT(*)::int FROM "Guestbook" WHERE "company"=$1 AND "cateid"=$2) AS messages,(SELECT COUNT(*)::int FROM "GuestBar" WHERE "company"=$1 AND "cateId"=$2) AS posts', [companyId, cateid]); if (Number(refs.children) || Number(refs.messages) || Number(refs.posts)) fail(409, '分类仍被下级分类、留言或帖子引用,不能删除'); await object.destroy({ useMasterKey: true }); await audit(context, 'delete-guest-category', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: false } }); }
|
|
|
if (className === 'ExamClass') { const classId = Number(object.get('cId')) || 0; const companyId = pointerId(object.get('company')); const refs = await Psql.one('SELECT (SELECT COUNT(*)::int FROM "ExamClass" WHERE "company"=$1 AND "cClassid"=$2) AS children,(SELECT COUNT(*)::int FROM "ExamSysQuestions" WHERE "company"=$1 AND "pClass"=$2) AS questions', [companyId, classId]); if (Number(refs.children) || Number(refs.questions)) fail(409, '试题分类仍被下级分类或试题引用,不能删除'); await object.destroy({ useMasterKey: true }); await audit(context, 'delete-exam-class', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: false } }); }
|
|
|
if (className === 'ExamPoint') { const pointId = Number(object.get('id')) || 0; const companyId = pointerId(object.get('company')); const children = await Psql.one('SELECT COUNT(*)::int AS count FROM "ExamPoint" WHERE "company"=$1 AND "tid"=$2', [companyId, pointId]); if (Number(children.count)) fail(409, '考点仍有下级考点,不能删除'); await object.destroy({ useMasterKey: true }); await audit(context, 'delete-exam-point', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: false } }); }
|
|
|
+ if (className === 'QuestionsKnowledge') { const knowledgeId = Number(object.get('kId')) || 0; const companyId = pointerId(object.get('company')); const refs = await Psql.one('SELECT (SELECT COUNT(*)::int FROM "QuestionsKnowledge" WHERE "company"=$1 AND "pid"=$2) AS children,(SELECT COUNT(*)::int FROM "ExamSysQuestions" WHERE "company"=$1 AND "pKnowledge"=$2) AS questions', [companyId, knowledgeId]); if (Number(refs.children) || Number(refs.questions)) fail(409, '知识点仍被下级知识点或试题引用,不能删除'); await object.destroy({ useMasterKey: true }); await audit(context, 'delete-knowledge', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: false } }); }
|
|
|
if (className === '_User') assertCanManageUser(context, object, 'lock');
|
|
|
if (className === 'Group') { const groupId = Number(object.get('groupId')) || 0; const companyId = pointerId(object.get('company')); const users = await Psql.one('SELECT COUNT(*)::int AS count FROM "_User" WHERE "company"=$1 AND COALESCE("legacyGroupId",0)=$2 AND ("isDeleted" IS NULL OR "isDeleted"=FALSE)', [companyId, groupId]); const children = await Psql.one('SELECT COUNT(*)::int AS count FROM "Group" WHERE "company"=$1 AND COALESCE("parentGroupId",0)=$2', [companyId, groupId]); if (Number(users.count) > 0) fail(409, '该用户组仍有用户,不能删除'); if (Number(children.count) > 0) fail(409, '该用户组仍有下级组,不能删除'); }
|
|
|
if (className === 'CommonModel') { object.set('status', -2); await object.save(null, { useMasterKey: true }); await audit(context, 'content-recycle', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: true, status: -2 } }); }
|