# 自动投放双服务部署

## 1. Jenkins 构建

构建统一完整镜像，不再构建 `tencent-realtime-control` 独立镜像：

```bash
set -e

REGISTRY="registry.cn-hangzhou.aliyuncs.com/stuuudy"
IMAGE="${REGISTRY}/ad-put-agent"

cd "${WORKSPACE}"
docker build \
  -f Dockerfile.auto_put_ad_mini \
  -t "${IMAGE}:latest" \
  -t "${IMAGE}:${BUILD_TIMESTAMP}" \
  .
docker push "${IMAGE}:latest"
docker push "${IMAGE}:${BUILD_TIMESTAMP}"

ssh -i /root/.ssh/piaoquan.pem root@8.222.142.37 \
  "sh /home/sh/deploy.sh ${BUILD_TIMESTAMP}"
```

## 2. ECS 文件

将仓库中的 `docker-compose.yml` 放到 `/home/server/docker-compose.yml`。
当前部署兼容 ECS 已安装的 Docker Compose `2.27.0`。

两个服务共用 `/home/server/.env`。首次部署从仓库根目录唯一生产模板
`runtime.env.example` 复制，组件目录内的 `.env.example` 仅保留本地兼容用途：

```bash
cp runtime.env.example /home/server/.env
chmod 600 /home/server/.env
docker compose version
```

原有数据库、ODPS、腾讯、飞书、OSS 和 AI 环境变量全部保留。

生产模板不配置 `CREATION_ONLY_ACCOUNT_IDS`；指定账户的一次性执行必须使用
下文的严格 `--account-id` 命令，非法账户会直接退出，不会退化成全账户。
创建流程的广告审批、创意审批和执行汇报统一发送到
`FEISHU_CREATION_CHAT_ID`；为空时才兼容回退 `FEISHU_OPERATOR_CHAT_ID`。

首次部署增加：

