|
|
@@ -15,7 +15,7 @@ const CLASS_LABELS = {
|
|
|
GradeCate: '多级字典分类', Grade: '多级字典选项',
|
|
|
Currency: '货币管理', SysHoliday: '节假日管理',
|
|
|
Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '商城运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
|
|
|
- GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科审核', Pub: '互动模块定义', PubTw: '提问互动提交', PubWTHD: '问题回答提交', PubZXDC: '在线调查提交', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', DesignAsk: '问卷调查', DesignQuestion: '问卷题目', DesignAnswer: '问卷答卷', 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: '在线调查提交', 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: '交付中心'
|
|
|
};
|
|
|
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'
|
|
|
@@ -51,6 +51,7 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
DesignAsk: new Set(['id','cdate','cuser','adminId','sourceKey']),
|
|
|
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']),
|
|
|
ServiceSeat: new Set(['sId','sAdminId','sRemrk','sDateTime','sourceKey']),
|
|
|
Temp: new Set(['id','useType','str3','str5','str6','describe','cdate','userId','sourceKey']),
|
|
|
StoreApplication: new Set(['id','userId','userName','addTime','storeState','storeCommendState','storeModelId','sourceKey']),
|
|
|
@@ -1128,6 +1129,24 @@ async function handler(request, response) {
|
|
|
if (String(input.className || '') !== 'FontPicShape' || 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('FontPicShape'); const query = new Parse.Query('FontPicShape'); 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-font-shape', 'FontPicShape', target.id); return response.json({ success: true, data: { action: 'delete', updated: targets.length, results: [] } });
|
|
|
}
|
|
|
+ if (operation === 'designResources') {
|
|
|
+ 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 || '').trim().toLowerCase(); const search = String(input.search || '').trim().toLowerCase(); if (type.length > 32 || search.length > 100) fail(400, '设计资源筛选条件无效');
|
|
|
+ const fields = await schemaFor('DesignRes'); const query = new Parse.Query('DesignRes'); applyTenant(query, fields, context, input.companyId); query.limit(1000); let results = await query.find({ useMasterKey: true }); results = results.filter((entry) => (!type || String(entry.get('ztype') || '').toLowerCase() === type) && (!search || [entry.get('name'),entry.get('useage'),entry.get('vpath'),entry.get('previewimg')].some((value) => String(value || '').toLowerCase().includes(search)))); results.sort((left, right) => Number(left.get('id') || 0) - Number(right.get('id') || 0)); const total = results.length; results = results.slice((page - 1) * pageSize, page * pageSize);
|
|
|
+ return response.json({ success: true, data: { className: 'DesignRes', page, pageSize, total, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'saveDesignResource') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
+ if (!objectId) fail(501, 'migration_blocked: 新增设计资源必须上传原文件并分配 DesignRes.id;当前 PostgreSQL Parse 文件适配器配置错误,且 id 与 Parse 保留属性冲突');
|
|
|
+ if (input.file || input.cover) fail(501, 'migration_blocked: 当前 Parse Server 的 SDK 与 /files REST 接口均错误调用 MongoDB 文件适配器,本项目使用 PostgreSQL;修复服务端文件存储前不能新增或替换设计资源文件');
|
|
|
+ const name = String(payload.name || '').trim(); const ztype = String(payload.ztype || '').trim(); const useage = String(payload.useage || '').trim(); const style = String(payload.style || ''); const use = String(payload.use || ''); const fun = String(payload.fun || ''); const zstatus = Number(payload.zstatus == null || payload.zstatus === '' ? 0 : payload.zstatus);
|
|
|
+ if (!name || name.length > 100 || !/^[a-zA-Z0-9_-]{1,32}$/.test(ztype) || useage.length > 100 || style.length > 20000 || use.length > 20000 || fun.length > 20000 || !Number.isInteger(zstatus) || zstatus < -100 || zstatus > 999) fail(400, '设计资源名称、类型、用途、状态或代码字段无效');
|
|
|
+ const fields = await schemaFor('DesignRes'); const query = new Parse.Query('DesignRes'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true });
|
|
|
+ target.set('name', name); target.set('ztype', ztype); target.set('useage', useage); target.set('style', style); target.set('use', use); target.set('fun', fun); target.set('zstatus', zstatus); 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-resource', 'DesignRes', target.id); return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'designResourceBatch') {
|
|
|
+ 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 === 'legacyFontPicBlocker') {
|
|
|
const action = String(input.action || ''); const blockers = { Index: '目标 Schema 缺少旧 FontPic 成品字体图主表与历史数据', FontPicInfo: '目标 Schema 缺少旧 FontPic 成品字体图详情数据', FontPicInfo_Submit: '目标 Schema 缺少旧 FontPic 成品字体图持久化模型', FontPic_API: '目标 Schema 缺少旧 FontPic 成品字体图可删除记录', Draft: '目标 Schema 缺少旧 FontPicDraft 草稿表与历史数据', DraftInfo: '目标 Schema 缺少旧 FontPicDraft 草稿详情数据', DraftInfo_Submit: '目标 Schema 缺少旧 FontPicDraft 草稿持久化模型', Draft_API: '目标 Schema 缺少旧 FontPicDraft 可删除记录' }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧字体图动作'); fail(501, 'migration_blocked: ' + blockers[action]);
|
|
|
}
|
|
|
@@ -1928,7 +1947,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','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','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));
|
|
|
@@ -1974,6 +1993,7 @@ async function handler(request, response) {
|
|
|
if (className === 'MisType') fail(400, 'OA 流程类型必须走专用保存流程');
|
|
|
if (className === 'PageStyle') fail(400, '黄页样式必须走专用保存流程');
|
|
|
if (className === 'PlatComp') fail(400, '协同办公企业必须走专用保存流程');
|
|
|
+ if (className === 'DesignRes') 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);
|
|
|
@@ -1996,6 +2016,7 @@ async function handler(request, response) {
|
|
|
if (className === 'MisType') fail(400, 'OA 流程类型必须走专用批量流程');
|
|
|
if (className === 'PageStyle') fail(400, '黄页样式必须走专用批量流程');
|
|
|
if (className === 'PlatComp') fail(400, '协同办公企业必须走专用批量流程');
|
|
|
+ if (className === 'DesignRes') 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 } }); }
|
|
|
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 } }); }
|