from pathlib import Path
import json,csv,hashlib,shutil
import xlsxwriter
P=Path(__file__).resolve().parent
raw=json.load(open('/tmp/active-users-project-breakdown.json'));period=json.load(open('/tmp/active-users-periods.json'))
d=json.load(open('/tmp/active-users-dashboard-default.json'))['data'];v=json.load(open('/tmp/active-users-dashboard-range.json'))['data']
proof=json.load(open('/tmp/active-users-browser-proof.json'))
assert sum(x['summary']['active_user_count'] for x in raw if x['query']=='default')==8451
assert sum(x['summary']['active_user_count'] for x in raw if x['query']=='range')==1258
monthly={pr:{r['period']:r['summary']['active_user_count'] for r in period if r['project']==pr and r['period']!='daily'} for pr in ['zhct_xkbg','zhct','zhct_stzc']}
assert {k:sum(x.values()) for k,x in monthly.items()}=={'zhct_xkbg':6846,'zhct':1488,'zhct_stzc':117}
short={'zhct_xkbg':'西康宾馆','zhct':'产业园','zhct_stzc':'首通智城','zhct_jc':'首都机场','zhct_jsr':'金斯瑞','zhct_saidi':'赛迪物业','zhct_sxjm':'山西焦煤','zhct_sh':'四川射洪'}
rmap={r['project']:r for r in raw if r['query']=='range'}
projectrows=[]
for r in sorted([r for r in raw if r['query']=='default'],key=lambda x:(x['summary']['active_user_count'],x['summary']['order_count']),reverse=True):
 s=r['summary'];rs=rmap[r['project']]['summary'];name=short.get(r['project'],r['name'].replace('智慧营养健康餐厅',''))
 if s['active_user_count']>0:note='已计入人次；跨日重复出现会重复累计。'
 elif s['order_count']>0:note='有历史订单但活跃为0；不能解释为无人使用，需查历史回填、人员标识和同步。'
 elif not s['connected_projects']:note='未取得项目快照；表内0不代表现场没有用户。'
 elif r['status']=='failed':note='持有历史快照、当前同步失败；无新增活跃不能代表没人使用。'
 else:note='已采到用户信息；当前中央统计未发现订单活跃。'
 projectrows.append([name,s['user_count'],s['order_count'],s['active_user_count'],s['active_user_count']/8451,rs['order_count'],rs['active_user_count'],{'success':'成功','failed':'失败','pending':'待同步'}.get(r['status'],r['status']),note])
with (P/'project-breakdown.csv').open('w',encoding='utf-8-sig',newline='') as f:
 w=csv.writer(f);w.writerow(['项目','登记账号快照','累计订单','累计订单活跃人次','贡献比例','8月3至8日订单','8月3至8日活跃人次','取数时同步状态','解释']);w.writerows(projectrows)