```dotenv
RTC_START_HOUR=12
RTC_STOP_HOUR=21
RTC_POLL_SECONDS=600
RTC_HIGH_CPM=250
RTC_LOW_CPM=190
RTC_BID_UP_RATIO=1.05
RTC_MAX_PARTITION_LAG_HOURS=2
RTC_APPLY_ENABLED=0
RTC_COMMAND_ENABLED=1
RTC_COMMAND_CHAT_ID=oc_xxx
RTC_COMMAND_ALLOWED_OPEN_IDS=ou_xxx
RTC_COMMAND_CONFIRM_TTL_MINUTES=10
RTC_COMMAND_NEXT_DELIVERY_HOUR=6
RTC_COMMAND_DRAFT_TTL_MINUTES=5
RTC_NL_COMMAND_ENABLED=0
RTC_COMMAND_LLM_MODEL=google/gemini-3-flash-preview
RTC_COMMAND_LLM_TIMEOUT_SECONDS=20
RTC_COMMAND_LLM_CONFIDENCE_THRESHOLD=0.85
OPEN_ROUTER_API_KEY=sk-or-v1-xxx

REVENUE_FORECAST_ENABLED=0
REVENUE_FORECAST_POLL_SECONDS=900
REVENUE_FORECAST_START_TIME=06:15
REVENUE_FORECAST_STOP_TIME=22:30
REVENUE_FORECAST_TREND_WINDOW_MINUTES=15
REVENUE_FORECAST_TREND_WINDOW_COUNT=2
REVENUE_FORECAST_TARGET_RATIO=3.5
REVENUE_FORECAST_MAX_LAG_MINUTES=20
REVENUE_FORECAST_VERSION=revenue_forecast_v6_weighted_30m_speed
REVENUE_SPEED_PARAMETER_VERSION=revenue_speed_params_v1
REVENUE_SPEED_LATEST_WEIGHT=0.66666667
REVENUE_SPEED_MINIMUM_SAMPLES=3

DAILY_CREATION_ENABLED=0
DAILY_CREATION_HOUR=10
DAILY_CREATION_MINUTE=30
DAILY_REVIEW_ENABLED=0
DAILY_REVIEW_INTERVAL_HOURS=2
DAILY_RUN_ON_STARTUP=0
DAILY_REVIEW_RUN_ON_STARTUP=0
DAILY_SYNC_DELIVERY_TEMPLATE=0

DAILY_ROI_ENABLED=0
DAILY_ROI_HOUR=11
DAILY_ROI_MINUTE=0
DAILY_ROI_RUN_ON_STARTUP=0
ROI_INTERNAL_TEST_ENABLED=0
ROI_INTERNAL_TEST_HOUR=15
ROI_INTERNAL_TEST_MINUTE=30
ROI_INTERNAL_TEST_DEDUP_ENABLED=1
ROI_FAILURE_FEISHU_CHAT_ID=
ROI_APPLY_ENABLED=0
ROI_SHEET_APPROVAL_ENABLED=0
ROI_SHEET_APPROVAL_POLL_SECONDS=60
ROI_APPROVAL_TTL_MINUTES=120
ROI_SCALE_RATIO=1.10
ROI_SCALE_COOLDOWN_DAYS=3
ROI_MAX_BASE_RATIO=2.00
ROI_SELF_STOP_COST_SOFT_LOWER_RATIO=0.045
ROI_SELF_STOP_COST_TARGET_RATIO=0.05
ROI_SELF_STOP_COST_SOFT_UPPER_RATIO=0.055
ROI_SELF_STOP_COST_HARD_CAP_RATIO=0.06
ROI_SELF_STOP_THREE_DAY_SHARE=0.60
ROI_SELF_STOP_ONE_DAY_SHARE=0.40
ROI_ONE_DAY_MIN_UV=200
ROI_ONE_DAY_HARD_STOP_ROI=0.20
ROI_ONE_DAY_STOP_QUANTILE=0.10
ROI_PARTNER_STOP_QUANTILE=0.20
ROI_UP_QUANTILE=0.80
ROI_FISSION_PARAMETER_VERSION=20260712_A0-A15_v2
ROI_FEISHU_CHAT_ID=oc_xxx
ROI_AGENCY_WEBHOOK_ENABLED=0
ROI_AGENCY_WEBHOOKS_JSON={}
```

多个操作人使用英文逗号分隔 `RTC_COMMAND_ALLOWED_OPEN_IDS`。
代码默认 `RTC_APPLY_ENABLED=0`，日级创建、审核扫描、ROI 和模板同步也默认关闭；
缺少显式生产配置时不会创建广告或修改腾讯。

## 3. ECS 部署脚本

`/home/sh/deploy.sh` 使用：

```bash
#!/bin/bash
set -e

echo "Qingqu@2019" |
  docker login \
    --username=stuuudys \
    registry.cn-hangzhou.aliyuncs.com \
    --password-stdin

VERSION=${1:-latest}
cd /home/server

VERSION=${VERSION} docker compose --env-file /dev/null pull
VERSION=${VERSION} docker compose --env-file /dev/null down --remove-orphans
VERSION=${VERSION} docker compose --env-file /dev/null up -d
VERSION=${VERSION} docker compose --env-file /dev/null ps
```

继续沿用部署脚本中的 `source /home/server/.env`，Compose 同时通过标准
`env_file` 将同一个文件传给两个容器，环境变量文件和加载入口不变。

## 4. 首次 dry-run

保持 `RTC_APPLY_ENABLED=0` 和两个 `DAILY_*_ENABLED=0`：

```bash
cd /home/server
docker compose --env-file /dev/null config --services
docker compose --env-file /dev/null up -d
docker compose --env-file /dev/null ps
docker compose --env-file /dev/null logs --tail=200 ad_control_service
docker compose --env-file /dev/null logs --tail=200 ad_daily_service
```

控制服务启动时自动执行实时状态表增量迁移。也可以显式验证：

