|
|
@@ -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: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', 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'
|
|
|
@@ -43,7 +43,9 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
Grade: new Set(['gradeId','cate','parentId','grade','sourceKey']),
|
|
|
Currency: new Set(['id','sourceKey']),
|
|
|
SysHoliday: new Set(['id','cdate','cadminId','cuserId','sourceKey']),
|
|
|
- Search: new Set(['id','type','state','time','adminId','orderId','linkType','linkState','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'])
|
|
|
};
|
|
|
|
|
|
function inputOf(request) {
|
|
|
@@ -989,6 +991,67 @@ async function handler(request, response) {
|
|
|
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 === 'adZones') {
|
|
|
+ const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(1000, Math.max(1, Number(input.pageSize) || 20)); const status = Number(input.status == null ? -100 : input.status); const type = String(input.type || '').trim(); const search = String(input.search || '').trim().toLowerCase();
|
|
|
+ if (![-100,0,99].includes(status) || type.length > 50 || search.length > 100) fail(400, '广告位筛选条件无效');
|
|
|
+ const fields = await schemaFor('AdZone'); const query = new Parse.Query('AdZone'); applyTenant(query, fields, context, input.companyId); query.limit(1000); let results = await query.find({ useMasterKey: true });
|
|
|
+ results = results.filter((entry) => (status === -100 || Number(entry.get('zstatus')) === status) && (!type || String(entry.get('ztype') || '') === type) && (!search || [entry.get('name'),entry.get('remind'),entry.get('ztype')].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: 'AdZone', page, pageSize, total, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'adInfos') {
|
|
|
+ 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 zoneId = String(input.zoneId || '').trim(); const search = String(input.search || '').trim().toLowerCase();
|
|
|
+ if (![-100,0,99].includes(status) || (zoneId && !/^\d+$/.test(zoneId)) || search.length > 100) fail(400, '广告内容筛选条件无效');
|
|
|
+ const fields = await schemaFor('AdInfo'); const query = new Parse.Query('AdInfo'); applyTenant(query, fields, context, input.companyId); query.limit(1000); let results = await query.find({ useMasterKey: true });
|
|
|
+ results = results.filter((entry) => (status === -100 || Number(entry.get('zstatus')) === status) && (!zoneId || String(entry.get('zoneId') || '') === zoneId) && (!search || [entry.get('name'),entry.get('linkUrl'),entry.get('text'),entry.get('remind')].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: 'AdInfo', page, pageSize, total, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'saveAdZone') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
+ if (!objectId) fail(501, 'migration_blocked: AdZone.id 与 Parse 对象保留 id 冲突,且省略 id 创建不会生成旧广告位编号,不能安全新增广告位');
|
|
|
+ const name = String(payload.name || '').trim(); const remind = String(payload.remind || '').trim(); const ztype = String(payload.ztype || '').trim(); const showType = String(payload.showType || '').trim(); const zstatus = Number(payload.zstatus);
|
|
|
+ if (!name || name.length > 100 || remind.length > 2000 || !ztype || ztype.length > 50 || !showType || showType.length > 50 || ![0,99].includes(zstatus)) fail(400, '广告位名称、类型、展示方式、状态或备注无效');
|
|
|
+ const fields = await schemaFor('AdZone'); const query = new Parse.Query('AdZone'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true });
|
|
|
+ target.set('name', name); target.set('remind', remind); target.set('ztype', ztype); target.set('showType', showType); target.set('zstatus', zstatus); const adminId = Number(context.current.get('legacyUserId')) || 0; if (adminId) target.set('cadmin', adminId);
|
|
|
+ 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-ad-zone', 'AdZone', target.id); return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'saveAdInfo') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
+ if (!objectId) fail(501, 'migration_blocked: AdInfo.id 与 Parse 对象保留 id 冲突,且省略 id 创建不会生成旧广告编号,不能安全新增广告内容');
|
|
|
+ const name = String(payload.name || '').trim(); const zoneId = String(input.zoneId == null ? payload.zoneId || '' : input.zoneId).trim(); const hasPriority = Object.prototype.hasOwnProperty.call(payload, 'priority'); const priority = hasPriority ? Number(payload.priority) : 0; const zstatus = Number(payload.zstatus);
|
|
|
+ const valueOfDate = (value) => value && typeof value === 'object' && value.iso ? value.iso : value; const sdate = new Date(valueOfDate(payload.sdate)); const edate = new Date(valueOfDate(payload.edate));
|
|
|
+ const strings = { linkUrl: String(payload.linkUrl || '').trim(), text: String(payload.text || ''), html: String(payload.html || ''), images: String(payload.images || '').trim(), image: String(payload.image || '').trim(), remind: String(payload.remind || '').trim(), style: String(payload.style || '') };
|
|
|
+ if (!name || name.length > 100 || !/^\d+$/.test(zoneId) || (hasPriority && (String(payload.priority).trim() === '' || !Number.isInteger(priority) || priority < 0 || priority > 1000000)) || ![0,99].includes(zstatus)) fail(400, '广告名称、广告位、权重或状态无效');
|
|
|
+ if (Number.isNaN(sdate.getTime()) || Number.isNaN(edate.getTime()) || edate < sdate) fail(400, '广告到期时间必须大于或等于开始时间');
|
|
|
+ if (strings.linkUrl.length > 2000 || strings.text.length > 20000 || strings.html.length > 200000 || strings.images.length > 10000 || strings.image.length > 2000 || strings.remind.length > 2000 || strings.style.length > 10000) fail(400, '广告内容字段超过允许长度');
|
|
|
+ const zoneFields = await schemaFor('AdZone'); const zoneQuery = new Parse.Query('AdZone'); applyTenant(zoneQuery, zoneFields, context, input.companyId); zoneQuery.limit(1000); const zones = await zoneQuery.find({ useMasterKey: true }); if (!zones.some((entry) => String(entry.get('id') || '') === zoneId)) fail(404, '目标广告位不存在或不属于当前帐套');
|
|
|
+ const fields = await schemaFor('AdInfo'); const query = new Parse.Query('AdInfo'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true });
|
|
|
+ target.set('name', name); target.set('zoneId', zoneId); if (hasPriority) target.set('priority', String(priority)); target.set('zstatus', zstatus); target.set('sdate', sdate); target.set('edate', edate); for (const [field, value] of Object.entries(strings)) if (Object.prototype.hasOwnProperty.call(payload, field)) target.set(field, value);
|
|
|
+ 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-ad-info', 'AdInfo', target.id); return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'adZoneBatch') {
|
|
|
+ if (String(input.className || '') !== 'AdZone') fail(400, '只能处理广告位'); const action = String(input.action || ''); if (!['active','pause','delete'].includes(action)) 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('AdZone'); const query = new Parse.Query('AdZone'); 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') { const zoneIds = new Set(targets.map((target) => String(target.get('id') || ''))); const infoFields = await schemaFor('AdInfo'); const refsQuery = new Parse.Query('AdInfo'); applyTenant(refsQuery, infoFields, context, input.companyId); refsQuery.limit(1000); const refs = await refsQuery.find({ useMasterKey: true }); if (refs.length >= 1000) fail(409, '广告内容数量超过安全核验上限,不能删除广告位'); if (refs.some((entry) => zoneIds.has(String(entry.get('zoneId') || '')))) fail(409, '广告位仍有关联广告内容,不能删除'); await Parse.Object.destroyAll(targets, { useMasterKey: true }); }
|
|
|
+ else { for (const target of targets) target.set('zstatus', action === 'active' ? 99 : 0); await Parse.Object.saveAll(targets, { useMasterKey: true }); }
|
|
|
+ for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'ad-zone-' + action, 'AdZone', target.id); return response.json({ success: true, data: { action, updated: targets.length, results: action === 'delete' ? [] : targets.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'adInfoBatch') {
|
|
|
+ if (String(input.className || '') !== 'AdInfo') fail(400, '只能处理广告内容'); const action = String(input.action || ''); if (!['audit','unaudit','delete'].includes(action)) 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('AdInfo'); const query = new Parse.Query('AdInfo'); 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 { for (const target of targets) target.set('zstatus', action === 'audit' ? 99 : 0); await Parse.Object.saveAll(targets, { useMasterKey: true }); }
|
|
|
+ for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'ad-info-' + action, 'AdInfo', target.id); return response.json({ success: true, data: { action, updated: targets.length, results: action === 'delete' ? [] : targets.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'adPreview') {
|
|
|
+ const objectId = String(input.zoneObjectId || ''); if (!objectId) fail(400, '缺少广告位 objectId'); const zoneFields = await schemaFor('AdZone'); const zoneQuery = new Parse.Query('AdZone'); applyTenant(zoneQuery, zoneFields, context, input.companyId); const zone = await zoneQuery.get(objectId, { useMasterKey: true }); const zoneId = String(zone.get('id') || '');
|
|
|
+ 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 === 'saveExamClass') {
|
|
|
const objectId = String(input.objectId || '');
|
|
|
const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
@@ -1252,7 +1315,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'].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'].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));
|
|
|
@@ -1287,6 +1350,7 @@ async function handler(request, response) {
|
|
|
if (className === 'Currency') fail(400, '货币必须走专用保存流程');
|
|
|
if (className === 'SysHoliday') fail(400, '节假日必须走专用保存流程');
|
|
|
if (className === 'Search') fail(400, '快捷入口必须走专用保存流程');
|
|
|
+ if (className === 'AdZone' || className === 'AdInfo') 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);
|
|
|
@@ -1299,6 +1363,7 @@ async function handler(request, response) {
|
|
|
if (operation === 'delete') {
|
|
|
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 === '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 } }); }
|