From 040987a3d34fcf6d30c0836911303b21b46bf7d3 Mon Sep 17 00:00:00 2001 From: yueyang <2495544247@qq.com> Date: Fri, 30 Oct 2026 13:30:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B1=95=E7=A4=BAOA=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E7=AD=89=E5=BE=85=E5=B0=8F=E6=97=B6=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/上游业务镜像清单.md | 2 +- .../DingProcessDetailMirrorSynchronizer.php | 18 +++++++++++------- .../EcommerceFinance/Support/OaProcessTime.php | 15 ++++++--------- .../process-details-content.blade.php | 4 ++-- .../Business/ProcessDetailsPageTest.php | 7 +++++-- .../EcommerceFinance/OaProcessTimeTest.php | 10 ++++++---- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/docs/上游业务镜像清单.md b/docs/上游业务镜像清单.md index b1344c15..a5f2eb70 100644 --- a/docs/上游业务镜像清单.md +++ b/docs/上游业务镜像清单.md @@ -35,7 +35,7 @@ | 待有效凭据后首次同步 | 普通商品当前成本价、商品类型和更新时间继续读取 `operations_replica.rpt.jst_sku`;`rpt.fys_skuCostPriceUpdate` 是一 SKU 一行的旧快照,不能充当变价历史。完整成本价变更仍读取聚水潭经典开放平台“商品操作日志查询”接口 `worklog.query`,只镜像操作内容含“采购价”的记录到 `jushuitan_cost_price_history_rows`;经典接口必须在服务器私有配置中使用同一应用的 `JUSHUITAN_PARTNER_ID / JUSHUITAN_PARTNER_KEY / JUSHUITAN_TOKEN`,不得把新开放平台的 App Key / App Secret / Access Token 混用。页面在首次同步完成前明确显示待同步或失败,不把 0 条解释为真实无历史。 | | 本次接入 | 流程明细读取 `operations_replica/rpt.ding_Workflow + rpt.ding_WorkflowScheme` 的结构化审批主表、费用明细和付款明细,写入 `ding_process_detail_mirror` 与 `ding_process_payment_mirror`。禁止先把明细表和付款表铺平成 CSV 后逐行求和;付款明细必须按“对应序号”关联到代表明细行,但合计始终读取独立付款事实。每个纳入范围的源审批至少保留一条页面记录,没有费用金额的审批以空金额显示,不静默隐藏。首次全量先运行 `business-mirror:sync-ding-process-details --full --dry-run` 校验,再使用 `business-mirror:sync-ding-process-details --full` 发布;日常通过 `updateAt/createAt` 48 小时重叠增量同步。当前纳入日常报销、应付、线上代发、种菜、快递面单、固定资产、日常物品、样品、一次性采买、板材、工资、外贸费用和种菜充值等 14 个财务流程;发现新的明细流程编码时全量同步报错并保留上一版。 | -流程明细另设独立定时任务,避免与每分钟科目映射消费争用同一上游目录时被静默跳过。正式发布脚本会覆盖安装 `/etc/cron.d/caiwu-ding-process-details`:每 5 分钟尝试运行 `business-mirror:sync-ding-process-details`,使用独立调度锁并在共用上游锁上最多等待 15 分钟;`scripts/install-ding-process-sync-cron.ps1` 仅作为手工修复入口。任务时间和镜像状态统一使用平台固定时区,审批等待时长按 12 小时区间展示。增量同步除跟进审批主表更新时间外,还以 `ding_WorkflowScheme` 的数字 `id` 高水位发现后补表单行,避免付款明细等新组件写入后主审批 `updateAt` 不变而被漏掉;原地改写同一组件仍由每周全量校准兜底。付款状态只按钉钉“出纳打款”节点判断,分为“出纳状态待同步 / 出纳未开始 / 出纳待打款 / 出纳退回 / 出纳已打款 / 流程终止 / 不涉及付款”。金额差异不参与付款状态判断,另以“付款差额 = 费用合计 − 已付款合计”展示。不能因为金额存在少量差异就覆盖出纳节点事实,也不能因为出纳已通过就隐藏金额差异。 +流程明细另设独立定时任务,避免与每分钟科目映射消费争用同一上游目录时被静默跳过。正式发布脚本会覆盖安装 `/etc/cron.d/caiwu-ding-process-details`:每 5 分钟尝试运行 `business-mirror:sync-ding-process-details`,使用独立调度锁并在共用上游锁上最多等待 15 分钟;`scripts/install-ding-process-sync-cron.ps1` 仅作为手工修复入口。任务时间和镜像状态统一使用平台固定时区,审批等待时长按实际小时展示。增量同步除跟进审批主表更新时间外,还以 `ding_WorkflowScheme` 的数字 `id` 高水位发现后补表单行,避免付款明细等新组件写入后主审批 `updateAt` 不变而被漏掉;原地改写同一组件仍由每周全量校准兜底。付款状态只按钉钉“出纳打款”节点判断,分为“出纳状态待同步 / 出纳未开始 / 出纳待打款 / 出纳退回 / 出纳已打款 / 流程终止 / 不涉及付款”。金额差异不参与付款状态判断,另以“付款差额 = 费用合计 − 已付款合计”展示。不能因为金额存在少量差异就覆盖出纳节点事实,也不能因为出纳已通过就隐藏金额差异。 `operations_replica` 缺失付款组件或进行中流程需要刷新出纳节点时,同步器复用现有钉钉应用凭据调用 `https://api.dingtalk.com/v1.0/workflow/processInstances`。官方接口同时返回表单、`tasks.taskGroupName` 和 `operationRecords.showName`,用于识别“出纳打款”节点及退回/待处理/已通过状态。付款来源标记为 `api`,付款事实仍进入 `ding_process_payment_mirror`;不会用官方接口覆盖副本已有的业务明细,只补缺失或空的付款字段,并独立保存“出纳状态待同步 / 出纳未开始 / 出纳待打款 / 出纳退回 / 出纳已打款 / 流程终止 / 不涉及付款”,`cashier_checked_at` 用于记录近期已检查时间。自动增量每 5 分钟按 `process_instance_id` 游标轮转最多 50 个缺失或需要刷新出纳状态的流程,避免单个失败记录阻塞队列或瞬时打满接口;人工核查可执行 `business-mirror:sync-ding-process-details --business-id=审批编号1,审批编号2 --dry-run`,确认后再去掉 `--dry-run`。单次最多 50 个审批编号。 diff --git a/modules/EcommerceFinance/Services/DingProcessDetailMirrorSynchronizer.php b/modules/EcommerceFinance/Services/DingProcessDetailMirrorSynchronizer.php index 3b65fcd4..076b690c 100644 --- a/modules/EcommerceFinance/Services/DingProcessDetailMirrorSynchronizer.php +++ b/modules/EcommerceFinance/Services/DingProcessDetailMirrorSynchronizer.php @@ -2015,6 +2015,7 @@ class DingProcessDetailMirrorSynchronizer $bankAmountTotal = (float) ($this->connection->table('ding_process_payment_mirror') ->where('source_scope', $scope) ->sum('amount') ?? 0); + $completedAt = OaProcessTime::now(); $values = [ 'source_key' => self::SOURCE, @@ -2041,16 +2042,16 @@ class DingProcessDetailMirrorSynchronizer 'high_watermark' => null, 'window_started_at' => null, 'window_ends_at' => null, - 'initial_completed_at' => $previous['initial_completed_at'] ?? $now, + 'initial_completed_at' => $previous['initial_completed_at'] ?? $completedAt, 'last_full_succeeded_at' => $full - ? $now + ? $completedAt : ($previous['last_full_succeeded_at'] ?? null), 'last_attempted_at' => $now, - 'last_succeeded_at' => $now, + 'last_succeeded_at' => $completedAt, 'last_upserted_rows' => count($details), 'mirror_row_count' => $detailCount, 'last_error_code' => null, - 'updated_at' => $now, + 'updated_at' => $completedAt, ]; $this->connection->table('business_mirror_table_states')->updateOrInsert( ['dataset_key' => $this->datasetKey()], @@ -2116,9 +2117,12 @@ class DingProcessDetailMirrorSynchronizer (int) config('upstream_mirrors.ding_process_details.sync_interval_minutes', 5), ); - return Carbon::parse((string) $state['last_succeeded_at']) - ->addMinutes($minutes) - ->lessThanOrEqualTo(now()); + $lastSucceededAt = OaProcessTime::parse($state['last_succeeded_at']); + + return $lastSucceededAt === null + || $lastSucceededAt + ->addMinutes($minutes) + ->lessThanOrEqualTo(OaProcessTime::now()); } /** @return array|null */ diff --git a/modules/EcommerceFinance/Support/OaProcessTime.php b/modules/EcommerceFinance/Support/OaProcessTime.php index c538b1c7..c5ca2cdb 100644 --- a/modules/EcommerceFinance/Support/OaProcessTime.php +++ b/modules/EcommerceFinance/Support/OaProcessTime.php @@ -8,8 +8,6 @@ use Throwable; final class OaProcessTime { - private const WAIT_STEP_HOURS = 12; - public static function timezone(): string { $timezone = trim((string) config('app.platform_timezone', '')); @@ -57,19 +55,18 @@ final class OaProcessTime } $hours = intdiv($startedAt->diffInSeconds($nowAt), 3600); - $steps = intdiv($hours, self::WAIT_STEP_HOURS); - if ($steps < 1) { - return '不足12小时'; + if ($hours < 1) { + return '不足1小时'; } - $days = intdiv($steps, 2); - $halfDays = $steps % 2; + $days = intdiv($hours, 24); + $remainingHours = $hours % 24; $parts = []; if ($days > 0) { $parts[] = $days . '天'; } - if ($halfDays > 0) { - $parts[] = self::WAIT_STEP_HOURS . '小时'; + if ($remainingHours > 0) { + $parts[] = $remainingHours . '小时'; } return implode('', $parts); diff --git a/resources/views/business/pages/financial-operations/process-details-content.blade.php b/resources/views/business/pages/financial-operations/process-details-content.blade.php index e0bfe045..8f29d046 100644 --- a/resources/views/business/pages/financial-operations/process-details-content.blade.php +++ b/resources/views/business/pages/financial-operations/process-details-content.blade.php @@ -45,7 +45,7 @@ if (($row->status ?? '') === '审批中') { return trim((string) ($row->current_task_name ?? '')) === '' - ? '无在途任务' + ? '审批节点同步中' : '审批人待补全'; } @@ -766,7 +766,7 @@ 同一笔付款不会因关联多条费用而重复计算。 付款状态按审批状态和已抓取的结构化付款事实判断;付款差额按“费用合计 − 付款明细合计”单独计算,不参与付款状态判断。 审批中的流程在出现结构化付款事实前显示“待付款”,已有付款事实时显示“已付款”。 - 当前审批人、进入时间和已等待时长来自钉钉在途任务同步,后台会持续补查未完成的审批流程;已等待按 12 小时区间显示。 + 当前审批人、进入时间和已等待时长来自钉钉在途任务同步,后台会持续补查未完成的审批流程;已等待按实际小时显示。 当前展示的是当前账号权限范围内可读取的钉钉OA审批流程;审批中的金额和行项目仍可能随流程变化。 企业级全量数据待领导开通 OA 权限后切换。

diff --git a/tests/Feature/Business/ProcessDetailsPageTest.php b/tests/Feature/Business/ProcessDetailsPageTest.php index eddbf59c..61adb1c6 100644 --- a/tests/Feature/Business/ProcessDetailsPageTest.php +++ b/tests/Feature/Business/ProcessDetailsPageTest.php @@ -28,7 +28,10 @@ class ProcessDetailsPageTest extends FeatureTestCase 'cashier_status' => 'completed', 'current_approver_name' => '王五', 'current_task_name' => '财务用途审批', - 'current_task_started_at' => now()->subMinutes(90)->startOfMinute()->format('Y-m-d H:i:s'), + 'current_task_started_at' => \Modules\EcommerceFinance\Support\OaProcessTime::now() + ->subMinutes(90) + ->startOfMinute() + ->format('Y-m-d H:i:s'), 'cashier_task_name' => '出纳打款', 'cashier_task_started_at' => now()->subHours(3)->startOfMinute()->format('Y-m-d H:i:s'), 'payment_fact_amount' => 0, @@ -106,7 +109,7 @@ class ProcessDetailsPageTest extends FeatureTestCase ->assertDontSee('财务用途审批') ->assertDontSee('出纳打款 · 已完成') ->assertSee('王五') - ->assertSee('不足12小时') + ->assertSee('1小时') ->assertSee('付款明细合计') ->assertSee('已付款') ->assertDontSee('付款待核实') diff --git a/tests/Unit/EcommerceFinance/OaProcessTimeTest.php b/tests/Unit/EcommerceFinance/OaProcessTimeTest.php index 48075f25..617fba78 100644 --- a/tests/Unit/EcommerceFinance/OaProcessTimeTest.php +++ b/tests/Unit/EcommerceFinance/OaProcessTimeTest.php @@ -20,7 +20,7 @@ class OaProcessTimeTest extends TestCase } #[DataProvider('waitingDurationProvider')] - public function test_waiting_duration_uses_twelve_hour_steps( + public function test_waiting_duration_uses_actual_full_hours( string $startedAt, string $expected, ): void { @@ -32,10 +32,12 @@ class OaProcessTimeTest extends TestCase public static function waitingDurationProvider(): array { return [ - 'less than first step' => ['2026-10-30 00:01:00', '不足12小时'], - 'first step' => ['2026-10-30 00:00:00', '12小时'], + 'less than one hour' => ['2026-10-30 11:01:00', '不足1小时'], + 'one hour' => ['2026-10-30 11:00:00', '1小时'], + 'eleven hours' => ['2026-10-30 00:01:00', '11小时'], + 'twelve hours' => ['2026-10-30 00:00:00', '12小时'], 'one day' => ['2026-10-29 12:00:00', '1天'], - 'one and a half days' => ['2026-10-29 00:00:00', '1天12小时'], + 'one day twelve hours' => ['2026-10-29 00:00:00', '1天12小时'], 'two days' => ['2026-10-28 12:00:00', '2天'], 'three days' => ['2026-10-27 12:00:00', '3天'], ]; -- 2.47.2