```bash
docker compose --env-file /dev/null run --rm \
  ad_control_service \
  python /app/examples/tencent_realtime_control/init_db.py
```

收入预测首次上线保持 `REVENUE_FORECAST_ENABLED=0`，先执行一次只读预测。命令会
读取 ODPS 并写入收入快照和预测结果表，不会调用腾讯或飞书：

```bash
docker compose --env-file /dev/null run --rm \
  ad_control_service \
  python /app/examples/tencent_realtime_control/build_revenue_speed_parameters.py \
  --start-date 20260727 --end-date 20260805 \
  --parameter-version revenue_speed_params_v1

docker compose --env-file /dev/null run --rm \
  ad_control_service \
  python /app/examples/tencent_realtime_control/run_revenue_forecast.py \
  --ignore-runtime-window
```

确认 `forecast_method=weighted_30m_speed`、参数版本、样本数、P50预测、P10/P90
经验区间、建议总成本、昨日非小程序预留成本和小程序目标成本后，
再设置 `REVENUE_FORECAST_ENABLED=1` 并只重建 `ad_control_service`。预留成本临时
读取 `opengid_base_data` 昨日完整日数据；缺失时返回 `WAIT_COST_DATA`，不生成
小程序预算。模型参数或连续窗口不可用时会输出 `WAIT_SPEED_MODEL` 和明确原因，
本轮不生成预测。
已发布参数版本不可覆盖；训练日期变化时发布新版本并同步修改环境变量。

执行无腾讯写入的 CPM dry-run：

```bash
docker compose --env-file /dev/null run --rm \
  ad_control_service \
  python /app/examples/tencent_realtime_control/run_once.py \
  --at 2026-07-25T12:00:00 \
  --cpm-override 220 \
  --force-refresh
```

飞书验证：

1. 在配置群 @机器人发送 `查看暂停状态`。
2. 发送 `查询今天自动化账户消耗`，确认返回腾讯实时汇总且不要求审批。
3. 发送一个单账户暂停命令，确认机器人返回预览和 `cmd_xxx`。
4. 回复确认，服务必须提示 dry-run 禁止写腾讯。

## 5. 指定账户执行一次创建流程

指定账户必须经过日服务入口，以复用生产 MySQL advisory lock，并仍然执行飞书每账户最新配置同步、白名单和投放模板校验：

```bash
docker compose run --rm ad_daily_service \
  python /app/examples/auto_put_ad_mini/run_daily_service.py \
  --run-creation-once \
  --account-id 84502339
```

多个账户重复传参：`--account-id 84502339 --account-id 81214386`。如只补创意、不新建广告，在服务名前增加 `-e CREATION_SKIP_PHASE0=1`。正常执行不限制日期，每个账户使用飞书表中日期最新的一行；账户 ID 非正整数、遗漏账户参数或最新配置未启用时均直接退出，不能退化成全账户执行。`--config-date` 仅用于历史复现。

## 6. 生产切换

1. 确认旧 `auto_put_ad_mini/server.py` 和旧实时容器不再运行。
2. 设置 `RTC_APPLY_ENABLED=1`，保持日级创建和 ROI 服务关闭。
3. 重新 `docker compose up -d`，观察一个完整实时周期。
4. 用单账户命令验证暂停、恢复、腾讯回读和飞书结果。
5. 设置 `DAILY_REVIEW_ENABLED=1`、`DAILY_SYNC_DELIVERY_TEMPLATE=1`。
6. 审核扫描稳定后设置 `DAILY_CREATION_ENABLED=1`。
7. 保持 `DAILY_RUN_ON_STARTUP=0`，防止部署时额外触发完整创建。

日级 ROI 单独分两阶段启用：

