|
|
@@ -20,7 +20,7 @@ const CLASS_LABELS = {
|
|
|
const ALLOWED_CLASSES = new Set([
|
|
|
'PageTemplate','PaperQuestions','PayPlat','Permission','PlatComp','App','DesignAnswer','DesignAsk','Attachment','DesignPage','DesignQuestion','DesignRes','Feedback','DesignScence','StudentAchieve','ContentArticle','DesignSiteInfo','Profile','AdInfo','AdZone','ARoleAuth','Baike','ExamSysQuestions','ContactInfo','VocabularyWord','AssessmentProfile','Agency','MemoryPracticeRecord','DeliveryCenter','CourseBinding','PracticeRecord','CourseAppointment','Account','SurveyItem','SurveyLog','LessonRecord','DailyStudyRecord','CommonModel','ContentPublish','Company','_Role','_User','CRMSAttr','Currency','Datadic','Datadiccategory','DesignTlp','DocModel','DocPermission','ExamClass','ExamSysPapers','ExamType','ExamPoint','ExTeacher','FontPicShape','FontPicShapeType','Grade','GradeCate','Group','GroupModel','GuestBar','Guestbook','Guestcate','MailTemp','Manager','MisProcedure','MisProLevel','MisSign','MisType','PageStyle','Model','ModelField','Node','NodeAuth','NodeModelTemplate','Product','PlatUserRole','Pub','PubTw','PubWTHD','PubZXDC','PublishNode','QuestionsKnowledge','Role','StoreProduct','SafeMobile','Search','SenTask','ServiceSeat','ShopFareTlp','ShopMoneyRegular','Special','StoreApplication','StoreStyle','SysCSSManage','SysHoliday','SysLog','Temp','ThirdPlatInfo','UserCredit','UserDummyPoint','UserFriendGroup','UserLevel','UserMoneyLog','UserPromotion','UserSIcon','UserUserPoint','UserExpDomP','UserExpHis'
|
|
|
]);
|
|
|
-const READ_ONLY_CLASSES = new Set(['_Role','Permission','ARoleAuth','DesignTlp','PayPlat','ThirdPlatInfo','GuestBar','DesignAnswer','Baike','ExamSysPapers','ExamSysQuestions','PaperQuestions','ExamType','ContentPublish','PublishNode','SysLog','Pub','PubTw','PubWTHD','PubZXDC']);
|
|
|
+const READ_ONLY_CLASSES = new Set(['_Role','Permission','ARoleAuth','PayPlat','ThirdPlatInfo','GuestBar','DesignAnswer','Baike','ExamSysPapers','ExamSysQuestions','PaperQuestions','ExamType','ContentPublish','PublishNode','SysLog','Pub','PubTw','PubWTHD','PubZXDC']);
|
|
|
const SYSTEM_FIELDS = new Set(['objectId','createdAt','updatedAt','ACL','company','password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','adminRoleKey','isAdmin','roles','role','isDisabled','legacyUserId','legacyGroupId','legacyUserData','legacyUserPlat','groupId']);
|
|
|
const HIDDEN_FIELDS = new Set(['password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','apiKey','appSecret','secret','token']);
|
|
|
const GENERIC_WRITE_TYPES = new Set(['String','Number','Boolean','Date','Pointer','Object','Array']);
|
|
|
@@ -53,7 +53,7 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
DesignAnswer: new Set(['id','askId','ip','cdate','userId','sourceKey']),
|
|
|
DesignRes: new Set(['id','vpath','previewimg','cdate','userid','sourceKey']),
|
|
|
DesignScence: new Set(['id','guid','comp','page','path','cdate','cuser','tlpId','ztype','scence','siteId','update','company','orderId','labelArr','resource','userName','accessPwd','sourceKey']),
|
|
|
- DesignTlp: new Set(['id','cdate','defBy','isDef','price','score','ztype','classId','company','sourceKey']),
|
|
|
+ DesignTlp: new Set(['id','cdate','isDef','ztype','company','sourceKey']),
|
|
|
Role: new Set(['roleId','ztype','zstatus','nodeId','auth','auth2','auth3','cadminId','cdate','sourceKey']),
|
|
|
ARoleAuth: new Set(['id','rid','adminId','sourceKey']),
|
|
|
ServiceSeat: new Set(['sId','sAdminId','sRemrk','sDateTime','sourceKey']),
|
|
|
@@ -1176,6 +1176,34 @@ async function handler(request, response) {
|
|
|
} catch (error) { if (clone && clone.id) await clone.destroy({ useMasterKey: true }).catch(() => undefined); if (template.id) await template.destroy({ useMasterKey: true }).catch(() => undefined); fail(422, '场景复制为模板失败: ' + String(error && error.message || error)); }
|
|
|
await Promise.all([template.fetch({ useMasterKey: true }), clone.fetch({ useMasterKey: true })]); await audit({ ...context, company }, 'create-template-from-design-scene', 'DesignScence', source.id); return response.json({ success: true, data: { template: serializeObject(template), scene: serializeObject(clone) } });
|
|
|
}
|
|
|
+ if (operation === 'designTemplates') {
|
|
|
+ const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20)); const status = Number(input.status == null ? -100 : input.status); const classId = Number(input.classId == null ? -100 : input.classId); const search = String(input.search || '').trim().toLowerCase(); if (![-100,-2].includes(status) || !Number.isInteger(classId) || classId < -100 || search.length > 100) fail(400, '模板状态、分类编号或搜索词无效');
|
|
|
+ const fields = await schemaFor('DesignTlp'); const query = new Parse.Query('DesignTlp'); applyTenant(query, fields, context, input.companyId); if (status === -2) query.equalTo('zstatus', -2); else query.notEqualTo('zstatus', -2); if (classId !== -100) query.equalTo('classId', classId); query.limit(1000); let results = await query.find({ useMasterKey: true }); if (results.length >= 1000) fail(409, '模板数量超过安全分页上限,请缩小帐套或分类范围'); results = results.filter((entry) => !search || [entry.get('tlpName'),entry.get('remind'),entry.get('defBy'),entry.get('previewImg')].some((value) => String(value || '').toLowerCase().includes(search))); results.sort((left, right) => Number(right.get('zstatus') || 0) - Number(left.get('zstatus') || 0) || Number(right.get('id') || 0) - Number(left.get('id') || 0)); const total = results.length; results = results.slice((page - 1) * pageSize, page * pageSize);
|
|
|
+ return response.json({ success: true, data: { className: 'DesignTlp', page, pageSize, total, status, classId, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'designTemplate') {
|
|
|
+ const objectId = String(input.objectId || ''); if (!objectId) fail(400, '缺少模板 objectId'); const fields = await schemaFor('DesignTlp'); const query = new Parse.Query('DesignTlp'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true }); const templateId = Number(target.get('id')) || 0; const sceneFields = await schemaFor('DesignScence'); const sceneQuery = new Parse.Query('DesignScence'); applyTenant(sceneQuery, sceneFields, { ...context, company: target.get('company') || context.company }, pointerId(target.get('company'))); sceneQuery.equalTo('tlpId', templateId); sceneQuery.equalTo('ztype', 1); sceneQuery.ascending('id'); sceneQuery.limit(1); const scene = templateId ? await sceneQuery.first({ useMasterKey: true }) : null;
|
|
|
+ return response.json({ success: true, data: { ...serializeObject(target), designScene: scene ? serializeObject(scene) : null } });
|
|
|
+ }
|
|
|
+ if (operation === 'designTemplateEditorTarget') {
|
|
|
+ const objectId = String(input.objectId || ''); if (!objectId) fail(400, '缺少模板 objectId'); const fields = await schemaFor('DesignTlp'); const query = new Parse.Query('DesignTlp'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true }); const templateId = Number(target.get('id')) || 0; if (!templateId) fail(409, '模板缺少旧数字 ID'); const sceneFields = await schemaFor('DesignScence'); const sceneQuery = new Parse.Query('DesignScence'); applyTenant(sceneQuery, sceneFields, { ...context, company: target.get('company') || context.company }, pointerId(target.get('company'))); sceneQuery.equalTo('tlpId', templateId); sceneQuery.equalTo('ztype', 1); sceneQuery.ascending('id'); sceneQuery.limit(1); const scene = await sceneQuery.first({ useMasterKey: true }); const guid = String(scene && scene.get('guid') || ''); if (!scene || !guid) fail(409, '模板没有可定位的设计场景 GUID');
|
|
|
+ return response.json({ success: true, data: { templateObjectId: target.id, templateId, sceneObjectId: scene.id, guid, legacyPath: '/design/default?id=' + encodeURIComponent(guid), available: false, reason: '旧可视化设计器前端资产、布局交互和渲染运行时未迁入 Angular' } });
|
|
|
+ }
|
|
|
+ if (operation === 'saveDesignTemplate') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {}; const tlpName = String(payload.tlpName || '').trim(); const previewImg = String(payload.previewImg || '').trim(); const remind = String(payload.remind || '').trim(); const defBy = String(payload.defBy || '').trim(); const classId = Number(payload.classId == null || payload.classId === '' ? 0 : payload.classId); const price = Number(payload.price == null || payload.price === '' ? 0 : payload.price); const score = Number(payload.score == null || payload.score === '' ? 0 : payload.score); const zstatus = Number(payload.zstatus == null || payload.zstatus === '' ? 0 : payload.zstatus);
|
|
|
+ if (!tlpName || tlpName.length > 200 || previewImg.length > 1000 || remind.length > 2000 || defBy.length > 100 || !Number.isInteger(classId) || classId < 0 || classId > 1000000000 || !Number.isFinite(price) || price < 0 || price > 1000000000 || !Number.isFinite(score) || Math.abs(score) > 1000000000 || !Number.isInteger(zstatus) || zstatus < -2 || zstatus > 999) 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('DesignTlp'); let target; let scene = null; const created = !objectId;
|
|
|
+ if (objectId) { const query = new Parse.Query('DesignTlp'); applyTenant(query, fields, context, input.companyId); target = await query.get(objectId, { useMasterKey: true }); companyId = pointerId(target.get('company')) || companyId; } else { target = new Parse.Object('DesignTlp'); target.set('company', company); target.set('sourceKey', 'cloud:design-template:' + companyId + ':' + Date.now() + ':' + Math.random().toString(36).slice(2,10)); target.set('cdate', new Date()); target.set('isDef', 0); }
|
|
|
+ target.set('tlpName', tlpName); target.set('classId', classId); target.set('ztype', 1); target.set('price', price); target.set('previewImg', previewImg); target.set('score', score); target.set('zstatus', zstatus); target.set('defBy', defBy); target.set('remind', remind);
|
|
|
+ try {
|
|
|
+ await target.save(null, { useMasterKey: true }); if (created) { const templateRows = await Psql.query('WITH lock_row AS MATERIALIZED (SELECT pg_advisory_xact_lock(hashtext(\'xiaoshu-design-template-id:\'||$1))),next_id AS MATERIALIZED (SELECT COALESCE(MAX("id"),0)+1 AS id FROM "DesignTlp",lock_row WHERE "company"=$1) UPDATE "DesignTlp" SET "id"=next_id.id,"sourceKey"=\'[["ID",\'||next_id.id::text||\']]\',"updatedAt"=NOW() FROM next_id WHERE "objectId"=$2 RETURNING next_id.id', [companyId, target.id]); const templateId = Number(templateRows[0] && templateRows[0].id); if (!templateId) throw new Error('无法分配模板编号'); const now = new Date(); const hex = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).slice(1); scene = new Parse.Object('DesignScence'); scene.set('company', target.get('company') || company); scene.set('sourceKey', 'cloud:design-template-scene:' + companyId + ':' + templateId + ':' + Date.now()); scene.set('tlpId', templateId); scene.set('ztype', 1); scene.set('status', 0); scene.set('previewImg', previewImg); scene.set('guid', hex()+hex()+'-'+hex()+'-4'+hex().slice(1)+'-'+hex()+'-'+hex()+hex()+hex()); scene.set('cdate', now); scene.set('update', now); scene.set('score', 0); scene.set('userId', 0); scene.set('userName', ''); await scene.save(null, { useMasterKey: true }); const sceneRows = await Psql.query('WITH lock_row AS MATERIALIZED (SELECT pg_advisory_xact_lock(hashtext(\'xiaoshu-design-scene-id:\'||$1))),next_id AS MATERIALIZED (SELECT COALESCE(MAX("id"),0)+1 AS id FROM "DesignScence",lock_row WHERE "company"=$1) UPDATE "DesignScence" SET "id"=next_id.id,"sourceKey"=\'[["ID",\'||next_id.id::text||\']]\',"updatedAt"=NOW() FROM next_id WHERE "objectId"=$2 RETURNING next_id.id', [companyId, scene.id]); if (!Number(sceneRows[0] && sceneRows[0].id)) throw new Error('无法分配模板场景编号'); } }
|
|
|
+ catch (error) { if (created && scene && scene.id) await scene.destroy({ useMasterKey: true }).catch(() => undefined); if (created && target.id) await target.destroy({ useMasterKey: true }).catch(() => undefined); fail(422, '模板保存失败: ' + String(error && error.message || error)); }
|
|
|
+ await target.fetch({ useMasterKey: true }); if (scene) await scene.fetch({ useMasterKey: true }); await audit({ ...context, company: target.get('company') || company }, created ? 'create-design-template' : 'update-design-template', 'DesignTlp', target.id); return response.json({ success: true, data: { ...serializeObject(target), designScene: scene ? serializeObject(scene) : null } });
|
|
|
+ }
|
|
|
+ if (operation === 'designTemplateBatch') {
|
|
|
+ if (String(input.className || '') !== 'DesignTlp') fail(400, '模板批量操作的数据类无效'); const action = String(input.action || ''); if (!['recycle','restore','delete'].includes(action)) fail(400, '不支持的模板批量操作'); const objectIds = [...new Set((Array.isArray(input.objectIds) ? input.objectIds : [input.objectId]).map((value) => String(value || '').trim()).filter(Boolean))]; if (!objectIds.length || objectIds.length > 100) fail(400, '每次请选择 1 至 100 个模板'); const fields = await schemaFor('DesignTlp'); const query = new Parse.Query('DesignTlp'); 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, '部分模板不存在或不属于当前帐套'); let deletedScenes = 0;
|
|
|
+ if (action === 'delete') { const sceneFields = await schemaFor('DesignScence'); const scenes = []; for (const target of targets) { const templateId = Number(target.get('id')) || 0; if (!templateId) fail(409, '模板缺少旧数字 ID'); const sceneQuery = new Parse.Query('DesignScence'); applyTenant(sceneQuery, sceneFields, { ...context, company: target.get('company') || context.company }, pointerId(target.get('company'))); sceneQuery.equalTo('tlpId', templateId); sceneQuery.equalTo('ztype', 1); sceneQuery.limit(1000); const linked = await sceneQuery.find({ useMasterKey: true }); if (linked.length >= 1000) fail(409, '模板关联场景超过安全删除上限'); scenes.push(...linked); } if (scenes.length) { await Parse.Object.destroyAll(scenes, { useMasterKey: true }); deletedScenes = scenes.length; } await Parse.Object.destroyAll(targets, { useMasterKey: true }); } else { for (const target of targets) target.set('zstatus', action === 'recycle' ? -2 : 0); await Parse.Object.saveAll(targets, { useMasterKey: true }); } for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'design-template-' + action, 'DesignTlp', target.id); return response.json({ success: true, data: { action, updated: targets.length, deletedScenes, results: action === 'delete' ? [] : targets.map(serializeObject) } });
|
|
|
+ }
|
|
|
if (operation === 'roles') {
|
|
|
const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20)); const type = String(input.type || 'admin').trim().toLowerCase(); const search = String(input.search || '').trim().toLowerCase(); if (!['admin','user','-100'].includes(type) || search.length > 100) fail(400, '角色类型或搜索词无效');
|
|
|
const fields = await schemaFor('Role'); const query = new Parse.Query('Role'); applyTenant(query, fields, context, input.companyId); query.limit(1000); let results = await query.find({ useMasterKey: true }); results = results.filter((entry) => (type === '-100' || String(entry.get('ztype') || 'admin').toLowerCase() === type) && (!search || [entry.get('roleName'),entry.get('description')].some((value) => String(value || '').toLowerCase().includes(search)))); results.sort((left, right) => Number(left.get('roleId') || 0) - Number(right.get('roleId') || 0)); const total = results.length; results = results.slice((page - 1) * pageSize, page * pageSize);
|
|
|
@@ -2069,6 +2097,7 @@ async function handler(request, response) {
|
|
|
if (className === 'PlatComp') fail(400, '协同办公企业必须走专用保存流程');
|
|
|
if (className === 'DesignRes') fail(400, '设计资源必须走专用保存流程');
|
|
|
if (className === 'DesignScence') fail(400, '可视化场景必须走专用保存流程');
|
|
|
+ if (className === 'DesignTlp') fail(400, '可视化模板必须走专用保存流程');
|
|
|
if (className === 'Role') fail(400, '系统角色必须走专用保存流程');
|
|
|
if (className === 'CommonModel' && !objectId) fail(400, '内容新增必须同时写入模型附表,不能走通用保存');
|
|
|
let object;
|
|
|
@@ -2094,6 +2123,7 @@ async function handler(request, response) {
|
|
|
if (className === 'PlatComp') fail(400, '协同办公企业必须走专用批量流程');
|
|
|
if (className === 'DesignRes') fail(400, '设计资源必须走专用批量流程');
|
|
|
if (className === 'DesignScence') fail(400, '可视化场景必须走专用批量流程');
|
|
|
+ if (className === 'DesignTlp') fail(400, '可视化模板必须走专用批量流程');
|
|
|
if (className === 'Role') fail(400, '系统角色必须走专用批量流程');
|
|
|
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 } }); }
|