|
|
@@ -2723,8 +2723,20 @@ function readingValue(value,depth=0){if(depth>6)fail(400,'阅读数据嵌套过
|
|
|
function readingClean(value,depth=0){if(value==null||depth>7)return value;if(Array.isArray(value))return value.map((item)=>readingClean(item,depth+1));if(value&&typeof value==='object'){if(depth>0&&value.objectId&&value.className){const className=String(value.className),rawId=String(value.objectId),prefix=className+'$',objectId=rawId.startsWith(prefix)?rawId.slice(prefix.length):rawId;return{__type:'Pointer',className,objectId};}const output={};for(const [key,item] of Object.entries(value)){if(key==='ACL'||SENSITIVE.test(key))continue;output[key]=readingClean(item,depth+1);}return output;}return value;}
|
|
|
function readingRecord(row){return readingClean(row&&typeof row.toJSON==='function'?row.toJSON():row);}
|
|
|
async function readingOwned(className,objectId,current){if(!/^[A-Za-z0-9_-]{10,40}$/.test(String(objectId||'')))fail(400,'阅读记录 ID 无效');const row=await new Parse.Query(className).get(String(objectId),{useMasterKey:true}),owner=row.get('user'),ownerId=String(owner&&(owner.id||owner.objectId)||owner||'').replace(/^_User\$/,'');if(className!=='PromptTemplate'&&ownerId!==String(current.id))fail(403,'无权访问该阅读记录');return row;}
|
|
|
-async function readingQuery(input,current){const className=readingClass(input.className),where=input.where&&typeof input.where==='object'?input.where:{},query=new Parse.Query(className),allowed=READING_FIELDS[className];for(const [key,raw] of Object.entries(where)){if(key==='user')continue;const root=key.split('.')[0];if(!allowed.has(root))fail(400,'阅读筛选字段不允许:'+key);if(raw&&typeof raw==='object'&&!Array.isArray(raw)&&!raw.__type){const operators=Object.keys(raw);if(operators.length!==1||!['$ne','$in'].includes(operators[0]))fail(400,'阅读筛选操作不允许');const value=readingValue(raw[operators[0]]);if(operators[0]==='$ne')query.notEqualTo(key,value);else query.containedIn(key,Array.isArray(value)?value:[value]);}else query.equalTo(key,readingValue(raw));}if(className!=='PromptTemplate'){const owned=await Psql.query('SELECT "objectId" FROM "'+className+'" WHERE "user" IN ($1,$2) LIMIT 5000',[current.id,'_User$'+current.id]),ids=owned.map((row)=>String(row.objectId));if(!ids.length)return[];query.containedIn('objectId',ids);}const order=String(input.order||'');if(order){const descending=order.startsWith('-'),field=order.replace(/^[-+]/,'');if(!['createdAt','updatedAt','index'].includes(field))fail(400,'阅读排序字段不允许');descending?query.descending(field):query.ascending(field);}query.limit(Math.min(100,Math.max(1,number(input.limit,20))));query.skip(Math.min(5000,Math.max(0,number(input.skip))));return(await query.find({useMasterKey:true})).map(readingRecord);}
|
|
|
-async function readingCreate(input,current){const className=readingClass(input.className,true),value=input.value&&typeof input.value==='object'?input.value:{},allowed=READING_FIELDS[className],row=new Parse.Object(className),deferred=value.createOptions,relations=[];for(const [key,item] of Object.entries(value)){if(key==='user'||key==='createOptions'||!allowed.has(key))continue;if(key==='parent'||key==='surveyItem'){const expected='SurveyItem',pointer=item&&typeof item==='object'?item:{};if(pointer.__type!=='Pointer'||pointer.className!==expected||!/^[A-Za-z0-9_-]{10,40}$/.test(String(pointer.objectId||'')))fail(400,'阅读关联记录无效');relations.push([key,pointer.objectId]);continue;}row.set(key,readingValue(item));}try{try{await row.save(null,{useMasterKey:true});}catch(error){if(!/valid Pointer/i.test(String(error&&error.message||error)))throw error;await new Promise((resolve)=>setTimeout(resolve,1200));await current.fetch({useMasterKey:true});await row.save(null,{useMasterKey:true});}if(deferred!==undefined&&allowed.has('createOptions')){row.set('createOptions',readingValue(deferred));await row.save(null,{useMasterKey:true});}await Psql.none('UPDATE "'+className+'" SET "user"=$1,"updatedAt"=CURRENT_TIMESTAMP WHERE "objectId"=$2',[className==='SurveyLog'?current.id:'_User$'+current.id,row.id]);for(const [field,pointer] of relations)await Psql.none('UPDATE "'+className+'" SET "'+field+'"=$1,"updatedAt"=CURRENT_TIMESTAMP WHERE "objectId"=$2',[pointer,row.id]);await row.fetch({useMasterKey:true});}catch(error){if(row.id)await row.destroy({useMasterKey:true}).catch(()=>undefined);fail(400,'阅读记录保存失败:'+String(error&&error.message||error));}return readingRecord(row);}
|
|
|
+async function readingQuery(input,current){
|
|
|
+ const className=readingClass(input.className),where=input.where&&typeof input.where==='object'?input.where:{},query=new Parse.Query(className),allowed=READING_FIELDS[className];let pointerIds=null;
|
|
|
+ for(const [key,raw] of Object.entries(where)){
|
|
|
+ if(key==='user')continue;const root=key.split('.')[0];if(!allowed.has(root))fail(400,'阅读筛选字段不允许:'+key);
|
|
|
+ if(['parent','surveyItem'].includes(key)&&raw&&raw.__type==='Pointer'){
|
|
|
+ if(raw.className!=='SurveyItem'||!/^[A-Za-z0-9_-]{10,40}$/.test(String(raw.objectId||'')))fail(400,'阅读关联记录无效');
|
|
|
+ const linked=await Psql.query('SELECT "objectId" FROM "'+className+'" WHERE "'+key+'" IN ($1,$2) LIMIT 5000',[String(raw.objectId),'SurveyItem$'+String(raw.objectId)]),ids=linked.map((row)=>String(row.objectId));pointerIds=pointerIds===null?ids:pointerIds.filter((id)=>ids.includes(id));continue;
|
|
|
+ }
|
|
|
+ if(raw&&typeof raw==='object'&&!Array.isArray(raw)&&!raw.__type){const operators=Object.keys(raw);if(operators.length!==1||!['$ne','$in'].includes(operators[0]))fail(400,'阅读筛选操作不允许');const value=readingValue(raw[operators[0]]);if(operators[0]==='$ne')query.notEqualTo(key,value);else query.containedIn(key,Array.isArray(value)?value:[value]);}else query.equalTo(key,readingValue(raw));
|
|
|
+ }
|
|
|
+ if(className!=='PromptTemplate'){const owned=await Psql.query('SELECT "objectId" FROM "'+className+'" WHERE "user" IN ($1,$2) LIMIT 5000',[current.id,'_User$'+current.id]),ownedIds=owned.map((row)=>String(row.objectId)),ids=pointerIds===null?ownedIds:ownedIds.filter((id)=>pointerIds.includes(id));if(!ids.length)return[];query.containedIn('objectId',ids);}else if(pointerIds!==null){if(!pointerIds.length)return[];query.containedIn('objectId',pointerIds);}
|
|
|
+ const order=String(input.order||'');if(order){const descending=order.startsWith('-'),field=order.replace(/^[-+]/,'');if(!['createdAt','updatedAt','index'].includes(field))fail(400,'阅读排序字段不允许');descending?query.descending(field):query.ascending(field);}query.limit(Math.min(100,Math.max(1,number(input.limit,20))));query.skip(Math.min(5000,Math.max(0,number(input.skip))));return(await query.find({useMasterKey:true})).map(readingRecord);
|
|
|
+}
|
|
|
+async function readingCreate(input,current){const className=readingClass(input.className,true),value=input.value&&typeof input.value==='object'?input.value:{},allowed=READING_FIELDS[className],row=new Parse.Object(className),deferred=value.createOptions,relations=[];for(const [key,item] of Object.entries(value)){if(key==='user'||key==='createOptions'||!allowed.has(key))continue;if(key==='parent'||key==='surveyItem'){const expected='SurveyItem',pointer=item&&typeof item==='object'?item:{};if(pointer.__type!=='Pointer'||pointer.className!==expected||!/^[A-Za-z0-9_-]{10,40}$/.test(String(pointer.objectId||'')))fail(400,'阅读关联记录无效');relations.push([key,expected+'$'+pointer.objectId]);continue;}row.set(key,readingValue(item));}try{try{await row.save(null,{useMasterKey:true});}catch(error){if(!/valid Pointer/i.test(String(error&&error.message||error)))throw error;await new Promise((resolve)=>setTimeout(resolve,1200));await current.fetch({useMasterKey:true});await row.save(null,{useMasterKey:true});}if(deferred!==undefined&&allowed.has('createOptions')){row.set('createOptions',readingValue(deferred));await row.save(null,{useMasterKey:true});}await Psql.none('UPDATE "'+className+'" SET "user"=$1,"updatedAt"=CURRENT_TIMESTAMP WHERE "objectId"=$2',['_User$'+current.id,row.id]);for(const [field,pointer] of relations)await Psql.none('UPDATE "'+className+'" SET "'+field+'"=$1,"updatedAt"=CURRENT_TIMESTAMP WHERE "objectId"=$2',[pointer,row.id]);await row.fetch({useMasterKey:true});}catch(error){if(row.id)await row.destroy({useMasterKey:true}).catch(()=>undefined);fail(400,'阅读记录保存失败:'+String(error&&error.message||error));}return readingRecord(row);}
|
|
|
async function readingUpdate(input,current){const className=readingClass(input.className,true),row=await readingOwned(className,input.objectId,current),patch=input.patch&&typeof input.patch==='object'?input.patch:{},allowed=READING_FIELDS[className];for(const [key,item] of Object.entries(patch)){if(key==='user'||!allowed.has(key))continue;row.set(key,readingValue(item));}await row.save(null,{useMasterKey:true});return readingRecord(row);}
|
|
|
async function readingUpdateUser(input,current){const patch=input.patch&&typeof input.patch==='object'?input.patch:{};for(const key of ['nickname','level'])if(patch[key]!==undefined)current.set(key,String(patch[key]).slice(0,100));await current.save(null,{useMasterKey:true});return readingRecord(current);}
|
|
|
async function readingAi(input,current){
|