当前 `north_star_roi_t15_v8` 使用版本化传播裂变参数，策略版本为 `roi_policy_v15`，报表版本为 `roi_report_v41`。T0 裂变人数取 `SUM(t0_fission_uv_root)`，不再读取旧字段 `t0裂变人数`。小程序按人群包和转化目标、
公众号按合作方和公众号匹配传播裂变系数；企微暂不进入本版报表、阈值和调控。首层效率收入读取 `效率收入`，T0 实际裂变收入读取当天的 `裂变效率收入`，
并以 T0 实际裂变收入乘传播裂变系数预测完整裂变收入。日级任务不会现场重算 cohort 参数。部署前应保持
`ROI_FISSION_PARAMETER_VERSION=20260712_A0-A15_v2`。参数必须先发布到 MySQL，
启动日志、数据库发布版本和 Excel 摘要中的参数版本、cohort 日期必须一致。
ROI 在线表只上传一次，并发送到 `ROI_FEISHU_CHAT_ID` 和
`FEISHU_OPERATOR_CHAT_ID`；两个变量相同时只发送一次。
ROI 表格使用获得链接者可编辑权限。默认隐藏的黄色【审批选择】列取消隐藏后只接受“批准”或“拒绝”，
数据库隐藏幂等键决定真实执行目标，表格中的账户、广告、创意、成本和 ROI 不作为写入参数。

审批表包含小程序创意级、小程序广告级和公众号的三日汇总与每日明细共 6 个主 Sheet，广告级两个 Sheet 默认隐藏。
金额、UV 和人数展示三日总量/3，ROI 和裂变率使用三日汇总后的加权口径。连续三天每天
首层 UV>200、成本>0 且 ROI 有效的小程序创意和公众号实体分别建立渠道独立样本池。公众号即转使用自身合格实体等权 P20；小程序使用 T-1 创意级实际成本5%软预算动态反推关停线和实体分位点，禁止把广告级重复成本加入分母。
广告级直接从 ODPS 原始数据按广告去重，复用小程序三日动态关停线但不进入创意成本预算样本池；低于关停线且广告 age>3 天时生成广告级关停建议，批准后暂停整个广告。
小程序三日持续低 ROI 与单日补充规则按 6:4 获得目标成本的60%/40%基础额度。两组分别按三日加权预测 ROI、T-1预测 ROI 从低到高取连续前缀；目标关停成本占昨日总成本5%，软上限5.5%，绝对上限6%，未用额度只在剩余候选间软流转。
三日正式判断严格优先：已满足连续三日正式样本条件的小程序创意，无论预算选择或最终动作如何，都不得进入单日判断。其余创意最新日首层 UV>200、成本>0、ROI有效且广告 age>3天时进入单日候选池，预测总效率 ROI 同时≤0.20和候选池实体等权P10才成为关停候选；不再使用历史保护。
三日汇总可见尾部列按“三日均值ROI → 当日效率ROI → 预测总效率ROI → 关停线 → 扩量线（P80） → 建议动作 → 建议说明”排列；三列分别是三日加权实际ROI、最新日实际ROI和三日加权预测ROI，不单列最新日预测ROI。小程序创意级、广告级可见关停动作分别显示为“关停创意”“关停广告”，底层仍保存统一动作值供执行器消费。创意关停按三日持续低 ROI、单日绝对低 ROI 且P10两类建议说明分组排序，同类内部按对应 ROI 升序。公众号不参与扩量，扩量线留空。
整体三日排名百分位、是否低于三日关停线、最新日UV、覆盖天数、昨日成本、关停规则、预算选择状态、实际关停成本占比、阈值样本状态和执行结果只保留为隐藏审计字段；底层动作原因通过“建议说明”展示。
每日明细 Sheet 的 `dt` 位于第一列并按日期倒序。主 Sheet 冻结首行和前 7 列，
关停线列为动态“关停线”；“关停线分位点”只进入运行摘要和数据库快照，不写入业务 Sheet。所有小程序创意级和广告级关停统一要求广告 age>3 天。汇总表将三日总 T0 裂变人数 / 三日总首层 UV 的加权比例展示为“日均T0裂变率”。合格小程序创意实体等权 P80 为扩量线，处于头部20%且广告 age≥3 时生成扩量建议。汇总顺序为关停、扩量、中间观察、条件不足观察；第一条扩量行和扩量后第一条观察行顶部均使用粗线分隔。三列ROI均使用深红—黄—绿色阶，绿色代表表现好。普通数值显示两位小数，UV、人数、数量和广告年龄显示整数；两个“裂变系数-总裂变UV/…”比率列固定显示两位小数。“传播裂变系数匹配”保留审计数据但默认隐藏。
创意级“当前创意状态”只对关停建议只读腾讯状态并显示正常、已停止或读取失败，其他行留空；“审批选择”默认隐藏。
完整主报表写入成功后，服务额外按“小程序投流”渠道的“代理名称”生成一代理一份 `roi_agency_advice_v9` 工作簿，文件名为 `YYYYMMDD_代理名称_调控建议.xlsx`。代理版可见 Sheet 名为“小程序创意调控建议”，并保留默认隐藏的“小程序广告级三日汇总”，不包含每日明细；包名、广告age、日均首层UV、建议说明、收入、预测总效率ROI、阈值、排名、两个裂变系数、日均T0裂变人数/率、审批和执行审计列均物理删除。“评分”使用最新日实际ROI并保留两位小数，不参与代理动作计算；建议动作仍按内部策略生成，并明确显示“关停创意”或“关停广告”。代理表生成不改变主报表、数据库快照或主审批链接。