(P/'aggregate-evidence.json').write_text(json.dumps({'summary_default':d['summary'],'filters_default':d['filters'],'summary_requested':v['summary'],'filters_requested':v['filters'],'browser':proof,'project_queries':raw,'period_queries':period,'source_snapshot_times':sorted(set(r['snapshot_at'] for r in raw+period))},ensure_ascii=False,indent=2))
report='''# 大屏“活跃8000”项目拆解与计算依据

**结论：8451是三个项目的累计订单活跃人次，不是8451个独立用户，更不是8000个App活跃用户。**

## 1. 先核对日期

用户链接携带2026-08-03至2026-08-08，但线上页面首次只请求 `/api/dataCenterDashboard`，没有传入日期参数。浏览器实测筛选框最终为2017-08-04至2026-09-05，显示8451人。按链接日期直接请求API，得到1258订单活跃人次、2135单、15304.58元订单流水。链接参数未生效与“人/人次”误标是两件独立问题。

## 2. 8451从哪里来

| 项目 | 登记账号快照 | 累计订单 | 累计订单活跃人次 | 贡献 |
| --- | ---: | ---: | ---: | ---: |
| 西康宾馆 | 352 | 129477 | 6846 | 81.0% |
| 产业园 | 2044 | 22841 | 1488 | 17.6% |
| 首通智城 | 289 | 2802 | 117 | 1.4% |
| 活跃字段合计 | — | — | 8451 | 100% |

8451 = 6846 + 1488 + 117。查询24项目分别取值后相加，与全项目接口完全一致。西康贡献最多，是“多天订单账号数累积”的主要来源；不能解释为西康有6846个不同人。登记账号数是最新人员表快照，不是这个历史区间的独立活跃人数。

## 3. 按月复算

| 项目 | 2017-08-04至2026-06-30 | 7月 | 8月 | 9月1—5日 | 合计人次 |
| --- | ---: | ---: | ---: | ---: | ---: |
| 西康宾馆 | 0 | 1854 | 4238 | 754 | 6846 |
| 产业园 | 0 | 738 | 692 | 58 | 1488 |
| 首通智城 | 0 | 0 | 11 | 106 | 117 |
| 合计 | 0 | 2592 | 4941 | 918 | 8451 |

西康和产业园7/1—7/19分别有4189与1061单，但活跃字段均为0；7/20分别出现161与65人次。当前历史人次至少在这两项目上与订单覆盖期不一致。不能用7月至8月总量直接推断增长；缺失回填、字段采集或功能变更的具体原因仍需生产同步记录与源库验证。

## 4. 用户链接6天的逐日复算

| 日期 | 西康宾馆人次 | 产业园人次 | 合计人次 |
| --- | ---: | ---: | ---: |
| 8/3 | 160 | 85 | 245 |
| 8/4 | 163 | 79 | 242 |
| 8/5 | 158 | 92 | 250 |
| 8/6 | 158 | 94 | 252 |
| 8/7 | 148 | 9 | 157 |
| 8/8 | 105 | 7 | 112 |
| 合计 | 892 | 366 | 1258 |

1258 = 892 + 366，也是六天按天统计结果之和。它不是6天内去重后的1258人。产业园8/7—8/8下单活跃减少，需核对当日开餐、使用与采集情况，不能仅据此归因为用户流失。

## 5. 计算的代码依据

数据链路：项目 `ydy_meal_order` → 按项目/自然日聚合 → `data_center.dc_daily_project_stats` → `DashboardService::activeUserVisits()` → 页面 `summary.active_user_count`。

项目每日去重：

```sql
SELECT meal_date,
       COUNT(DISTINCT NULLIF(NULLIF(CAST(staff_uuid AS CHAR), ''), '0')) AS active_user_count
FROM ydy_meal_order
WHERE meal_date BETWEEN :date_from AND :date_to
GROUP BY meal_date;
```

实际字段按表结构依次选择 `staff_uuid`、`user_id`、`user_uuid`。这是列存在性选择，不是每条订单逐行兜底；若先选字段存在但某行为空，该行不计入人数。该代码没有额外限制支付成功、完成状态或删除标记，因此不能直接称为“真实付费消费人数”。

页面区间合计：

```sql
SELECT SUM(active_user_count)
FROM dc_daily_project_stats
WHERE stat_date BETWEEN :date_from AND :date_to;
```

选择项目时增加project_code过滤；选择餐段时改从dc_daily_meal_stats按日期/餐段累计。同一账号同一日多单只计1，同一账号跨5日计5；跨项目也没有统一自然人身份去重。

**示例（解释规则，非真实人员记录）：** 张三周一3笔、周二2笔订单，日活分别为1和1，累计人次为2，独立活跃人数为1。

## 6. 为什么不能据此判断公司真实用户规模

- 注册项目24个，有历史快照13个，累计有订单8个，但活跃字段非零仅3个；接入/有订单/活跃统计覆盖不是一回事。
- 首都机场814893单、金斯瑞729867单、赛迪131433单、山西焦煤2790单、四川射洪24单，活跃字段全部为0。前四项目取数时同步失败；射洪取数成功但活跃仍0。不能据此说这些项目没有用户。
- 滨州、央美、三全等没有中央快照的项目不会贡献到这个总数；可能已有现场运行，不能把采集未接入当业务未使用。
- 28599是各项目最新登记账号快照合计；8451是部分日期/项目累计人次，两者不能相除当活跃率。
- 8451不是页面访问、登录或App/H5/小程序活跃。原系统另有mobile_product_activity统计设计，不能与顶部订单活跃混用。

## 7. 适合周报的表述

“大屏当前记录8451累计订单活跃人次，其中西康宾馆6846、产业园1488、首通智城117。由于跨天重复累计且部分项目历史活跃未覆盖，暂不等同于独立用户规模。8月3日至8日已采到的两个项目为1258活跃人次、2135单；下一步统一指标命名、修正日期筛选并补齐项目统计。”

## 8. 下一步应改什么（本次仅诊断，未改线上）

1. 页面顶部改名“累计订单活跃人次”，明确同账号跨天重复计数。
2. 初始化读取date_from/date_to/project/meal_segment，分享链接打开后应与筛选框和API一致。
3. 另算真正的周期独立用户：在源订单层按“项目＋用户标识”跨日期COUNT DISTINCT，或建立受控匿名用户级事实表；现有日汇总人数无法还原跨日重叠。
4. 把采集失败、历史缺口、标识为空与真实0分开；各项目展示数据截止日和字段覆盖状态。
5. 若要“有效消费活跃”，先确认成功支付/退款/撤销/删除/测试订单规则，再用同口径数据重算。

## 证据边界

本次只读线上页面和API，按24项目、3个贡献项目的月度及两个项目的逐日回算，核心活跃数、订单数与登记账号数均与全表一致。读取时点为2026-09-05 17:47起，分项快照显示17:52:35与17:55:49；过程中数据中心同步有运行状态，结果为冻结观察值，后续刷新可能变化。

本地store代码基线301815bd695483a5709d2fac4d6b7f29e1334b49，相关实现提交7071217d0。代码与API按日求和行为一致，但本次未取得生产部署SHA或源库逐人级结果。配置指定的本地业务库连接清单不存在，因此没有越过接口去读取人员明细；真实跨日去重人数尚不能提供。

定位：DashboardService.php:37、1132—1160；SyncService.php:128—140、481—489；网页index.html:1394—1396、1428、1563。生产未修改，没有触发统计回填或业务交易。
'''
(P/'analysis.md').write_text(report)
wb=xlsxwriter.Workbook(P/'活跃8451人次_项目拆解与计算依据.xlsx')
def fmt(**x):return wb.add_format({'font_name':'微软雅黑','font_size':12,'text_wrap':True,'valign':'top',**x})
normal=fmt(border=1,border_color='CBD7E5');blue=fmt(bg_color='E7F1FC',font_color='1557A0',border=1,border_color='CBD7E5');orange=fmt(bg_color='FFF0DE',font_color='A44B0D',border=1,border_color='CBD7E5');head=fmt(bg_color='17365D',font_color='FFFFFF',bold=True);pct=fmt(num_format='0.0%',border=1,border_color='CBD7E5');num=fmt(num_format='#,##0',border=1,border_color='CBD7E5')
def new(name,title,n,widths):
 s=wb.add_worksheet(name);s.hide_gridlines(2);s.merge_range(0,0,0,n-1,title,head);s.set_row(0,32)
 for i,width in enumerate(widths):s.set_column(i,i,width)
 s.set_landscape();s.set_paper(8);s.fit_to_pages(1,0);s.set_margins(.3,.3,.4,.4);s.repeat_rows(0,2);s.set_footer('&L活跃口径拆解｜2026-09-05&R&P / &N');return s
