function publicUser(user) { return { objectId: user.id, username: user.get('username') || '', realname: user.get('realname') || '', email: user.get('email') || '', mobile: user.get('mobile') || '', department: user.get('department') || '', status: user.get('status') || 'active', isAdmin: user.get('isAdmin') === true, role: user.get('role') || (user.get('isAdmin') ? 'admin' : 'user'), createdTime: user.createdAt?.toISOString?.() || '', }; } async function requireUser(request) { if (!request.user) throw new Parse.Error(Parse.Error.SESSION_MISSING, '需要登录'); if (request.user.get('status') === 'disabled') { throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, '账号已禁用'); } return request.user; } async function requireAdmin(request) { const user = await requireUser(request); if (user.get('isAdmin') !== true) { throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, '需要管理员权限'); } return user; } async function queryRows(className, asins = [], limit = 1000) { const query = new Parse.Query(className); if (asins.length) query.containedIn('asin', asins); query.limit(Math.min(Math.max(limit, 1), 5000)); const rows = await query.find({ useMasterKey: true }); return rows.map(row => row.toJSON()); } const CNY_RATES = { CNY: 1, USD: 7.20, CAD: 5.25, MXN: 0.38, EUR: 7.80, GBP: 9.40, JPY: 0.048, AUD: 4.70, BRL: 1.30, SEK: 0.68, PLN: 1.85, SGD: 5.50, AED: 1.96, SAR: 1.92, }; function roundMoney(value) { return Math.round((Number(value) || 0) * 100) / 100; } function toCny(amount, currency) { const code = String(currency || '').trim().toUpperCase(); const rate = CNY_RATES[code]; if (!rate) return null; return roundMoney(amount * rate); } const PERMISSION_CLASS = 'UserProductPermission'; function cleanKeys(values) { return [...new Set((Array.isArray(values) ? values : []).map(value => String(value || '').trim()).filter(Boolean))]; } function emptyPolicy() { return { status: 'active', scopeType: 'product', productLayer: 'own_store_product', mode: 'store_designer_intersection', storeKeys: [], designerKeys: [], stores: [], designers: [], rules: [], permissionVersion: 1, }; } function policyFromRow(row) { if (!row) return emptyPolicy(); const policy = row.get('policy') || {}; return { ...emptyPolicy(), ...policy, objectId: row.id, status: policy.status || 'active', storeKeys: cleanKeys(policy.storeKeys), designerKeys: cleanKeys(policy.designerKeys), stores: Array.isArray(policy.stores) ? policy.stores : [], designers: Array.isArray(policy.designers) ? policy.designers : [], rules: Array.isArray(policy.rules) ? policy.rules : [], permissionVersion: Number(policy.permissionVersion || 1), }; } async function getPermissionRow(user) { const query = new Parse.Query(PERMISSION_CLASS); query.equalTo('user', user); return query.first({ useMasterKey: true }); } async function getPermissionPolicy(user) { return policyFromRow(await getPermissionRow(user)); } function normalizePermissionPolicy(input) { const source = input && typeof input === 'object' ? input : {}; const policy = emptyPolicy(); policy.mode = source.mode === 'store_designer_matrix' ? source.mode : 'store_designer_intersection'; policy.storeKeys = cleanKeys(source.storeKeys); policy.designerKeys = cleanKeys(source.designerKeys); policy.stores = Array.isArray(source.stores) ? source.stores : []; policy.designers = Array.isArray(source.designers) ? source.designers : []; policy.rules = Array.isArray(source.rules) ? source.rules : []; return policy; } async function savePermissionPolicy(target, input, updatedBy) { const policy = normalizePermissionPolicy(input); const row = await getPermissionRow(target) || new Parse.Object(PERMISSION_CLASS); row.set('user', target); row.set('policy', policy); row.set('updatedBy', updatedBy); await row.save(null, { useMasterKey: true }); return policy; } function permissionScope(user, policy = emptyPolicy()) { const isAdmin = user.get('isAdmin') === true; return { isAdmin, status: user.get('status') || 'active', permissionVersion: policy.permissionVersion || 1, productScope: { all: isAdmin, productLayer: 'own_store_product', mode: policy.mode || 'store_designer_intersection', storeKeys: isAdmin ? [] : policy.storeKeys, designerKeys: isAdmin ? [] : policy.designerKeys, stores: isAdmin ? [] : policy.stores, designers: isAdmin ? [] : policy.designers, }, }; } function rowMatchesPermission(row, policy) { const stores = new Set(policy.storeKeys); const designers = new Set(policy.designerKeys); if (!stores.size && !designers.size) return false; const storeCandidates = [row.get('shopId'), row.get('storeId'), row.get('shopName'), row.get('storeName'), row.get('sellerId'), row.get('userAccount')]; const userAccount = String(row.get('userAccount') || '').trim(); const sellerId = String(row.get('sellerId') || '').trim(); const site = String(row.get('site') || '').trim(); if (userAccount && site) storeCandidates.push(`${userAccount}::${site}`); if (sellerId && site) storeCandidates.push(`${sellerId}::${site}`); const designerCandidates = [row.get('designerId'), row.get('designerName')]; const storeMatched = storeCandidates.some(value => stores.has(String(value || '').trim())); const designerMatched = designerCandidates.some(value => designers.has(String(value || '').trim())); if (stores.size && !storeMatched) return false; if (designers.size && !designerMatched) return false; return true; } function productSummary(row) { const data = row.toJSON(); return { objectId: row.id, asin: data.asin || '', parentAsin: data.parentAsin || '', sellerSku: data.sellerSku || '', productSku: data.productSku || '', itemName: data.itemName || '', site: data.site || '', userAccount: data.userAccount || data.shopName || '', designerName: data.designerName || '', imageUrl: data.imageUrl || '', shopId: data.shopId || '', shopName: data.shopName || '', }; } async function findAll(queryFactory, maxRows = 10000) { const rows = []; while (rows.length < maxRows) { const query = queryFactory(); query.skip(rows.length); query.limit(Math.min(1000, maxRows - rows.length)); const page = await query.find({ useMasterKey: true }); rows.push(...page); if (page.length < 1000) break; } return rows; } Parse.Cloud.define('managed_user_admin', async request => { const user = await requireUser(request); const { action, ...params } = request.params || {}; if (action === 'resolveMyScope') { const policy = user.get('isAdmin') === true ? emptyPolicy() : await getPermissionPolicy(user); return { success: true, data: permissionScope(user, policy) }; } if (action === 'getProductDimensions') { const rows = await queryRows('ProductDetail', params.asins || []); const dimensions = {}; rows.forEach(row => { if (row.asin) dimensions[row.asin] = row; }); return { success: true, data: { dimensions } }; } if (action === 'getAsinSkuMappings') { const rows = await queryRows('AsinSkuMapping', params.asins || []); const mappings = {}; rows.forEach(row => { if (row.asin) mappings[row.asin] = row; }); return { success: true, data: { mappings } }; } if (action === 'findAsinSkuRowsByAsins' || action === 'findAsinSkuRowsByFields') { const identifiers = params.asins || params.identifiers || []; return { success: true, data: { rows: await queryRows('AsinSkuMapping', identifiers) } }; } if (action === 'findProductDetailRows') { const asinRows = await queryRows('ProductDetail', params.asins || []); const parentQuery = new Parse.Query('ProductDetail'); if ((params.parentAsins || []).length) parentQuery.containedIn('parentAsin', params.parentAsins); else parentQuery.equalTo('objectId', '__none__'); parentQuery.limit(1000); const parentRows = (await parentQuery.find({ useMasterKey: true })).map(row => row.toJSON()); return { success: true, data: { asinRows, parentRows } }; } if (action === 'resolveDimensionFilterAsins' || action === 'findDimensionAsinsBySearch') { return { success: true, data: { asins: null } }; } if (action === 'listAccessibleStoreProducts') { const page = Math.max(Number(params.page || 1), 1); const pageSize = Math.min(Math.max(Number(params.pageSize || 50), 1), 5000); const isAdmin = user.get('isAdmin') === true; const policy = isAdmin ? emptyPolicy() : await getPermissionPolicy(user); const rows = await findAll(() => new Parse.Query('ProductDetail')); const visible = isAdmin ? rows : rows.filter(row => rowMatchesPermission(row, policy)); const start = (page - 1) * pageSize; return { success: true, data: { products: visible.slice(start, start + pageSize).map(productSummary), total: visible.length, page, pageSize, stats: { total: visible.length, active: visible.length, fba: 0, fbm: 0 }, siteDistribution: [], options: { sites: [], shops: [], designers: [], fulfillments: [], operators: [] }, scope: permissionScope(user, policy), }, }; } await requireAdmin(request); if (action === 'list') { const page = Math.max(Number(params.page || 1), 1); const pageSize = Math.min(Math.max(Number(params.pageSize || 20), 1), 100); const query = new Parse.Query(Parse.User); const filters = params.filters || {}; if (filters.username) query.matches('username', filters.username, 'i'); if (filters.realname) query.matches('realname', filters.realname, 'i'); if (filters.status) query.equalTo('status', filters.status); if (filters.department) query.equalTo('department', filters.department); query.skip((page - 1) * pageSize); query.limit(pageSize); const [users, total] = await Promise.all([ query.find({ useMasterKey: true }), query.count({ useMasterKey: true }), ]); return { success: true, data: { users: users.map(publicUser), total } }; } if (action === 'create') { const target = new Parse.User(); ['username', 'password', 'realname', 'email', 'mobile', 'department', 'status', 'isAdmin'].forEach(key => { if (params[key] !== undefined && params[key] !== '') target.set(key, params[key]); }); target.set('role', params.isAdmin ? 'admin' : 'user'); await target.save(null, { useMasterKey: true }); if (params.isAdmin !== true && params.permissionPolicy) { await savePermissionPolicy(target, params.permissionPolicy, user); } return { success: true, data: publicUser(target) }; } if (['update', 'resetPassword', 'delete'].includes(action)) { const target = await new Parse.Query(Parse.User).get(params.objectId, { useMasterKey: true }); if (action === 'delete') { const existing = await getPermissionRow(target); if (existing) await existing.destroy({ useMasterKey: true }); await target.destroy({ useMasterKey: true }); } if (action === 'resetPassword') { target.set('password', params.password); target.set('forceChangePassword', Boolean(params.forceChange)); await target.save(null, { useMasterKey: true }); } if (action === 'update') { ['realname', 'email', 'mobile', 'department', 'status', 'isAdmin'].forEach(key => { if (params[key] !== undefined) target.set(key, params[key]); }); target.set('role', params.isAdmin ? 'admin' : 'user'); await target.save(null, { useMasterKey: true }); if (params.permissionPolicy) { if (params.isAdmin === true) { const existing = await getPermissionRow(target); if (existing) await existing.destroy({ useMasterKey: true }); } else { await savePermissionPolicy(target, params.permissionPolicy, user); } } } return { success: true, data: action === 'delete' ? {} : publicUser(target) }; } if (action === 'listOptions') { const rows = await findAll(() => new Parse.Query('ProductDetail')); const stores = new Map(); const designers = new Map(); rows.forEach(row => { const shopId = String(row.get('shopId') || row.get('storeId') || '').trim(); const shopName = String(row.get('shopName') || row.get('storeName') || '').trim(); if (shopId || shopName) { const key = shopId || shopName; const current = stores.get(key) || { key, value: key, label: shopName || shopId, count: 0 }; current.count += 1; stores.set(key, current); } const designerId = String(row.get('designerId') || '').trim(); const designerName = String(row.get('designerName') || '').trim(); if (designerId || designerName) { const key = designerId || designerName; const current = designers.get(key) || { key, value: key, label: designerName || designerId, designerId, designerName, count: 0, }; current.count += 1; designers.set(key, current); } }); const byLabel = (left, right) => left.label.localeCompare(right.label, 'zh-CN'); return { success: true, data: { stores: [...stores.values()].sort(byLabel), designers: [...designers.values()].sort(byLabel), }, }; } if (action === 'getPolicy') { const target = await new Parse.Query(Parse.User).get(params.objectId, { useMasterKey: true }); return { success: true, data: await getPermissionPolicy(target) }; } if (action === 'savePolicy') { const target = await new Parse.Query(Parse.User).get(params.objectId, { useMasterKey: true }); await savePermissionPolicy(target, params.permissionPolicy || {}, user); return { success: true, data: {} }; } if (action === 'previewPolicy') { const policy = normalizePermissionPolicy(params.permissionPolicy || params); const rows = await findAll(() => new Parse.Query('ProductDetail')); const visible = rows.filter(row => rowMatchesPermission(row, policy)); return { success: true, data: { total: visible.length, samples: visible.slice(0, 8).map(productSummary) } }; } throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Unknown action: ${action}`); }); Parse.Cloud.define('dashboard_metrics', async request => { await requireUser(request); const shopId = String(request.params?.shop || '').trim(); const shop = shopId ? Parse.Object.extend('Shop').createWithoutData(shopId) : null; const startDate = request.params?.startDate ? new Date(request.params.startDate) : null; const endDate = request.params?.endDate ? new Date(request.params.endDate) : null; const hasStartDate = startDate && !Number.isNaN(startDate.getTime()); const hasEndDate = endDate && !Number.isNaN(endDate.getTime()); const productQuery = new Parse.Query('Product'); const reviewQuery = new Parse.Query('SorftimeReviews'); const returnQuery = new Parse.Query('ReturnRecord'); if (shop) { productQuery.equalTo('shop', shop); reviewQuery.equalTo('shop', shop); returnQuery.equalTo('shop', shop); } if (hasStartDate) returnQuery.greaterThanOrEqualTo('returnDate', startDate); if (hasEndDate) returnQuery.lessThanOrEqualTo('returnDate', endDate); productQuery.limit(1000); reviewQuery.limit(5000); const [products, orders, reviews, returnCount] = await Promise.all([ productQuery.find({ useMasterKey: true }), findAll(() => { const query = new Parse.Query('Order'); if (shop) query.equalTo('shop', shop); if (hasStartDate) query.greaterThanOrEqualTo('orderDate', startDate); if (hasEndDate) query.lessThanOrEqualTo('orderDate', endDate); return query; }), reviewQuery.find({ useMasterKey: true }), returnQuery.count({ useMasterKey: true }), ]); const productJson = products.map(row => row.toJSON()); const reviewJson = reviews.map(row => row.toJSON()); const ratings = productJson.map(row => Number(row.ratings || row.rating || 0)).filter(value => value > 0); const averageRating = ratings.length ? ratings.reduce((sum, value) => sum + value, 0) / ratings.length : 0; const positiveReviews = reviewJson.filter(row => Number(row.star || row.rating || row.Star || 0) >= 4).length; const positiveEmotionRate = reviewJson.length ? positiveReviews / reviewJson.length * 100 : (averageRating > 0 ? Math.max(0, Math.min(100, (averageRating - 1) / 4 * 100)) : 0); const currencyByMarketplace = { ATVPDKIKX0DER: 'USD', A2EUQ1WTGCTBG2: 'CAD', A1AM78C64UM0Y8: 'MXN', A2Q3Y263D00KWC: 'BRL', A1F83G8C2ARO7P: 'GBP', A1PA6795UKMFR9: 'EUR', A13V1IB3VIYZZH: 'EUR', A1RKKUPIHCS9HS: 'EUR', APJ6JRA9NG5V4: 'EUR', A1805IZSGTT6HS: 'EUR', A2NODRKZP88ZB9: 'SEK', A1C3SOZRARQ6R3: 'PLN', AMEN7PMS3EDWL: 'EUR', A28R8C7NBKEWEA: 'EUR', A1VC38T7YXB528: 'JPY', A39IBJ37TRP1C6: 'AUD', A19VAU5U5O7RUS: 'SGD', A2VIGQ35RCS4UG: 'AED', A17E79C6D8DWNP: 'SAR', }; const salesByCurrency = {}; const salesCnyByCurrency = {}; const unknownCurrencies = new Set(); let totalSalesCny = 0; orders.forEach(row => { const amount = Number(row.get('totalAmount') || 0); if (!amount) return; const currency = String( row.get('currency') || currencyByMarketplace[row.get('marketplaceId')] || 'UNKNOWN' ).toUpperCase(); salesByCurrency[currency] = (salesByCurrency[currency] || 0) + amount; const cnyAmount = toCny(amount, currency); if (cnyAmount == null) { unknownCurrencies.add(currency); return; } salesCnyByCurrency[currency] = (salesCnyByCurrency[currency] || 0) + cnyAmount; totalSalesCny += cnyAmount; }); Object.keys(salesByCurrency).forEach(currency => { salesByCurrency[currency] = roundMoney(salesByCurrency[currency]); }); Object.keys(salesCnyByCurrency).forEach(currency => { salesCnyByCurrency[currency] = roundMoney(salesCnyByCurrency[currency]); }); totalSalesCny = roundMoney(totalSalesCny); const currencies = Object.keys(salesByCurrency).sort(); const mixedCurrency = currencies.length > 1; return { success: true, data: { positiveEmotionRate: Math.round(positiveEmotionRate * 10) / 10, satisfactionScore: Math.round(averageRating * 100) / 100, returnRate: orders.length ? Math.round(returnCount / orders.length * 1000) / 10 : 0, vocVolume: reviewJson.length || productJson.reduce((sum, row) => sum + Number(row.ratingsCount || 0), 0), newCustomerRate: 0, totalOrders: orders.length, totalSales: totalSalesCny, averageOrderPrice: orders.length ? roundMoney(totalSalesCny / orders.length) : 0, currency: 'CNY', mixedCurrency, salesByCurrency, salesCnyByCurrency, fxBaseCurrency: 'CNY', fxMissingCurrencies: [...unknownCurrencies].sort(), trendChange: 0, trendChangeType: 'stable', }, }; }); Parse.Cloud.define('upsert_amazon_product', async request => { await requireUser(request); const raw = request.params?.data?.Data || request.params?.data || request.params?.product || {}; const asin = String(raw.Asin || raw.ASIN || raw.asin || '').trim().toUpperCase(); if (!asin) throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'ASIN is required'); const query = new Parse.Query('AmazonProduct'); query.equalTo('asin', asin); let product = null; try { product = await query.first({ useMasterKey: true }); } catch (error) { const message = String(error?.message || error || ''); if (!message.includes('does not exist') && !message.includes('non-existent class')) throw error; } if (!product) product = new Parse.Object('AmazonProduct'); const fields = { asin, parentAsin: raw.ParentAsin || raw.ParentASIN || raw.parentAsin || '', title: raw.Title || raw.title || '', brand: raw.Brand || raw.brand || '', imageUrl: Array.isArray(raw.Photo) ? raw.Photo[0] : (raw.Photo || raw.imageUrl || ''), photo: Array.isArray(raw.Photo) ? raw.Photo[0] : (raw.Photo || raw.photo || ''), category: Array.isArray(raw.Category || raw.category) ? String((raw.Category || raw.category)[0] || '') : String(raw.Category || raw.category || ''), bsrCategory: raw.BsrCategory || raw.bsrCategory || [], price: Number(raw.Price || raw.price || 0), salesPrice: Number(raw.SalesPrice || raw.salesPrice || raw.Price || raw.price || 0), ratings: Number(raw.Ratings || raw.ratings || raw.Rating || raw.rating || 0), rating: Number(raw.Ratings || raw.ratings || raw.Rating || raw.rating || 0), ratingsCount: Number(raw.RatingsCount || raw.ratingsCount || 0), rank: Number(raw.Rank || raw.rank || 0), listingSalesVolumeOfMonth: Number(raw.ListingSalesVolumeOfMonth || raw.listingSalesVolumeOfMonth || 0), sellerId: raw.BuyboxSellerId || raw.sellerId || '', source: 'sorftime', rawData: raw, }; Object.entries(fields).forEach(([key, value]) => product.set(key, value)); await product.save(null, { useMasterKey: true }); return { success: true, data: product.toJSON() }; }); Parse.Cloud.define('return_records', async request => { await requireUser(request); const query = new Parse.Query('ReturnRecord'); const shopId = String(request.params?.shop || '').trim(); if (shopId) query.equalTo('shop', Parse.Object.extend('Shop').createWithoutData(shopId)); const startDate = request.params?.startDate ? new Date(request.params.startDate) : null; const endDate = request.params?.endDate ? new Date(request.params.endDate) : null; if (startDate && !Number.isNaN(startDate.getTime())) query.greaterThanOrEqualTo('returnDate', startDate); if (endDate && !Number.isNaN(endDate.getTime())) query.lessThanOrEqualTo('returnDate', endDate); query.descending('returnDate'); query.limit(5000); const rows = await query.find({ useMasterKey: true }); return { success: true, data: rows.map(row => row.toJSON()) }; });