forked from lunanting/cangku
633 lines
18 KiB
JavaScript
633 lines
18 KiB
JavaScript
const dataSource = require('../config/database');
|
|
const planningDatabase = require('../services/planningDatabase');
|
|
const { ensurePlanningPddSkuDimensionSchema } = require('../services/planningSupportSchema');
|
|
|
|
const localDb = planningDatabase;
|
|
const systemDb = dataSource.local;
|
|
|
|
const PDD_ACCESS_DEPARTMENT_MAP = {
|
|
'运营中心A': '运营中心A',
|
|
'运营中心B': '运营中心B',
|
|
'运营中心C': '运营中心C',
|
|
'运营中心G': '运营中心G',
|
|
'品牌中心': '龙晨品牌中心',
|
|
'龙晨品牌中心': '龙晨品牌中心',
|
|
};
|
|
|
|
const SPU_TABLE = 'op_pdd_sku_dimension_spu_daily';
|
|
const SKU_TABLE = 'op_pdd_sku_dimension_sku_daily';
|
|
const GOODS_TABLE = 'op_pdd_sku_dimension_goods_daily';
|
|
const CACHE_TTL_MS = 60 * 1000;
|
|
|
|
const MAIN_COLUMNS = [
|
|
{ key: 'spu', title: 'SPU' },
|
|
{ key: 'goodsIdCount', title: '铺货商品id数' },
|
|
{ key: 'activeGoodsIdCount', title: '动销商品id数' },
|
|
{ key: 'skuListingActiveRate', title: 'SPU铺货动销率' },
|
|
{ key: 'skuIdEfficiency', title: 'SPU链接效能' },
|
|
{ key: 'isPriceBreak', title: '是否破价' },
|
|
{ key: 'payOrdrAmt', title: '近7天真实销售额' },
|
|
{ key: 'feeRatio', title: '近7天真实费比' },
|
|
{ key: 'afterSaleRate', title: '近7天真实售后率' },
|
|
{ key: 'goodsUv', title: '近7天访客数' },
|
|
{ key: 'orderNum', title: '近7天订单量' },
|
|
{ key: 'payOrdrUsrCnt', title: '近7天买家数' },
|
|
{ key: 'payOrdrGoodsQty', title: '近7天成交件数' },
|
|
{ key: 'ctr', title: '近7天点击率' },
|
|
{ key: 'goodsVcr', title: '近7天成交转化率' },
|
|
{ key: 'customerUnitPrice', title: '近7天客单价' },
|
|
{ key: 'pieceUnitPrice', title: '近7天件单价' },
|
|
{ key: 'spend', title: '近7天总花费' },
|
|
];
|
|
|
|
const SKU_DETAIL_COLUMNS = [
|
|
{ key: 'skuCode', title: 'SKU货号' },
|
|
...MAIN_COLUMNS.slice(1),
|
|
];
|
|
|
|
const GOODS_DETAIL_COLUMNS = [
|
|
{ key: 'goodsId', title: '商品id' },
|
|
{ key: 'goodsName', title: '商品名称' },
|
|
{ key: 'hdThumbUrl', title: '商品主图' },
|
|
{ key: 'shopName', title: '店铺名称' },
|
|
{ key: 'idOwner', title: 'id负责人' },
|
|
{ key: 'ownerDept', title: '所属部门' },
|
|
{ key: 'goodsUv', title: '近7天访客数' },
|
|
{ key: 'inquiryNum', title: '近7天询单量' },
|
|
{ key: 'goodsFavCnt', title: '近7天收藏用户' },
|
|
{ key: 'goodsFavNum', title: '近7天收藏量' },
|
|
{ key: 'orderUserCount', title: '近7天下单人数' },
|
|
{ key: 'ctr', title: '近7天点击率' },
|
|
{ key: 'clickConversionRate', title: '近7天点击转化率' },
|
|
{ key: 'goodsVcr', title: '近7天成交转化率' },
|
|
{ key: 'ordrVstrRto', title: '近7天下单率' },
|
|
{ key: 'payOrdrRto', title: '近7天成交率' },
|
|
{ key: 'imprUsrCnt', title: '近7天流失指数' },
|
|
{ key: 'unpaidUserCount', title: '近7天未支付人数' },
|
|
{ key: 'payOrdrAmt', title: '近7天真实销售额' },
|
|
{ key: 'orderNum', title: '近7天订单量' },
|
|
{ key: 'payOrdrUsrCnt', title: '近7天买家数' },
|
|
{ key: 'payOrdrGoodsQty', title: '近7天成交件数' },
|
|
{ key: 'customerUnitPrice', title: '近7天客单价' },
|
|
{ key: 'pieceUnitPrice', title: '近7天件单价' },
|
|
{ key: 'spend', title: '近7天总花费' },
|
|
{ key: 'feeRatio', title: '近7天真实费比' },
|
|
{ key: 'afterSaleRate', title: '近7天真实售后率' },
|
|
{ key: 'afterSaleCount', title: '近7天售后单量' },
|
|
{ key: 'badReviewCount', title: '近7天差评量' },
|
|
{ key: 'isPriceBreak', title: '是否破价' },
|
|
];
|
|
|
|
const MAIN_SORT_FIELDS = {
|
|
spu: '`spu`',
|
|
goodsIdCount: '`goodsIdCount`',
|
|
activeGoodsIdCount: '`activeGoodsIdCount`',
|
|
skuListingActiveRate: '`skuListingActiveRate`',
|
|
skuIdEfficiency: '`skuIdEfficiency`',
|
|
isPriceBreak: '`isPriceBreak`',
|
|
payOrdrAmt: '`payOrdrAmt`',
|
|
feeRatio: '`feeRatio`',
|
|
afterSaleRate: '`afterSaleRate`',
|
|
goodsUv: '`goodsUv`',
|
|
orderNum: '`orderNum`',
|
|
payOrdrUsrCnt: '`payOrdrUsrCnt`',
|
|
payOrdrGoodsQty: '`payOrdrGoodsQty`',
|
|
ctr: '`ctr`',
|
|
goodsVcr: '`goodsVcr`',
|
|
customerUnitPrice: '`customerUnitPrice`',
|
|
pieceUnitPrice: '`pieceUnitPrice`',
|
|
spend: '`spend`',
|
|
};
|
|
|
|
const valueCache = new Map();
|
|
|
|
function getMemoryCache(key) {
|
|
const cached = valueCache.get(key);
|
|
if (!cached) return null;
|
|
if (cached.expiresAt <= Date.now()) {
|
|
valueCache.delete(key);
|
|
return null;
|
|
}
|
|
return cached.value;
|
|
}
|
|
|
|
function setMemoryCache(key, value, ttlMs = CACHE_TTL_MS) {
|
|
valueCache.set(key, {
|
|
value,
|
|
expiresAt: Date.now() + ttlMs,
|
|
});
|
|
}
|
|
|
|
function normalizeMultiValueQueryParam(value) {
|
|
const rawValues = Array.isArray(value) ? value : [ value ];
|
|
return rawValues
|
|
.flatMap((item) => String(item || '').split(/[,\uff0c]/))
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function normalizePddDepartments(...departmentValues) {
|
|
const result = new Set();
|
|
departmentValues
|
|
.filter(Boolean)
|
|
.flatMap((value) => String(value).split(/[\/,,、]/))
|
|
.map((value) => value.trim())
|
|
.filter(Boolean)
|
|
.forEach((value) => {
|
|
const mapped = PDD_ACCESS_DEPARTMENT_MAP[value];
|
|
if (mapped) {
|
|
result.add(mapped);
|
|
}
|
|
});
|
|
return Array.from(result);
|
|
}
|
|
|
|
function getEffectiveDepartmentFilters(accessScope, departmentValues = []) {
|
|
const requestedDepartments = normalizePddDepartments(...departmentValues);
|
|
if (accessScope?.isAllData) {
|
|
return requestedDepartments;
|
|
}
|
|
|
|
const allowedDepartments = Array.isArray(accessScope?.allowedDepartments)
|
|
? accessScope.allowedDepartments
|
|
: [];
|
|
|
|
if (allowedDepartments.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (requestedDepartments.length === 0) {
|
|
return allowedDepartments;
|
|
}
|
|
|
|
const allowedSet = new Set(allowedDepartments);
|
|
return requestedDepartments.filter((item) => allowedSet.has(item));
|
|
}
|
|
|
|
async function getPddDataAccessScope(userId) {
|
|
const rows = await systemDb.query(
|
|
`SELECT
|
|
u.id,
|
|
u.username,
|
|
u.role,
|
|
u.department_names,
|
|
d.name AS department_name
|
|
FROM users u
|
|
LEFT JOIN departments d ON u.department_id = d.id
|
|
WHERE u.id = ?
|
|
LIMIT 1`,
|
|
[ userId ]
|
|
);
|
|
|
|
const user = rows[0];
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
if (user.role === 'admin') {
|
|
return {
|
|
isAllData: true,
|
|
allowedDepartments: [],
|
|
};
|
|
}
|
|
|
|
return {
|
|
isAllData: false,
|
|
allowedDepartments: normalizePddDepartments(user.department_names, user.department_name),
|
|
};
|
|
}
|
|
|
|
async function getLatestSnapshotInfo(tableName) {
|
|
await ensurePlanningPddSkuDimensionSchema();
|
|
const cacheKey = `pdd:sku-snapshot:latest:${tableName}`;
|
|
const cached = getMemoryCache(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
const rows = await localDb.query(
|
|
`SELECT
|
|
DATE_FORMAT(\`statDate\`, '%Y-%m-%d') AS statDate,
|
|
DATE_FORMAT(\`startDate\`, '%Y-%m-%d') AS startDate,
|
|
DATE_FORMAT(\`endDate\`, '%Y-%m-%d') AS endDate
|
|
FROM \`${tableName}\`
|
|
ORDER BY \`statDate\` DESC
|
|
LIMIT 1`
|
|
);
|
|
|
|
const value = rows[0] || null;
|
|
setMemoryCache(cacheKey, value, CACHE_TTL_MS);
|
|
return value;
|
|
}
|
|
|
|
function buildLikeAnyClause(columnName, values, params) {
|
|
if (!Array.isArray(values) || values.length === 0) {
|
|
return '';
|
|
}
|
|
const placeholders = values.map((value) => {
|
|
params.push(`%${value}%`);
|
|
return `${columnName} LIKE ?`;
|
|
});
|
|
return `(${placeholders.join(' OR ')})`;
|
|
}
|
|
|
|
function getSortSql(sortField, sortOrder, defaultExpr, defaultDirection = 'DESC', tieBreaker = '') {
|
|
const fieldExpr = MAIN_SORT_FIELDS[sortField] || defaultExpr;
|
|
const direction = sortOrder === 'ascend' ? 'ASC' : (sortOrder === 'descend' ? 'DESC' : defaultDirection);
|
|
return `${fieldExpr} ${direction}${tieBreaker}`;
|
|
}
|
|
|
|
function formatBoolText(value) {
|
|
return Number(value || 0) === 1 ? '是' : '否';
|
|
}
|
|
|
|
function normalizeMainRows(rows = []) {
|
|
return rows.map((row) => ({
|
|
...row,
|
|
isPriceBreak: formatBoolText(row.isPriceBreak),
|
|
}));
|
|
}
|
|
|
|
function normalizeGoodsRows(rows = []) {
|
|
return rows.map((row) => ({
|
|
...row,
|
|
isPriceBreak: formatBoolText(row.isPriceBreak),
|
|
}));
|
|
}
|
|
|
|
async function fetchSpuList({
|
|
statDate,
|
|
startDate,
|
|
endDate,
|
|
page,
|
|
pageSize,
|
|
sortField,
|
|
sortOrder,
|
|
spu,
|
|
shopNames,
|
|
departments,
|
|
idOwners,
|
|
accessScope,
|
|
}) {
|
|
const whereParts = [ '`statDate` = ?' ];
|
|
const params = [ statDate ];
|
|
|
|
if (spu) {
|
|
whereParts.push('`spu` LIKE ?');
|
|
params.push(`%${spu}%`);
|
|
}
|
|
|
|
const shopClause = buildLikeAnyClause('`shopNameList`', shopNames, params);
|
|
if (shopClause) whereParts.push(shopClause);
|
|
|
|
const departmentFilters = getEffectiveDepartmentFilters(accessScope, departments);
|
|
if (departmentFilters === null) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
columns: MAIN_COLUMNS,
|
|
rows: [],
|
|
total: 0,
|
|
page,
|
|
pageSize,
|
|
startDate,
|
|
endDate,
|
|
sortField: '',
|
|
sortOrder: '',
|
|
},
|
|
};
|
|
}
|
|
if (Array.isArray(departmentFilters) && departmentFilters.length === 0 && departments.length > 0) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
columns: MAIN_COLUMNS,
|
|
rows: [],
|
|
total: 0,
|
|
page,
|
|
pageSize,
|
|
startDate,
|
|
endDate,
|
|
sortField: '',
|
|
sortOrder: '',
|
|
},
|
|
};
|
|
}
|
|
const departmentClause = buildLikeAnyClause('`ownerDeptList`', departmentFilters, params);
|
|
if (departmentClause) whereParts.push(departmentClause);
|
|
|
|
const idOwnerClause = buildLikeAnyClause('`idOwnerList`', idOwners, params);
|
|
if (idOwnerClause) whereParts.push(idOwnerClause);
|
|
|
|
const whereSql = `WHERE ${whereParts.join(' AND ')}`;
|
|
const totalRows = await localDb.query(
|
|
`SELECT COUNT(1) AS total FROM \`${SPU_TABLE}\` ${whereSql}`,
|
|
params
|
|
);
|
|
const total = Number(totalRows?.[0]?.total || 0);
|
|
|
|
const orderBySql = getSortSql(sortField, sortOrder, '`payOrdrAmt`', 'DESC', ', `spu` ASC');
|
|
const listRows = await localDb.query(
|
|
`SELECT
|
|
\`spu\`,
|
|
\`goodsIdCount\`,
|
|
\`activeGoodsIdCount\`,
|
|
\`skuListingActiveRate\`,
|
|
\`skuIdEfficiency\`,
|
|
\`isPriceBreak\`,
|
|
\`payOrdrAmt\`,
|
|
\`feeRatio\`,
|
|
\`afterSaleRate\`,
|
|
\`goodsUv\`,
|
|
\`orderNum\`,
|
|
\`payOrdrUsrCnt\`,
|
|
\`payOrdrGoodsQty\`,
|
|
\`ctr\`,
|
|
\`goodsVcr\`,
|
|
\`customerUnitPrice\`,
|
|
\`pieceUnitPrice\`,
|
|
\`spend\`
|
|
FROM \`${SPU_TABLE}\`
|
|
${whereSql}
|
|
ORDER BY ${orderBySql}
|
|
LIMIT ? OFFSET ?`,
|
|
[ ...params, pageSize, (page - 1) * pageSize ]
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
columns: MAIN_COLUMNS,
|
|
rows: normalizeMainRows(listRows),
|
|
total,
|
|
page,
|
|
pageSize,
|
|
startDate,
|
|
endDate,
|
|
sortField: MAIN_SORT_FIELDS[sortField] ? sortField : '',
|
|
sortOrder: MAIN_SORT_FIELDS[sortField] ? (sortOrder || 'descend') : '',
|
|
},
|
|
};
|
|
}
|
|
|
|
async function fetchSkuDetail({ spu, accessScope }) {
|
|
const latest = await getLatestSnapshotInfo(SKU_TABLE);
|
|
if (!latest?.statDate) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
spu,
|
|
startDate: null,
|
|
endDate: null,
|
|
columns: SKU_DETAIL_COLUMNS,
|
|
rows: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
const whereParts = [ '`statDate` = ?', '`spu` = ?' ];
|
|
const params = [ latest.statDate, spu ];
|
|
const departmentFilters = getEffectiveDepartmentFilters(accessScope, []);
|
|
if (departmentFilters === null) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
spu,
|
|
startDate: latest.startDate,
|
|
endDate: latest.endDate,
|
|
columns: SKU_DETAIL_COLUMNS,
|
|
rows: [],
|
|
},
|
|
};
|
|
}
|
|
if (Array.isArray(departmentFilters) && departmentFilters.length > 0) {
|
|
const scopeClause = buildLikeAnyClause('`ownerDeptList`', departmentFilters, params);
|
|
if (scopeClause) whereParts.push(scopeClause);
|
|
}
|
|
|
|
const rows = await localDb.query(
|
|
`SELECT
|
|
\`skuCode\`,
|
|
\`goodsIdCount\`,
|
|
\`activeGoodsIdCount\`,
|
|
\`skuListingActiveRate\`,
|
|
\`skuIdEfficiency\`,
|
|
\`isPriceBreak\`,
|
|
\`payOrdrAmt\`,
|
|
\`feeRatio\`,
|
|
\`afterSaleRate\`,
|
|
\`goodsUv\`,
|
|
\`orderNum\`,
|
|
\`payOrdrUsrCnt\`,
|
|
\`payOrdrGoodsQty\`,
|
|
\`ctr\`,
|
|
\`goodsVcr\`,
|
|
\`customerUnitPrice\`,
|
|
\`pieceUnitPrice\`,
|
|
\`spend\`
|
|
FROM \`${SKU_TABLE}\`
|
|
WHERE ${whereParts.join(' AND ')}
|
|
ORDER BY \`payOrdrAmt\` DESC, \`goodsIdCount\` DESC, \`skuCode\` ASC`,
|
|
params
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
spu,
|
|
startDate: latest.startDate,
|
|
endDate: latest.endDate,
|
|
columns: SKU_DETAIL_COLUMNS,
|
|
rows: normalizeMainRows(rows),
|
|
},
|
|
};
|
|
}
|
|
|
|
async function fetchGoodsDetail({ skuCode, accessScope }) {
|
|
const latest = await getLatestSnapshotInfo(GOODS_TABLE);
|
|
if (!latest?.statDate) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
skuCode,
|
|
startDate: null,
|
|
endDate: null,
|
|
columns: GOODS_DETAIL_COLUMNS,
|
|
rows: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
const whereParts = [ '`statDate` = ?', '`skuCode` = ?' ];
|
|
const params = [ latest.statDate, skuCode ];
|
|
const departmentFilters = getEffectiveDepartmentFilters(accessScope, []);
|
|
if (departmentFilters === null) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
skuCode,
|
|
startDate: latest.startDate,
|
|
endDate: latest.endDate,
|
|
columns: GOODS_DETAIL_COLUMNS,
|
|
rows: [],
|
|
},
|
|
};
|
|
}
|
|
if (Array.isArray(departmentFilters) && departmentFilters.length > 0) {
|
|
whereParts.push(`\`ownerDept\` IN (${new Array(departmentFilters.length).fill('?').join(', ')})`);
|
|
params.push(...departmentFilters);
|
|
}
|
|
|
|
const rows = await localDb.query(
|
|
`SELECT
|
|
\`goodsId\`,
|
|
\`goodsName\`,
|
|
\`hdThumbUrl\`,
|
|
\`shopName\`,
|
|
\`idOwner\`,
|
|
\`ownerDept\`,
|
|
\`goodsUv\`,
|
|
\`inquiryNum\`,
|
|
\`goodsFavCnt\`,
|
|
\`goodsFavNum\`,
|
|
\`orderUserCount\`,
|
|
\`ctr\`,
|
|
\`clickConversionRate\`,
|
|
\`goodsVcr\`,
|
|
\`ordrVstrRto\`,
|
|
\`payOrdrRto\`,
|
|
\`imprUsrCnt\`,
|
|
\`unpaidUserCount\`,
|
|
\`payOrdrAmt\`,
|
|
\`orderNum\`,
|
|
\`payOrdrUsrCnt\`,
|
|
\`payOrdrGoodsQty\`,
|
|
\`customerUnitPrice\`,
|
|
\`pieceUnitPrice\`,
|
|
\`spend\`,
|
|
\`feeRatio\`,
|
|
\`afterSaleRate\`,
|
|
\`afterSaleCount\`,
|
|
\`badReviewCount\`,
|
|
\`isPriceBreak\`
|
|
FROM \`${GOODS_TABLE}\`
|
|
WHERE ${whereParts.join(' AND ')}
|
|
ORDER BY \`payOrdrAmt\` DESC, \`goodsUv\` DESC, \`goodsId\` ASC`,
|
|
params
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
skuCode,
|
|
startDate: latest.startDate,
|
|
endDate: latest.endDate,
|
|
columns: GOODS_DETAIL_COLUMNS,
|
|
rows: normalizeGoodsRows(rows),
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = function registerPddSkuDimensionBoard({
|
|
router,
|
|
authMiddleware,
|
|
}) {
|
|
router.get('/pdd/product-dashboard/sku-dimension-board/list', authMiddleware, async (req, res) => {
|
|
try {
|
|
const userId = req.user?.sub || req.user?.id || req.user?.userId;
|
|
if (!userId) {
|
|
return res.status(401).json({ success: false, error: '未获取到登录用户信息' });
|
|
}
|
|
|
|
const accessScope = await getPddDataAccessScope(userId);
|
|
if (!accessScope) {
|
|
return res.status(403).json({ success: false, error: '未获取到数据权限' });
|
|
}
|
|
|
|
const latest = await getLatestSnapshotInfo(SPU_TABLE);
|
|
const page = Math.max(parseInt(req.query.page || '1', 10), 1);
|
|
const pageSize = Math.min(Math.max(parseInt(req.query.pageSize || '20', 10), 1), 500);
|
|
const sortField = String(req.query.sortField || '').trim();
|
|
const sortOrder = String(req.query.sortOrder || '').trim().toLowerCase();
|
|
const spu = String(req.query.spu || '').trim();
|
|
const shopNames = normalizeMultiValueQueryParam(req.query.shopName);
|
|
const departments = normalizeMultiValueQueryParam(req.query.department);
|
|
const idOwners = normalizeMultiValueQueryParam(req.query.idOwners);
|
|
|
|
if (!latest?.statDate) {
|
|
return res.json({
|
|
success: true,
|
|
data: {
|
|
columns: MAIN_COLUMNS,
|
|
rows: [],
|
|
total: 0,
|
|
page,
|
|
pageSize,
|
|
startDate: null,
|
|
endDate: null,
|
|
sortField: '',
|
|
sortOrder: '',
|
|
},
|
|
});
|
|
}
|
|
|
|
const result = await fetchSpuList({
|
|
statDate: latest.statDate,
|
|
startDate: latest.startDate,
|
|
endDate: latest.endDate,
|
|
page,
|
|
pageSize,
|
|
sortField,
|
|
sortOrder,
|
|
spu,
|
|
shopNames,
|
|
departments,
|
|
idOwners,
|
|
accessScope,
|
|
});
|
|
|
|
res.json(result);
|
|
} catch (error) {
|
|
res.status(error.statusCode || 500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
router.get('/pdd/product-dashboard/sku-dimension-board/sku-detail', authMiddleware, async (req, res) => {
|
|
try {
|
|
const spu = String(req.query.spu || '').trim();
|
|
if (!spu) {
|
|
return res.status(400).json({ success: false, error: 'spu is required' });
|
|
}
|
|
|
|
const userId = req.user?.sub || req.user?.id || req.user?.userId;
|
|
const accessScope = await getPddDataAccessScope(userId);
|
|
if (!accessScope) {
|
|
return res.status(403).json({ success: false, error: '未获取到数据权限' });
|
|
}
|
|
|
|
res.json(await fetchSkuDetail({ spu, accessScope }));
|
|
} catch (error) {
|
|
res.status(error.statusCode || 500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
router.get('/pdd/product-dashboard/sku-dimension-board/goods-detail', authMiddleware, async (req, res) => {
|
|
try {
|
|
const skuCode = String(req.query.skuCode || '').trim();
|
|
if (!skuCode) {
|
|
return res.status(400).json({ success: false, error: 'skuCode is required' });
|
|
}
|
|
|
|
const userId = req.user?.sub || req.user?.id || req.user?.userId;
|
|
const accessScope = await getPddDataAccessScope(userId);
|
|
if (!accessScope) {
|
|
return res.status(403).json({ success: false, error: '未获取到数据权限' });
|
|
}
|
|
|
|
res.json(await fetchGoodsDetail({ skuCode, accessScope }));
|
|
} catch (error) {
|
|
res.status(error.statusCode || 500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
};
|