|
|
@@ -15,7 +15,7 @@ const CLASS_LABELS = {
|
|
|
GradeCate: '多级字典分类', Grade: '多级字典选项',
|
|
|
Currency: '货币管理', SysHoliday: '节假日管理',
|
|
|
Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
|
|
|
- GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科', Search: '搜索记录', Agency: '代理机构', DeliveryCenter: '交付中心'
|
|
|
+ GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科', Search: '后台快捷入口', 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'
|
|
|
@@ -42,7 +42,8 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
GradeCate: new Set(['cateId','sourceKey']),
|
|
|
Grade: new Set(['gradeId','cate','parentId','grade','sourceKey']),
|
|
|
Currency: new Set(['id','sourceKey']),
|
|
|
- SysHoliday: new Set(['id','cdate','cadminId','cuserId','sourceKey'])
|
|
|
+ SysHoliday: new Set(['id','cdate','cadminId','cuserId','sourceKey']),
|
|
|
+ Search: new Set(['id','type','state','time','adminId','orderId','linkType','linkState','sourceKey'])
|
|
|
};
|
|
|
|
|
|
function inputOf(request) {
|
|
|
@@ -932,6 +933,41 @@ async function handler(request, response) {
|
|
|
if (String(input.className || '') !== 'SysHoliday' || 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('SysHoliday'); const query = new Parse.Query('SysHoliday'); 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 }, 'holiday-delete', 'SysHoliday', target.id); return response.json({ success: true, data: { action: 'delete', updated: targets.length, results: [] } });
|
|
|
}
|
|
|
+ if (operation === 'searchNavigations') {
|
|
|
+ const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20)); const type = Number(input.type == null ? 1 : input.type); const state = Number(input.state == null ? -100 : input.state); const elite = Number(input.elite == null ? -100 : input.elite);
|
|
|
+ if (![1,2].includes(type) || ![-100,1,2].includes(state) || ![-100,0,1].includes(elite)) fail(400, '快捷入口筛选条件无效');
|
|
|
+ const fields = await schemaFor('Search'); const query = new Parse.Query('Search'); applyTenant(query, fields, context, input.companyId); query.limit(1000); const search = String(input.search || '').trim().toLowerCase();
|
|
|
+ let results = await query.find({ useMasterKey: true }); results = results.filter((entry) => Number(entry.get('type')) === type && (state === -100 || Number(entry.get('state')) === state) && (elite === -100 || (elite === 1 ? Number(entry.get('eliteLevel')) > 0 : Number(entry.get('eliteLevel')) === 0)) && (!search || String(entry.get('name') || '').toLowerCase().includes(search) || String(entry.get('fileUrl') || '').toLowerCase().includes(search)));
|
|
|
+ results.sort((left, right) => Number(left.get('orderId') || 0) - Number(right.get('orderId') || 0) || 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: 'Search', page, pageSize, total, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'saveSearchNavigation') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
+ if (!objectId) fail(501, 'migration_blocked: Search.id 与 Parse 对象保留 id 冲突,且 PostgreSQL 物理表缺少可写旧 ID 列,不能安全新增快捷入口');
|
|
|
+ const name = String(payload.name || '').trim(); const fileUrl = String(payload.fileUrl || '').trim(); const ico = String(payload.ico || '').trim(); const bkcolor = String(payload.bkcolor || '').trim(); const userGroup = String(payload.userGroup || '').trim();
|
|
|
+ const openType = Number(payload.openType); const mobile = Number(payload.mobile); const size = Number(payload.size); const eliteLevel = Number(payload.eliteLevel);
|
|
|
+ if (!name || name.length > 100) fail(400, '快捷入口名称长度应为 1 至 100 位');
|
|
|
+ if (!fileUrl || fileUrl.length > 500 || !fileUrl.startsWith('/') || fileUrl.startsWith('//') || /^[a-z]+:/i.test(fileUrl)) fail(400, '快捷入口地址必须是 1 至 500 位的站内路径');
|
|
|
+ if (ico.length > 100 || (bkcolor && !/^#[0-9a-f]{6}$/i.test(bkcolor))) fail(400, '图标或背景颜色格式无效');
|
|
|
+ if (![0,1].includes(openType) || ![0,1].includes(mobile) || !Number.isInteger(size) || size < 1 || size > 4 || !Number.isInteger(eliteLevel) || eliteLevel < 0 || eliteLevel > 10) fail(400, '快捷入口展示参数无效');
|
|
|
+ if (userGroup && !/^\d+(,\d+)*$/.test(userGroup)) fail(400, '用户组必须使用逗号分隔的正整数 ID');
|
|
|
+ const fields = await schemaFor('Search'); const query = new Parse.Query('Search'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true });
|
|
|
+ target.set('name', name); target.set('fileUrl', fileUrl); target.set('ico', ico); target.set('openType', openType); target.set('mobile', mobile); target.set('size', size); target.set('bkcolor', bkcolor); target.set('userGroup', userGroup); target.set('eliteLevel', eliteLevel);
|
|
|
+ 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-search-navigation', 'Search', target.id); return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'searchNavigationBatch') {
|
|
|
+ if (String(input.className || '') !== 'Search') fail(400, '只能处理快捷入口'); const action = String(input.action || '');
|
|
|
+ if (action === 'starturl') fail(501, 'migration_blocked: 目标云函数架构没有旧 SiteConfig.SiteOption.Admin_StartUrl 持久层,不能伪造全局后台起始页');
|
|
|
+ if (!['enable','disable','elite','unelite','delete','order'].includes(action)) fail(400, '不支持的快捷入口批量操作');
|
|
|
+ let requestedIds = Array.isArray(input.objectIds) ? input.objectIds : [input.objectId]; let orders = [];
|
|
|
+ if (action === 'order') { orders = Array.isArray(input.orders) ? input.orders.map((entry) => ({ objectId: String(entry && entry.objectId || '').trim(), orderId: Number(entry && entry.orderId) })) : []; if (!orders.length || orders.length > 100 || orders.some((entry) => !entry.objectId || !Number.isInteger(entry.orderId) || entry.orderId < 1 || entry.orderId > 1000000)) fail(400, '排序必须包含 1 至 100 条有效入口及正整数顺序'); requestedIds = orders.map((entry) => entry.objectId); }
|
|
|
+ const objectIds = [...new Set(requestedIds.map((value) => String(value || '').trim()).filter(Boolean))]; if (!objectIds.length || objectIds.length > 100 || (action === 'order' && objectIds.length !== orders.length)) fail(400, '每次请选择 1 至 100 条唯一快捷入口');
|
|
|
+ const fields = await schemaFor('Search'); const query = new Parse.Query('Search'); 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') await Parse.Object.destroyAll(targets, { useMasterKey: true });
|
|
|
+ else { const orderMap = new Map(orders.map((entry) => [entry.objectId, entry.orderId])); for (const target of targets) { if (action === 'enable') target.set('state', 1); else if (action === 'disable') target.set('state', 2); else if (action === 'elite') target.set('eliteLevel', 1); else if (action === 'unelite') target.set('eliteLevel', 0); else target.set('orderId', orderMap.get(target.id)); } await Parse.Object.saveAll(targets, { useMasterKey: true }); }
|
|
|
+ for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'search-navigation-' + action, 'Search', 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 : {};
|
|
|
@@ -1195,7 +1231,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'].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'].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));
|
|
|
@@ -1229,6 +1265,7 @@ async function handler(request, response) {
|
|
|
if (className === 'GradeCate' || className === 'Grade') fail(400, '多级数据字典必须走专用保存流程');
|
|
|
if (className === 'Currency') fail(400, '货币必须走专用保存流程');
|
|
|
if (className === 'SysHoliday') fail(400, '节假日必须走专用保存流程');
|
|
|
+ if (className === 'Search') 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);
|