|
|
@@ -11,6 +11,7 @@ const CLASS_LABELS = {
|
|
|
CourseBinding: '课程绑定', CourseAppointment: '课程预约', LessonRecord: '上课记录', DailyStudyRecord: '每日学习记录', PracticeRecord: '练习记录', MemoryPracticeRecord: '抗遗忘记录',
|
|
|
VocabularyWord: '词库', CommonModel: '通用内容', ContentArticle: '文章内容', ContentPublish: '内容发布', Node: '栏目节点', NodeAuth: '节点权限', Model: '内容模型', ModelField: '模型字段',
|
|
|
ExamClass: '试题分类', ExamSysQuestions: '考试题库', ExamSysPapers: '试卷', ExamType: '考试类型', ExamPoint: '知识点', PaperQuestions: '试卷题目',
|
|
|
+ Datadiccategory: '数据字典分类', Datadic: '数据字典项',
|
|
|
Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
|
|
|
GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科', Search: '搜索记录', Agency: '代理机构', DeliveryCenter: '交付中心'
|
|
|
};
|
|
|
@@ -33,7 +34,9 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
ExamClass: new Set(['cId','cClassid','cOrderBy','cClassType','sourceKey']),
|
|
|
ExamPoint: new Set(['id','tid','orderBy','addUser','addTime','sourceKey']),
|
|
|
QuestionsKnowledge: new Set(['kId','kClassId','kOrderBy','pid','cuser','cdate','sourceKey']),
|
|
|
- ExTeacher: new Set(['id','tclsss','addUser','creatTime','sourceKey'])
|
|
|
+ ExTeacher: new Set(['id','tclsss','addUser','creatTime','sourceKey']),
|
|
|
+ Datadiccategory: new Set(['diccateid','sourceKey']),
|
|
|
+ Datadic: new Set(['dicid','diccate','sourceKey'])
|
|
|
};
|
|
|
|
|
|
function inputOf(request) {
|
|
|
@@ -688,6 +691,112 @@ async function handler(request, response) {
|
|
|
if (operation === 'barMedal') {
|
|
|
fail(501, 'migration_blocked: 目标 Schema 缺少旧 ZL_Guest_Medals 勋章及用户帖子授予关系,无法安全授予或撤销帖子勋章');
|
|
|
}
|
|
|
+ if (operation === 'saveDictionaryCategory') {
|
|
|
+ const objectId = String(input.objectId || '');
|
|
|
+ const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
+ const categoryName = String(payload.categoryname || '').trim();
|
|
|
+ const isUsed = payload.isused == null ? true : payload.isused === true || payload.isused === 'true';
|
|
|
+ if (!categoryName || categoryName.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('Datadiccategory');
|
|
|
+ let target;
|
|
|
+ if (objectId) {
|
|
|
+ const query = new Parse.Query('Datadiccategory'); applyTenant(query, fields, context, input.companyId); target = await query.get(objectId, { useMasterKey: true });
|
|
|
+ companyId = pointerId(target.get('company')) || companyId;
|
|
|
+ } else target = new Parse.Object('Datadiccategory');
|
|
|
+ const duplicateQuery = new Parse.Query('Datadiccategory'); 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('categoryname') || '').trim().toLowerCase() === categoryName.toLowerCase())) fail(409, '同一帐套的字典分类名称不能重复');
|
|
|
+ target.set('categoryname', categoryName); target.set('isused', isUsed); 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 allCategories = new Parse.Query('Datadiccategory'); allCategories.limit(1000);
|
|
|
+ const allItems = new Parse.Query('Datadic'); allItems.limit(1000);
|
|
|
+ const [categoryRows, itemRows] = await Promise.all([allCategories.find({ useMasterKey: true }), allItems.find({ useMasterKey: true })]);
|
|
|
+ const nextId = Math.max(0, ...categoryRows.map((entry) => Number(entry.get('diccateid')) || 0), ...itemRows.map((entry) => Number(entry.get('diccate')) || 0)) + 1;
|
|
|
+ target.set('diccateid', nextId); target.set('sourceKey', '[["Diccateid",' + 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-dictionary-category' : 'create-dictionary-category', 'Datadiccategory', target.id);
|
|
|
+ return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'saveDictionaryItem') {
|
|
|
+ const objectId = String(input.objectId || '');
|
|
|
+ const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
+ const itemName = String(payload.dicname || '').trim();
|
|
|
+ const categoryId = Number(input.categoryId == null ? payload.diccate || 0 : input.categoryId);
|
|
|
+ const isUsed = payload.isused == null ? true : payload.isused === true || payload.isused === 'true';
|
|
|
+ if (!itemName || itemName.length > 50) fail(400, '字典项名称长度应为 1 至 50 位');
|
|
|
+ if (!Number.isInteger(categoryId) || categoryId < 1) 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 fields = await schemaFor('Datadic');
|
|
|
+ let target;
|
|
|
+ if (objectId) {
|
|
|
+ const query = new Parse.Query('Datadic'); applyTenant(query, fields, context, input.companyId); target = await query.get(objectId, { useMasterKey: true });
|
|
|
+ companyId = pointerId(target.get('company')) || companyId;
|
|
|
+ } else target = new Parse.Object('Datadic');
|
|
|
+ const categoryFields = await schemaFor('Datadiccategory'); const categoryQuery = new Parse.Query('Datadiccategory'); applyTenant(categoryQuery, categoryFields, context, input.companyId); categoryQuery.equalTo('diccateid', categoryId);
|
|
|
+ const category = await categoryQuery.first({ useMasterKey: true });
|
|
|
+ if (!category || pointerId(category.get('company')) !== companyId) fail(404, '字典分类不存在或不属于当前帐套');
|
|
|
+ const duplicateQuery = new Parse.Query('Datadic'); applyTenant(duplicateQuery, fields, context, input.companyId); duplicateQuery.equalTo('diccate', categoryId); duplicateQuery.limit(1000);
|
|
|
+ const duplicates = await duplicateQuery.find({ useMasterKey: true });
|
|
|
+ if (duplicates.some((entry) => entry.id !== objectId && String(entry.get('dicname') || '').trim().toLowerCase() === itemName.toLowerCase())) fail(409, '同一字典分类中的字典项名称不能重复');
|
|
|
+ target.set('dicname', itemName); target.set('diccate', categoryId); target.set('isused', isUsed); 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 allItems = new Parse.Query('Datadic'); allItems.limit(1000); const itemRows = await allItems.find({ useMasterKey: true });
|
|
|
+ const nextId = Math.max(0, ...itemRows.map((entry) => Number(entry.get('dicid')) || 0)) + 1;
|
|
|
+ target.set('dicid', nextId); target.set('sourceKey', '[["Dicid",' + 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-dictionary-item' : 'create-dictionary-item', 'Datadic', target.id);
|
|
|
+ return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'dictionaryCategories') {
|
|
|
+ const fields = await schemaFor('Datadiccategory'); const query = new Parse.Query('Datadiccategory'); applyTenant(query, fields, context, input.companyId);
|
|
|
+ if (input.enabledOnly === true) query.equalTo('isused', true);
|
|
|
+ query.ascending('diccateid'); query.limit(1000);
|
|
|
+ const results = await query.find({ useMasterKey: true });
|
|
|
+ return response.json({ success: true, data: { results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'dictionaryBatch') {
|
|
|
+ const className = String(input.className || ''); const action = String(input.action || '');
|
|
|
+ if (!['Datadiccategory','Datadic'].includes(className) || !['enable','disable','delete'].includes(action)) 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 (action === 'delete' && className === 'Datadiccategory') {
|
|
|
+ const companyIds = [...new Set(targets.map((target) => pointerId(target.get('company'))).filter(Boolean))]; const categoryIds = targets.map((target) => Number(target.get('diccateid')) || 0);
|
|
|
+ if (companyIds.length !== 1 || categoryIds.some((id) => id < 1)) fail(409, '字典分类缺少有效帐套或编号');
|
|
|
+ const itemFields = await schemaFor('Datadic'); const refs = new Parse.Query('Datadic'); applyTenant(refs, itemFields, context, companyIds[0]); refs.containedIn('diccate', categoryIds);
|
|
|
+ if (await refs.count({ useMasterKey: true })) fail(409, '字典分类仍有字典项,不能删除');
|
|
|
+ }
|
|
|
+ if (action === 'delete') await Parse.Object.destroyAll(targets, { useMasterKey: true });
|
|
|
+ else { for (const target of targets) target.set('isused', action === 'enable'); await Parse.Object.saveAll(targets, { useMasterKey: true }); }
|
|
|
+ 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 === 'saveExamClass') {
|
|
|
const objectId = String(input.objectId || '');
|
|
|
const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
@@ -957,7 +1066,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','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','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);
|
|
|
@@ -981,6 +1090,7 @@ async function handler(request, response) {
|
|
|
if (className === 'ExamPoint') fail(400, '考点必须走专用保存流程');
|
|
|
if (className === 'QuestionsKnowledge') fail(400, '知识点必须走专用保存流程');
|
|
|
if (className === 'ExTeacher') fail(400, '考试教师必须走专用保存流程');
|
|
|
+ if (className === 'Datadiccategory' || className === 'Datadic') 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);
|
|
|
@@ -997,6 +1107,7 @@ async function handler(request, response) {
|
|
|
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 === '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 === '_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 } }); }
|