|
|
@@ -15,12 +15,12 @@ const CLASS_LABELS = {
|
|
|
GradeCate: '多级字典分类', Grade: '多级字典选项',
|
|
|
Currency: '货币管理', SysHoliday: '节假日管理',
|
|
|
Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '商城运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
|
|
|
- GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科审核', Pub: '互动模块定义', PubTw: '提问互动提交', PubWTHD: '问题回答提交', PubZXDC: '在线调查提交', Role: '系统角色', ARoleAuth: '角色权限', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', DesignAsk: '问卷调查', DesignQuestion: '问卷题目', DesignAnswer: '问卷答卷', DesignRes: '可视化设计资源', ServiceSeat: '客服席位', Temp: '客服欢迎语', StoreApplication: '店铺管理', StoreStyle: '店铺样式', PageStyle: '黄页样式', PlatComp: '协同办公企业', ContentTagKey: '内容标签词库', UserLevel: '积分等级', UserMoneyLog: '资金积分流水', UserPromotion: '用户推广关系', CRMSAttr: 'CRM 客户类型', SysLog: '系统日志', MisType: 'OA 流程类型', Agency: '代理机构', DeliveryCenter: '交付中心'
|
|
|
+ GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科审核', Pub: '互动模块定义', PubTw: '提问互动提交', PubWTHD: '问题回答提交', PubZXDC: '在线调查提交', Role: '系统角色', ARoleAuth: '角色权限', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', DesignAsk: '问卷调查', DesignQuestion: '问卷题目', DesignAnswer: '问卷答卷', DesignRes: '可视化设计资源', DesignScence: '可视化场景', DesignTlp: '可视化模板', ServiceSeat: '客服席位', Temp: '客服欢迎语', StoreApplication: '店铺管理', StoreStyle: '店铺样式', PageStyle: '黄页样式', PlatComp: '协同办公企业', ContentTagKey: '内容标签词库', UserLevel: '积分等级', UserMoneyLog: '资金积分流水', UserPromotion: '用户推广关系', CRMSAttr: 'CRM 客户类型', SysLog: '系统日志', MisType: 'OA 流程类型', Agency: '代理机构', DeliveryCenter: '交付中心'
|
|
|
};
|
|
|
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','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','DesignTlp','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']);
|
|
|
@@ -52,6 +52,8 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
DesignQuestion: new Set(['id','askId','cdate','cuser','orderId','sourceKey']),
|
|
|
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']),
|
|
|
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']),
|
|
|
@@ -1149,6 +1151,31 @@ async function handler(request, response) {
|
|
|
if (String(input.className || '') !== 'DesignRes' || 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('DesignRes'); const query = new Parse.Query('DesignRes'); 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, '部分设计资源不存在或不属于当前帐套'); await Parse.Object.destroyAll(targets, { useMasterKey: true }); for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'delete-design-resource', 'DesignRes', target.id); return response.json({ success: true, data: { action: 'delete', updated: targets.length, results: [] } });
|
|
|
}
|
|
|
+ if (operation === 'designScenes') {
|
|
|
+ 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 search = String(input.search || '').trim().toLowerCase(); if (![-100,-2].includes(status) || search.length > 100) fail(400, '场景状态或搜索词无效');
|
|
|
+ const fields = await schemaFor('DesignScence'); const query = new Parse.Query('DesignScence'); applyTenant(query, fields, context, input.companyId); query.equalTo('ztype', 0); query.equalTo('tlpId', 0); if (status === -2) query.equalTo('status', -2); else query.notEqualTo('status', -2); query.limit(1000); let results = await query.find({ useMasterKey: true }); if (results.length >= 1000) fail(409, '场景数量超过安全分页上限,请先缩小帐套范围'); results = results.filter((entry) => !search || [entry.get('title'),entry.get('userName'),entry.get('remind'),entry.get('path')].some((value) => String(value || '').toLowerCase().includes(search))); results.sort((left, right) => 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: 'DesignScence', page, pageSize, total, status, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'saveDesignScene') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {}; if (!objectId) fail(501, 'migration_blocked: 旧 SceneAdd 只允许编辑现有场景;新建场景依赖已丢失的可视化设计器初始化协议');
|
|
|
+ const title = String(payload.title || '').trim(); const userId = Number(payload.userId); const score = Number(payload.score == null || payload.score === '' ? 0 : payload.score); const status = Number(payload.status == null || payload.status === '' ? 0 : payload.status); const seflag = String(payload.seflag || '').trim(); const previewImg = String(payload.previewImg || '').trim(); const thumbImg = String(payload.thumbImg || '').trim(); const remind = String(payload.remind || '').trim(); const meta = String(payload.meta || '');
|
|
|
+ if (title.length > 200 || !Number.isInteger(userId) || userId < 1 || !Number.isFinite(score) || Math.abs(score) > 1000000000 || !Number.isInteger(status) || status < -2 || status > 999 || seflag.length > 255 || previewImg.length > 1000 || thumbImg.length > 1000 || remind.length > 2000 || meta.length > 20000) fail(400, '场景标题、用户、积分、状态、图片路径或元数据无效');
|
|
|
+ const fields = await schemaFor('DesignScence'); const query = new Parse.Query('DesignScence'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true }); if (Number(target.get('ztype')) !== 0 || Number(target.get('tlpId')) !== 0) fail(409, '该记录不是可编辑的普通场景'); const companyId = pointerId(target.get('company')) || pointerId(context.company); if (!companyId) fail(409, '场景缺少帐套'); const owner = await Psql.oneOrNone('SELECT "username" FROM "_User" WHERE "company"=$1 AND (COALESCE("legacyUserId",0)=$2 OR CASE WHEN COALESCE("legacyUserData"->>\'UserID\',\'\') ~ \'^[0-9]+$\' THEN ("legacyUserData"->>\'UserID\')::numeric ELSE 0 END=$2) AND COALESCE("isDeleted",FALSE)=FALSE LIMIT 1', [companyId, userId]); if (!owner) fail(404, '场景所属用户不存在或不属于当前帐套');
|
|
|
+ target.set('title', title); target.set('userId', userId); target.set('userName', String(owner.username || '')); target.set('score', score); target.set('status', status); target.set('seflag', seflag); target.set('previewImg', previewImg); target.set('thumbImg', thumbImg); target.set('remind', remind); target.set('meta', meta); target.set('update', new Date()); try { await target.save(null, { useMasterKey: true }); } catch (error) { fail(422, '场景元数据写入失败: ' + String(error && error.message || error)); } await target.fetch({ useMasterKey: true }); await audit({ ...context, company: target.get('company') || context.company }, 'update-design-scene', 'DesignScence', target.id); return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'designSceneBatch') {
|
|
|
+ if (String(input.className || '') !== 'DesignScence') 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('DesignScence'); const query = new Parse.Query('DesignScence'); applyTenant(query, fields, context, input.companyId); query.containedIn('objectId', objectIds); query.equalTo('ztype', 0); query.equalTo('tlpId', 0); query.limit(objectIds.length); const targets = await query.find({ useMasterKey: true }); if (targets.length !== objectIds.length) fail(404, '部分场景不存在、不属于当前帐套或不是普通场景'); if (action === 'delete') await Parse.Object.destroyAll(targets, { useMasterKey: true }); else { for (const target of targets) { target.set('status', action === 'recycle' ? -2 : 0); target.set('update', new Date()); } await Parse.Object.saveAll(targets, { useMasterKey: true }); } for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'design-scene-' + action, 'DesignScence', target.id); return response.json({ success: true, data: { action, updated: targets.length, results: action === 'delete' ? [] : targets.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'createDesignSceneTemplate') {
|
|
|
+ const objectId = String(input.objectId || ''); if (!objectId) fail(400, '缺少场景 objectId'); const fields = await schemaFor('DesignScence'); const query = new Parse.Query('DesignScence'); applyTenant(query, fields, context, input.companyId); const source = await query.get(objectId, { useMasterKey: true }); if (Number(source.get('ztype')) !== 0 || Number(source.get('tlpId')) !== 0) fail(409, '只能将普通场景复制为模板'); const company = source.get('company') || context.company; const companyId = pointerId(company); if (!companyId) fail(409, '场景缺少帐套'); const now = new Date(); const suffix = Date.now() + ':' + Math.random().toString(36).slice(2,10); const template = new Parse.Object('DesignTlp'); let clone = null;
|
|
|
+ template.set('company', company); template.set('sourceKey', 'cloud:design-template:' + companyId + ':' + suffix); template.set('tlpName', String(source.get('title') || '')); template.set('previewImg', String(source.get('previewImg') || '')); template.set('ztype', 1); template.set('zstatus', 0); template.set('score', 0); template.set('price', 0); template.set('isDef', 0); template.set('cdate', now);
|
|
|
+ try {
|
|
|
+ await template.save(null, { useMasterKey: true }); 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, template.id]); const templateId = Number(templateRows[0] && templateRows[0].id); if (!templateId) throw new Error('无法分配模板编号');
|
|
|
+ clone = new Parse.Object('DesignScence'); for (const name of ['comp','meta','page','path','cuser','remind','scence','siteId','orderId','labelArr','resource','thumbImg','userId','userName','previewImg']) { const value = source.get(name); if (value !== undefined && fields[name]) clone.set(name, toParseValue(fields[name], safeValue(value))); } const hex = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).slice(1); clone.set('guid', hex()+hex()+'-'+hex()+'-4'+hex().slice(1)+'-'+hex()+'-'+hex()+hex()+hex()); clone.set('title', String(source.get('title') || '')); clone.set('company', company); clone.set('sourceKey', 'cloud:design-scene-template:' + companyId + ':' + suffix); clone.set('cdate', now); clone.set('update', now); clone.set('tlpId', templateId); clone.set('ztype', 1); clone.set('status', 0); clone.set('score', 0); clone.set('accessPwd', ''); clone.set('seflag', ''); await clone.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, clone.id]); if (!Number(sceneRows[0] && sceneRows[0].id)) throw new Error('无法分配模板场景编号');
|
|
|
+ } 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 === '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);
|
|
|
@@ -1994,7 +2021,7 @@ async function handler(request, response) {
|
|
|
const classWritable = !READ_ONLY_CLASSES.has(className);
|
|
|
if (operation === 'schema') {
|
|
|
const fieldList = Object.entries(fields).filter(([name]) => !HIDDEN_FIELDS.has(name) && !/(?:password|secret|sessiontoken|masterkey|privatekey)/i.test(name)).map(([name, field]) => ({ name, type: field.type, targetClass: field.targetClass, required: field.required === true, writable: classWritable && !isSystemField(className, name) && GENERIC_WRITE_TYPES.has(field.type) }));
|
|
|
- return response.json({ success: true, data: { className, label: CLASS_LABELS[className] || className, fields: fieldList, writable: classWritable, creatable: classWritable && !['CommonModel','Model','ModelField','Guestbook','Currency','SysHoliday','Search','AdZone','AdInfo','FontPicShape','FontPicShapeType','DesignAsk','DesignQuestion','DesignRes','Temp','StoreApplication','StoreStyle','UserLevel'].includes(className), supportsSoftDelete: Boolean(fields.isDeleted) } });
|
|
|
+ return response.json({ success: true, data: { className, label: CLASS_LABELS[className] || className, fields: fieldList, writable: classWritable, creatable: classWritable && !['CommonModel','Model','ModelField','Guestbook','Currency','SysHoliday','Search','AdZone','AdInfo','FontPicShape','FontPicShapeType','DesignAsk','DesignQuestion','DesignRes','DesignScence','Temp','StoreApplication','StoreStyle','UserLevel'].includes(className), supportsSoftDelete: Boolean(fields.isDeleted) } });
|
|
|
}
|
|
|
if (operation === 'list') {
|
|
|
const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20));
|
|
|
@@ -2041,6 +2068,7 @@ async function handler(request, response) {
|
|
|
if (className === 'PageStyle') fail(400, '黄页样式必须走专用保存流程');
|
|
|
if (className === 'PlatComp') fail(400, '协同办公企业必须走专用保存流程');
|
|
|
if (className === 'DesignRes') fail(400, '设计资源必须走专用保存流程');
|
|
|
+ if (className === 'DesignScence') fail(400, '可视化场景必须走专用保存流程');
|
|
|
if (className === 'Role') fail(400, '系统角色必须走专用保存流程');
|
|
|
if (className === 'CommonModel' && !objectId) fail(400, '内容新增必须同时写入模型附表,不能走通用保存');
|
|
|
let object;
|
|
|
@@ -2065,6 +2093,7 @@ async function handler(request, response) {
|
|
|
if (className === 'PageStyle') fail(400, '黄页样式必须走专用批量流程');
|
|
|
if (className === 'PlatComp') fail(400, '协同办公企业必须走专用批量流程');
|
|
|
if (className === 'DesignRes') fail(400, '设计资源必须走专用批量流程');
|
|
|
+ if (className === 'DesignScence') 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 } }); }
|