|
|
@@ -18,7 +18,7 @@ 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'
|
|
|
]);
|
|
|
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']);
|
|
|
+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']);
|
|
|
|
|
|
@@ -258,6 +258,57 @@ async function handler(request, response) {
|
|
|
for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'user-' + action, '_User', target.id);
|
|
|
return response.json({ success: true, data: { action, updated: targets.length, revokedSessions, group: group ? serializeObject(group) : null, results: targets.map(serializeObject) } });
|
|
|
}
|
|
|
+ if (operation === 'saveGroup') {
|
|
|
+ const objectId = String(input.objectId || '');
|
|
|
+ const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
|
|
|
+ const groupName = String(payload.groupName || '').trim();
|
|
|
+ const description = String(payload.description || groupName).trim();
|
|
|
+ const parentGroupId = payload.parentGroupId === undefined || payload.parentGroupId === '' ? 0 : Number(payload.parentGroupId);
|
|
|
+ if (!groupName || groupName.length > 100) fail(400, '用户组名称为必填项且不能超过 100 位');
|
|
|
+ if (description.length > 500) fail(400, '用户组说明不能超过 500 位');
|
|
|
+ if (!Number.isInteger(parentGroupId) || parentGroupId < 0) 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 groupFields = await schemaFor('Group');
|
|
|
+ let target;
|
|
|
+ let currentGroupId = 0;
|
|
|
+ if (objectId) {
|
|
|
+ const query = new Parse.Query('Group'); applyTenant(query, groupFields, context, input.companyId); target = await query.get(objectId, { useMasterKey: true });
|
|
|
+ currentGroupId = Number(target.get('groupId')) || 0;
|
|
|
+ } else target = new Parse.Object('Group');
|
|
|
+ const duplicate = await Psql.oneOrNone('SELECT "objectId" FROM "Group" WHERE "company"=$1 AND LOWER(COALESCE("groupName",\'\'))=LOWER($2) AND ($3=\'\' OR "objectId"<>$3) LIMIT 1', [companyId, groupName, objectId]);
|
|
|
+ if (duplicate) fail(409, '当前帐套已存在同名用户组');
|
|
|
+ if (parentGroupId) {
|
|
|
+ let cursor = parentGroupId; const visited = new Set();
|
|
|
+ while (cursor) {
|
|
|
+ if (cursor === currentGroupId || visited.has(cursor)) fail(400, '父用户组不能形成循环');
|
|
|
+ visited.add(cursor);
|
|
|
+ const parent = await Psql.oneOrNone('SELECT "parentGroupId" AS parent_id FROM "Group" WHERE "company"=$1 AND "groupId"=$2 LIMIT 1', [companyId, cursor]);
|
|
|
+ if (!parent) fail(404, '父用户组不存在或不属于当前帐套');
|
|
|
+ cursor = Number(parent.parent_id) || 0;
|
|
|
+ if (visited.size > 100) fail(400, '用户组层级过深');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ target.set('company', company); target.set('groupName', groupName); target.set('description', description); target.set('parentGroupId', parentGroupId);
|
|
|
+ const stringFields = ['otherName','enroll','signImg'];
|
|
|
+ const numberFields = ['orderId','companyGroup','vipgroup','rebateRate','credit','vipnum','upPoint','upSicon','favCount','consumeType','ccountPerDay','upGradeMoney'];
|
|
|
+ for (const name of stringFields) if (payload[name] !== undefined && groupFields[name]) target.set(name, String(payload[name] || '').trim());
|
|
|
+ for (const name of numberFields) if (payload[name] !== undefined && payload[name] !== '') { const value = Number(payload[name]); if (!Number.isFinite(value)) fail(400, name + ' 必须是数字'); if (groupFields[name]) target.set(name, value); }
|
|
|
+ if (payload.regSelect !== undefined && groupFields.regSelect) target.set('regSelect', payload.regSelect === true || payload.regSelect === 'true');
|
|
|
+ if (!objectId && groupFields.sourceKey) target.set('sourceKey', 'cloud:admin-group:' + companyId + ':' + Date.now() + ':' + Math.random().toString(36).slice(2,10));
|
|
|
+ await target.save(null, { useMasterKey: true });
|
|
|
+ if (!objectId) {
|
|
|
+ try {
|
|
|
+ const rows = await Psql.query('WITH lock_row AS MATERIALIZED (SELECT pg_advisory_xact_lock(hashtext(\'xiaoshu-group-id\'))), next_id AS MATERIALIZED (SELECT COALESCE(MAX("groupId"),0)+1 AS id FROM "Group",lock_row WHERE "company"=$1 AND COALESCE("groupId",0)>0) UPDATE "Group" SET "groupId"=next_id.id,"updatedAt"=NOW() FROM next_id WHERE "objectId"=$2 RETURNING next_id.id', [companyId, target.id]);
|
|
|
+ currentGroupId = Number(rows[0] && rows[0].id) || 0;
|
|
|
+ if (!currentGroupId) throw new Error('无法分配用户组编号');
|
|
|
+ } catch (error) { await target.destroy({ useMasterKey: true }).catch(() => undefined); throw error; }
|
|
|
+ }
|
|
|
+ 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) });
|
|
|
+ }
|
|
|
const className = String(input.className || '');
|
|
|
assertClass(className);
|
|
|
const fields = await schemaFor(className);
|
|
|
@@ -284,6 +335,7 @@ async function handler(request, response) {
|
|
|
if (operation === 'save') {
|
|
|
if (!classWritable) fail(403, '该系统类不允许通用编辑');
|
|
|
if (className === '_User' && !objectId) fail(400, '新增用户必须走专用开户流程');
|
|
|
+ if (className === 'Group') 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 : {};
|
|
|
@@ -295,6 +347,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 === '_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 (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) } });
|