CaiWuXiTong/deploy/activate-release.sh
limeng e12c9c20ec fix(部署): 聚水潭发货数据定时同步失败自动重试并留日志,晚上补拉一次 (#1183)
每晚 01:28 只跑一次 jushuitan:sync-shipping --days=3,输出丢到 /dev/null;
副本库偶发超时时整次同步失败且无记录,最近几天大量漏单(如 11-06 只同步到 43%、11-08 68%)。
- run-finance-batch.sh:发货数据同步输出含"同步失败"时自动重试(默认 3 次、间隔 5 分钟),
  每次输出写 /opt/caiwuxitong/jst-sync-shipping.log(超 5MB 轮转);只有警告不重试;
- cron 增加 21:28 再拉最近 3 天,补齐凌晨商品明细未到齐/超时漏掉的包裹。

注:BusinessMirrorScheduleTest 在 main 上已有 2 个与本改动无关的失败(compose.jiangling.yml 的
config_db 内存参数、app 服务配置断言),本次未改动。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhry6eiuwf

> 开工时标题使用 `WIP: <任务名称>` 并先填写“任务占用与边界”。Codex 完成独立合并前自审后补齐其余内容并移除 `WIP:`。

## 任务占用与边界

- 任务名称:
- 实际开发者:
- 任务来源/团队分工:
- 目标结果:
- 预计修改路径:
- 将写入的公共契约(路由、Controller 方法、Repository、表/视图、`source_key`、`scope`、权限名、菜单键或共享组件):
- 仅复用、明确不修改的公共契约:
- 数据源、表/视图和字段:
- 上游 `source_key` / `scope`:
- 权限名、菜单键或共享组件:
- 明确不修改的范围:

## 开工碰撞检查

- [ ] Codex 已执行 `scripts/codex-task-preflight.ps1`
- [ ] 已检查最新 `main` 和所有远程任务分支
- [ ] 已搜索主线既有页面、接口、Repository 和数据查询
- [ ] 已把“将写入”和“仅复用、不修改”的公共契约分开;仅复用项不作为单独阻断条件
- [ ] 未发现实际写入重叠;或已在下方写明相关分支、具体重叠项和协调结论

相关分支、重叠项与协调结论:

## 实际改动

- 完成内容:
- Codex 实际修改的文件:
- 新增或变更的公共契约:
- 与原计划的差异:

## 验证证据

- 正常流程:
- 空数据/错误输入/无权限场景:
- 受影响旧功能:
- 自动测试及结果:
- 实际页面、接口或只读业务查询结果:

## 风险检查

- [ ] 不涉及金额、权限、审批、数据库迁移、生产配置或数据修复
- [ ] 如涉及上述高风险内容,已在下方写明影响、回滚和所需授权
- [ ] 未提交 `.env`、密码、Token、私钥、真实财务数据、生产日志或构建产物
- [ ] 前端改动符合 `DESIGN.md`;无前端改动则写“不适用”

高风险影响、回滚和授权状态:

## 合并前复查

- [ ] 已重新拉取远程引用并用实际修改范围再次运行碰撞检查
- [ ] PR 不再是 `WIP:`,所有测试结果已经补齐
- [ ] Codex 已重新读取原始需求和验收标准,并逐文件审核 `origin/main...任务分支` 的完整 diff
- [ ] Codex 自审确认无越界修改、重复实现、敏感内容、未处理冲突和未验证结果
- [ ] 合并顺序不会覆盖其他未合并任务
- [ ] 已授权当前任务的 Codex 执行 Squash merge、验证 `main` 并清理任务分支
- [ ] 如涉及生产迁移、数据修复、删除、覆盖配置或部署,已另行取得对应高风险授权

Reviewed-on: #1183
2026-11-15 14:07:08 +08:00

295 lines
19 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
set -eu
IMAGE_ARCHIVE="${1:?IMAGE_ARCHIVE is required}"
IMAGE_TAG="${2:?IMAGE_TAG is required}"
REMOTE_DIR="${3:-/opt/caiwuxitong}"
DEPLOY_SHA="${4:?DEPLOY_SHA is required}"
RUN_MIGRATIONS="${5:-0}"
KEEP_IMAGES="${6:-2}"
EXPECTED_PREVIOUS_SHA="${7:?Checked production revision is required; update the deployment scripts}"
DEPLOY_ACTOR="${8:-unknown}"
RELEASE_APP_URL="${9:-}"
RELEASE_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
. "$RELEASE_DIR/release-guard.sh"
IMAGE_NAME="caiwu_akaunting"
LOCK_FILE="$REMOTE_DIR/.deploy.lock"
cleanup() {
rm -f "$IMAGE_ARCHIVE"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if [ "$(id -u)" -ne 0 ]; then
echo "activate-release.sh must run as root" >&2
exit 1
fi
if [ ! -f "$REMOTE_DIR/runtime/.env" ]; then
echo "Missing $REMOTE_DIR/runtime/.env; run bootstrap-server.sh first" >&2
exit 1
fi
clickhouse_env="$REMOTE_DIR/runtime/clickhouse.env"
if [ ! -f "$clickhouse_env" ]; then
clickhouse_password="$(openssl rand -hex 24)"
umask 077
{
printf '%s\n' \
'CLICKHOUSE_DB=caiwu_business_analytics' \
'CLICKHOUSE_USER=caiwu_app' \
"CLICKHOUSE_PASSWORD=$clickhouse_password" \
'CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1' \
'BUSINESS_CLICKHOUSE_ENABLED=true' \
'BUSINESS_CLICKHOUSE_URL=http://clickhouse:8123' \
'BUSINESS_CLICKHOUSE_DATABASE=caiwu_business_analytics' \
'BUSINESS_CLICKHOUSE_USERNAME=caiwu_app' \
"BUSINESS_CLICKHOUSE_PASSWORD=$clickhouse_password"
} > "$clickhouse_env"
chmod 600 "$clickhouse_env"
fi
if ! grep -q '^CLICKHOUSE_PASSWORD=.' "$clickhouse_env" \
|| ! grep -q '^BUSINESS_CLICKHOUSE_PASSWORD=.' "$clickhouse_env"; then
echo "Incomplete $clickhouse_env; refusing to overwrite private ClickHouse credentials" >&2
exit 1
fi
if [ ! -f "$REMOTE_DIR/runtime/config-db.env" ]; then
echo "Missing $REMOTE_DIR/runtime/config-db.env; run bootstrap-server.sh first" >&2
exit 1
fi
if ! grep -q '^BUSINESS_STORAGE_DRIVER=qiniu$' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null \
|| ! grep -q '^QINIU_ACCESS_KEY=.' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null \
|| ! grep -q '^QINIU_SECRET_KEY=.' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null; then
echo "Private business storage runtime is incomplete" >&2
exit 1
fi
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "Another CaiWuXiTong deployment is already running" >&2
exit 1
fi
RELEASE_STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
RELEASE_RESULT=blocked
trap 'release_status=$?; caiwu_record_release_result "$release_status"; cleanup' EXIT
# 容器替换会终止其中的同步进程;与生产刷新任务共享同一把锁。
exec 8>/run/caiwu-upstream-sync.lock
echo "Waiting for active upstream synchronization before replacing the application container"
if ! flock -w 1800 8; then
echo "Upstream synchronization is still running; application container was not replaced" >&2
exit 1
fi
PREVIOUS_IMAGE="$(docker inspect caiwu_app --format '{{.Config.Image}}' 2>/dev/null || true)"
caiwu_verify_release_base
RELEASE_RESULT=running
restore_previous_image() {
case "$PREVIOUS_IMAGE" in
"$IMAGE_NAME":*)
IMAGE_TAG="${PREVIOUS_IMAGE#*:}"
RUN_MIGRATIONS=0
export IMAGE_TAG RUN_MIGRATIONS
docker compose -f compose.jiangling.yml up -d app || true
;;
esac
}
docker load --input "$IMAGE_ARCHIVE"
rm -f "$IMAGE_ARCHIVE"
LOADED_SHA="$(docker image inspect "$IMAGE_NAME:$IMAGE_TAG" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')"
if [ "$LOADED_SHA" != "$DEPLOY_SHA" ]; then
echo "Image revision mismatch: expected $DEPLOY_SHA, got $LOADED_SHA" >&2
exit 1
fi
caiwu_install_release_files
cd "$REMOTE_DIR"
export IMAGE_TAG RUN_MIGRATIONS
# Pull the independent columnar runtime before migrations or app switching.
# A registry/network failure therefore leaves both the database and current app untouched.
docker compose -f compose.jiangling.yml pull clickhouse
if [ "$RUN_MIGRATIONS" = "1" ]; then
# 先创建独立业务镜像库并授权应用账号,再启动会执行迁移的新应用。
# 该脚本幂等;已有数据库只会补齐授权,不会覆盖业务数据。
docker compose -f compose.jiangling.yml up -d config_db
config_db_attempt=1
while [ "$config_db_attempt" -le 24 ]; do
if [ "$(docker inspect caiwu_config_db --format '{{.State.Health.Status}}' 2>/dev/null || true)" = "healthy" ]; then
break
fi
sleep 5
config_db_attempt=$((config_db_attempt + 1))
done
if [ "$(docker inspect caiwu_config_db --format '{{.State.Health.Status}}' 2>/dev/null || true)" != "healthy" ]; then
docker compose -f compose.jiangling.yml logs --tail 80 config_db >&2 || true
echo "Shared MariaDB did not become healthy before mirror database initialization" >&2
exit 1
fi
docker exec caiwu_config_db sh /docker-entrypoint-initdb.d/10-config-db-security.sh
fi
docker compose -f compose.jiangling.yml up -d --remove-orphans
clickhouse_healthy=0
clickhouse_attempt=1
while [ "$clickhouse_attempt" -le 24 ]; do
if [ "$(docker inspect caiwu_clickhouse --format '{{.State.Health.Status}}' 2>/dev/null || true)" = "healthy" ]; then
clickhouse_healthy=1
break
fi
sleep 5
clickhouse_attempt=$((clickhouse_attempt + 1))
done
if [ "$clickhouse_healthy" -ne 1 ]; then
docker compose -f compose.jiangling.yml logs --tail 120 clickhouse >&2 || true
restore_previous_image
echo "ClickHouse did not become healthy; previous application image was restored when available" >&2
exit 1
fi
healthy=0
attempt=1
while [ "$attempt" -le 30 ]; do
if curl -fsS -o /dev/null http://127.0.0.1:18088/serviceworker.js; then
healthy=1
break
fi
sleep 5
attempt=$((attempt + 1))
done
if [ "$healthy" -ne 1 ]; then
docker compose -f compose.jiangling.yml logs --tail 120 app >&2 || true
restore_previous_image
echo "Release failed health check; previous image was restored when available" >&2
exit 1
fi
if ! docker exec -u www-data caiwu_app php artisan storage:business-files:sync --require-qiniu; then
docker compose -f compose.jiangling.yml logs --tail 120 app >&2 || true
restore_previous_image
echo "Release failed private business storage verification; previous image was restored when available" >&2
exit 1
fi
if ! curl -fsS -o /dev/null http://127.0.0.1:18088/serviceworker.js; then
restore_previous_image
echo "Release failed after business storage activation; previous image was restored when available" >&2
exit 1
fi
printf '%s\n' "$DEPLOY_SHA" > "$REMOTE_DIR/.deployed-sha.tmp"
mv "$REMOTE_DIR/.deployed-sha.tmp" "$REMOTE_DIR/.deployed-sha"
# 只运行已登记的上游后台读取和 E 签宝状态轮询,不启用整个 Laravel
# 调度器,避免意外启动账单提醒等既有任务。上游读取共用一把锁,E 签宝单独加锁。
# 普通商品优先高频增量,并在配置的每周低峰窗口自动继续全量校准。
cat > /etc/cron.d/caiwu-jiansun-snapshots <<'EOF'
CRON_TZ=Asia/Shanghai
* * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables jiansun.dbo.jst_refund_view --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-catalog --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan upstream:snapshots:refresh-all --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables --quiet' >/dev/null 2>&1
# 店铺经营报表按 15 分钟刷新;单轮覆盖 50000 行回看窗口,避免空闲时持续重读千万级账单。
7,22,37,52 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-store-report --max-batches=12 --quiet >/dev/null 2>&1
# 店铺候选平时仅读镜像变更;夜间全量核对历史名称。冷页面不扫描订单宽表。
45 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan order-data:warm-shop-candidates --quiet >/dev/null 2>&1
45 2 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh order-data:warm-shop-candidates --full --quiet >/dev/null 2>&1
# 页面字段自动跟随上游:先刷新数据字典,再把上游新增字段补进镜像并建列,页面自动多出这一列。
40 6 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:follow-upstream-columns --quiet >/dev/null 2>&1
# 店铺基础信息每日备份:避开凌晨同步与字段跟随窗口,按店铺保留当天完整资料。
0 7 * * * root /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:archive-store-information --quiet >/dev/null 2>&1
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-third-party-warehouse-storage --max-batches=0 --quiet && /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:warm-report-caches --quiet' >/dev/null 2>&1
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --quiet >/dev/null 2>&1
# 镜像价格观测独立保存,按索引分批续传,不替代正式操作历史。
3,13,23,33,43,53 * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --source=snapshot --page-size=500 --max-pages=20 --quiet >/dev/null 2>&1
0 5 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables operations_replica.rpt.jst_Orders --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-ding-online-daifa-price --quiet' >/dev/null 2>&1
# 聚水潭订单标签明细:先核对 2026-10-01 起逐日覆盖,补历史缺口,再刷新最近 7 天。
10 6 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh jst-order-labels:sync-daily --from=2026-10-01 --company=1 --quiet >/dev/null 2>&1
# 业绩明细种菜金额:每天 09:30 核查标签覆盖,同步本月/上月种菜报销并重算种菜金额。
30 9 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh store-receipt:planting-daily --from=2026-10-01 --company=1 --quiet >/dev/null 2>&1
# 商品月加权与成本:先算月加权单价,再同步商品成本,避免成本先于价格生成。
0 18 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh finance:monthly-weighted-product-cost:auto >/dev/null 2>&1
# 店铺违规信息预警:每天 05:30 把副本库抓取结果追平到统一表(本地补充字段不覆盖)。
30 5 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan store-violations:sync --quiet >/dev/null 2>&1
# 顾家发货明细:每天补齐货号到物料编号的唯一映射;无唯一命中时保留空值。
50 5 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-kuka-posting-material-map --quiet >/dev/null 2>&1
# 店铺违规预警推送:P0/P1 每天 09:00 起每 5 分钟检查;P2 每日 10:00 汇总(未配置 Webhook 时自动跳过)。
*/5 9-21 * * * root /usr/bin/flock -n /run/caiwu-violation-push.lock /usr/bin/docker exec -u www-data caiwu_app php artisan store-violations:push-instant --quiet >/dev/null 2>&1
0 10 * * * root /usr/bin/flock -n /run/caiwu-violation-push.lock /usr/bin/docker exec -u www-data caiwu_app php artisan store-violations:push-digest --quiet >/dev/null 2>&1
# 种菜报销:每天 10:25 生成当天批次,汇总推到钉钉群并 @ 报销人(未配置机器人时跳过;只读)。
25 10 * * * root /usr/bin/flock -n /run/caiwu-planting-digest.lock /usr/bin/docker exec -u www-data caiwu_app php artisan planting-reimbursement:daily-digest --quiet >/dev/null 2>&1
# 进销存月报导入后检查仓库简称映射;每日补偿检查避免导入与提醒步骤间断。
15 10 * * * root /usr/bin/flock -n /run/caiwu-inventory-warehouse-alert.lock /usr/bin/docker exec -u www-data caiwu_app php artisan inventory:sync-warehouse-mapping-alert --quiet >/dev/null 2>&1
# 供应商管理每天北京时间 08:15 完整校准档案、合同、联系人与打款资料。
15 8 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-supplier-management --quiet >/dev/null 2>&1
# 退款待确认四张基础镜像每天 06:00 在独立批容器完整跑完一轮,确保 10:00 前完成;
# 当前退款状态在上面的分钟任务中优先增量刷新。
0 6 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh business-mirror:sync-tables --daily-only --max-batches=0 --quiet >/dev/null 2>&1
*/2 * * * * root /usr/bin/flock -n /run/caiwu-esign-polling.lock /usr/bin/docker exec -u www-data caiwu_app php artisan esign:poll-signatures --quiet >/dev/null 2>&1
# 钉钉入驻审批通过后回传内部快递资料;与上游全量同步分开,单轮最多25单、115秒。
*/2 * * * * root /usr/bin/flock -n /run/caiwu-courier-profile-sync.lock /usr/bin/timeout 115 /usr/bin/docker exec -u www-data caiwu_app php artisan courier-onboarding:sync-dingtalk-profiles --limit=25 --quiet >/dev/null 2>&1
# 财务台账:每天 02:30 同步 AI 库动账并重算流水;10:30 生成前一晚店铺余额快照。
30 2 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan finance-ledger:sync-rpt --days=7 --merge --quiet >/dev/null 2>&1
30 10 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan finance-ledger:sync-shop-data --balance-only --quiet >/dev/null 2>&1
# 财务流程明细每周完整校准一次,修正超过 48 小时重叠窗口才出现的上游改单或晚到付款。
20 2 * * 0 root /usr/bin/flock -w 3600 /run/caiwu-ding-process-details.lock /bin/sh /opt/caiwuxitong/run-finance-batch.sh business-mirror:sync-ding-process-details --full --quiet >/dev/null 2>&1
# 钉钉财务日报每 15 分钟增量同步;次日 12:00 前补交的日志会及时归入前一统计日。
*/15 * * * * root /usr/bin/flock -n /run/caiwu-ding-worklog-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-ding-worklogs --quiet >/dev/null 2>&1
# 钉钉考勤每 15 分钟同步;休息、请假和调休日不参与日志漏写扣分。
*/15 * * * * root /usr/bin/flock -n /run/caiwu-ding-attendance-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-ding-attendance --quiet >/dev/null 2>&1
# 科目映射保存后保留自动重跑队列,每分钟在受限容器内串行消费。
* * * * * root /usr/bin/flock -n /run/caiwu-account-mapping-resync.lock /bin/sh /opt/caiwuxitong/run-finance-batch.sh store-receipt:auto-resync --quiet >/dev/null 2>&1
* * * * * root /usr/bin/flock -n /run/caiwu-account-bills-export.lock /bin/sh /opt/caiwuxitong/run-finance-batch.sh account-bills:exports:run --quiet >/dev/null 2>&1
28 1 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh jushuitan:sync-shipping --days=3 >/dev/null 2>&1
# 聚水潭发货数据:凌晨部分出库单商品明细未到齐、副本库也可能超时,晚上再补拉最近 3 天。
28 21 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh jushuitan:sync-shipping --days=3 >/dev/null 2>&1
# 头条放心购(店铺收款账户账单 › 抖音账户)资金流水:每天 09:30 定点把该渠道所用
# 原始账单镜像追平一次,保证上班前当日/前一日流水已进系统,而不是等每分钟轮转慢慢走。
30 9 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables operations_replica.rpt.jst_originalBillInfo --max-batches=20 --quiet >/dev/null 2>&1
# 附加费结算:每天滚动重算最近 7 天;每周一再把最近 90 天全量重算一次,
# 补齐晚到的退货进仓与审批变化(超长费按出库日期、退回费/拦截费按查件工单创建时间)。
30 6 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan courier-surcharge:pull-overlength --days=7 --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan courier-surcharge:pull-return --days=7 --quiet' >/dev/null 2>&1
40 6 * * 1 root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan courier-surcharge:pull-overlength --days=90 --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan courier-surcharge:pull-return --days=90 --quiet' >/dev/null 2>&1
EOF
chmod 644 /etc/cron.d/caiwu-jiansun-snapshots
# OA 流程明细单独使用 5 分钟增量任务,避免被共用上游锁中的分钟级任务长期饿死;
# 每周全量校准仍保留在上面的主计划中。
cat > /etc/cron.d/caiwu-ding-process-details <<'EOF'
CRON_TZ=Asia/Shanghai
2-59/5 * * * * root CAIWU_BATCH_LOCK_WAIT=900 /usr/bin/flock -n /run/caiwu-ding-process-details.lock /bin/sh /opt/caiwuxitong/run-finance-batch.sh business-mirror:sync-ding-process-details --quiet >/dev/null 2>&1
# OA 页面每小时最多提交一次当前页审批中的流程;有请求时才启动定向刷新批处理。
* * * * * root CAIWU_BATCH_LOCK_WAIT=900 /usr/bin/flock -n /run/caiwu-ding-process-refresh.lock /bin/sh /opt/caiwuxitong/run-finance-batch.sh business-mirror:process-ding-refresh-requests --quiet >/dev/null 2>&1
EOF
chmod 644 /etc/cron.d/caiwu-ding-process-details
docker image ls "$IMAGE_NAME" --format '{{.Repository}}:{{.Tag}}' \
| awk '!seen[$0]++' \
| tail -n "+$((KEEP_IMAGES + 1))" \
| while IFS= read -r old_image; do
[ -n "$old_image" ] && docker image rm "$old_image" >/dev/null 2>&1 || true
done
docker image prune -f --filter 'label=com.caiwu.project=CaiWuXiTong' >/dev/null || true
docker compose -f compose.jiangling.yml ps
echo "DEPLOYED_SHA=$DEPLOY_SHA"
# Keep the variable for rollback diagnostics without printing any secret.
: "$PREVIOUS_SHA"