fix: 展示OA真实等待小时并优化同步状态 #784

Merged
yueyang merged 1 commits from fix/yueyang-oa-wait-hours into main 2026-10-30 14:50:35 +08:00
6 changed files with 31 additions and 25 deletions

View File

@ -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 个审批编号。

View File

@ -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<string,mixed>|null */

View File

@ -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);

View File

@ -45,7 +45,7 @@
if (($row->status ?? '') === '审批中') {
return trim((string) ($row->current_task_name ?? '')) === ''
? '无在途任务'
? '审批节点同步中'
: '审批人待补全';
}
@ -766,7 +766,7 @@
同一笔付款不会因关联多条费用而重复计算。
付款状态按审批状态和已抓取的结构化付款事实判断;付款差额按“费用合计 − 付款明细合计”单独计算,不参与付款状态判断。
审批中的流程在出现结构化付款事实前显示“待付款”,已有付款事实时显示“已付款”。
当前审批人、进入时间和已等待时长来自钉钉在途任务同步,后台会持续补查未完成的审批流程;已等待按 12 小时区间显示。
当前审批人、进入时间和已等待时长来自钉钉在途任务同步,后台会持续补查未完成的审批流程;已等待按实际小时显示。
当前展示的是当前账号权限范围内可读取的钉钉OA审批流程;审批中的金额和行项目仍可能随流程变化。
企业级全量数据待领导开通 OA 权限后切换。
</p>

View File

@ -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('付款待核实')

View File

@ -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天'],
];