|
|
@@ -15,7 +15,7 @@ const CLASS_LABELS = {
|
|
|
GradeCate: '多级字典分类', Grade: '多级字典选项',
|
|
|
Currency: '货币管理', SysHoliday: '节假日管理',
|
|
|
Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
|
|
|
- GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', Agency: '代理机构', DeliveryCenter: '交付中心'
|
|
|
+ GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', 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','UserSIcon','UserUserPoint','UserExpDomP','UserExpHis'
|
|
|
@@ -45,7 +45,9 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
SysHoliday: new Set(['id','cdate','cadminId','cuserId','sourceKey']),
|
|
|
Search: new Set(['id','type','state','time','adminId','orderId','linkType','linkState','sourceKey']),
|
|
|
AdZone: new Set(['id','cdate','cadmin','image','sourceKey']),
|
|
|
- AdInfo: new Set(['id','cdate','zoneId','ztype','extend1','extend2','extend3','sourceKey'])
|
|
|
+ AdInfo: new Set(['id','cdate','zoneId','ztype','extend1','extend2','extend3','sourceKey']),
|
|
|
+ FontPicShape: new Set(['id','shape','typeId','userId','userName','sourceKey','createTime','updateTime']),
|
|
|
+ FontPicShapeType: new Set(['id','sourceKey','createTime','updateTime'])
|
|
|
};
|
|
|
|
|
|
function inputOf(request) {
|
|
|
@@ -1052,6 +1054,43 @@ async function handler(request, response) {
|
|
|
const infoFields = await schemaFor('AdInfo'); const infoQuery = new Parse.Query('AdInfo'); applyTenant(infoQuery, infoFields, context, input.companyId); infoQuery.limit(1000); const now = Date.now(); let results = await infoQuery.find({ useMasterKey: true }); results = results.filter((entry) => String(entry.get('zoneId') || '') === zoneId && Number(entry.get('zstatus')) === 99 && (!entry.get('sdate') || new Date(entry.get('sdate')).getTime() <= now) && (!entry.get('edate') || new Date(entry.get('edate')).getTime() >= now)); results.sort((left, right) => Number(right.get('priority') || 0) - Number(left.get('priority') || 0) || Number(left.get('id') || 0) - Number(right.get('id') || 0));
|
|
|
return response.json({ success: true, data: { zone: serializeObject(zone), total: results.length, results: results.map(serializeObject) } });
|
|
|
}
|
|
|
+ if (operation === 'fontShapeTypes') {
|
|
|
+ const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(1000, Math.max(1, Number(input.pageSize) || 20)); const search = String(input.search || '').trim().toLowerCase(); if (search.length > 100) fail(400, '图形分类搜索词过长');
|
|
|
+ const fields = await schemaFor('FontPicShapeType'); const query = new Parse.Query('FontPicShapeType'); applyTenant(query, fields, context, input.companyId); query.limit(1000); let results = await query.find({ useMasterKey: true }); results = results.filter((entry) => !search || [entry.get('name'),entry.get('remarks')].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: 'FontPicShapeType', page, pageSize, total, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'fontShapes') {
|
|
|
+ const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20)); const typeId = Number(input.typeId || 0); const search = String(input.search || '').trim().toLowerCase(); if (!Number.isInteger(typeId) || typeId < 0 || search.length > 100) fail(400, '图形素材筛选条件无效');
|
|
|
+ const fields = await schemaFor('FontPicShape'); const query = new Parse.Query('FontPicShape'); applyTenant(query, fields, context, input.companyId); query.limit(1000); let results = await query.find({ useMasterKey: true }); results = results.filter((entry) => (!typeId || Number(entry.get('typeId')) === typeId) && (!search || [entry.get('name'),entry.get('remarks'),entry.get('userName')].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: 'FontPicShape', page, pageSize, total, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'saveFontShapeType') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {}; if (!objectId) fail(501, 'migration_blocked: FontPicShapeType.id 与 Parse 保留 id 冲突,且省略 id 创建不会生成旧分类编号,不能安全新增图形分类');
|
|
|
+ const name = String(payload.name || '').trim(); const remarks = String(payload.remarks || '').trim(); if (!name || name.length > 100 || remarks.length > 2000) fail(400, '图形分类名称或备注无效');
|
|
|
+ const fields = await schemaFor('FontPicShapeType'); const query = new Parse.Query('FontPicShapeType'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true }); target.set('name', name); target.set('remarks', remarks); target.set('updateTime', 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-font-shape-type', 'FontPicShapeType', target.id); return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'saveFontShape') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {}; if (!objectId) fail(501, 'migration_blocked: FontPicShape.id 与 Parse 保留 id 冲突,且省略 id 创建不会生成旧素材编号,不能安全新增图形素材');
|
|
|
+ if (input.file) fail(501, 'migration_blocked: 当前 Parse Server 的 SDK 与 /files REST 接口均错误调用 MongoDB 文件适配器,而本项目使用 PostgreSQL;修复服务端文件存储配置前不能替换图形文件');
|
|
|
+ const name = String(payload.name || '').trim(); const remarks = String(payload.remarks || '').trim(); const typeId = Number(input.typeId == null ? payload.typeId || 0 : input.typeId); if (!name || name.length > 100 || remarks.length > 2000 || !Number.isInteger(typeId) || typeId < 1) fail(400, '图形素材名称、分类或备注无效');
|
|
|
+ const typeFields = await schemaFor('FontPicShapeType'); const typeQuery = new Parse.Query('FontPicShapeType'); applyTenant(typeQuery, typeFields, context, input.companyId); typeQuery.limit(1000); const types = await typeQuery.find({ useMasterKey: true }); if (!types.some((entry) => Number(entry.get('id')) === typeId)) fail(404, '目标图形分类不存在或不属于当前帐套');
|
|
|
+ const fields = await schemaFor('FontPicShape'); const query = new Parse.Query('FontPicShape'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true });
|
|
|
+ target.set('name', name); target.set('typeId', typeId); target.set('remarks', remarks); target.set('updateTime', 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-font-shape', 'FontPicShape', target.id); return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'fontShapeTypeBatch') {
|
|
|
+ if (String(input.className || '') !== 'FontPicShapeType' || 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('FontPicShapeType'); const query = new Parse.Query('FontPicShapeType'); 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, '部分图形分类不存在或不属于当前帐套'); const typeIds = new Set(targets.map((entry) => Number(entry.get('id'))).filter((value) => value > 0));
|
|
|
+ if (typeIds.size) { const shapeFields = await schemaFor('FontPicShape'); const refsQuery = new Parse.Query('FontPicShape'); applyTenant(refsQuery, shapeFields, context, input.companyId); refsQuery.limit(1000); const refs = await refsQuery.find({ useMasterKey: true }); if (refs.length >= 1000) fail(409, '图形素材数量超过安全核验上限,不能删除分类'); if (refs.some((entry) => typeIds.has(Number(entry.get('typeId'))))) fail(409, '图形分类仍有素材引用,不能删除'); }
|
|
|
+ await Parse.Object.destroyAll(targets, { useMasterKey: true }); for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'delete-font-shape-type', 'FontPicShapeType', target.id); return response.json({ success: true, data: { action: 'delete', updated: targets.length, results: [] } });
|
|
|
+ }
|
|
|
+ if (operation === 'fontShapeBatch') {
|
|
|
+ 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 === '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]);
|
|
|
+ }
|
|
|
if (operation === 'saveExamClass') {
|
|
|
const objectId = String(input.objectId || '');
|
|
|
const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
@@ -1315,7 +1354,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'].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'].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));
|
|
|
@@ -1351,6 +1390,7 @@ async function handler(request, response) {
|
|
|
if (className === 'SysHoliday') fail(400, '节假日必须走专用保存流程');
|
|
|
if (className === 'Search') fail(400, '快捷入口必须走专用保存流程');
|
|
|
if (className === 'AdZone' || className === 'AdInfo') fail(400, '广告位与广告内容必须走专用保存流程');
|
|
|
+ if (className === 'FontPicShape' || className === 'FontPicShapeType') 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);
|
|
|
@@ -1364,6 +1404,7 @@ async function handler(request, response) {
|
|
|
if (!classWritable || !objectId) fail(400, '该记录不允许删除'); const query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId); const object = await query.get(objectId, { useMasterKey: true });
|
|
|
if (className === 'Model' || className === 'ModelField') fail(501, 'migration_blocked: 删除模型或字段需要同步删除 PostgreSQL 物理表/列并验证历史数据,托管云函数中未开放此 DDL 流程');
|
|
|
if (className === 'AdZone' || className === 'AdInfo') fail(400, '广告位与广告内容必须走专用批量删除流程');
|
|
|
+ if (className === 'FontPicShape' || className === 'FontPicShapeType') 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 } }); }
|