正式任务每天北京时间11:00执行。任务固定以T-1为结束日，并在创建MySQL运行批次、生成报表和发送飞书前校验 `loghubods.opengid_base_data` 的精确T-1分区；小程序投流或公众号投流相关数据任一为空即失败关闭，禁止静默回退到更早分区。

总表继续由飞书应用按 `ROI_FEISHU_CHAT_ID` / `FEISHU_OPERATOR_CHAT_ID` 发布。代理表发布由 `ROI_AGENCY_WEBHOOK_ENABLED` 独立控制，完整机器人地址仅允许保存在密钥系统或真实 `.env` 的 `ROI_AGENCY_WEBHOOKS_JSON`，JSON key 使用代理简称。飞书应用先上传并开放在线表链接，再由对应代理机器人发送可点击卡片；未配置代理记录为跳过，不回退到总群。同一 `run_id + 代理 + 代理报表版本` 幂等审计，已上传表格在通知重试时复用，避免重复建表。机器人地址只保存哈希指纹，不能写入数据库或日志。

`ROI_INTERNAL_TEST_ENABLED=1` 时，服务每天北京时间15:30注册独立内部任务。该任务复算后使用空管理账户集合，不生成可审批腾讯动作，只把“自动化投放”分表发送到 `ROI_AGENCY_WEBHOOKS_JSON` 的“内部”路由，不发送完整主表、正式总群或代理群；缺少该分表时任务失败并告警。它不注册startup任务，服务重启不会主动补发。正式任务始终去重；内部测试由 `ROI_INTERNAL_TEST_DEDUP_ENABLED` 控制，生产默认`1`，测试环境显式设为`0`时每次触发生成独立测试run并重新发送。

11:00正式任务或15:30内部测试的子进程启动失败或非零退出时，调度服务通过飞书应用向 `ROI_FAILURE_FEISHU_CHAT_ID` 发送红色失败告警；该变量为空时回退 `FEISHU_OPERATOR_CHAT_ID`。失败告警本身发送失败只记录错误日志，不会吞掉或替换原任务异常。

1. 首次部署先执行下方参数发布命令并完成回读校验。
2. 设置 `DAILY_ROI_ENABLED=1`、`ROI_APPLY_ENABLED=0`、`ROI_SHEET_APPROVAL_ENABLED=0`，观察11:00的T-1来源校验、ODPS计算、数据库快照和可编辑飞书表。
3. 核对阈值、账户可执行范围、黄色审批列和动作数量。
4. 再设置 `ROI_APPLY_ENABLED=1`、`ROI_SHEET_APPROVAL_ENABLED=1`，重建 `ad-control-service`；批准一条低风险测试行，核对腾讯回读、表格结果和群通知。
5. 生产审批有效期默认 120 分钟；过期行不能执行。表格逐行选择“拒绝”不会产生腾讯写操作。
6. 不允许 `DAILY_ROI_ENABLED=0`、`ROI_APPLY_ENABLED=1`，也不允许仅开启表格审批而关闭 ROI 写操作；服务启动阶段会拒绝不完整配置。

