| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- 'use strict';
- // 数据采集与智能分析路由:同步任务、数据湖查询/聚合/导出、经营周报 HTML、智能问数
- const { sendJson, readBody } = require('../util.js');
- const { ASK_CLIENT_CODES } = require('../middleware.js');
- const { makeChatCfg } = require('../context.js');
- const naposService = require('../../napos-service.js');
- const nlQuery = require('../../nl-query.js');
- const reportHtml = require('../../report-html.js');
- function register(app, ctx) {
- // 不限方法(保持拆分前行为:该路由未做方法检查)
- app.any('/api/reports/status', async ({ res }) => {
- const napos = await naposService.naposStatus();
- sendJson(res, 200, { napos, entities: naposService.store.listEntities() });
- });
- // ---------- 智能问数(NL2Query,对话模型驱动) ----------
- app.post('/api/reports/ask', async ({ req, res }) => {
- const body = await readBody(req);
- const entity = body.entity || (naposService.store.listEntities()[0] || {}).key;
- if (!entity) { const e = new Error('暂无数据主体,请先完成一次同步'); e.code = 'ASK_NO_ENTITY'; throw e; }
- try {
- const result = await nlQuery.ask({ q: body.q, entity, cfg: makeChatCfg(ctx), svc: naposService, history: Array.isArray(body.history) ? body.history : undefined });
- sendJson(res, 200, result);
- } catch (e) {
- // 参数类错误码 → 400(错误边界统一处理),其余(模型/网络失败)保持 502
- if (!ASK_CLIENT_CODES.has(e.code)) e.status = 502;
- throw e;
- }
- });
- app.get('/api/reports/report-html', async ({ res, query }) => {
- const entity = query.entity || (naposService.store.listEntities()[0] || {}).key;
- if (!entity) {
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
- return res.end('<meta charset="utf-8"><body style="font-family:sans-serif;padding:40px">暂无数据主体,请先在「数据报表」完成一次同步。</body>');
- }
- const range = nlQuery.lakeDateRange(entity, naposService);
- const from = /^\d{8}$/.test(query.from || '') ? query.from : range.from;
- const to = /^\d{8}$/.test(query.to || '') ? query.to : range.to;
- const html = reportHtml.buildReportHtml({ entity, from, to });
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
- return res.end(html);
- });
- app.get('/api/reports/entities', async ({ res }) => {
- sendJson(res, 200, { entities: naposService.store.listEntities() });
- });
- app.get('/api/reports/tree', async ({ res, query }) => {
- try {
- const tree = await naposService.fetchTree({ refresh: query.refresh === '1' });
- sendJson(res, 200, {
- merchant: tree.merchant, chains: tree.chains,
- shops: tree.shops.map((s) => ({ shopId: String(s.shopId), shopName: s.shopName, chainId: s.chainId ? String(s.chainId) : '', chainName: s.chainName || '', busyLevel: s.busyLevel || null })),
- entity: tree.entity,
- });
- } catch (e) {
- // 取数通道不可达属于上游失败,保持 502
- e.status = 502;
- throw e;
- }
- });
- app.get('/api/reports/columns', async ({ res }) => {
- sendJson(res, 200, { columns: naposService.REPORT_COLUMNS });
- });
- app.post('/api/reports/sync', async ({ req, res }) => {
- const body = await readBody(req);
- naposService.runSync({
- from: body.from, to: body.to,
- scope: body.scope || 'all',
- chainId: body.chainId || '',
- shopIds: Array.isArray(body.shopIds) ? body.shopIds : undefined,
- }).catch(() => {});
- return sendJson(res, 200, { ok: true, ...naposService.syncSnapshot() });
- });
- app.get('/api/reports/sync/status', async ({ res }) => {
- sendJson(res, 200, naposService.syncSnapshot());
- });
- app.post('/api/reports/sync/cancel', async ({ res }) => {
- sendJson(res, 200, naposService.requestSyncCancel());
- });
- app.get('/api/reports/daily', async ({ res, query }) => {
- sendJson(res, 200, naposService.queryReport({
- entity: query.entity, from: query.from, to: query.to,
- shopId: query.shopId,
- shops: query.shops ? query.shops.split(',').filter(Boolean) : undefined,
- scope: query.scope || 'shop', q: query.q,
- }));
- });
- app.get('/api/reports/aggregate', async ({ res, query }) => {
- sendJson(res, 200, naposService.aggregateReport({
- entity: query.entity, from: query.from, to: query.to,
- group: query.group || 'shop',
- shops: query.shops ? query.shops.split(',').filter(Boolean) : undefined,
- }));
- });
- app.get('/api/reports/export', async ({ res, query }) => {
- const group = query.group === 'date' ? 'date' : (query.group === 'shop' ? 'shop' : null);
- let rows;
- if (group) {
- const agg = naposService.aggregateReport({ entity: query.entity, from: query.from, to: query.to, group });
- rows = agg.rows;
- } else {
- const daily = naposService.queryReport({
- entity: query.entity, from: query.from, to: query.to,
- shopId: query.shopId,
- shops: query.shops ? query.shops.split(',').filter(Boolean) : undefined,
- scope: query.scope || 'shop', q: query.q,
- });
- rows = daily.rows;
- }
- const columns = naposService.REPORT_COLUMNS;
- const csv = naposService.rowsToCsv(columns, rows);
- res.writeHead(200, {
- 'Content-Type': 'text/csv; charset=utf-8',
- 'Content-Disposition': `attachment; filename="report-${query.entity || 'all'}-${query.from || ''}-${query.to || ''}.csv"`,
- });
- return res.end(csv);
- });
- app.get('/api/reports/syncs', async ({ res, query }) => {
- const key = naposService.store.sanitizeKey(query.entity);
- sendJson(res, 200, { runs: naposService.store.loadSyncLogs(key) });
- });
- app.del('/api/reports/data', async ({ res, query }) => {
- if (!query.entity) return sendJson(res, 400, { error: '缺少 entity 参数' });
- naposService.store.deleteEntityData(query.entity);
- ctx.logs.pushSystemLog(`数据报表:已清除主体 ${query.entity} 的本地数据`, 'warn');
- sendJson(res, 200, { ok: true });
- });
- }
- module.exports = { register };
|