# 美团券 ETL 定时任务 — 运维手册

> 对象:维护「美团 CPS 券抓取入库」这套定时任务的同事。
> 🔒 登录信息(服务器 IP / 账号密码 / DB 密码)见**私密交接清单**,**不入库**。
> 运行账号 `cps` **无 sudo**,本手册所有操作都不需要 root。

## 它是什么
每小时全量抓美团 CPS 券(北京)→ upsert 进 PostgreSQL 的 `meituan_coupon` 表,作为 App「销量最高 / 智能推荐」两个 tab 的数据源。一轮约 140 秒、~2700 条;靠 `cps` 用户的 crontab 常驻,关终端也照跑。

## 代码 / 文件在哪(都在生产服务器上)
| 项 | 路径 |
|---|---|
| 代码目录 | `/opt/shaguabijia-app-server` |
| 抓取脚本 | `scripts/pull_meituan_coupons.py` |
| Python(venv) | `/opt/shaguabijia-app-server/.venv/bin/python` |
| 配置(美团凭证 / DB 连接串) | `/opt/shaguabijia-app-server/.env`(`MT_CPS_*` / `DATABASE_URL`)|
| 日志 | `/home/cps/meituan_etl.log` |
| 运行锁 | `/tmp/meituan_etl.lock`(根治版)或 `~/data/.meituan_etl.lock`(过渡版,见文末)|
| 定时配置 | `cps` 用户 crontab(`crontab -l`)|
| 入库表 | PostgreSQL `shaguabijia` 库的 `meituan_coupon` |

## 怎么看在不在跑 / 健康
```bash
crontab -l                      # 看定时任务(应有每小时第 17 分那条)
tail -n 20 ~/meituan_etl.log    # 看最近几轮:本轮完成 / 入库条数 / 有无报错
# 查库存量与新鲜度(走 .env 连接串,不用输 DB 密码):
cd /opt/shaguabijia-app-server && .venv/bin/python -c "from sqlalchemy import text; from app.db.session import engine; print(engine.connect().execute(text('select count(*) total, count(*) filter (where sale_volume_num is not null) sales, max(last_seen) last_run from meituan_coupon')).one())"
```
**健康标准**:`last_run` 在最近 1–2 小时内、`total` 两三千、日志最近一轮是 `本轮完成`(不是 `Permission denied` / Traceback)。

## 怎么启动
定时任务已在 `cps` 的 crontab 里常驻,**随系统 cron 服务自动运行,无需手动启动**。若 crontab 被清空、要重新装回:`crontab -e`,粘下面这行存盘。

```cron
# 根治版(锁文件已改到 /tmp 后用这条,推荐):
17 * * * * cd /opt/shaguabijia-app-server && .venv/bin/python -m scripts.pull_meituan_coupons --once --prune-hours 24 >> /home/cps/meituan_etl.log 2>&1
```

**手动立即跑一轮**(不等下一个整点,比如刚发现库空):
```bash
cd /opt/shaguabijia-app-server && .venv/bin/python -m scripts.pull_meituan_coupons --once --prune-hours 24
```
前台跑、约 140 秒,出现 `本轮完成` 即成功。

## 怎么关闭(停用定时任务)
```bash
crontab -e
```
在那一行最前面加 `#` 注释掉(**推荐**,保留备查),存盘即停;要彻底删就删那一行。
> ⚠️ 别用 `crontab -r`——它会**清空你所有 cron**,不只这一条。

## 怎么终止(杀掉正在跑的那一轮)
```bash
pgrep -af pull_meituan_coupons     # 查在跑的进程和 PID
pkill -f pull_meituan_coupons      # 杀掉它(或 kill <PID>)
rm -f /tmp/meituan_etl.lock ~/data/.meituan_etl.lock   # 清掉残留锁,否则下一轮可能"跳过本轮"
```
> 脚本有 **30 分钟陈旧锁自动接管**机制:即使没手动清锁,30 分钟后下一轮也会自愈;急着立刻重跑才需手动 `rm` 锁。

## 调整频率
`crontab -e` 改那行最前面的 cron 表达式:
| 频率 | 表达式 |
|---|---|
| 每小时第 17 分(当前) | `17 * * * *` |
| 每 30 分钟 | `*/30 * * * *` |
| 每 2 小时 | `17 */2 * * *` |
> ⚠️ 别调太密:一轮 ~140 秒,且要守美团限流(脚本已配速到 ~2–3 req/s,远低于 ~15 req/s 红线;402 内置退避重试)。**每小时一轮足够**,勿并行、勿调小页间 sleep。

## 脚本参数
- `--once`:跑一轮(定时任务用这个)
- `--loop --interval 600`:常驻循环,每 600 秒一轮(调试用)
- `--prune-hours N`:清理超过 N 小时没再出现的旧券(默认 24;`0`=不清)
- 锁文件位置可用环境变量 `MEITUAN_ETL_LOCK=/path/to.lock` 覆盖
> 详见脚本头部注释:`head -30 scripts/pull_meituan_coupons.py`

## 已知问题与根治(运行锁权限)
- **现象**:cron 报 `PermissionError: [Errno 13] ... 'data/.meituan_etl.lock'`,每轮抓不进数据、表为空。
- **根因**:旧版脚本把运行锁建在代码目录的 `data/` 下,而 `data/` 属 root、`cps` 不可写(交接清单里"已给写权限"实际不成立)。
- **根治(已在代码层修复)**:脚本把锁默认改到 `/tmp/meituan_etl.lock`(任何账号可写),并支持 `MEITUAN_ETL_LOCK` 覆盖。**部署该版本后,用上面「根治版」那条干净 crontab 即可。**
- **过渡写法(根治版部署前的临时方案)**:crontab 里 `cd /home/cps` 把锁落到家目录、再用 `PYTHONPATH` 让代码仍能 import:
  ```cron
  17 * * * * cd /home/cps && PYTHONPATH=/opt/shaguabijia-app-server /opt/shaguabijia-app-server/.venv/bin/python -m scripts.pull_meituan_coupons --once --prune-hours 24 >> /home/cps/meituan_etl.log 2>&1
  ```
  根治版上线后,把这条换回上面的「根治版」标准写法即可。

## 注意事项
- **只抓北京**:脚本里 `city_id` 硬编码,扩城市需改脚本。
- **表行数 > 单轮抓取数**属正常:美团 `product_view_sign` 会轮换,跨轮累积;查询侧有 `DISTINCT ON(dedup_key)` 按「品牌\|名\|价」去重,用户看到的不重复;旧券 24h 后被 prune 清掉。
- **别和 systemd 双跑**:仓库 `deploy/meituan-etl.{service,timer}` 是**未来由 root 纳入 systemd 正规部署**的备选,和本 user cron **二选一、别同时开**(否则重复抓、浪费限流额度)。当前生产用 user cron。
- **改脚本 / 改部署**:走 git + PR,让有 root 的人 `/opt/deploy.sh` 部署(`git reset` + `alembic upgrade` + 重启);`cps` 账号改不了 `/opt` 下的代码。