首次启用前可手工运行一个“不发飞书、不写腾讯”的计算批次。该命令会读取 ODPS、
初始化/写入 ROI 数据库快照并在挂载目录生成本地 Excel：

```bash
docker compose run --rm ad_daily_service \
  python /app/examples/auto_put_ad_mini/run_daily_roi.py \
  --end-date 20260725
```

确认数据库快照和本地报告后，再增加 `--send-feishu` 验证在线表、黄色审批列和链接编辑权限。
此时必须保持 `ROI_APPLY_ENABLED=0`、`ROI_SHEET_APPROVAL_ENABLED=0`，不会轮询或执行表格选择。

如果同一日期窗口的上游历史数据被修订，必须提供稳定的数据修订标识生成新批次，不能覆盖或删除已发布快照：

```bash
docker compose run --rm ad_daily_service \
  python /app/examples/auto_put_ad_mini/run_daily_roi.py \
  --end-date 20260729 \
  --source-revision datafix_20260730_1 \
  --send-feishu
```

相同 `--source-revision` 重复执行会复用同一批次；新的修订必须使用新的标识。日常调度不传此参数。

离线复算新参数必须与日级任务分开执行。以下命令默认只生成 SQL，不访问 ODPS：

```bash
docker compose run --rm ad_daily_service \
  python /app/examples/auto_put_ad_mini/refresh_roi_fission_multiplier.py \
  --cohort-date 20260712 \
  --sql-only
```

去掉 `--sql-only` 才会固定执行小程序、公众号和企微三条只读 cohort SQL，并生成草稿 CSV/Excel。草稿不会自动
进入生产；必须审计并在 `roi_control/fission_multiplier.py` 登记版本和文件 SHA256。
小程序和公众号写入联合发布草稿，企微另行生成独立草稿。企微正式版本只需
登记企微 CSV 的 SHA256，数据库参数行只允许合作方精确匹配；不存在企微文件或参数行的旧版本继续使用默认参考系数 2.5。
首次上线或切换版本前，显式发布到 MySQL：

```bash
docker compose run --rm ad_daily_service \
  python /app/examples/auto_put_ad_mini/refresh_roi_fission_multiplier.py \
  --publish-version 20260712_A0-A15_v2 \
  --published-by production-deploy
```

脚本会初始化参数表、写入不可变版本并立即回读校验。相同版本相同内容重复执行会
安全跳过；相同版本内容不同会拒绝覆盖。发布成功后再设置
`ROI_FISSION_PARAMETER_VERSION` 并启用日级 ROI。

## 6. 验收与回滚

验收：

```bash
docker compose --env-file /dev/null ps
docker compose --env-file /dev/null logs --since=30m ad_control_service
docker compose --env-file /dev/null logs --since=30m ad_daily_service
```

应确认：

- 两个容器均为 healthy。
- 实时循环每 10 分钟运行且没有双实例锁冲突。
- 飞书只有一个 WebSocket 消费者。
- 10:30 只出现一次创建任务。
- 审核扫描每 2 小时最多一个实例。
- 11:00正式任务只生成一个相同指标/策略版本的ROI批次，同日重跑不会重复发布；15:30内部测试是否去重由独立变量控制。
- ROI 审批执行与实时 CPM 调控不并发写腾讯，基础出价调整后两边状态一致。

回滚时将 `VERSION` 改为上一个镜像版本并重新部署。数据库新增表和列可以保留，
旧代码不会读取这些字段，不需要执行破坏性回滚。
