cangku/backend/src/api/channel.js

415 lines
13 KiB
JavaScript

const express = require('express');
const fs = require('fs');
const authMiddleware = require('../middleware/auth');
const {
getChannelList,
exportChannelListCsv,
createChannelExportJob,
getChannelExportJob,
getChannelExportJobFile,
getChannelDashboard,
getChannelIdAnalysisList,
getChannelIdAnalysisDashboard,
getChannelSpuList,
getChannelStockMonitorDashboard,
getChannelDetail,
getChannelLargeCustomerList,
getChannelLargeCustomerDashboard,
getChannelLargeCustomerDetail,
refreshChannelLargeCustomerReadStore,
} = require('../services/channelAnalysis');
const router = express.Router();
function parseRangeHeader(rangeHeader, totalSize) {
if (!rangeHeader) return null;
const match = String(rangeHeader).match(/^bytes=(\d*)-(\d*)$/);
if (!match) return { invalid: true };
let start = match[1] === '' ? null : Number.parseInt(match[1], 10);
let end = match[2] === '' ? null : Number.parseInt(match[2], 10);
if (start === null && end === null) return { invalid: true };
if (start === null) {
const suffixLength = end;
if (!Number.isFinite(suffixLength) || suffixLength <= 0) return { invalid: true };
start = Math.max(totalSize - suffixLength, 0);
end = totalSize - 1;
} else {
if (!Number.isFinite(start) || start < 0) return { invalid: true };
end = end === null ? totalSize - 1 : end;
}
if (!Number.isFinite(end) || end < start || start >= totalSize) {
return { invalid: true };
}
return {
start,
end: Math.min(end, totalSize - 1),
};
}
function setCsvDownloadHeaders(res, { filename, totalSize, contentLength, range }) {
const encodedFilename = encodeURIComponent(filename || `channel-data-${Date.now()}.csv`);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFilename}`);
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Length', String(contentLength));
res.setHeader('Cache-Control', 'private, no-store');
if (range) {
res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${totalSize}`);
}
}
router.get('/list', authMiddleware, async (req, res) => {
try {
const data = await getChannelList({
startDate: req.query.startDate,
endDate: req.query.endDate,
page: req.query.page,
pageSize: req.query.pageSize,
keyword: req.query.keyword,
sortField: req.query.sortField,
sortOrder: req.query.sortOrder,
channels: req.query.channels,
statuses: req.query.statuses,
purchasers: req.query.purchasers,
minAfterSaleAmountRate: req.query.minAfterSaleAmountRate,
maxAfterSaleAmountRate: req.query.maxAfterSaleAmountRate,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道数据失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道数据失败',
});
}
});
router.get('/export', authMiddleware, async (req, res) => {
try {
const data = await exportChannelListCsv({
startDate: req.query.startDate,
endDate: req.query.endDate,
keyword: req.query.keyword,
sortField: req.query.sortField,
sortOrder: req.query.sortOrder,
channels: req.query.channels,
statuses: req.query.statuses,
purchasers: req.query.purchasers,
minAfterSaleAmountRate: req.query.minAfterSaleAmountRate,
maxAfterSaleAmountRate: req.query.maxAfterSaleAmountRate,
});
const encodedFilename = encodeURIComponent(data.filename || `channel-data-${Date.now()}.csv`);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFilename}`);
res.setHeader('X-Total-Count', String(data.total || 0));
res.setHeader('X-Export-Mode', data.exportMode || 'exact-enriched');
res.send(data.csv);
} catch (error) {
console.error('导出渠道数据失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '导出渠道数据失败',
});
}
});
router.post('/export-jobs', authMiddleware, async (req, res) => {
try {
const data = createChannelExportJob({
startDate: req.body.startDate,
endDate: req.body.endDate,
keyword: req.body.keyword,
sortField: req.body.sortField,
sortOrder: req.body.sortOrder,
channels: req.body.channels,
statuses: req.body.statuses,
purchasers: req.body.purchasers,
minAfterSaleAmountRate: req.body.minAfterSaleAmountRate,
maxAfterSaleAmountRate: req.body.maxAfterSaleAmountRate,
}, req.user);
res.json({ success: true, data });
} catch (error) {
console.error('创建渠道数据导出任务失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '创建渠道数据导出任务失败',
});
}
});
router.get('/export-jobs/:jobId', authMiddleware, async (req, res) => {
try {
const data = await getChannelExportJob(req.params.jobId);
res.json({ success: true, data });
} catch (error) {
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道数据导出任务失败',
});
}
});
router.get('/export-jobs/:jobId/download', authMiddleware, async (req, res) => {
try {
const { filePath, filename } = await getChannelExportJobFile(req.params.jobId);
const stat = await fs.promises.stat(filePath);
const totalSize = stat.size;
const range = parseRangeHeader(req.headers.range, totalSize);
if (range?.invalid) {
res.status(416).setHeader('Content-Range', `bytes */${totalSize}`);
res.end();
return;
}
if (range) {
const contentLength = range.end - range.start + 1;
setCsvDownloadHeaders(res, { filename, totalSize, contentLength, range });
res.status(206);
fs.createReadStream(filePath, { start: range.start, end: range.end }).pipe(res);
return;
}
setCsvDownloadHeaders(res, { filename, totalSize, contentLength: totalSize });
fs.createReadStream(filePath).pipe(res);
} catch (error) {
console.error('下载渠道数据导出文件失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '下载渠道数据导出文件失败',
});
}
});
router.get('/dashboard', authMiddleware, async (req, res) => {
try {
const data = await getChannelDashboard({
startDate: req.query.startDate,
endDate: req.query.endDate,
keyword: req.query.keyword,
limit: req.query.limit,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道多维分析仪表盘失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道多维分析仪表盘失败',
});
}
});
router.get('/spu-list', authMiddleware, async (req, res) => {
try {
const data = await getChannelSpuList({
startDate: req.query.startDate,
endDate: req.query.endDate,
page: req.query.page,
pageSize: req.query.pageSize,
keyword: req.query.keyword,
sortField: req.query.sortField,
sortOrder: req.query.sortOrder,
statuses: req.query.statuses,
purchasers: req.query.purchasers,
minAfterSaleAmountRate: req.query.minAfterSaleAmountRate,
maxAfterSaleAmountRate: req.query.maxAfterSaleAmountRate,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道SPU图片数据失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道SPU图片数据失败',
});
}
});
router.get('/stock-monitor-dashboard', authMiddleware, async (req, res) => {
try {
const data = await getChannelStockMonitorDashboard({
startDate: req.query.startDate,
endDate: req.query.endDate,
keyword: req.query.keyword,
limit: req.query.limit,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道库存影响仪表盘失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道库存影响仪表盘失败',
});
}
});
router.get('/id-analysis-dashboard', authMiddleware, async (req, res) => {
try {
const data = await getChannelIdAnalysisDashboard({
startDate: req.query.startDate,
endDate: req.query.endDate,
keyword: req.query.keyword,
limit: req.query.limit,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道ID多维分析仪表盘失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道ID多维分析仪表盘失败',
});
}
});
router.get('/id-analysis', authMiddleware, async (req, res) => {
try {
const data = await getChannelIdAnalysisList({
startDate: req.query.startDate,
endDate: req.query.endDate,
page: req.query.page,
pageSize: req.query.pageSize,
keyword: req.query.keyword,
sortField: req.query.sortField,
sortOrder: req.query.sortOrder,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道ID分析失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道ID分析失败',
});
}
});
router.get('/detail', authMiddleware, async (req, res) => {
try {
const data = await getChannelDetail({
startDate: req.query.startDate,
endDate: req.query.endDate,
channelName: req.query.channelName,
shopName: req.query.shopName,
skuCode: req.query.skuCode,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道明细失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道明细失败',
});
}
});
router.get('/large-customers', authMiddleware, async (req, res) => {
try {
const data = await getChannelLargeCustomerList({
startMonth: req.query.startMonth,
endMonth: req.query.endMonth,
page: req.query.page,
pageSize: req.query.pageSize,
keyword: req.query.keyword,
sortField: req.query.sortField,
sortOrder: req.query.sortOrder,
minSingleOrderQty: req.query.minSingleOrderQty,
autoSync: req.query.autoSync,
channels: req.query.channels,
shops: req.query.shops,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道大客户分析失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道大客户分析失败',
});
}
});
router.get('/large-customers/dashboard', authMiddleware, async (req, res) => {
try {
const data = await getChannelLargeCustomerDashboard({
startMonth: req.query.startMonth,
endMonth: req.query.endMonth,
keyword: req.query.keyword,
minSingleOrderQty: req.query.minSingleOrderQty,
channels: req.query.channels,
shops: req.query.shops,
limit: req.query.limit,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道大客户多维分析仪表盘失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道大客户多维分析仪表盘失败',
});
}
});
router.get('/large-customers/detail', authMiddleware, async (req, res) => {
try {
const data = await getChannelLargeCustomerDetail({
startMonth: req.query.startMonth,
endMonth: req.query.endMonth,
buyerId: req.query.buyerId,
channelName: req.query.channelName,
shopName: req.query.shopName,
channels: req.query.channels,
shops: req.query.shops,
minSingleOrderQty: req.query.minSingleOrderQty,
limit: req.query.limit,
});
res.json({ success: true, data });
} catch (error) {
console.error('获取渠道大客户订单明细失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取渠道大客户订单明细失败',
});
}
});
router.post('/large-customers/refresh', authMiddleware, async (req, res) => {
try {
const data = await refreshChannelLargeCustomerReadStore({
startMonth: req.body.startMonth,
endMonth: req.body.endMonth,
page: req.body.page,
pageSize: req.body.pageSize,
keyword: req.body.keyword,
sortField: req.body.sortField,
sortOrder: req.body.sortOrder,
minSingleOrderQty: req.body.minSingleOrderQty,
channels: req.body.channels,
shops: req.body.shops,
});
res.json({ success: true, data });
} catch (error) {
console.error('刷新渠道大客户分析失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '刷新渠道大客户分析失败',
});
}
});
module.exports = router;