s=new('结论与计算口径','8451是累计订单活跃人次，不是8451个独立用户',2,[30,130])
explain=[
('先说结论','8451 = 西康6846 + 产业园1488 + 首通智城117。其他21项目当前活跃字段为0，但不能全部解释为没人使用。'),
('日期没有跟随链接','用户链接是8/3—8/8；线上首次请求没有日期参数，实际默认2017-08-04—2026-09-05。指定链接日期的正确接口结果是1258人次。'),
('第一步：按日去重','每项目每天在ydy_meal_order里对非空非0人员标识COUNT DISTINCT，同人一天多单记1。日期取meal_date。'),
('第二步：跨日相加','中央库SUM(dc_daily_project_stats.active_user_count)。同一人连续5天下单会贡献5人次，不是5个人。'),
('选哪个人员字段','代码按表结构优先staff_uuid，其次user_id，再次user_uuid；不是每行自动兜底。字段为空/0的订单不贡献人数。'),
('并非付费消费人数','当前可读代码没有过滤pay_status、订单完成状态、is_delete；不能将有订单记录直接等于成功消费。'),
('历史存在缺口','西康/产业园7/1—19都有订单，活跃为0；7/20开始出现非零。前期订单和后期活跃覆盖不同。'),
('覆盖不完整','24项目维表；13有历史快照；8有累计订单；3活跃字段非零。机场、金斯瑞、赛迪等有大量历史订单但活跃为0，且取数时同步失败。'),
('真实独立人数','日汇总没有保留跨日账号重叠，无法还原周期去重人数。本次没有人员级数据，需在源订单或匿名用户级事实重算。'),
('不要算的比例','8451/28599不是活跃率：分子是部分项目/时间的人次，分母是各项目最新登记账号快照。'),
('周报怎么讲','当前大屏记录8451累计订单活跃人次，主要来自西康宾馆；8/3—8/8为1258人次、2135单。跨天累计与项目采集缺口尚未校准，暂不代表独立用户规模。'),
('改进顺序','先改人次名称及URL日期读取；再补同步/历史覆盖；最后按明确的业务行为口径统计真正独立用户。本次未修改线上。'),
('来源与核验','线上页面＋dataCenterDashboard API＋store DashboardService/SyncService。本次24项目求和、月度求和、6日求和均一致。精确源库去重与生产部署SHA未核验。')]
for i,(a,b) in enumerate(explain,2):s.write(i,0,a,normal);s.write(i,1,b,blue if i<5 else normal);s.set_row(i,58 if i in [3,8,9,12,14] else 48)
s.print_area(0,0,14,1)
s=new('24项目拆解','项目贡献拆解｜活跃单位为人次，0值须结合采集状态解释',9,[22,18,20,24,15,23,26,19,68])
headers=['项目','登记账号快照','累计订单数','累计订单活跃人次','贡献比例','8/3—8/8订单','8/3—8/8活跃人次','取数时同步','说明']
for j,h in enumerate(headers):s.write(2,j,h,head)
for i,r in enumerate(projectrows,3):
 for j,x in enumerate(r):
  if j==4:s.write_formula(i,j,f'=D{i+1}/$D$28',pct,x)
  else:s.write(i,j,x,num if j in [1,2,3,5,6] else normal)
 s.set_row(i,61)
