|
|
@@ -267,6 +267,39 @@ function platformNum(row, column) {
|
|
|
return numOrNull(row.platform[column]);
|
|
|
}
|
|
|
|
|
|
+/** 零单门店诊断:用平台字段区分「未开业 / 闭店 / 无曝光 / 无进店 / 无转化」 */
|
|
|
+function diagnoseZeroOrder(row) {
|
|
|
+ const p = row.platform;
|
|
|
+ const orders = Number(row.order_cnt);
|
|
|
+ if (!(orders === 0)) return null;
|
|
|
+ if (!p) {
|
|
|
+ return { reason: 'unknown', reasonText: '缺少平台明细,无法判断' };
|
|
|
+ }
|
|
|
+ const bizHours = String(p['营业时长'] == null ? '' : p['营业时长']).trim();
|
|
|
+ const zeroBiz = /^0\s*小时\s*0\s*分$/.test(bizHours) || bizHours === '0' || bizHours === '';
|
|
|
+ const abnormal = numOrNull(p['异常关店时长']) || 0;
|
|
|
+ const exposure = numOrNull(p['曝光人数']);
|
|
|
+ const visit = numOrNull(p['进店人数']);
|
|
|
+ const validShop = String(p['是否有效门店'] || '').trim();
|
|
|
+
|
|
|
+ if (zeroBiz) {
|
|
|
+ if (validShop === '否') {
|
|
|
+ return { reason: 'not-open', reasonText: '未开业(非有效门店,当日无营业时长)', bizHours, abnormal, exposure, visit, validShop };
|
|
|
+ }
|
|
|
+ if (abnormal > 0) {
|
|
|
+ return { reason: 'closed', reasonText: `当日闭店(异常关店 ${abnormal} 分钟,无营业时长)`, bizHours, abnormal, exposure, visit, validShop };
|
|
|
+ }
|
|
|
+ return { reason: 'not-open', reasonText: '未营业(当日无营业时长,也未记录异常关店)', bizHours, abnormal, exposure, visit, validShop };
|
|
|
+ }
|
|
|
+ if (exposure === 0) {
|
|
|
+ return { reason: 'no-traffic', reasonText: '营业中但无曝光(未进流量池)', bizHours, abnormal, exposure, visit, validShop };
|
|
|
+ }
|
|
|
+ if (visit === 0) {
|
|
|
+ return { reason: 'no-visit', reasonText: `有曝光 ${exposure} 但无进店`, bizHours, abnormal, exposure, visit, validShop };
|
|
|
+ }
|
|
|
+ return { reason: 'no-conversion', reasonText: `有进店 ${visit} 但零下单(转化问题)`, bizHours, abnormal, exposure, visit, validShop };
|
|
|
+}
|
|
|
+
|
|
|
/** 一次聚合:KPI 口径集中在这里,前端不再二次计算 */
|
|
|
function aggregateRows(rows) {
|
|
|
let revenue = 0;
|
|
|
@@ -396,6 +429,24 @@ async function buildDashboard(store, { from, to, schools, shops }) {
|
|
|
order: cur.buyers || cur.orders,
|
|
|
};
|
|
|
|
|
|
+ const zeroOrder = [];
|
|
|
+ for (const row of rows) {
|
|
|
+ const diagnosis = diagnoseZeroOrder(row);
|
|
|
+ if (!diagnosis) continue;
|
|
|
+ zeroOrder.push({
|
|
|
+ date: row.date,
|
|
|
+ shopId: row.shopId,
|
|
|
+ shopName: row.shopName,
|
|
|
+ school: row.school || '',
|
|
|
+ orders: 0,
|
|
|
+ ...diagnosis,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const zeroOrderSummary = zeroOrder.reduce((acc, item) => {
|
|
|
+ acc[item.reason] = (acc[item.reason] || 0) + 1;
|
|
|
+ return acc;
|
|
|
+ }, {});
|
|
|
+
|
|
|
const structure = [...byDate.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([date, list]) => {
|
|
|
const agg = aggregateRows(list);
|
|
|
return { date, income: agg.revenue, expense: agg.expense, subsidy: agg.subsidy };
|
|
|
@@ -409,6 +460,8 @@ async function buildDashboard(store, { from, to, schools, shops }) {
|
|
|
trend,
|
|
|
ranking,
|
|
|
bottom,
|
|
|
+ zeroOrder,
|
|
|
+ zeroOrderSummary,
|
|
|
shopCount: allShops.length,
|
|
|
funnel,
|
|
|
structure,
|
|
|
@@ -422,7 +475,8 @@ const INSIGHT_SYSTEM_PROMPT = [
|
|
|
'2. 必须引用 JSON 里的具体数字(金额保留两位小数、人数/单量用整数、比率用百分比);',
|
|
|
'3. 指出风险(下滑、零单门店、成本占比高、漏斗转化偏低等),并给一句可执行建议;',
|
|
|
'4. 数据不足(如 prev 为 null、只有 1 天)就直接说明,不要编造;',
|
|
|
- '5. 每条不超过 60 字,不要输出 JSON、标题或前后缀。',
|
|
|
+ '5. 说到零单门店时,必须引用 zeroOrder[].reasonText 的具体原因(未开业/闭店/无曝光/无进店/无转化),不要笼统说「零单门店」;',
|
|
|
+ '6. 每条不超过 60 字,不要输出 JSON、标题或前后缀。',
|
|
|
].join('\n');
|
|
|
|
|
|
function insightContext(data) {
|
|
|
@@ -450,6 +504,13 @@ function insightContext(data) {
|
|
|
shopCount: data.shopCount,
|
|
|
topShops: (data.ranking || []).slice(0, 5).map((s) => ({ name: s.shopName, revenue: s.revenue, orders: s.orders })),
|
|
|
bottomShops: (data.bottom || []).map((s) => ({ name: s.shopName, revenue: s.revenue, orders: s.orders })),
|
|
|
+ zeroOrderSummary: data.zeroOrderSummary || {},
|
|
|
+ zeroOrder: (data.zeroOrder || []).slice(0, 20).map((z) => ({
|
|
|
+ date: z.date,
|
|
|
+ shop: z.shopName,
|
|
|
+ reason: z.reason,
|
|
|
+ reasonText: z.reasonText,
|
|
|
+ })),
|
|
|
structure: (data.structure || []).map((s) => ({
|
|
|
date: s.date,
|
|
|
income: s.income,
|