| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548 |
- 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()) };
- });
|