s.write(27,0,'总计',head)
for j,cache in [(1,28599),(2,1834127),(3,8451),(5,2135),(6,1258)]:s.write_formula(27,j,f'=SUM({chr(65+j)}4:{chr(65+j)}27)',num,cache)
s.write_formula(27,4,'=SUM(E4:E27)',pct,1);s.merge_range(28,0,28,8,'0值仅是中央表当前数值。最新登记账号、历史订单、人次分别统计，不作为统一活跃率。',orange);s.set_row(28,38);s.autofilter(2,0,26,8);s.freeze_panes(3,1);s.print_area(0,0,28,8)
s=new('逐月逐日复算','把8451与1258分别按日期加回来',6,[25,27,26,26,27,24])
for j,h in enumerate(['项目','7月之前','2026年7月','2026年8月','9/1—9/5','累计人次']):s.write(2,j,h,head)
for i,pr in enumerate(['zhct_xkbg','zhct','zhct_stzc'],3):
 s.write(i,0,short[pr],normal)
 vals=[monthly[pr][k] for k in ['before-july','july','august','september']]
 for j,x in enumerate(vals,1):s.write(i,j,x,num)
 s.write_formula(i,5,f'=SUM(B{i+1}:E{i+1})',num,sum(vals));s.set_row(i,32)
s.write(6,0,'合计',head)
for j,cached in [(1,0),(2,2592),(3,4941),(4,918),(5,8451)]:s.write_formula(6,j,f'=SUM({chr(65+j)}4:{chr(65+j)}6)',num,cached)
s.merge_range(8,0,8,5,'注意：7月并非完整活跃覆盖。西康和产业园7/1—19有订单但活跃0，不能直接比较7月和8月判断增长。',orange);s.set_row(8,46)
s.merge_range(10,0,10,5,'8月3—8日：每天去重，跨天再相加',head)
for j,h in enumerate(['日期','西康人次','产业园人次','合计人次','西康订单','产业园订单']):s.write(11,j,h,head)
for i,day in enumerate(range(3,9),12):
 date=f'2026-08-{day:02d}';xs=next(r['summary'] for r in period if r['project']=='zhct_xkbg' and r['period']=='daily' and r['from']==date);ys=next(r['summary'] for r in period if r['project']=='zhct' and r['period']=='daily' and r['from']==date)
 for j,x in enumerate([date,xs['active_user_count'],ys['active_user_count'],None,xs['order_count'],ys['order_count']]):
  if j==3:s.write_formula(i,j,f'=SUM(B{i+1}:C{i+1})',num,xs['active_user_count']+ys['active_user_count'])
  else:s.write(i,j,x,normal if j==0 else num)
 s.set_row(i,32)
s.write(18,0,'合计',head)
for j,cache in [(1,892),(2,366),(3,1258),(4,1589),(5,546)]:s.write_formula(18,j,f'=SUM({chr(65+j)}13:{chr(65+j)}18)',num,cache)
s.merge_range(20,0,20,5,'示例：张三周一3笔、周二2笔订单 → 每天各记1 → 累计2人次 → 独立用户仍只有1人。',blue);s.set_row(20,42)
s.merge_range(22,0,22,5,'执行只读回算，没有修改大屏或生产统计。原始聚合与浏览器请求已留本地证据；本表不含个人身份数据。',normal);s.set_row(22,40);s.print_area(0,0,22,5)
wb.close()
(P/'.gitignore').write_text('*.xlsx\n*.pdf\n*.csv\n*.png\naggregate-evidence.json\n')
shutil.copy2('/tmp/active-users-browser-proof.png',P/'browser-proof.png')
(P/'browser-proof.json').write_text(json.dumps(proof,ensure_ascii=False,indent=2))
print('ANALYSIS_CREATED',str(P),len(projectrows))
