|
|
@@ -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: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', DesignAsk: '问卷调查', DesignQuestion: '问卷题目', DesignAnswer: '问卷答卷', ServiceSeat: '客服席位', Temp: '客服欢迎语', StoreApplication: '店铺管理', StoreStyle: '店铺样式', ContentTagKey: '内容标签词库', UserLevel: '积分等级', UserMoneyLog: '资金积分流水', CRMSAttr: 'CRM 客户类型', SysLog: '系统日志', Agency: '代理机构', DeliveryCenter: '交付中心'
|
|
|
+ GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科审核', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', DesignAsk: '问卷调查', DesignQuestion: '问卷题目', DesignAnswer: '问卷答卷', ServiceSeat: '客服席位', Temp: '客服欢迎语', StoreApplication: '店铺管理', StoreStyle: '店铺样式', ContentTagKey: '内容标签词库', UserLevel: '积分等级', UserMoneyLog: '资金积分流水', 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','UserSIcon','UserUserPoint','UserExpDomP','UserExpHis'
|
|
|
@@ -57,7 +57,8 @@ const CLASS_SYSTEM_FIELDS = {
|
|
|
StoreStyle: new Set(['id','cdate','zstatus','sourceKey']),
|
|
|
UserLevel: new Set(['id','cdate','ztype','addon1','addon2','addon3','addon4','addon5','orderId','storeId','zstatus','sourceKey','discountRate']),
|
|
|
CRMSAttr: new Set(['id','ztype','cdate','zstatus','value2','value3','sourceKey']),
|
|
|
- ShopFareTlp: new Set(['id','cdate','userId','adminId','isFree','regionBan','ems','mail','sourceKey'])
|
|
|
+ ShopFareTlp: new Set(['id','cdate','userId','adminId','isFree','regionBan','ems','mail','sourceKey']),
|
|
|
+ MisType: new Set(['id','createTime','sourceKey'])
|
|
|
};
|
|
|
const USER_MONEY_LOG_CLASSES = { 1:'UserExpDomP', 2:'UserSIcon', 3:'UserExpHis', 4:'UserUserPoint', 5:'UserDummyPoint', 6:'UserCredit' };
|
|
|
const USER_MONEY_LOG_LABELS = { 1:'余额', 2:'银币', 3:'经验', 4:'积分', 5:'虚拟币', 6:'信用分' };
|
|
|
@@ -1389,6 +1390,37 @@ async function handler(request, response) {
|
|
|
Index: '目标 Schema 缺少旧 MarkDown 主表、MDName/MDPath/MDStatus/MDPosition 等历史记录', MD: '目标没有可新建或编辑的 MarkDown 记录', Index_submit: '目标缺少 MarkDown 持久层,当前 Parse 文件适配器也不能替代旧 wwwroot/UploadFiles/MarkDown 本地上传目录', Dels: '目标没有 Markdown 记录与对应服务器文件可同步删除', Preview: '目标缺少 MarkDown 文件、SiteInfo.SiteUrl 与旧 /PreView/md 渲染运行时'
|
|
|
}; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧 Markdown 动作'); fail(501, 'migration_blocked: ' + blockers[action]);
|
|
|
}
|
|
|
+ if (operation === 'legacyMessageBlocker') {
|
|
|
+ const action = String(input.action || ''); const blockers = {
|
|
|
+ Message: '目标 Schema 缺少旧 Message 站内信主表与收发历史;MailTemp 仅是邮件模板,语义不等价', MessageSend: '目标没有可编辑的站内信草稿、收件人、抄送和附件记录', Message_Add: '目标缺少站内信发送与草稿持久层,不能保存 Sender/Incept/CCUser/Attachment', MessageRead: '目标没有站内信详情与已读状态记录', Message_API: '目标没有可删除的站内信记录'
|
|
|
+ }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧站内信动作'); fail(501, 'migration_blocked: ' + blockers[action]);
|
|
|
+ }
|
|
|
+ if (operation === 'legacyMobileBlocker') {
|
|
|
+ const action = String(input.action || ''); const blockers = {
|
|
|
+ Index: '旧动作只有已丢失的 Mobile Razor 视图,发布包没有页面结构、交互或资源基线可恢复', MobileBrower: '旧移动浏览器预览页只有已丢失的 Razor 视图,目标没有对应渲染运行时'
|
|
|
+ }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧 Mobile 动作'); fail(501, 'migration_blocked: ' + blockers[action]);
|
|
|
+ }
|
|
|
+ if (operation === 'legacyNodeRemainderBlocker') {
|
|
|
+ const action = String(input.action || ''); const blockers = {
|
|
|
+ ZhiDing: '旧动作会把帐套内全部 ZStatus=99 栏目强制设为 depth=1、parentId=0,破坏现有规范化栏目树;现有 saveNode/nodeBatch 已在每次移动时安全维护深度', UnionNode_Merge: '旧合并同时改写 CommonModel 与未迁入的 ZL_Commodities 商品节点,并可删除来源栏目;缺商品主表时不能执行非原子半合并'
|
|
|
+ }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧栏目剩余动作'); fail(501, 'migration_blocked: ' + blockers[action]);
|
|
|
+ }
|
|
|
+ if (operation === 'misTypes') {
|
|
|
+ const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20)); const search = String(input.search || '').trim(); if (search.length > 100) fail(400, '流程类型搜索词不能超过 100 个字符'); const fields = await schemaFor('MisType'); const query = new Parse.Query('MisType'); applyTenant(query, fields, context, input.companyId); if (search) query.matches('typeName', escapeRegex(search), 'i'); query.ascending('id'); const total = await query.count({ useMasterKey: true }); query.skip((page - 1) * pageSize); query.limit(pageSize); const results = await query.find({ useMasterKey: true }); return response.json({ success: true, data: { className: 'MisType', page, pageSize, total, results: results.map(serializeObject) } });
|
|
|
+ }
|
|
|
+ if (operation === 'saveMisType') {
|
|
|
+ const objectId = String(input.objectId || ''); const payload = input.fields && typeof input.fields === 'object' ? input.fields : {}; const typeName = String(payload.typeName || '').trim(); const typeDescribe = String(payload.typeDescribe || '').trim(); if (!typeName || typeName.length > 100 || typeDescribe.length > 1000) fail(400, '流程类型名称为必填项且名称或说明过长'); let companyId = pointerId(context.company); if (context.isSuperAdmin && input.companyId) companyId = String(input.companyId); if (!companyId) fail(400, '流程类型必须指定帐套'); const company = await new Parse.Query('Company').get(companyId, { useMasterKey: true }); const fields = await schemaFor('MisType'); let target; let created = false; if (objectId) { const query = new Parse.Query('MisType'); applyTenant(query, fields, context, input.companyId); target = await query.get(objectId, { useMasterKey: true }); companyId = pointerId(target.get('company')) || companyId; } else { target = new Parse.Object('MisType'); target.set('sourceKey', 'cloud:mis-type:' + companyId + ':' + Date.now() + ':' + Math.random().toString(36).slice(2,10)); target.set('company', company); target.set('createTime', new Date()); created = true; }
|
|
|
+ const duplicate = await Psql.oneOrNone('SELECT "objectId" FROM "MisType" WHERE "company"=$1 AND "objectId"<>$2 AND LOWER(TRIM(COALESCE("typeName",\'\')))=LOWER($3) LIMIT 1', [companyId, objectId, typeName]); if (duplicate) fail(409, '同一帐套的流程类型名称不能重复'); target.set('typeName', typeName); target.set('typeDescribe', typeDescribe); try { await target.save(null, { useMasterKey: true }); if (created) { const rows = await Psql.query('WITH lock_row AS MATERIALIZED (SELECT pg_advisory_xact_lock(hashtext(\'xiaoshu-mis-type-id:\'||$1))),next_id AS MATERIALIZED (SELECT COALESCE(MAX("id"),0)+1 AS id FROM "MisType",lock_row WHERE "company"=$1) UPDATE "MisType" SET "id"=next_id.id,"sourceKey"=\'[["ID",\'||next_id.id::text||\']]\',"updatedAt"=NOW() FROM next_id WHERE "objectId"=$2 RETURNING next_id.id', [companyId, target.id]); if (!Number(rows[0] && rows[0].id)) throw new Error('无法分配流程类型编号'); } } catch (error) { if (created && target.id) await target.destroy({ useMasterKey: true }).catch(() => undefined); fail(422, '流程类型保存失败: ' + String(error && error.message || error)); } await target.fetch({ useMasterKey: true }); await audit({ ...context, company: target.get('company') || company }, objectId ? 'update-mis-type' : 'create-mis-type', 'MisType', target.id); return response.json({ success: true, data: serializeObject(target) });
|
|
|
+ }
|
|
|
+ if (operation === 'misTypeBatch') {
|
|
|
+ if (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('MisType'); const query = new Parse.Query('MisType'); 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, '部分流程类型不存在或不属于当前帐套'); for (const target of targets) { const refs = await Psql.one('SELECT COUNT(*)::int AS count FROM "MisProcedure" WHERE "company"=$1 AND COALESCE("typeId",0)=$2', [pointerId(target.get('company')), Number(target.get('id')) || 0]); if (Number(refs.count)) fail(409, '流程类型仍被 OA 流程引用,不能删除'); } await Parse.Object.destroyAll(targets, { useMasterKey: true }); for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'delete-mis-type', 'MisType', target.id); return response.json({ success: true, data: { action: 'delete', updated: targets.length } });
|
|
|
+ }
|
|
|
+ if (operation === 'legacyOABlocker') {
|
|
|
+ const action = String(input.action || ''); const blockers = {
|
|
|
+ Default: '目标缺少旧 OA_AuditFlow 审核流主表与历史数据', FlowInfo: '目标没有旧审核流名称、模型、模板、角色用户和会签配置记录', FlowInfo_Submit: '目标缺少 OA_AuditFlow 持久层,MisProcedure 是不同版本流程结构', Flow_API: '目标没有可删除的旧审核流记录', SelFlowModel: '目标缺少旧审核流可绑定的 Mis_Model 与套红模板关系', FlowItem: '目标缺少 OA_AuditFlowItem 审核步骤表', FlowItemInfo: '目标没有审核步骤、绑定用户、父步骤、会签、转交和退回记录', FlowItemInfo_Submit: '目标缺少审核步骤持久层', FlowItem_API: '目标没有可删除的审核步骤记录',
|
|
|
+ AddMisModel: '目标 DocModel 与旧 Mis_Model 的 ModelContent/BindNode/DocType/WordPath 套红结构不等价', AddMisModel_Submit: '目标缺少旧 Mis_Model 持久层和 FileGuid 套红模板文件运行时', AddSign: '目标 MisSign 是考勤时间表,不是 OA_Sign 签章模型', AddSign_Submit: '目标缺少 OA_Sign 图片、名称、口令、密钥、所属用户与创建人持久层,不能保存明文签章口令', AddSign_Delete: '目标没有可删除的 OA_Sign 签章记录', ApplyManage: '目标缺少 OA_Document 文档申请主表', ApplyManage_Delete: '目标没有可删除的 OA 文档申请记录', MisModelManage: '目标缺少旧 Mis_Model 套红文档模型数据', MisModelManage_Delete: '目标没有可删除的旧套红文档模型', OAConfig: '旧动作只有已丢失的 OAConfig Razor 视图,目标没有对应站点配置模型', SelModelFieds: '目标 Model 数据可只读查看,但旧按 ModelType 选择并回填字段的 OA Razor 交互未迁入', SignManage: '目标缺少 OA_Sign 签章列表;MisSign 是不等价的考勤时间表'
|
|
|
+ }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧 OA 动作'); fail(501, 'migration_blocked: ' + blockers[action]);
|
|
|
+ }
|
|
|
if (operation === 'legacyDesignInteractiveBlocker') {
|
|
|
const action = String(input.action || ''); const blockers = {
|
|
|
InteractiveList: '目标 Schema 缺少旧 Design_Pub 交互提交表及 sceneid、uid、fname 数据;DesignAnswer 和 Pub 语义不等价',
|
|
|
@@ -1862,6 +1894,7 @@ async function handler(request, response) {
|
|
|
if (className === 'UserLevel') fail(400, '积分等级必须走专用保存流程');
|
|
|
if (className === 'CRMSAttr') fail(400, 'CRM 客户类型必须走专用保存流程');
|
|
|
if (className === 'ShopFareTlp') fail(400, '商城运费模板必须走专用保存流程');
|
|
|
+ if (className === 'MisType') fail(400, 'OA 流程类型必须走专用保存流程');
|
|
|
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);
|
|
|
@@ -1881,6 +1914,7 @@ async function handler(request, response) {
|
|
|
if (className === 'StoreApplication' || className === 'StoreStyle') fail(400, '店铺与店铺样式必须走专用批量流程');
|
|
|
if (className === 'CRMSAttr') fail(400, 'CRM 客户类型必须走专用批量流程');
|
|
|
if (className === 'ShopFareTlp') fail(400, '商城运费模板必须走专用批量流程');
|
|
|
+ if (className === 'MisType') fail(400, 'OA 流程类型必须走专用批量流程');
|
|
|
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 } }); }
|