|
|
@@ -21,6 +21,7 @@ const READ_ONLY_CLASSES = new Set(['_Role','Permission','PayPlat','ThirdPlatInfo
|
|
|
const SYSTEM_FIELDS = new Set(['objectId','createdAt','updatedAt','ACL','company','password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','adminRoleKey','isAdmin','roles','role','isDisabled','legacyUserId','legacyGroupId','legacyUserData','groupId']);
|
|
|
const HIDDEN_FIELDS = new Set(['password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','apiKey','appSecret','secret','token']);
|
|
|
const GENERIC_WRITE_TYPES = new Set(['String','Number','Boolean','Date','Pointer','Object','Array']);
|
|
|
+const CLASS_SYSTEM_FIELDS = { CommonModel: new Set(['generalId','modelId','nodeId','itemId','tableName','status','isDeleted','sourceKey','orderId']) };
|
|
|
|
|
|
function inputOf(request) {
|
|
|
const body = request.body || {};
|
|
|
@@ -82,6 +83,7 @@ async function revokeSessions(targets) {
|
|
|
function assertClass(className) {
|
|
|
if (!ALLOWED_CLASSES.has(className)) fail(400, '不允许访问该数据类');
|
|
|
}
|
|
|
+function isSystemField(className, name) { return SYSTEM_FIELDS.has(name) || Boolean(CLASS_SYSTEM_FIELDS[className] && CLASS_SYSTEM_FIELDS[className].has(name)); }
|
|
|
async function schemaFor(className) {
|
|
|
assertClass(className);
|
|
|
const schema = await new Parse.Schema(className).get({ useMasterKey: true });
|
|
|
@@ -309,13 +311,46 @@ async function handler(request, response) {
|
|
|
await target.fetch({ useMasterKey: true }); await audit({ ...context, company }, objectId ? 'update-group' : 'create-group', 'Group', target.id);
|
|
|
return response.json({ success: true, data: serializeObject(target) });
|
|
|
}
|
|
|
+ if (operation === 'contentBatch') {
|
|
|
+ const action = String(input.action || '');
|
|
|
+ if (!['status','recycle','recover','move'].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 contentFields = await schemaFor('CommonModel');
|
|
|
+ const query = new Parse.Query('CommonModel'); applyTenant(query, contentFields, 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, '部分内容不存在或不属于当前帐套');
|
|
|
+ let status = null;
|
|
|
+ let nodeId = null;
|
|
|
+ if (action === 'status') {
|
|
|
+ status = Number(input.status);
|
|
|
+ if (![-3,-1,0,99].includes(status)) fail(400, '不支持的内容状态');
|
|
|
+ } else if (action === 'recycle') status = -2;
|
|
|
+ else if (action === 'recover') status = 0;
|
|
|
+ else {
|
|
|
+ nodeId = Number(input.nodeId);
|
|
|
+ if (!Number.isInteger(nodeId) || nodeId < 1) fail(400, '请选择有效目标节点');
|
|
|
+ const companyIds = [...new Set(targets.map((target) => pointerId(target.get('company'))).filter(Boolean))];
|
|
|
+ if (companyIds.length !== 1) fail(400, '批量移动的内容必须属于同一帐套');
|
|
|
+ const node = await Psql.oneOrNone('SELECT "objectId" FROM "Node" WHERE "company"=$1 AND "nodeId"=$2 LIMIT 1', [companyIds[0], nodeId]);
|
|
|
+ if (!node) fail(404, '目标节点不存在或不属于当前帐套');
|
|
|
+ }
|
|
|
+ for (const target of targets) {
|
|
|
+ if (status !== null) target.set('status', status);
|
|
|
+ if (nodeId !== null) target.set('nodeId', nodeId);
|
|
|
+ }
|
|
|
+ await Parse.Object.saveAll(targets, { useMasterKey: true });
|
|
|
+ for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'content-' + action, 'CommonModel', target.id);
|
|
|
+ return response.json({ success: true, data: { action, updated: targets.length, status, nodeId, results: targets.map(serializeObject) } });
|
|
|
+ }
|
|
|
const className = String(input.className || '');
|
|
|
assertClass(className);
|
|
|
const fields = await schemaFor(className);
|
|
|
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 && !SYSTEM_FIELDS.has(name) && GENERIC_WRITE_TYPES.has(field.type) }));
|
|
|
- return response.json({ success: true, data: { className, label: CLASS_LABELS[className] || className, fields: fieldList, writable: classWritable, supportsSoftDelete: Boolean(fields.isDeleted) } });
|
|
|
+ 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 && className !== 'CommonModel', 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));
|
|
|
@@ -336,10 +371,11 @@ async function handler(request, response) {
|
|
|
if (!classWritable) fail(403, '该系统类不允许通用编辑');
|
|
|
if (className === '_User' && !objectId) fail(400, '新增用户必须走专用开户流程');
|
|
|
if (className === 'Group') 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);
|
|
|
const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
- for (const [name, value] of Object.entries(payload)) { if (!fields[name] || SYSTEM_FIELDS.has(name)) continue; if (!GENERIC_WRITE_TYPES.has(fields[name].type)) fail(400, '字段不允许通用编辑: ' + name); if (value === null) object.unset(name); else object.set(name, toParseValue(fields[name], value)); }
|
|
|
+ for (const [name, value] of Object.entries(payload)) { if (!fields[name] || isSystemField(className, name)) continue; if (!GENERIC_WRITE_TYPES.has(fields[name].type)) fail(400, '字段不允许通用编辑: ' + name); if (value === null) object.unset(name); else object.set(name, toParseValue(fields[name], value)); }
|
|
|
if (fields.company && context.company) object.set('company', context.company); if (fields.isDeleted && !objectId) object.set('isDeleted', false);
|
|
|
await object.save(null, { useMasterKey: true }); await audit(context, objectId ? 'update' : 'create', className, object.id);
|
|
|
return response.json({ success: true, data: serializeObject(object) });
|
|
|
@@ -348,6 +384,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 === '_User') assertCanManageUser(context, object, 'lock');
|
|
|
if (className === 'Group') { const groupId = Number(object.get('groupId')) || 0; const companyId = pointerId(object.get('company')); const users = await Psql.one('SELECT COUNT(*)::int AS count FROM "_User" WHERE "company"=$1 AND COALESCE("legacyGroupId",0)=$2 AND ("isDeleted" IS NULL OR "isDeleted"=FALSE)', [companyId, groupId]); const children = await Psql.one('SELECT COUNT(*)::int AS count FROM "Group" WHERE "company"=$1 AND COALESCE("parentGroupId",0)=$2', [companyId, groupId]); if (Number(users.count) > 0) fail(409, '该用户组仍有用户,不能删除'); if (Number(children.count) > 0) fail(409, '该用户组仍有下级组,不能删除'); }
|
|
|
+ if (className === 'CommonModel') { object.set('status', -2); await object.save(null, { useMasterKey: true }); await audit(context, 'content-recycle', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: true, status: -2 } }); }
|
|
|
if (fields.isDeleted) { object.set('isDeleted', true); await object.save(null, { useMasterKey: true }); } else await object.destroy({ useMasterKey: true });
|
|
|
if (className === '_User') await revokeSessions([object]);
|
|
|
await audit(context, 'delete', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: Boolean(fields.isDeleted) } });
|