|
|
@@ -12,6 +12,7 @@ const CLASS_LABELS = {
|
|
|
VocabularyWord: '词库', CommonModel: '通用内容', ContentArticle: '文章内容', ContentPublish: '内容发布', Node: '栏目节点', NodeAuth: '节点权限', Model: '内容模型', ModelField: '模型字段',
|
|
|
ExamClass: '试题分类', ExamSysQuestions: '考试题库', ExamSysPapers: '试卷', ExamType: '考试类型', ExamPoint: '知识点', PaperQuestions: '试卷题目',
|
|
|
Datadiccategory: '数据字典分类', Datadic: '数据字典项',
|
|
|
+ GradeCate: '多级字典分类', Grade: '多级字典选项',
|
|
|
Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
|
|
|
GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科', Search: '搜索记录', Agency: '代理机构', DeliveryCenter: '交付中心'
|
|
|
};
|
|
|
@@ -36,7 +37,9 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
QuestionsKnowledge: new Set(['kId','kClassId','kOrderBy','pid','cuser','cdate','sourceKey']),
|
|
|
ExTeacher: new Set(['id','tclsss','addUser','creatTime','sourceKey']),
|
|
|
Datadiccategory: new Set(['diccateid','sourceKey']),
|
|
|
- Datadic: new Set(['dicid','diccate','sourceKey'])
|
|
|
+ Datadic: new Set(['dicid','diccate','sourceKey']),
|
|
|
+ GradeCate: new Set(['cateId','sourceKey']),
|
|
|
+ Grade: new Set(['gradeId','cate','parentId','grade','sourceKey'])
|
|
|
};
|
|
|
|
|
|
function inputOf(request) {
|
|
|
@@ -797,6 +800,87 @@ async function handler(request, response) {
|
|
|
for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'dictionary-' + action, className, target.id);
|
|
|
return response.json({ success: true, data: { action, updated: targets.length, results: action === 'delete' ? [] : targets.map(serializeObject) } });
|
|
|
}
|
|
|
+ if (operation === 'saveGradeCategory') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
+ const cateName = String(payload.cateName || '').trim(); const remark = String(payload.remark || '').trim();
|
|
|
+ const aliases = String(payload.gradeField || '').split(/\r?\n|\|/).map((value) => value.trim()).filter(Boolean); const gradeField = aliases.join('|');
|
|
|
+ if (!cateName || cateName.length > 50) fail(400, '多级字典分类名称长度应为 1 至 50 位');
|
|
|
+ if (remark.length > 255) fail(400, '多级字典分类备注不能超过 255 位');
|
|
|
+ if (aliases.length < 2 || gradeField.length > 500) fail(400, '层级别名至少填写 2 级,且总长度不能超过 500 位');
|
|
|
+ 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 fields = await schemaFor('GradeCate'); let target;
|
|
|
+ if (objectId) { const query = new Parse.Query('GradeCate'); applyTenant(query, fields, context, input.companyId); target = await query.get(objectId, { useMasterKey: true }); companyId = pointerId(target.get('company')) || companyId; }
|
|
|
+ else target = new Parse.Object('GradeCate');
|
|
|
+ const duplicateQuery = new Parse.Query('GradeCate'); applyTenant(duplicateQuery, fields, context, input.companyId); duplicateQuery.limit(1000); const duplicates = await duplicateQuery.find({ useMasterKey: true });
|
|
|
+ if (duplicates.some((entry) => entry.id !== objectId && String(entry.get('cateName') || '').trim().toLowerCase() === cateName.toLowerCase())) fail(409, '同一帐套的多级字典分类名称不能重复');
|
|
|
+ target.set('cateName', cateName); target.set('remark', remark); target.set('gradeField', gradeField); target.set('company', company);
|
|
|
+ if (objectId) { try { await target.save(null, { useMasterKey: true }); } catch (error) { fail(422, '多级字典分类写入失败: ' + String(error && error.message || error)); } }
|
|
|
+ else {
|
|
|
+ let lastError;
|
|
|
+ for (let attempt = 0; attempt < 5; attempt++) {
|
|
|
+ const categories = new Parse.Query('GradeCate'); categories.limit(1000); const options = new Parse.Query('Grade'); options.limit(1000);
|
|
|
+ const [categoryRows, optionRows] = await Promise.all([categories.find({ useMasterKey: true }), options.find({ useMasterKey: true })]);
|
|
|
+ const nextId = Math.max(0, ...categoryRows.map((entry) => Number(entry.get('cateId')) || 0), ...optionRows.map((entry) => Number(entry.get('cate')) || 0)) + 1;
|
|
|
+ target.set('cateId', nextId); target.set('sourceKey', '[["CateID",' + nextId + ']]');
|
|
|
+ try { await target.save(null, { useMasterKey: true }); lastError = null; break; }
|
|
|
+ catch (error) { lastError = error; if (!(Number(error && error.code) === 137 || /duplicate|unique/i.test(String(error && error.message || '')))) break; }
|
|
|
+ }
|
|
|
+ if (lastError || !target.id) fail(422, '多级字典分类原子编号写入失败: ' + String(lastError && lastError.message || lastError || '无法分配编号'));
|
|
|
+ }
|
|
|
+ await audit({ ...context, company }, objectId ? 'update-grade-category' : 'create-grade-category', 'GradeCate', target.id);
|
|
|
+ return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'saveGradeOption') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {}; const gradeName = String(payload.gradeName || '').trim();
|
|
|
+ if (!gradeName || gradeName.length > 50) fail(400, '多级字典选项名称长度应为 1 至 50 位');
|
|
|
+ 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 fields = await schemaFor('Grade'); let target; let categoryId; let parentId; let level;
|
|
|
+ if (objectId) {
|
|
|
+ const query = new Parse.Query('Grade'); applyTenant(query, fields, context, input.companyId); target = await query.get(objectId, { useMasterKey: true }); companyId = pointerId(target.get('company')) || companyId;
|
|
|
+ categoryId = Number(target.get('cate')) || 0; parentId = Number(target.get('parentId')) || 0; level = Number(target.get('grade')) || 0;
|
|
|
+ } else { target = new Parse.Object('Grade'); categoryId = Number(input.categoryId == null ? payload.cate || 0 : input.categoryId); parentId = Number(input.parentId == null ? payload.parentId || 0 : input.parentId); level = 1; }
|
|
|
+ if (!Number.isInteger(categoryId) || categoryId < 1 || !Number.isInteger(parentId) || parentId < 0) fail(400, '多级字典分类或父选项编号无效');
|
|
|
+ const categoryFields = await schemaFor('GradeCate'); const categoryQuery = new Parse.Query('GradeCate'); applyTenant(categoryQuery, categoryFields, context, input.companyId); categoryQuery.equalTo('cateId', categoryId); const category = await categoryQuery.first({ useMasterKey: true });
|
|
|
+ if (!category || pointerId(category.get('company')) !== companyId) fail(404, '多级字典分类不存在或不属于当前帐套');
|
|
|
+ const aliases = String(category.get('gradeField') || '').split('|').map((value) => value.trim()).filter(Boolean);
|
|
|
+ if (!objectId && parentId > 0) { const parentQuery = new Parse.Query('Grade'); applyTenant(parentQuery, fields, context, input.companyId); parentQuery.equalTo('gradeId', parentId); const parent = await parentQuery.first({ useMasterKey: true }); if (!parent || Number(parent.get('cate')) !== categoryId || pointerId(parent.get('company')) !== companyId) fail(404, '父选项不存在、分类不一致或不属于当前帐套'); level = Number(parent.get('grade')) + 1; }
|
|
|
+ if (!Number.isInteger(level) || level < 1 || level > aliases.length) fail(409, '当前层级已超过分类定义的层级别名');
|
|
|
+ const duplicateQuery = new Parse.Query('Grade'); applyTenant(duplicateQuery, fields, context, input.companyId); duplicateQuery.equalTo('cate', categoryId); duplicateQuery.equalTo('parentId', parentId); duplicateQuery.limit(1000); const duplicates = await duplicateQuery.find({ useMasterKey: true });
|
|
|
+ if (duplicates.some((entry) => entry.id !== objectId && String(entry.get('gradeName') || '').trim().toLowerCase() === gradeName.toLowerCase())) fail(409, '同一父级下的多级字典选项名称不能重复');
|
|
|
+ target.set('gradeName', gradeName); target.set('cate', categoryId); target.set('parentId', parentId); target.set('grade', level); target.set('company', company);
|
|
|
+ if (objectId) { try { await target.save(null, { useMasterKey: true }); } catch (error) { fail(422, '多级字典选项写入失败: ' + String(error && error.message || error)); } }
|
|
|
+ else {
|
|
|
+ let lastError;
|
|
|
+ for (let attempt = 0; attempt < 5; attempt++) {
|
|
|
+ const allOptions = new Parse.Query('Grade'); allOptions.limit(1000); const optionRows = await allOptions.find({ useMasterKey: true }); const nextId = Math.max(0, ...optionRows.map((entry) => Number(entry.get('gradeId')) || 0)) + 1;
|
|
|
+ target.set('gradeId', nextId); target.set('sourceKey', '[["GradeID",' + nextId + ']]');
|
|
|
+ try { await target.save(null, { useMasterKey: true }); lastError = null; break; }
|
|
|
+ catch (error) { lastError = error; if (!(Number(error && error.code) === 137 || /duplicate|unique/i.test(String(error && error.message || '')))) break; }
|
|
|
+ }
|
|
|
+ if (lastError || !target.id) fail(422, '多级字典选项原子编号写入失败: ' + String(lastError && lastError.message || lastError || '无法分配编号'));
|
|
|
+ }
|
|
|
+ await audit({ ...context, company }, objectId ? 'update-grade-option' : 'create-grade-option', 'Grade', target.id);
|
|
|
+ return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'gradeCategories') {
|
|
|
+ const fields = await schemaFor('GradeCate'); const query = new Parse.Query('GradeCate'); applyTenant(query, fields, context, input.companyId); query.ascending('cateId'); query.limit(1000); const results = await query.find({ useMasterKey: true });
|
|
|
+ return response.json({ success: true, data: { results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'gradeOptions') {
|
|
|
+ const categoryId = Number(input.categoryId || 0); const parentId = input.parentId == null || input.parentId === '' ? -1 : Number(input.parentId); if (!Number.isInteger(categoryId) || categoryId < 1 || !Number.isInteger(parentId) || parentId < -1) fail(400, '多级字典筛选条件无效');
|
|
|
+ const fields = await schemaFor('Grade'); const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20)); const query = new Parse.Query('Grade'); applyTenant(query, fields, context, input.companyId); query.equalTo('cate', categoryId); if (parentId >= 0) query.equalTo('parentId', parentId);
|
|
|
+ const search = String(input.search || '').trim(); if (search) query.matches('gradeName', escapeRegex(search), 'i'); const total = await query.count({ useMasterKey: true }); query.ascending('gradeId'); query.skip((page - 1) * pageSize); query.limit(pageSize); const results = await query.find({ useMasterKey: true });
|
|
|
+ return response.json({ success: true, data: { className: 'Grade', page, pageSize, total, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'gradeBatch') {
|
|
|
+ const className = String(input.className || ''); if (!['GradeCate','Grade'].includes(className) || String(input.action || '') !== 'delete') fail(400, '不支持的多级字典批量操作');
|
|
|
+ const requestedIds = Array.isArray(input.objectIds) ? input.objectIds : [input.objectId]; const objectIds = [...new Set(requestedIds.map((value) => String(value || '').trim()).filter(Boolean))]; if (!objectIds.length || objectIds.length > 100) fail(400, '每次请选择 1 至 100 条多级字典记录');
|
|
|
+ const fields = await schemaFor(className); const query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId); query.containedIn('objectId', objectIds); query.limit(objectIds.length); const targets = await query.find({ useMasterKey: true }); if (targets.length !== objectIds.length) fail(404, '部分多级字典记录不存在或不属于当前帐套');
|
|
|
+ if (className === 'GradeCate') { const ids = targets.map((target) => Number(target.get('cateId')) || 0); const optionFields = await schemaFor('Grade'); const refs = new Parse.Query('Grade'); applyTenant(refs, optionFields, context, input.companyId); refs.containedIn('cate', ids); if (await refs.count({ useMasterKey: true })) fail(409, '多级字典分类仍有选项,不能删除'); }
|
|
|
+ else { const ids = targets.map((target) => Number(target.get('gradeId')) || 0); const refs = new Parse.Query('Grade'); applyTenant(refs, fields, context, input.companyId); refs.containedIn('parentId', ids); if (await refs.count({ useMasterKey: true })) fail(409, '多级字典选项仍有下级选项,不能删除'); }
|
|
|
+ await Parse.Object.destroyAll(targets, { useMasterKey: true }); for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'grade-delete', className, target.id);
|
|
|
+ return response.json({ success: true, data: { action: 'delete', updated: targets.length, results: [] } });
|
|
|
+ }
|
|
|
if (operation === 'saveExamClass') {
|
|
|
const objectId = String(input.objectId || '');
|
|
|
const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
@@ -1066,7 +1150,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','categoryname','dicname','cClassName','testPoint','kName','tname','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','categoryname','dicname','cateName','gradeName','cClassName','testPoint','kName','tname','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);
|
|
|
@@ -1091,6 +1175,7 @@ async function handler(request, response) {
|
|
|
if (className === 'QuestionsKnowledge') fail(400, '知识点必须走专用保存流程');
|
|
|
if (className === 'ExTeacher') fail(400, '考试教师必须走专用保存流程');
|
|
|
if (className === 'Datadiccategory' || className === 'Datadic') fail(400, '数据字典必须走专用保存流程');
|
|
|
+ if (className === 'GradeCate' || className === 'Grade') 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);
|
|
|
@@ -1108,6 +1193,7 @@ async function handler(request, response) {
|
|
|
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 === 'Datadiccategory' || className === 'Datadic') { if (className === 'Datadiccategory') { const categoryId = Number(object.get('diccateid')) || 0; const itemFields = await schemaFor('Datadic'); const refs = new Parse.Query('Datadic'); applyTenant(refs, itemFields, context, pointerId(object.get('company'))); refs.equalTo('diccate', categoryId); if (await refs.count({ useMasterKey: true })) fail(409, '字典分类仍有字典项,不能删除'); } await object.destroy({ useMasterKey: true }); await audit(context, 'delete-dictionary', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: false } }); }
|
|
|
+ if (className === 'GradeCate' || className === 'Grade') { const referenceFields = await schemaFor('Grade'); const refs = new Parse.Query('Grade'); applyTenant(refs, referenceFields, context, pointerId(object.get('company'))); if (className === 'GradeCate') refs.equalTo('cate', Number(object.get('cateId')) || 0); else refs.equalTo('parentId', Number(object.get('gradeId')) || 0); if (await refs.count({ useMasterKey: true })) fail(409, className === 'GradeCate' ? '多级字典分类仍有选项,不能删除' : '多级字典选项仍有下级选项,不能删除'); await object.destroy({ useMasterKey: true }); await audit(context, 'delete-grade', 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 } }); }
|