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
This commit is contained in:
limeng 2026-11-15 14:07:08 +08:00
parent d3532bf0da
commit e12c9c20ec
4 changed files with 64 additions and 0 deletions

View File

@ -253,6 +253,8 @@ CRON_TZ=Asia/Shanghai
* * * * * 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

View File

@ -257,6 +257,8 @@ CRON_TZ=Asia/Shanghai
* * * * * 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

View File

@ -64,4 +64,28 @@ esac
RUN_MIGRATIONS=0
export IMAGE_TAG RUN_MIGRATIONS
# Mutating jobs hold the upstream lock; read-only exports use their own single-worker lock.
if [ "$1" = 'jushuitan:sync-shipping' ]; then
# 聚水潭副本库偶发超时会让整次发货数据同步失败(当晚最近几天漏单):
# 输出里有"同步失败"时自动重试,每次输出留日志;只有警告(未写"同步失败")不重试。
sync_log="${CAIWU_JST_SYNC_LOG:-$REMOTE_DIR/jst-sync-shipping.log}"
sync_attempts="${CAIWU_JST_SYNC_ATTEMPTS:-3}"
if [ -f "$sync_log" ] && [ "$(wc -c <"$sync_log")" -gt 5000000 ]; then
mv -f "$sync_log" "$sync_log.1"
fi
sync_out="$(mktemp)"
trap 'rm -f "$sync_out"' EXIT
attempt=1
while :; do
echo "== $(date '+%Y-%m-%d %H:%M:%S') attempt $attempt/$sync_attempts: $*" >>"$sync_log"
rc=0
docker compose -f compose.jiangling.yml --profile batch run --rm -T --no-deps batch "$@" >"$sync_out" 2>&1 || rc=$?
tr '\r' '\n' <"$sync_out" | grep -v '已扫描' | tail -n 60 >>"$sync_log" || true
echo "== rc=$rc" >>"$sync_log"
if [ "$rc" -eq 0 ] || ! grep -q '同步失败' "$sync_out" || [ "$attempt" -ge "$sync_attempts" ]; then
exit "$rc"
fi
attempt=$((attempt + 1))
sleep "${CAIWU_JST_SYNC_RETRY_WAIT:-300}"
done
fi
docker compose -f compose.jiangling.yml --profile batch run --rm -T --no-deps batch "$@"

View File

@ -35,6 +35,42 @@ class FinanceBatchIsolationTest extends TestCase
}
}
public function test_shipping_sync_retries_after_upstream_failure_and_keeps_a_log(): void
{
$directory = sys_get_temp_dir() . '/caiwu-jst-retry-' . bin2hex(random_bytes(6));
mkdir($directory);
// 第一次同步输出"同步失败"(副本库超时),第二次成功
file_put_contents($directory . '/docker', '#!/bin/sh' . "\n"
. 'if [ "$1" = inspect ]; then echo caiwu_akaunting:123456789abc; exit 0; fi' . "\n"
. 'n=$(cat "$CAIWU_REMOTE_DIR/count" 2>/dev/null || echo 0); n=$((n + 1)); echo $n > "$CAIWU_REMOTE_DIR/count"' . "\n"
. 'if [ $n -eq 1 ]; then echo "同步失败:Adaptive Server connection timed out"; exit 1; fi' . "\n"
. 'echo "同步完成"; exit 0' . "\n");
chmod($directory . '/docker', 0700);
try {
$process = new Process(
['sh', dirname(__DIR__, 3) . '/deploy/run-finance-batch.sh', 'jushuitan:sync-shipping', '--days=3'],
$directory,
[
'PATH' => $directory . ':' . getenv('PATH'), 'CAIWU_REMOTE_DIR' => $directory,
'CAIWU_UPSTREAM_LOCK' => $directory . '/sync.lock', 'CAIWU_BATCH_LOCK_WAIT' => '0.1',
'CAIWU_JST_SYNC_RETRY_WAIT' => '0',
],
);
$process->run();
$this->assertTrue($process->isSuccessful(), $process->getErrorOutput());
$this->assertSame('2', trim((string) file_get_contents($directory . '/count')));
$log = (string) file_get_contents($directory . '/jst-sync-shipping.log');
$this->assertStringContainsString('attempt 1/3', $log);
$this->assertStringContainsString('同步失败', $log);
$this->assertStringContainsString('attempt 2/3', $log);
$this->assertStringContainsString('同步完成', $log);
} finally {
foreach (glob($directory . '/*') as $path) { unlink($path); }
rmdir($directory);
}
}
/** @param array<string,string|null> $environment */
private function runBatch(
string $directory,