> $details * @param list> $payments * @return list> */ public function allocate( array $details, array $payments, string $scope, Carbon $now, ): array { $detailsByBusiness = []; foreach ($details as $detail) { $businessId = trim((string) ($detail['business_id'] ?? '')); $lineNo = (int) ($detail['line_no'] ?? 0); if ($businessId === '' || $lineNo <= 0) { continue; } $detailsByBusiness[$businessId][$lineNo] = [ 'amount_cents' => $this->amountCents($detail), ]; } $rows = []; foreach ($payments as $payment) { $businessId = trim((string) ($payment['business_id'] ?? '')); $paymentNo = (int) ($payment['payment_no'] ?? 0); $amountCents = $this->amountCents($payment); $businessDetails = $detailsByBusiness[$businessId] ?? []; if ($businessId === '' || $paymentNo <= 0 || $amountCents === 0 || $businessDetails === []) { continue; } $lineNumbers = $this->lineNumbers( (string) ($payment['sequence_spec'] ?? ''), $businessDetails, $amountCents, ); if ($lineNumbers === []) { continue; } $allocated = $this->allocateCents( $amountCents, array_map( static fn (int $lineNo): int => (int) $businessDetails[$lineNo]['amount_cents'], $lineNumbers, ), ); $paidAt = trim((string) ($payment['paid_at'] ?? '')); $paidMonth = preg_match('/^\d{4}-\d{2}/', $paidAt, $matches) === 1 ? $matches[0] : ''; foreach ($lineNumbers as $index => $lineNo) { $rows[] = [ 'source_scope' => $scope, 'business_id' => $businessId, 'process_instance_id' => (string) ($payment['process_instance_id'] ?? ''), 'payment_no' => $paymentNo, 'line_no' => $lineNo, 'paid_month' => $paidMonth, 'paid_at' => $paidAt, 'amount' => number_format($allocated[$index] / 100, 2, '.', ''), 'account_code' => $payment['account_code'] ?? null, 'account_name' => $payment['account_name'] ?? null, 'payment_method' => $payment['payment_method'] ?? null, 'synced_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]; } } return $rows; } /** @param array $row */ private function amountCents(array $row): int { if (array_key_exists('amount_cents', $row)) { return (int) $row['amount_cents']; } return (int) round((float) ($row['amount'] ?? 0) * 100); } /** * @param array $businessDetails * @return list */ private function lineNumbers( string $sequenceSpec, array $businessDetails, int $amountCents, ): array { $availableLines = array_map('intval', array_keys($businessDetails)); sort($availableLines); if ($availableLines === []) { return []; } preg_match_all('/\d+/', $sequenceSpec, $matches); $requested = array_values(array_unique(array_map('intval', $matches[0] ?? []))); $matched = array_values(array_intersect($requested, $availableLines)); if ($matched !== []) { return $matched; } $exactLines = []; foreach ($businessDetails as $lineNo => $detail) { if ((int) $detail['amount_cents'] === $amountCents) { $exactLines[] = (int) $lineNo; } } if (count($exactLines) === 1) { return $exactLines; } // 序号为空或引用了不存在的明细行时,仍按全部明细金额分摊, // 保证每一笔已付款都能进入对应审批,不因单行数据错误整笔漏计。 return $availableLines; } /** * @param list $weights * @return list */ private function allocateCents(int $amountCents, array $weights): array { $count = count($weights); if ($count === 0) { return []; } $totalWeight = array_sum($weights); if ($totalWeight <= 0) { $base = intdiv($amountCents, $count); $allocated = array_fill(0, $count, $base); $allocated[$count - 1] += $amountCents - ($base * $count); return $allocated; } $allocated = []; $used = 0; foreach ($weights as $index => $weight) { if ($index === $count - 1) { $value = $amountCents - $used; } else { $value = intdiv($amountCents * $weight, $totalWeight); $used += $value; } $allocated[] = $value; } return $allocated; } }