managed-user.service.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. function publicUser(user) {
  2. return {
  3. objectId: user.id,
  4. username: user.get('username') || '',
  5. realname: user.get('realname') || '',
  6. email: user.get('email') || '',
  7. mobile: user.get('mobile') || '',
  8. department: user.get('department') || '',
  9. status: user.get('status') || 'active',
  10. isAdmin: user.get('isAdmin') === true,
  11. role: user.get('role') || (user.get('isAdmin') ? 'admin' : 'user'),
  12. createdTime: user.createdAt?.toISOString?.() || '',
  13. };
  14. }
  15. async function requireUser(request) {
  16. if (!request.user) throw new Parse.Error(Parse.Error.SESSION_MISSING, '需要登录');
  17. if (request.user.get('status') === 'disabled') {
  18. throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, '账号已禁用');
  19. }
  20. return request.user;
  21. }
  22. async function requireAdmin(request) {
  23. const user = await requireUser(request);
  24. if (user.get('isAdmin') !== true) {
  25. throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, '需要管理员权限');
  26. }
  27. return user;
  28. }
  29. async function queryRows(className, asins = [], limit = 1000) {
  30. const query = new Parse.Query(className);
  31. if (asins.length) query.containedIn('asin', asins);
  32. query.limit(Math.min(Math.max(limit, 1), 5000));
  33. const rows = await query.find({ useMasterKey: true });
  34. return rows.map(row => row.toJSON());
  35. }
  36. const CNY_RATES = {
  37. CNY: 1,
  38. USD: 7.20,
  39. CAD: 5.25,
  40. MXN: 0.38,
  41. EUR: 7.80,
  42. GBP: 9.40,
  43. JPY: 0.048,
  44. AUD: 4.70,
  45. BRL: 1.30,
  46. SEK: 0.68,
  47. PLN: 1.85,
  48. SGD: 5.50,
  49. AED: 1.96,
  50. SAR: 1.92,
  51. };
  52. function roundMoney(value) {
  53. return Math.round((Number(value) || 0) * 100) / 100;
  54. }
  55. function toCny(amount, currency) {
  56. const code = String(currency || '').trim().toUpperCase();
  57. const rate = CNY_RATES[code];
  58. if (!rate) return null;
  59. return roundMoney(amount * rate);
  60. }
  61. const PERMISSION_CLASS = 'UserProductPermission';
  62. function cleanKeys(values) {
  63. return [...new Set((Array.isArray(values) ? values : []).map(value => String(value || '').trim()).filter(Boolean))];
  64. }
  65. function emptyPolicy() {
  66. return {
  67. status: 'active',
  68. scopeType: 'product',
  69. productLayer: 'own_store_product',
  70. mode: 'store_designer_intersection',
  71. storeKeys: [],
  72. designerKeys: [],
  73. stores: [],
  74. designers: [],
  75. rules: [],
  76. permissionVersion: 1,
  77. };
  78. }
  79. function policyFromRow(row) {
  80. if (!row) return emptyPolicy();
  81. const policy = row.get('policy') || {};
  82. return {
  83. ...emptyPolicy(),
  84. ...policy,
  85. objectId: row.id,
  86. status: policy.status || 'active',
  87. storeKeys: cleanKeys(policy.storeKeys),
  88. designerKeys: cleanKeys(policy.designerKeys),
  89. stores: Array.isArray(policy.stores) ? policy.stores : [],
  90. designers: Array.isArray(policy.designers) ? policy.designers : [],
  91. rules: Array.isArray(policy.rules) ? policy.rules : [],
  92. permissionVersion: Number(policy.permissionVersion || 1),
  93. };
  94. }
  95. async function getPermissionRow(user) {
  96. const query = new Parse.Query(PERMISSION_CLASS);
  97. query.equalTo('user', user);
  98. return query.first({ useMasterKey: true });
  99. }
  100. async function getPermissionPolicy(user) {
  101. return policyFromRow(await getPermissionRow(user));
  102. }
  103. function normalizePermissionPolicy(input) {
  104. const source = input && typeof input === 'object' ? input : {};
  105. const policy = emptyPolicy();
  106. policy.mode = source.mode === 'store_designer_matrix' ? source.mode : 'store_designer_intersection';
  107. policy.storeKeys = cleanKeys(source.storeKeys);
  108. policy.designerKeys = cleanKeys(source.designerKeys);
  109. policy.stores = Array.isArray(source.stores) ? source.stores : [];
  110. policy.designers = Array.isArray(source.designers) ? source.designers : [];
  111. policy.rules = Array.isArray(source.rules) ? source.rules : [];
  112. return policy;
  113. }
  114. async function savePermissionPolicy(target, input, updatedBy) {
  115. const policy = normalizePermissionPolicy(input);
  116. const row = await getPermissionRow(target) || new Parse.Object(PERMISSION_CLASS);
  117. row.set('user', target);
  118. row.set('policy', policy);
  119. row.set('updatedBy', updatedBy);
  120. await row.save(null, { useMasterKey: true });
  121. return policy;
  122. }
  123. function permissionScope(user, policy = emptyPolicy()) {
  124. const isAdmin = user.get('isAdmin') === true;
  125. return {
  126. isAdmin,
  127. status: user.get('status') || 'active',
  128. permissionVersion: policy.permissionVersion || 1,
  129. productScope: {
  130. all: isAdmin,
  131. productLayer: 'own_store_product',
  132. mode: policy.mode || 'store_designer_intersection',
  133. storeKeys: isAdmin ? [] : policy.storeKeys,
  134. designerKeys: isAdmin ? [] : policy.designerKeys,
  135. stores: isAdmin ? [] : policy.stores,
  136. designers: isAdmin ? [] : policy.designers,
  137. },
  138. };
  139. }
  140. function rowMatchesPermission(row, policy) {
  141. const stores = new Set(policy.storeKeys);
  142. const designers = new Set(policy.designerKeys);
  143. if (!stores.size && !designers.size) return false;
  144. const storeCandidates = [row.get('shopId'), row.get('storeId'), row.get('shopName'), row.get('storeName'), row.get('sellerId'), row.get('userAccount')];
  145. const userAccount = String(row.get('userAccount') || '').trim();
  146. const sellerId = String(row.get('sellerId') || '').trim();
  147. const site = String(row.get('site') || '').trim();
  148. if (userAccount && site) storeCandidates.push(`${userAccount}::${site}`);
  149. if (sellerId && site) storeCandidates.push(`${sellerId}::${site}`);
  150. const designerCandidates = [row.get('designerId'), row.get('designerName')];
  151. const storeMatched = storeCandidates.some(value => stores.has(String(value || '').trim()));
  152. const designerMatched = designerCandidates.some(value => designers.has(String(value || '').trim()));
  153. if (stores.size && !storeMatched) return false;
  154. if (designers.size && !designerMatched) return false;
  155. return true;
  156. }
  157. function productSummary(row) {
  158. const data = row.toJSON();
  159. return {
  160. objectId: row.id,
  161. asin: data.asin || '',
  162. parentAsin: data.parentAsin || '',
  163. sellerSku: data.sellerSku || '',
  164. productSku: data.productSku || '',
  165. itemName: data.itemName || '',
  166. site: data.site || '',
  167. userAccount: data.userAccount || data.shopName || '',
  168. designerName: data.designerName || '',
  169. imageUrl: data.imageUrl || '',
  170. shopId: data.shopId || '',
  171. shopName: data.shopName || '',
  172. };
  173. }
  174. async function findAll(queryFactory, maxRows = 10000) {
  175. const rows = [];
  176. while (rows.length < maxRows) {
  177. const query = queryFactory();
  178. query.skip(rows.length);
  179. query.limit(Math.min(1000, maxRows - rows.length));
  180. const page = await query.find({ useMasterKey: true });
  181. rows.push(...page);
  182. if (page.length < 1000) break;
  183. }
  184. return rows;
  185. }
  186. Parse.Cloud.define('managed_user_admin', async request => {
  187. const user = await requireUser(request);
  188. const { action, ...params } = request.params || {};
  189. if (action === 'resolveMyScope') {
  190. const policy = user.get('isAdmin') === true ? emptyPolicy() : await getPermissionPolicy(user);
  191. return { success: true, data: permissionScope(user, policy) };
  192. }
  193. if (action === 'getProductDimensions') {
  194. const rows = await queryRows('ProductDetail', params.asins || []);
  195. const dimensions = {};
  196. rows.forEach(row => { if (row.asin) dimensions[row.asin] = row; });
  197. return { success: true, data: { dimensions } };
  198. }
  199. if (action === 'getAsinSkuMappings') {
  200. const rows = await queryRows('AsinSkuMapping', params.asins || []);
  201. const mappings = {};
  202. rows.forEach(row => { if (row.asin) mappings[row.asin] = row; });
  203. return { success: true, data: { mappings } };
  204. }
  205. if (action === 'findAsinSkuRowsByAsins' || action === 'findAsinSkuRowsByFields') {
  206. const identifiers = params.asins || params.identifiers || [];
  207. return { success: true, data: { rows: await queryRows('AsinSkuMapping', identifiers) } };
  208. }
  209. if (action === 'findProductDetailRows') {
  210. const asinRows = await queryRows('ProductDetail', params.asins || []);
  211. const parentQuery = new Parse.Query('ProductDetail');
  212. if ((params.parentAsins || []).length) parentQuery.containedIn('parentAsin', params.parentAsins);
  213. else parentQuery.equalTo('objectId', '__none__');
  214. parentQuery.limit(1000);
  215. const parentRows = (await parentQuery.find({ useMasterKey: true })).map(row => row.toJSON());
  216. return { success: true, data: { asinRows, parentRows } };
  217. }
  218. if (action === 'resolveDimensionFilterAsins' || action === 'findDimensionAsinsBySearch') {
  219. return { success: true, data: { asins: null } };
  220. }
  221. if (action === 'listAccessibleStoreProducts') {
  222. const page = Math.max(Number(params.page || 1), 1);
  223. const pageSize = Math.min(Math.max(Number(params.pageSize || 50), 1), 5000);
  224. const isAdmin = user.get('isAdmin') === true;
  225. const policy = isAdmin ? emptyPolicy() : await getPermissionPolicy(user);
  226. const rows = await findAll(() => new Parse.Query('ProductDetail'));
  227. const visible = isAdmin ? rows : rows.filter(row => rowMatchesPermission(row, policy));
  228. const start = (page - 1) * pageSize;
  229. return {
  230. success: true,
  231. data: {
  232. products: visible.slice(start, start + pageSize).map(productSummary),
  233. total: visible.length,
  234. page,
  235. pageSize,
  236. stats: { total: visible.length, active: visible.length, fba: 0, fbm: 0 },
  237. siteDistribution: [],
  238. options: { sites: [], shops: [], designers: [], fulfillments: [], operators: [] },
  239. scope: permissionScope(user, policy),
  240. },
  241. };
  242. }
  243. await requireAdmin(request);
  244. if (action === 'list') {
  245. const page = Math.max(Number(params.page || 1), 1);
  246. const pageSize = Math.min(Math.max(Number(params.pageSize || 20), 1), 100);
  247. const query = new Parse.Query(Parse.User);
  248. const filters = params.filters || {};
  249. if (filters.username) query.matches('username', filters.username, 'i');
  250. if (filters.realname) query.matches('realname', filters.realname, 'i');
  251. if (filters.status) query.equalTo('status', filters.status);
  252. if (filters.department) query.equalTo('department', filters.department);
  253. query.skip((page - 1) * pageSize);
  254. query.limit(pageSize);
  255. const [users, total] = await Promise.all([
  256. query.find({ useMasterKey: true }),
  257. query.count({ useMasterKey: true }),
  258. ]);
  259. return { success: true, data: { users: users.map(publicUser), total } };
  260. }
  261. if (action === 'create') {
  262. const target = new Parse.User();
  263. ['username', 'password', 'realname', 'email', 'mobile', 'department', 'status', 'isAdmin'].forEach(key => {
  264. if (params[key] !== undefined && params[key] !== '') target.set(key, params[key]);
  265. });
  266. target.set('role', params.isAdmin ? 'admin' : 'user');
  267. await target.save(null, { useMasterKey: true });
  268. if (params.isAdmin !== true && params.permissionPolicy) {
  269. await savePermissionPolicy(target, params.permissionPolicy, user);
  270. }
  271. return { success: true, data: publicUser(target) };
  272. }
  273. if (['update', 'resetPassword', 'delete'].includes(action)) {
  274. const target = await new Parse.Query(Parse.User).get(params.objectId, { useMasterKey: true });
  275. if (action === 'delete') {
  276. const existing = await getPermissionRow(target);
  277. if (existing) await existing.destroy({ useMasterKey: true });
  278. await target.destroy({ useMasterKey: true });
  279. }
  280. if (action === 'resetPassword') {
  281. target.set('password', params.password);
  282. target.set('forceChangePassword', Boolean(params.forceChange));
  283. await target.save(null, { useMasterKey: true });
  284. }
  285. if (action === 'update') {
  286. ['realname', 'email', 'mobile', 'department', 'status', 'isAdmin'].forEach(key => {
  287. if (params[key] !== undefined) target.set(key, params[key]);
  288. });
  289. target.set('role', params.isAdmin ? 'admin' : 'user');
  290. await target.save(null, { useMasterKey: true });
  291. if (params.permissionPolicy) {
  292. if (params.isAdmin === true) {
  293. const existing = await getPermissionRow(target);
  294. if (existing) await existing.destroy({ useMasterKey: true });
  295. } else {
  296. await savePermissionPolicy(target, params.permissionPolicy, user);
  297. }
  298. }
  299. }
  300. return { success: true, data: action === 'delete' ? {} : publicUser(target) };
  301. }
  302. if (action === 'listOptions') {
  303. const rows = await findAll(() => new Parse.Query('ProductDetail'));
  304. const stores = new Map();
  305. const designers = new Map();
  306. rows.forEach(row => {
  307. const shopId = String(row.get('shopId') || row.get('storeId') || '').trim();
  308. const shopName = String(row.get('shopName') || row.get('storeName') || '').trim();
  309. if (shopId || shopName) {
  310. const key = shopId || shopName;
  311. const current = stores.get(key) || { key, value: key, label: shopName || shopId, count: 0 };
  312. current.count += 1;
  313. stores.set(key, current);
  314. }
  315. const designerId = String(row.get('designerId') || '').trim();
  316. const designerName = String(row.get('designerName') || '').trim();
  317. if (designerId || designerName) {
  318. const key = designerId || designerName;
  319. const current = designers.get(key) || {
  320. key,
  321. value: key,
  322. label: designerName || designerId,
  323. designerId,
  324. designerName,
  325. count: 0,
  326. };
  327. current.count += 1;
  328. designers.set(key, current);
  329. }
  330. });
  331. const byLabel = (left, right) => left.label.localeCompare(right.label, 'zh-CN');
  332. return {
  333. success: true,
  334. data: {
  335. stores: [...stores.values()].sort(byLabel),
  336. designers: [...designers.values()].sort(byLabel),
  337. },
  338. };
  339. }
  340. if (action === 'getPolicy') {
  341. const target = await new Parse.Query(Parse.User).get(params.objectId, { useMasterKey: true });
  342. return { success: true, data: await getPermissionPolicy(target) };
  343. }
  344. if (action === 'savePolicy') {
  345. const target = await new Parse.Query(Parse.User).get(params.objectId, { useMasterKey: true });
  346. await savePermissionPolicy(target, params.permissionPolicy || {}, user);
  347. return { success: true, data: {} };
  348. }
  349. if (action === 'previewPolicy') {
  350. const policy = normalizePermissionPolicy(params.permissionPolicy || params);
  351. const rows = await findAll(() => new Parse.Query('ProductDetail'));
  352. const visible = rows.filter(row => rowMatchesPermission(row, policy));
  353. return { success: true, data: { total: visible.length, samples: visible.slice(0, 8).map(productSummary) } };
  354. }
  355. throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Unknown action: ${action}`);
  356. });
  357. Parse.Cloud.define('dashboard_metrics', async request => {
  358. await requireUser(request);
  359. const shopId = String(request.params?.shop || '').trim();
  360. const shop = shopId ? Parse.Object.extend('Shop').createWithoutData(shopId) : null;
  361. const startDate = request.params?.startDate ? new Date(request.params.startDate) : null;
  362. const endDate = request.params?.endDate ? new Date(request.params.endDate) : null;
  363. const hasStartDate = startDate && !Number.isNaN(startDate.getTime());
  364. const hasEndDate = endDate && !Number.isNaN(endDate.getTime());
  365. const productQuery = new Parse.Query('Product');
  366. const reviewQuery = new Parse.Query('SorftimeReviews');
  367. const returnQuery = new Parse.Query('ReturnRecord');
  368. if (shop) {
  369. productQuery.equalTo('shop', shop);
  370. reviewQuery.equalTo('shop', shop);
  371. returnQuery.equalTo('shop', shop);
  372. }
  373. if (hasStartDate) returnQuery.greaterThanOrEqualTo('returnDate', startDate);
  374. if (hasEndDate) returnQuery.lessThanOrEqualTo('returnDate', endDate);
  375. productQuery.limit(1000);
  376. reviewQuery.limit(5000);
  377. const [products, orders, reviews, returnCount] = await Promise.all([
  378. productQuery.find({ useMasterKey: true }),
  379. findAll(() => {
  380. const query = new Parse.Query('Order');
  381. if (shop) query.equalTo('shop', shop);
  382. if (hasStartDate) query.greaterThanOrEqualTo('orderDate', startDate);
  383. if (hasEndDate) query.lessThanOrEqualTo('orderDate', endDate);
  384. return query;
  385. }),
  386. reviewQuery.find({ useMasterKey: true }),
  387. returnQuery.count({ useMasterKey: true }),
  388. ]);
  389. const productJson = products.map(row => row.toJSON());
  390. const reviewJson = reviews.map(row => row.toJSON());
  391. const ratings = productJson.map(row => Number(row.ratings || row.rating || 0)).filter(value => value > 0);
  392. const averageRating = ratings.length ? ratings.reduce((sum, value) => sum + value, 0) / ratings.length : 0;
  393. const positiveReviews = reviewJson.filter(row => Number(row.star || row.rating || row.Star || 0) >= 4).length;
  394. const positiveEmotionRate = reviewJson.length
  395. ? positiveReviews / reviewJson.length * 100
  396. : (averageRating > 0 ? Math.max(0, Math.min(100, (averageRating - 1) / 4 * 100)) : 0);
  397. const currencyByMarketplace = {
  398. ATVPDKIKX0DER: 'USD', A2EUQ1WTGCTBG2: 'CAD', A1AM78C64UM0Y8: 'MXN', A2Q3Y263D00KWC: 'BRL',
  399. A1F83G8C2ARO7P: 'GBP', A1PA6795UKMFR9: 'EUR', A13V1IB3VIYZZH: 'EUR', A1RKKUPIHCS9HS: 'EUR',
  400. APJ6JRA9NG5V4: 'EUR', A1805IZSGTT6HS: 'EUR', A2NODRKZP88ZB9: 'SEK', A1C3SOZRARQ6R3: 'PLN',
  401. AMEN7PMS3EDWL: 'EUR', A28R8C7NBKEWEA: 'EUR', A1VC38T7YXB528: 'JPY', A39IBJ37TRP1C6: 'AUD',
  402. A19VAU5U5O7RUS: 'SGD', A2VIGQ35RCS4UG: 'AED', A17E79C6D8DWNP: 'SAR',
  403. };
  404. const salesByCurrency = {};
  405. const salesCnyByCurrency = {};
  406. const unknownCurrencies = new Set();
  407. let totalSalesCny = 0;
  408. orders.forEach(row => {
  409. const amount = Number(row.get('totalAmount') || 0);
  410. if (!amount) return;
  411. const currency = String(
  412. row.get('currency') || currencyByMarketplace[row.get('marketplaceId')] || 'UNKNOWN'
  413. ).toUpperCase();
  414. salesByCurrency[currency] = (salesByCurrency[currency] || 0) + amount;
  415. const cnyAmount = toCny(amount, currency);
  416. if (cnyAmount == null) {
  417. unknownCurrencies.add(currency);
  418. return;
  419. }
  420. salesCnyByCurrency[currency] = (salesCnyByCurrency[currency] || 0) + cnyAmount;
  421. totalSalesCny += cnyAmount;
  422. });
  423. Object.keys(salesByCurrency).forEach(currency => {
  424. salesByCurrency[currency] = roundMoney(salesByCurrency[currency]);
  425. });
  426. Object.keys(salesCnyByCurrency).forEach(currency => {
  427. salesCnyByCurrency[currency] = roundMoney(salesCnyByCurrency[currency]);
  428. });
  429. totalSalesCny = roundMoney(totalSalesCny);
  430. const currencies = Object.keys(salesByCurrency).sort();
  431. const mixedCurrency = currencies.length > 1;
  432. return {
  433. success: true,
  434. data: {
  435. positiveEmotionRate: Math.round(positiveEmotionRate * 10) / 10,
  436. satisfactionScore: Math.round(averageRating * 100) / 100,
  437. returnRate: orders.length ? Math.round(returnCount / orders.length * 1000) / 10 : 0,
  438. vocVolume: reviewJson.length || productJson.reduce((sum, row) => sum + Number(row.ratingsCount || 0), 0),
  439. newCustomerRate: 0,
  440. totalOrders: orders.length,
  441. totalSales: totalSalesCny,
  442. averageOrderPrice: orders.length ? roundMoney(totalSalesCny / orders.length) : 0,
  443. currency: 'CNY',
  444. mixedCurrency,
  445. salesByCurrency,
  446. salesCnyByCurrency,
  447. fxBaseCurrency: 'CNY',
  448. fxMissingCurrencies: [...unknownCurrencies].sort(),
  449. trendChange: 0,
  450. trendChangeType: 'stable',
  451. },
  452. };
  453. });
  454. Parse.Cloud.define('upsert_amazon_product', async request => {
  455. await requireUser(request);
  456. const raw = request.params?.data?.Data || request.params?.data || request.params?.product || {};
  457. const asin = String(raw.Asin || raw.ASIN || raw.asin || '').trim().toUpperCase();
  458. if (!asin) throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'ASIN is required');
  459. const query = new Parse.Query('AmazonProduct');
  460. query.equalTo('asin', asin);
  461. let product = null;
  462. try {
  463. product = await query.first({ useMasterKey: true });
  464. } catch (error) {
  465. const message = String(error?.message || error || '');
  466. if (!message.includes('does not exist') && !message.includes('non-existent class')) throw error;
  467. }
  468. if (!product) product = new Parse.Object('AmazonProduct');
  469. const fields = {
  470. asin,
  471. parentAsin: raw.ParentAsin || raw.ParentASIN || raw.parentAsin || '',
  472. title: raw.Title || raw.title || '',
  473. brand: raw.Brand || raw.brand || '',
  474. imageUrl: Array.isArray(raw.Photo) ? raw.Photo[0] : (raw.Photo || raw.imageUrl || ''),
  475. photo: Array.isArray(raw.Photo) ? raw.Photo[0] : (raw.Photo || raw.photo || ''),
  476. category: Array.isArray(raw.Category || raw.category)
  477. ? String((raw.Category || raw.category)[0] || '')
  478. : String(raw.Category || raw.category || ''),
  479. bsrCategory: raw.BsrCategory || raw.bsrCategory || [],
  480. price: Number(raw.Price || raw.price || 0),
  481. salesPrice: Number(raw.SalesPrice || raw.salesPrice || raw.Price || raw.price || 0),
  482. ratings: Number(raw.Ratings || raw.ratings || raw.Rating || raw.rating || 0),
  483. rating: Number(raw.Ratings || raw.ratings || raw.Rating || raw.rating || 0),
  484. ratingsCount: Number(raw.RatingsCount || raw.ratingsCount || 0),
  485. rank: Number(raw.Rank || raw.rank || 0),
  486. listingSalesVolumeOfMonth: Number(raw.ListingSalesVolumeOfMonth || raw.listingSalesVolumeOfMonth || 0),
  487. sellerId: raw.BuyboxSellerId || raw.sellerId || '',
  488. source: 'sorftime',
  489. rawData: raw,
  490. };
  491. Object.entries(fields).forEach(([key, value]) => product.set(key, value));
  492. await product.save(null, { useMasterKey: true });
  493. return { success: true, data: product.toJSON() };
  494. });
  495. Parse.Cloud.define('return_records', async request => {
  496. await requireUser(request);
  497. const query = new Parse.Query('ReturnRecord');
  498. const shopId = String(request.params?.shop || '').trim();
  499. if (shopId) query.equalTo('shop', Parse.Object.extend('Shop').createWithoutData(shopId));
  500. const startDate = request.params?.startDate ? new Date(request.params.startDate) : null;
  501. const endDate = request.params?.endDate ? new Date(request.params.endDate) : null;
  502. if (startDate && !Number.isNaN(startDate.getTime())) query.greaterThanOrEqualTo('returnDate', startDate);
  503. if (endDate && !Number.isNaN(endDate.getTime())) query.lessThanOrEqualTo('returnDate', endDate);
  504. query.descending('returnDate');
  505. query.limit(5000);
  506. const rows = await query.find({ useMasterKey: true });
  507. return { success: true, data: rows.map(row => row.toJSON()) };
  508. });