prepare('SELECT * FROM users WHERE username = :u');
$stmt->execute([':u' => $username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
$_SESSION['uid'] = (int)$user['id'];
header('Location: index.php');
exit;
}
flash('error', '用户名或密码错误');
header('Location: index.php?action=login');
exit;
}
$flash = getFlash();
?>
= e($flash['msg']) ?>
HTML;
}
function fetchDirectories(PDO $pdo): array
{
return $pdo->query('SELECT * FROM directories ORDER BY level ASC, id ASC')->fetchAll();
}
function safePathPart(string $name): string
{
$name = trim($name);
$name = preg_replace('/[\/\\\\:\*\?"<>\|]+/u', '_', $name);
$name = preg_replace('/\s+/u', '_', (string)$name);
$name = trim((string)$name, '._');
return $name !== '' ? $name : 'unnamed';
}
function buildArchiveUploadSubdir(PDO $pdo, int $archiveId): string
{
$stmt = $pdo->prepare('SELECT id,name,directory_id FROM archives WHERE id=:id');
$stmt->execute([':id' => $archiveId]);
$archive = $stmt->fetch();
if (!$archive) {
return 'unknown/archive_' . $archiveId;
}
$dirs = fetchDirectories($pdo);
$map = [];
foreach ($dirs as $d) {
$map[(int)$d['id']] = $d;
}
$parts = [];
$cur = (int)$archive['directory_id'];
while (isset($map[$cur])) {
$parts[] = safePathPart((string)$map[$cur]['name']);
$pid = $map[$cur]['parent_id'];
if ($pid === null) {
break;
}
$cur = (int)$pid;
}
$parts = array_reverse($parts);
$parts[] = 'archive_' . (int)$archive['id'] . '_' . safePathPart((string)$archive['name']);
return implode('/', $parts);
}
function maskUsername(string $username): string
{
$len = mb_strlen($username);
if ($len <= 2) {
return $username;
}
return mb_substr($username, 0, 1) . '***' . mb_substr($username, -1);
}
function roleLabel(string $role): string
{
return $role === 'admin' ? '管理员' : '普通用户';
}
function repaymentPlanStorePath(): string
{
return __DIR__ . '/../data/repayment_plans.json';
}
function loadRepaymentPlans(): array
{
$path = repaymentPlanStorePath();
if (!is_file($path)) {
return [];
}
$raw = file_get_contents($path);
if ($raw === false || $raw === '') {
return [];
}
$data = json_decode($raw, true);
return is_array($data) ? $data : [];
}
function saveRepaymentPlans(array $data): void
{
$path = repaymentPlanStorePath();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
}
function getArchiveRepaymentPlan(array $all, int $archiveId): array
{
return $all[(string)$archiveId] ?? ['enabled' => false, 'plans' => []];
}
function financeStorePath(): string
{
return __DIR__ . '/../data/archive_finance.json';
}
function loadFinanceConfigs(): array
{
$path = financeStorePath();
if (!is_file($path)) {
return [];
}
$raw = file_get_contents($path);
if ($raw === false || $raw === '') {
return [];
}
$data = json_decode($raw, true);
return is_array($data) ? $data : [];
}
function saveFinanceConfigs(array $data): void
{
$path = financeStorePath();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
}
function getArchiveFinanceConfig(array $all, int $archiveId): array
{
$cfg = $all[(string)$archiveId] ?? [];
return [
'interest_mode' => $cfg['interest_mode'] ?? 'monthly',
'settlement_rule' => $cfg['settlement_rule'] ?? 'monthly_day',
'settlement_day' => (int)($cfg['settlement_day'] ?? 21),
'overdue_float_pct' => (float)($cfg['overdue_float_pct'] ?? 50),
'manual_overdue' => !empty($cfg['manual_overdue']),
'manual_early_settled' => !empty($cfg['manual_early_settled']),
'remark' => (string)($cfg['remark'] ?? ''),
'manual_principal_repaid' => (float)($cfg['manual_principal_repaid'] ?? 0),
'manual_interest_paid' => (float)($cfg['manual_interest_paid'] ?? 0),
];
}
/** 计息用本金:优先剩余额度,否则授信额度 */
function effectivePrincipalForInterest(array $archive): float
{
if (isset($archive['remaining_limit']) && $archive['remaining_limit'] !== null && $archive['remaining_limit'] !== '') {
return max(0.0, (float)$archive['remaining_limit']);
}
if ($archive['credit_limit'] === null || $archive['credit_limit'] === '') {
return 0.0;
}
return max(0.0, (float)$archive['credit_limit']);
}
function archiveStatusWithConfig(?string $endDate, array $financeCfg): string
{
if (!empty($financeCfg['manual_early_settled'])) {
return '已完成';
}
if (!empty($financeCfg['manual_overdue'])) {
return '逾期中';
}
// 与 helpers::archiveStatus 一致;内联避免因 helpers 未同步部署仍按旧逻辑显示「到期自动已完成」
if (!$endDate) {
return '待更新';
}
$today = date('Y-m-d');
if ($endDate >= $today) {
return '履行中';
}
return '待更新';
}
/** 便捷查询汇总:履行中与逾期中均视为未结清存续业务 */
function archiveStatusIsOngoingAggregate(?string $endDate, array $financeCfg): bool
{
$s = archiveStatusWithConfig($endDate, $financeCfg);
return $s === '履行中' || $s === '逾期中';
}
/** 与当前结息周期对齐的区间起点(按 refDate 所在周期),用于「当前应付利息」 */
function interestPeriodStartForRefDate(array $archive, array $financeCfg, string $refDate): string
{
$startDate = $archive['start_date'] ?: $refDate;
$mode = (string)($financeCfg['interest_mode'] ?? 'monthly');
$settlementRule = (string)($financeCfg['settlement_rule'] ?? 'monthly_day');
$day = max(1, min(28, (int)($financeCfg['settlement_day'] ?? 20)));
$periodStart = $startDate;
if ($settlementRule === 'monthly_day' && $mode !== 'bullet') {
$curYm = date('Y-m', strtotime($refDate));
$thisSettle = $curYm . '-' . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
if ($refDate >= $thisSettle) {
$periodStart = $thisSettle;
} else {
$periodStart = date('Y-m-d', strtotime('-1 month', strtotime($thisSettle)));
}
} elseif ($settlementRule === 'quarterly_day' && $mode !== 'bullet') {
$m = (int)date('n', strtotime($refDate));
$qStartMonth = ((int)(($m - 1) / 3)) * 3 + 1;
$curQuarterSettle = date('Y', strtotime($refDate)) . '-' . str_pad((string)$qStartMonth, 2, '0', STR_PAD_LEFT) . '-' . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
if ($refDate >= $curQuarterSettle) {
$periodStart = $curQuarterSettle;
} else {
$periodStart = date('Y-m-d', strtotime('-3 month', strtotime($curQuarterSettle)));
}
}
if ($periodStart < $startDate) {
$periodStart = $startDate;
}
return $periodStart;
}
/**
* 下一结息日所在「整段结息周期」的起始日:上一结息日与起息日孰晚。
* 用于结息日应付利息(自周期起点至结息日,而非自今日至结息日)。
*/
function settlementPeriodStartForNextSettle(string $nextSettle, array $archive, array $financeCfg): string
{
$startDate = $archive['start_date'] ?: $nextSettle;
$mode = (string)($financeCfg['interest_mode'] ?? 'monthly');
$settlementRule = (string)($financeCfg['settlement_rule'] ?? 'monthly_day');
$day = max(1, min(28, (int)($financeCfg['settlement_day'] ?? 20)));
if ($settlementRule === 'maturity_once' || $mode === 'bullet') {
return $startDate;
}
$prevAnchor = $startDate;
if ($settlementRule === 'monthly_day') {
$prevAnchor = date('Y-m-d', strtotime('-1 month', strtotime($nextSettle)));
} elseif ($settlementRule === 'quarterly_day') {
$prevAnchor = date('Y-m-d', strtotime('-3 months', strtotime($nextSettle)));
}
return max($startDate, $prevAnchor);
}
/** 自 periodStart 起至 throughDate(含区间逻辑与 calcCurrentInterest 一致)的应计利息,不含「已归还利息」扣减 */
function calcAccruedInterestForRange(array $archive, array $financeCfg, array $repaymentPlans, string $periodStart, string $throughDate): float
{
$principal = effectivePrincipalForInterest($archive);
$annualRate = $archive['annual_rate'] === null ? 0.0 : (float)$archive['annual_rate'];
if ($principal <= 0 || $annualRate <= 0 || $throughDate < $periodStart) {
return 0.0;
}
$status = archiveStatusWithConfig($archive['end_date'], $financeCfg);
$overdueRate = $annualRate * (1 + ((float)($financeCfg['overdue_float_pct'] ?? 0) / 100));
$events = [];
$overduePrincipal = 0.0;
foreach ($repaymentPlans as $p) {
$amount = (float)($p['amount'] ?? 0);
if ($amount <= 0) {
continue;
}
$pStatus = (string)($p['status'] ?? 'pending');
if ($pStatus === 'paid' && !empty($p['paid_at'])) {
$paidAt = (string)$p['paid_at'];
if ($paidAt >= $periodStart && $paidAt <= $throughDate) {
$events[] = ['date' => $paidAt, 'type' => 'paid', 'amount' => $amount];
}
continue;
}
if ($pStatus === 'overdue') {
$dueDate = (string)($p['due_date'] ?? '');
if ($dueDate === '' || $dueDate > $throughDate) {
continue;
}
if ($dueDate < $periodStart) {
$overduePrincipal += $amount;
} else {
$events[] = ['date' => $dueDate, 'type' => 'overdue_start', 'amount' => $amount];
}
}
}
usort($events, function ($a, $b) {
$cmp = strcmp((string)$a['date'], (string)$b['date']);
if ($cmp !== 0) {
return $cmp;
}
$orderA = ($a['type'] ?? '') === 'overdue_start' ? 0 : 1;
$orderB = ($b['type'] ?? '') === 'overdue_start' ? 0 : 1;
return $orderA <=> $orderB;
});
$interest = 0.0;
$cursor = $periodStart;
$currentPrincipal = $principal;
foreach ($events as $ev) {
$evDate = (string)($ev['date'] ?? '');
if ($evDate > $cursor) {
$days = (strtotime($evDate) - strtotime($cursor)) / 86400;
if ($days > 0) {
$normalPrincipal = max(0, $currentPrincipal - $overduePrincipal);
$interest += $normalPrincipal * ($annualRate / 100) * ($days / 360);
$interest += $overduePrincipal * ($overdueRate / 100) * ($days / 360);
}
}
$amount = (float)($ev['amount'] ?? 0);
if (($ev['type'] ?? '') === 'overdue_start') {
$overduePrincipal = min($currentPrincipal, $overduePrincipal + $amount);
} elseif (($ev['type'] ?? '') === 'paid') {
$currentPrincipal = max(0, $currentPrincipal - $amount);
$overduePrincipal = min($overduePrincipal, $currentPrincipal);
}
$cursor = $evDate;
}
if ($throughDate > $cursor) {
$days = (strtotime($throughDate) - strtotime($cursor)) / 86400;
if ($days > 0) {
$normalPrincipal = max(0, $currentPrincipal - $overduePrincipal);
if ($overduePrincipal <= 0 && $status === '逾期中') {
$interest += $currentPrincipal * ($overdueRate / 100) * ($days / 360);
} else {
$interest += $normalPrincipal * ($annualRate / 100) * ($days / 360);
$interest += $overduePrincipal * ($overdueRate / 100) * ($days / 360);
}
}
}
return round($interest, 2);
}
function applyInterestPaidCredit(float $accrued, array $financeCfg): float
{
$paid = (float)($financeCfg['manual_interest_paid'] ?? 0);
return round(max(0.0, $accrued - $paid), 2);
}
function calcCurrentInterest(array $archive, array $financeCfg, array $repaymentPlans): float
{
$today = date('Y-m-d');
$periodStart = interestPeriodStartForRefDate($archive, $financeCfg, $today);
$raw = calcAccruedInterestForRange($archive, $financeCfg, $repaymentPlans, $periodStart, $today);
return applyInterestPaidCredit($raw, $financeCfg);
}
/** 最近一个结息日应付的整周期利息(与利息查询一致) */
function calcSettlementPeriodInterest(array $archive, array $financeCfg, array $repaymentPlans, string $todayRef): float
{
$mode = (string)($financeCfg['interest_mode'] ?? 'monthly');
$annualRate = $archive['annual_rate'] === null ? 0.0 : (float)$archive['annual_rate'];
if (effectivePrincipalForInterest($archive) <= 0 || $annualRate <= 0) {
return 0.0;
}
$nextSettle = $mode === 'bullet'
? (((string)($archive['end_date'] ?? '') !== '' && (string)$archive['end_date'] >= $todayRef) ? (string)$archive['end_date'] : null)
: nextSettlementDate($todayRef, $mode, (int)($financeCfg['settlement_day'] ?? 21));
if ($nextSettle === null || $nextSettle === '') {
return 0.0;
}
$periodStart = settlementPeriodStartForNextSettle($nextSettle, $archive, $financeCfg);
$raw = calcAccruedInterestForRange($archive, $financeCfg, $repaymentPlans, $periodStart, $nextSettle);
return applyInterestPaidCredit($raw, $financeCfg);
}
function recalcRemainingLimitByPlans(PDO $pdo, int $archiveId, array $plans): void
{
if ($archiveId <= 0) {
return;
}
$st = $pdo->prepare('SELECT credit_limit FROM archives WHERE id=:id');
$st->execute([':id' => $archiveId]);
$credit = $st->fetchColumn();
if ($credit === false || $credit === null) {
return;
}
$paidSum = 0.0;
foreach ($plans as $p) {
if ((string)($p['status'] ?? '') === 'paid') {
$paidSum += (float)($p['amount'] ?? 0);
}
}
$financeAll = loadFinanceConfigs();
$manualPrincipal = (float)(getArchiveFinanceConfig($financeAll, $archiveId)['manual_principal_repaid'] ?? 0);
$remaining = max(0, (float)$credit - $paidSum - $manualPrincipal);
$pdo->prepare('UPDATE archives SET remaining_limit=:r,updated_at=:u WHERE id=:id')
->execute([':r' => $remaining, ':u' => now(), ':id' => $archiveId]);
}
function uploadArchiveFiles(PDO $pdo, int $archiveId, array $files): void
{
if ($archiveId <= 0 || empty($files) || empty($files['name']) || !is_array($files['name'])) {
return;
}
$names = $files['name'];
$tmpNames = $files['tmp_name'] ?? [];
$sizes = $files['size'] ?? [];
$errors = $files['error'] ?? [];
$subDir = buildArchiveUploadSubdir($pdo, $archiveId);
$targetDir = rtrim(UPLOAD_DIR . '/' . $subDir, '/');
if (!is_dir($targetDir)) {
mkdir($targetDir, 0775, true);
}
for ($i = 0; $i < count($names); $i++) {
if ((int)($errors[$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
continue;
}
if ((int)($sizes[$i] ?? 0) > MAX_FILE_SIZE) {
continue;
}
$original = (string)$names[$i];
$ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
if (!in_array($ext, ALLOWED_EXTENSIONS, true)) {
continue;
}
$normalized = normalizeFileName($original);
$stored = $subDir . '/' . uniqid('f_', true) . '_' . $normalized;
if (move_uploaded_file((string)$tmpNames[$i], UPLOAD_DIR . '/' . $stored)) {
$pdo->prepare('INSERT INTO files(archive_id,original_name,stored_name,file_ext,file_size,created_at) VALUES(:a,:o,:s,:e,:z,:c)')
->execute([':a' => $archiveId, ':o' => $original, ':s' => $stored, ':e' => $ext, ':z' => (int)$sizes[$i], ':c' => now()]);
}
}
}
function deleteApprovalStorePath(): string
{
return __DIR__ . '/../data/delete_approvals.json';
}
function loadDeleteApprovals(): array
{
$path = deleteApprovalStorePath();
if (!is_file($path)) {
return [];
}
$raw = file_get_contents($path);
if ($raw === false || $raw === '') {
return [];
}
$data = json_decode($raw, true);
return is_array($data) ? $data : [];
}
function saveDeleteApprovals(array $data): void
{
$path = deleteApprovalStorePath();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
}
function currentUrlFromServer(): string
{
$uri = (string)($_SERVER['REQUEST_URI'] ?? '');
if ($uri === '' || strpos($uri, 'index.php') === false) {
return 'index.php';
}
return $uri;
}
function redirectBackOr(string $fallback): void
{
$posted = trim((string)($_POST['redirect_to'] ?? ''));
$query = trim((string)($_GET['redirect_to'] ?? ''));
$ref = trim((string)($_SERVER['HTTP_REFERER'] ?? ''));
$target = $posted !== '' ? $posted : ($query !== '' ? $query : $ref);
if ($target === '' || strpos($target, 'index.php') === false) {
$target = $fallback;
}
header('Location: ' . $target);
exit;
}
function fetchArchiveSimpleMap(PDO $pdo): array
{
$rows = $pdo->query('SELECT id,name,loan_institution,borrower,archive_kind FROM archives')->fetchAll();
$map = [];
foreach ($rows as $r) {
$id = (int)($r['id'] ?? 0);
if ($id <= 0) {
continue;
}
$map[$id] = [
'name' => (string)($r['name'] ?? ''),
'loan_institution' => (string)($r['loan_institution'] ?? ''),
'borrower' => (string)($r['borrower'] ?? ''),
'archive_kind' => normalizeArchiveKind((string)($r['archive_kind'] ?? '')),
];
}
return $map;
}
function approvalTargetLabel(array $ap, array $archiveMap): string
{
$type = (string)($ap['target_type'] ?? '');
if ($type === 'archive_batch') {
$ids = array_values(array_unique(array_filter(array_map('intval', (array)($ap['target_ids'] ?? [])))));
$labels = [];
foreach (array_slice($ids, 0, 5) as $aid) {
if (!isset($archiveMap[$aid])) {
$labels[] = 'ID:' . $aid . '(已删除)';
continue;
}
$arc = $archiveMap[$aid];
$labels[] = '[' . archiveKindLabel((string)$arc['archive_kind']) . '] ' . (string)$arc['name'];
}
$more = count($ids) > 5 ? ' 等' . count($ids) . '条' : '';
return '批量' . (int)($ap['target_count'] ?? count($ids)) . '条:' . implode(';', $labels) . $more;
}
$aid = (int)($ap['target_id'] ?? 0);
if ($aid <= 0) {
return 'ID:0';
}
if (!isset($archiveMap[$aid])) {
return 'ID:' . $aid . '(已删除/不存在)';
}
$arc = $archiveMap[$aid];
$name = (string)$arc['name'];
$inst = (string)$arc['loan_institution'];
$borrower = (string)$arc['borrower'];
return 'ID:' . $aid . ' [' . archiveKindLabel((string)$arc['archive_kind']) . '] ' . $name . ' / ' . $inst . ' / ' . $borrower;
}
function nextSettlementDate(string $today, string $mode, int $day): ?string
{
if ($mode === 'bullet') {
return null;
}
$day = max(1, min(28, $day));
if ($mode === 'quarterly') {
$m = (int)date('n', strtotime($today));
$qStartMonth = ((int)(($m - 1) / 3)) * 3 + 1;
$cand = date('Y', strtotime($today)) . '-' . str_pad((string)$qStartMonth, 2, '0', STR_PAD_LEFT) . '-' . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
if ($cand <= $today) {
$cand = date('Y-m-d', strtotime('+3 month', strtotime($cand)));
}
return $cand;
}
$curYm = date('Y-m', strtotime($today));
$cand = $curYm . '-' . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
if ($cand <= $today) {
$cand = date('Y-m-d', strtotime('+1 month', strtotime($cand)));
}
return $cand;
}
function ensureDirectoryPath(PDO $pdo, string $lv1, string $lv2, string $lv3): int
{
$clean = function (string $v): string {
$v = str_replace("\xC2\xA0", ' ', $v);
$v = preg_replace('/\s+/u', ' ', trim($v));
return (string)$v;
};
$lv1 = $clean($lv1);
$lv2 = $clean($lv2);
$lv3 = $clean($lv3);
$findOrCreate = function (?int $parentId, int $level, string $name) use ($pdo): int {
if ($parentId === null) {
$q = $pdo->prepare('SELECT id FROM directories WHERE name=:n AND level=:l AND parent_id IS NULL');
$q->execute([':n' => $name, ':l' => $level]);
} else {
$q = $pdo->prepare('SELECT id FROM directories WHERE name=:n AND level=:l AND parent_id=:p');
$q->execute([':n' => $name, ':l' => $level, ':p' => $parentId]);
}
$id = (int)$q->fetchColumn();
if ($id > 0) {
return $id;
}
$ins = $pdo->prepare('INSERT INTO directories(name,parent_id,level,created_at) VALUES(:n,:p,:l,:c)');
$ins->bindValue(':n', $name);
$ins->bindValue(':p', $parentId, $parentId === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
$ins->bindValue(':l', $level, PDO::PARAM_INT);
$ins->bindValue(':c', now());
$ins->execute();
return (int)$pdo->lastInsertId();
};
$id1 = $findOrCreate(null, 1, $lv1);
$id2 = $findOrCreate($id1, 2, $lv2);
return $findOrCreate($id2, 3, $lv3);
}
function hardDeleteArchive(PDO $pdo, int $archiveId): void
{
$stmt = $pdo->prepare('SELECT stored_name FROM files WHERE archive_id=:a');
$stmt->execute([':a' => $archiveId]);
foreach ($stmt->fetchAll() as $f) {
$filePath = UPLOAD_DIR . '/' . $f['stored_name'];
if (is_file($filePath)) {
unlink($filePath);
}
}
$pdo->prepare('DELETE FROM archives WHERE id=:id')->execute([':id' => $archiveId]);
}
function loanInstitutionStorePath(): string
{
return __DIR__ . '/../data/loan_institutions.json';
}
function defaultLoanInstitutions(): array
{
return [
'河南农商银行登封支行',
'郑州银行登封支行',
'浦发银行郑州金水支行',
'中国银行登封支行',
'建设银行登封支行',
'邮储银行登封支行',
'河南农商银行郑州关虎屯支行',
'中信银行登封支行',
];
}
function loadLoanInstitutions(): array
{
$path = loanInstitutionStorePath();
if (!is_file($path)) {
return defaultLoanInstitutions();
}
$raw = file_get_contents($path);
if ($raw === false || $raw === '') {
return defaultLoanInstitutions();
}
$arr = json_decode($raw, true);
if (!is_array($arr)) {
return defaultLoanInstitutions();
}
$out = [];
foreach ($arr as $x) {
$v = trim((string)$x);
if ($v !== '') {
$out[] = $v;
}
}
return $out ?: defaultLoanInstitutions();
}
function saveLoanInstitutions(array $items): void
{
$out = [];
foreach ($items as $x) {
$v = trim((string)$x);
if ($v !== '') {
$out[] = $v;
}
}
$out = array_values(array_unique($out));
if (empty($out)) {
$out = defaultLoanInstitutions();
}
$path = loanInstitutionStorePath();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
file_put_contents($path, json_encode($out, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
}
function autoArchiveName(string $borrower, ?float $creditLimit): string
{
$b = trim($borrower);
if ($b === '') {
return '';
}
$wan = $creditLimit === null ? '' : rtrim(rtrim(number_format($creditLimit / 10000, 2, '.', ''), '0'), '.');
return $wan === '' ? $b : ($b . $wan . '万元');
}
/** 对外担保:担保人 +「对外担保」+ 授信/10000 +「万元」 */
function autoExternalGuaranteeArchiveName(string $guarantor, ?float $creditLimit): string
{
$g = trim($guarantor);
if ($g === '') {
return '';
}
$wan = $creditLimit === null ? '' : rtrim(rtrim(number_format($creditLimit / 10000, 2, '.', ''), '0'), '.');
return $wan === '' ? '' : ($g . '对外担保' . $wan . '万元');
}
function ensureAutoBankLoanDirectory(PDO $pdo, ?string $startDate, string $loanInstitution): int
{
$year = $startDate ? date('Y', strtotime((string) $startDate)) : '';
$bank = trim($loanInstitution);
if ($year === '' || $bank === '') {
return 0;
}
return ensureDirectoryPath($pdo, '银行贷款', $year, $bank);
}
function ensureAutoExternalGuaranteeDirectory(PDO $pdo, ?string $startDate, string $guarantor): int
{
$year = $startDate ? date('Y', strtotime((string) $startDate)) : '';
$g = trim($guarantor);
if ($year === '' || $g === '') {
return 0;
}
return ensureDirectoryPath($pdo, '对外担保', $year, $g);
}
function collectAncestorIds(array $dirs, ?int $selectedId): array
{
if (!$selectedId) {
return [];
}
$parentMap = [];
foreach ($dirs as $d) {
$parentMap[(int)$d['id']] = $d['parent_id'] === null ? null : (int)$d['parent_id'];
}
$ids = [$selectedId];
$cur = $selectedId;
while (isset($parentMap[$cur]) && $parentMap[$cur] !== null) {
$cur = (int)$parentMap[$cur];
$ids[] = $cur;
}
return array_values(array_unique($ids));
}
/** 二级目录排序:从名称中提取年份(如 2025、2025年)升序;无年份则按名称排在后段。 */
function directoryLevel2TimeSortKey(string $name): int
{
$name = trim($name);
if ($name === '') {
return 9999;
}
if (preg_match('/^(\d{4})(?:\b|年)/u', $name, $m)) {
return (int) $m[1];
}
if (preg_match('/(\d{4})/u', $name, $m)) {
return (int) $m[1];
}
return 9999;
}
function buildTreeHtml(array $dirs, ?int $selectedId, bool $admin, array $pathIds): string
{
$byParent = [];
$dirMap = [];
foreach ($dirs as $d) {
$pid = $d['parent_id'] === null ? 0 : (int) $d['parent_id'];
$byParent[$pid][] = $d;
$dirMap[(int)$d['id']] = $d;
}
foreach ($byParent as &$children) {
if ($children === []) {
continue;
}
if ((int) ($children[0]['level'] ?? 0) === 2) {
usort($children, static function (array $a, array $b): int {
$ka = directoryLevel2TimeSortKey((string) $a['name']);
$kb = directoryLevel2TimeSortKey((string) $b['name']);
if ($ka !== $kb) {
return $ka <=> $kb;
}
return strcmp((string) $a['name'], (string) $b['name']);
});
}
}
unset($children);
$pathSet = array_flip($pathIds);
$rootNameById = [];
foreach ($dirMap as $id => $d) {
$cur = $id;
$rootName = (string)$d['name'];
while (isset($dirMap[$cur]) && $dirMap[$cur]['parent_id'] !== null) {
$cur = (int)$dirMap[$cur]['parent_id'];
if (!isset($dirMap[$cur])) {
break;
}
$rootName = (string)$dirMap[$cur]['name'];
}
$rootNameById[$id] = $rootName;
}
$render = function ($parent) use (&$render, $byParent, $selectedId, $admin, $pathSet, $rootNameById): string {
if (empty($byParent[$parent])) {
return '';
}
$html = '
';
foreach ($byParent[$parent] as $d) {
$id = (int)$d['id'];
$name = e($d['name']);
$hasChildren = !empty($byParent[$id]);
$isCurrent = $selectedId === $id;
$isAncestor = isset($pathSet[$id]);
$linkClass = 'tree-link' . ($isCurrent ? ' current' : ($isAncestor ? ' ancestor' : ''));
$actions = '';
$kindQ = '';
$rootName = (string)($rootNameById[$id] ?? '');
if ($rootName === '银行贷款') {
$kindQ = '&kind=' . ARCHIVE_KIND_BANK_LOAN;
} elseif ($rootName === '对外担保') {
$kindQ = '&kind=' . ARCHIVE_KIND_EXTERNAL_GUARANTEE;
}
if ($admin) {
$actions .= '';
$actions .= '✎';
if ((int)$d['level'] < 3) {
$actions .= '+';
}
$actions .= '🗑';
$actions .= '';
}
$html .= '- ';
if ($hasChildren) {
$open = $isAncestor ? ' open' : '';
$html .= '
';
} else {
$html .= '';
}
$html .= $render($id);
if ($hasChildren) {
$html .= ' ';
}
$html .= ' ';
}
$html .= '
';
return $html;
};
return $render(0);
}
/** 便捷查询侧栏 / 便捷查询页:折叠菜单内链接 */
function quickQueryMenuHtml(): string
{
$links = [
['query_payable_interest', '应付本息查询'],
['query_external_guarantee', '对外担保查询'],
['query_totals', '查询总金额'],
['interest_calculator', '利息计算器'],
['acceptance_discount_calculator', '承兑贴现计算器'],
['repayment_history', '历史还款记录查询'],
];
$inner = '';
foreach ($links as $pair) {
$inner .= '
' . e($pair[1]) . '';
}
return '';
}
if ($action === 'new_dir') {
requireAdmin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim((string)($_POST['name'] ?? ''));
$parentId = (int)($_POST['parent_id'] ?? 0);
if ($name === '') {
flash('error', '目录名称不能为空');
header('Location: index.php');
exit;
}
$level = 1;
$parentValue = null;
if ($parentId > 0) {
$stmt = $pdo->prepare('SELECT * FROM directories WHERE id=:id');
$stmt->execute([':id' => $parentId]);
$p = $stmt->fetch();
if (!$p) {
flash('error', '父目录不存在');
header('Location: index.php');
exit;
}
$level = (int)$p['level'] + 1;
$parentValue = $parentId;
if ($level > 3) {
flash('error', '最多只支持三级目录');
header('Location: index.php?dir_id=' . $parentId);
exit;
}
}
$stmt = $pdo->prepare('INSERT INTO directories(name,parent_id,level,created_at) VALUES(:n,:p,:l,:c)');
$stmt->bindValue(':n', $name);
$stmt->bindValue(':p', $parentValue, $parentValue === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
$stmt->bindValue(':l', $level, PDO::PARAM_INT);
$stmt->bindValue(':c', now());
$stmt->execute();
flash('ok', '目录已创建');
header('Location: index.php' . ($parentId ? '?dir_id=' . $parentId : ''));
exit;
}
$parentId = (int)($_GET['parent_id'] ?? 0);
pageHeader($me); ?>
prepare('SELECT * FROM directories WHERE id=:id');
$stmt->execute([':id' => $id]);
$d = $stmt->fetch();
if (!$d) {
flash('error', '目录不存在');
header('Location: index.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim((string)($_POST['name'] ?? ''));
if ($name === '') {
flash('error', '目录名称不能为空');
} else {
$up = $pdo->prepare('UPDATE directories SET name=:n WHERE id=:id');
$up->execute([':n' => $name, ':id' => $id]);
flash('ok', '目录已更新');
header('Location: index.php?dir_id=' . $id);
exit;
}
}
pageHeader($me); ?>
prepare('SELECT COUNT(*) FROM directories WHERE parent_id=:id');
$c1->execute([':id' => $id]);
$c2 = $pdo->prepare('SELECT COUNT(*) FROM archives WHERE directory_id=:id');
$c2->execute([':id' => $id]);
if ((int)$c1->fetchColumn() > 0 || (int)$c2->fetchColumn() > 0) {
flash('error', '目录非空,不能删除');
header('Location: index.php?dir_id=' . $id);
exit;
}
$del = $pdo->prepare('DELETE FROM directories WHERE id=:id');
$del->execute([':id' => $id]);
flash('ok', '目录已删除');
header('Location: index.php');
exit;
}
if ($action === 'save_archive') {
requireAdmin();
$id = (int)($_POST['id'] ?? 0);
$directoryId = (int)($_POST['directory_id'] ?? 0);
$archiveKind = normalizeArchiveKind((string)($_POST['archive_kind'] ?? ''));
$loanInstitution = trim((string)($_POST['loan_institution'] ?? ''));
$borrower = trim((string)($_POST['borrower'] ?? ''));
$guarantorEarly = trim((string)($_POST['guarantor'] ?? ''));
$creditLimitVal = ($_POST['credit_limit'] ?? '') === '' ? null : (float)$_POST['credit_limit'];
$startDateVal = $_POST['start_date'] ?: null;
if ($archiveKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE) {
$name = autoExternalGuaranteeArchiveName($guarantorEarly, $creditLimitVal);
if ($name === '') {
flash('error', '对外担保需填写担保人、授信额度(用于自动生成档案名称)');
header('Location: index.php');
exit;
}
} else {
$name = autoArchiveName($borrower, $creditLimitVal);
if ($name === '') {
flash('error', '借款主体与授信额度必填(用于自动生成档案名称)');
header('Location: index.php');
exit;
}
}
if (isset($_POST['loan_institutions_json'])) {
$json = json_decode((string)$_POST['loan_institutions_json'], true);
if (is_array($json)) {
saveLoanInstitutions($json);
}
}
if ($directoryId > 0) {
$check = $pdo->prepare('SELECT level FROM directories WHERE id=:id');
$check->execute([':id' => $directoryId]);
$lv = (int)$check->fetchColumn();
if ($lv !== 3) {
flash('error', '手动选择目录时必须是三级目录');
header('Location: index.php?dir_id=' . $directoryId);
exit;
}
} else {
if ($archiveKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE) {
$directoryId = ensureAutoExternalGuaranteeDirectory($pdo, $startDateVal, $guarantorEarly);
if ($directoryId <= 0) {
flash('error', '未选择三级目录时,对外担保需填写起始日与担保人,系统将自动创建目录:对外担保 / 年份 / 担保人');
header('Location: index.php');
exit;
}
} else {
$directoryId = ensureAutoBankLoanDirectory($pdo, $startDateVal, $loanInstitution);
if ($directoryId <= 0) {
flash('error', '未选择三级目录时,银行贷款需填写贷款机构与起始日,系统将自动创建目录:银行贷款 / 年份 / 贷款机构');
header('Location: index.php');
exit;
}
}
}
$planEnabled = ($_POST['plan_enabled'] ?? '0') === '1';
$allPlans = loadRepaymentPlans();
$existing = $id > 0 ? getArchiveRepaymentPlan($allPlans, $id) : ['enabled' => false, 'plans' => []];
$existingMap = [];
foreach (($existing['plans'] ?? []) as $ep) {
if (!empty($ep['id'])) {
$existingMap[(string)$ep['id']] = $ep;
}
}
$dueDates = (array)($_POST['plan_due_date'] ?? []);
$amounts = (array)($_POST['plan_amount'] ?? []);
$planIds = (array)($_POST['plan_id'] ?? []);
$planStatuses = (array)($_POST['plan_status'] ?? []);
$delayDates = (array)($_POST['plan_delay_date'] ?? []);
$plans = [];
$paidSum = 0.0;
if ($planEnabled) {
$n = max(count($dueDates), count($amounts), count($planStatuses));
for ($i = 0; $i < $n; $i++) {
$due = trim((string)($dueDates[$i] ?? ''));
$amtRaw = trim((string)($amounts[$i] ?? ''));
if ($due === '' || $amtRaw === '') {
continue;
}
$amt = (float)$amtRaw;
if ($amt <= 0) {
continue;
}
$pid = trim((string)($planIds[$i] ?? ''));
$status = (string)($planStatuses[$i] ?? 'pending');
if (!in_array($status, ['pending', 'paid', 'delayed', 'overdue'], true)) {
$status = 'pending';
}
$today = date('Y-m-d');
if (($status === 'delayed' || $status === 'overdue') && $due > $today) {
$status = 'pending';
}
$delayDate = trim((string)($delayDates[$i] ?? ''));
if ($status === 'delayed' && $delayDate !== '') {
$due = $delayDate;
}
$old = $existingMap[$pid] ?? null;
$paidAt = null;
if ($status === 'paid') {
$paidAt = ($old && ($old['status'] ?? '') === 'paid' && !empty($old['paid_at'])) ? $old['paid_at'] : date('Y-m-d');
$paidSum += $amt;
}
$plans[] = [
'id' => $pid !== '' ? $pid : uniqid('rp_', true),
'due_date' => $due,
'amount' => $amt,
'status' => $status,
'paid_at' => $paidAt,
'updated_at' => now(),
];
}
}
$financeAllWork = loadFinanceConfigs();
$rawPrevFinance = $id > 0 ? ($financeAllWork[(string)$id] ?? []) : [];
$prevManualPrincipal = (float)($rawPrevFinance['manual_principal_repaid'] ?? 0);
$prevManualInterest = (float)($rawPrevFinance['manual_interest_paid'] ?? 0);
$addPrincipal = max(0, (float)($_POST['principal_repayment_now'] ?? 0));
$addInterest = max(0, (float)($_POST['interest_repayment_now'] ?? 0));
if ($creditLimitVal !== null) {
$repayCap = max(0, $creditLimitVal - $paidSum - $prevManualPrincipal);
if ($addPrincipal > $repayCap + 1e-6) {
flash('error', '本次归还本金超过可还金额(授信 − 分期已还 − 已累计归还本金)');
header('Location: index.php?action=archive_form' . ($id > 0 ? ('&id=' . $id) : ''));
exit;
}
}
$oldEndDate = '';
if ($id > 0) {
$stEnd = $pdo->prepare('SELECT end_date FROM archives WHERE id=:id');
$stEnd->execute([':id' => $id]);
$oldEndRow = $stEnd->fetchColumn();
if ($oldEndRow !== false && $oldEndRow !== null) {
$oldEndDate = (string)$oldEndRow;
}
}
$loanExtendTo = trim((string)($_POST['loan_extend_to'] ?? ''));
$endDatePosted = ($_POST['end_date'] ?? '') !== '' ? (string)$_POST['end_date'] : null;
$endDateFinal = $endDatePosted;
if ($loanExtendTo !== '') {
if ($startDateVal && $loanExtendTo < $startDateVal) {
flash('error', '延期后的到期日不能早于起始日');
header('Location: index.php?action=archive_form' . ($id > 0 ? ('&id=' . $id) : ''));
exit;
}
if ($oldEndDate !== '' && $loanExtendTo < $oldEndDate) {
flash('error', '延期后的到期日不能早于当前到期日');
header('Location: index.php?action=archive_form' . ($id > 0 ? ('&id=' . $id) : ''));
exit;
}
$endDateFinal = $loanExtendTo;
}
$manualPrincipalTotal = $prevManualPrincipal + $addPrincipal;
$manualInterestTotal = $prevManualInterest + $addInterest;
$remainingAuto = $creditLimitVal === null ? null : max(0, $creditLimitVal - $paidSum - $manualPrincipalTotal);
$payload = [
':name' => $name,
':directory_id' => $directoryId,
':loan_institution' => $loanInstitution,
':borrower' => $borrower,
':credit_limit' => $creditLimitVal,
':remaining_limit' => $remainingAuto,
':start_date' => $startDateVal,
':end_date' => $endDateFinal,
':annual_rate' => ($_POST['annual_rate'] ?? '') === '' ? null : (float)$_POST['annual_rate'],
':guarantee_type' => trim((string)($_POST['guarantee_type'] ?? '')),
':guarantor' => $guarantorEarly,
':archive_kind' => $archiveKind,
':updated_at' => now(),
];
if ($id > 0) {
$sql = 'UPDATE archives SET name=:name,directory_id=:directory_id,archive_kind=:archive_kind,loan_institution=:loan_institution,borrower=:borrower,credit_limit=:credit_limit,remaining_limit=:remaining_limit,start_date=:start_date,end_date=:end_date,annual_rate=:annual_rate,guarantee_type=:guarantee_type,guarantor=:guarantor,updated_at=:updated_at WHERE id=:id';
$payload[':id'] = $id;
$pdo->prepare($sql)->execute($payload);
$archiveId = $id;
flash('ok', '档案已更新(' . date('H:i:s') . ')');
} else {
$sql = 'INSERT INTO archives(name,directory_id,archive_kind,loan_institution,borrower,credit_limit,remaining_limit,start_date,end_date,annual_rate,guarantee_type,guarantor,created_at,updated_at) VALUES(:name,:directory_id,:archive_kind,:loan_institution,:borrower,:credit_limit,:remaining_limit,:start_date,:end_date,:annual_rate,:guarantee_type,:guarantor,:created_at,:updated_at)';
$payload[':created_at'] = now();
$pdo->prepare($sql)->execute($payload);
$archiveId = (int)$pdo->lastInsertId();
flash('ok', '档案已创建(' . date('H:i:s') . ')');
}
if ($planEnabled) {
$allPlans[(string)$archiveId] = ['enabled' => true, 'plans' => $plans];
} else {
$allPlans[(string)$archiveId] = ['enabled' => false, 'plans' => []];
}
saveRepaymentPlans($allPlans);
$financeAll = loadFinanceConfigs();
$interestMode = (string)($_POST['interest_mode'] ?? 'monthly');
if (!in_array($interestMode, ['monthly', 'quarterly', 'bullet'], true)) {
$interestMode = 'monthly';
}
$settlementRule = $interestMode === 'quarterly' ? 'quarterly_day' : ($interestMode === 'bullet' ? 'maturity_once' : 'monthly_day');
$settlementDay = $interestMode === 'bullet' ? 0 : max(1, min(28, (int)($_POST['settlement_day'] ?? 21)));
$todaySave = date('Y-m-d');
$endDatePosted = $endDateFinal !== null && $endDateFinal !== '' ? (string)$endDateFinal : '';
$postedEarlyRaw = $_POST['is_early_settled'] ?? null;
$prevFinance = getArchiveFinanceConfig($financeAll, $archiveId);
if ($endDatePosted !== '' && $endDatePosted < $todaySave) {
// 已过到期日:禁止在编辑页首次标「已完成」(须走「待更新」);已是已完成的保留,除非表单显式提交为否
if (!empty($prevFinance['manual_early_settled'])) {
$manualEarlySettled = ($postedEarlyRaw === null) ? true : ($postedEarlyRaw === '1');
} else {
$manualEarlySettled = false;
}
} else {
$manualEarlySettled = $endDatePosted !== '' && ($postedEarlyRaw === '1');
}
$manualOverdue = (($_POST['is_overdue'] ?? '0') === '1');
if ($manualEarlySettled) {
$manualOverdue = false;
}
$remarkRaw = trim((string)($_POST['archive_remark'] ?? ''));
if (function_exists('mb_substr')) {
$remarkRaw = mb_substr($remarkRaw, 0, 4000, 'UTF-8');
} elseif (strlen($remarkRaw) > 4000) {
$remarkRaw = substr($remarkRaw, 0, 4000);
}
$financeMergeBase = $financeAll[(string)$archiveId] ?? [];
$financeAll[(string)$archiveId] = array_merge($financeMergeBase, [
'interest_mode' => $interestMode,
'settlement_rule' => $settlementRule,
'settlement_day' => $settlementDay,
'overdue_float_pct' => max(0, (float)($_POST['overdue_float_pct'] ?? 50)),
'manual_overdue' => $manualOverdue,
'manual_early_settled' => $manualEarlySettled,
'remark' => $remarkRaw,
'manual_principal_repaid' => $manualPrincipalTotal,
'manual_interest_paid' => $manualInterestTotal,
'updated_at' => now(),
]);
saveFinanceConfigs($financeAll);
uploadArchiveFiles($pdo, $archiveId, $_FILES['new_files'] ?? []);
header('Location: index.php?action=archive_form&id=' . $archiveId);
exit;
}
if ($action === 'reminder_action') {
requireAdmin();
$op = (string)($_POST['op'] ?? $_GET['op'] ?? '');
$archiveId = (int)($_POST['archive_id'] ?? $_GET['archive_id'] ?? 0);
$planId = trim((string)($_POST['plan_id'] ?? $_GET['plan_id'] ?? ''));
$redirectType = (string)($_POST['redirect_type'] ?? $_GET['redirect_type'] ?? 'all');
if (!in_array($redirectType, ['all', 'due', 'plan', 'overdue'], true)) {
$redirectType = 'all';
}
$back = 'index.php?action=reminders&type=' . $redirectType;
if ($archiveId <= 0) {
flash('error', '参数错误');
redirectBackOr($back);
}
if ($op === 'early_settle') {
$financeAll = loadFinanceConfigs();
$cfg = getArchiveFinanceConfig($financeAll, $archiveId);
$cfg['manual_early_settled'] = true;
$cfg['manual_overdue'] = false;
$cfg['updated_at'] = now();
$financeAll[(string)$archiveId] = $cfg;
saveFinanceConfigs($financeAll);
flash('ok', '已设为提前还款,状态改为已完成');
redirectBackOr($back);
}
if ($op === 'plan_delay') {
$newDue = trim((string)($_POST['new_due_date'] ?? $_GET['new_due_date'] ?? ''));
if ($planId === '' || $newDue === '') {
flash('error', '延期参数不完整');
redirectBackOr($back);
}
$allPlans = loadRepaymentPlans();
$cfg = getArchiveRepaymentPlan($allPlans, $archiveId);
if (empty($cfg['enabled']) || empty($cfg['plans']) || !is_array($cfg['plans'])) {
flash('error', '分期计划不存在');
redirectBackOr($back);
}
$ok = false;
foreach ($cfg['plans'] as &$p) {
if ((string)($p['id'] ?? '') !== $planId) {
continue;
}
$p['due_date'] = $newDue;
$p['status'] = 'delayed';
$p['paid_at'] = null;
$p['updated_at'] = now();
$ok = true;
break;
}
unset($p);
if ($ok) {
$allPlans[(string)$archiveId] = $cfg;
saveRepaymentPlans($allPlans);
flash('ok', '还款计划已延期');
} else {
flash('error', '未找到对应还款计划');
}
redirectBackOr($back);
}
if ($op === 'overdue_paid') {
$allPlans = loadRepaymentPlans();
$cfg = getArchiveRepaymentPlan($allPlans, $archiveId);
$handled = false;
if ($planId !== '' && !empty($cfg['enabled']) && !empty($cfg['plans']) && is_array($cfg['plans'])) {
foreach ($cfg['plans'] as &$p) {
if ((string)($p['id'] ?? '') !== $planId) {
continue;
}
$p['status'] = 'paid';
$p['paid_at'] = date('Y-m-d');
$p['updated_at'] = now();
$handled = true;
break;
}
unset($p);
if ($handled) {
$allPlans[(string)$archiveId] = $cfg;
saveRepaymentPlans($allPlans);
recalcRemainingLimitByPlans($pdo, $archiveId, (array)$cfg['plans']);
flash('ok', '逾期分期已标记为已归还');
}
} else {
$financeAll = loadFinanceConfigs();
$f = getArchiveFinanceConfig($financeAll, $archiveId);
$f['manual_early_settled'] = true;
$f['manual_overdue'] = false;
$f['updated_at'] = now();
$financeAll[(string)$archiveId] = $f;
saveFinanceConfigs($financeAll);
flash('ok', '整笔逾期已归还,状态改为已完成');
$handled = true;
}
if (!$handled) {
flash('error', '未找到可处理的逾期项');
}
redirectBackOr($back);
}
flash('error', '不支持的操作');
redirectBackOr($back);
}
if ($action === 'pending_update_action') {
requireAdmin();
$op = (string)($_POST['op'] ?? '');
$archiveId = (int)($_POST['archive_id'] ?? 0);
$planId = trim((string)($_POST['plan_id'] ?? ''));
$todayPu = date('Y-m-d');
$dirIdPost = (int)($_POST['dir_id'] ?? 0);
$back = 'index.php?action=pending_updates' . ($dirIdPost > 0 ? ('&dir_id=' . $dirIdPost) : '');
if ($archiveId <= 0) {
flash('error', '参数错误');
redirectBackOr($back);
}
if ($op === 'pending_loan_repaid' || $op === 'pending_loan_overdue') {
$st = $pdo->prepare('SELECT end_date FROM archives WHERE id=:id');
$st->execute([':id' => $archiveId]);
$endRow = $st->fetchColumn();
$endStr = $endRow !== false && $endRow !== null ? (string)$endRow : '';
if ($endStr === '' || $endStr >= $todayPu) {
flash('error', '该贷款尚未到期待更新处理');
redirectBackOr($back);
}
$financeAll = loadFinanceConfigs();
$f = getArchiveFinanceConfig($financeAll, $archiveId);
if (archiveStatusWithConfig($endStr, $f) !== '待更新') {
flash('error', '该项已处理或状态已变更');
redirectBackOr($back);
}
if ($op === 'pending_loan_repaid') {
$f['manual_early_settled'] = true;
$f['manual_overdue'] = false;
$f['updated_at'] = now();
$financeAll[(string)$archiveId] = $f;
saveFinanceConfigs($financeAll);
flash('ok', '已标记为已还款,状态改为已完成');
} else {
$f['manual_overdue'] = true;
$f['manual_early_settled'] = false;
$f['updated_at'] = now();
$financeAll[(string)$archiveId] = $f;
saveFinanceConfigs($financeAll);
flash('ok', '已标记为已逾期,状态改为逾期中');
}
redirectBackOr($back);
}
if ($op === 'pending_plan_repaid' || $op === 'pending_plan_overdue') {
if ($planId === '') {
flash('error', '参数错误');
redirectBackOr($back);
}
$allPlans = loadRepaymentPlans();
$cfg = getArchiveRepaymentPlan($allPlans, $archiveId);
$handled = false;
if (!empty($cfg['enabled']) && !empty($cfg['plans']) && is_array($cfg['plans'])) {
foreach ($cfg['plans'] as &$p) {
if ((string)($p['id'] ?? '') !== $planId) {
continue;
}
$pst = (string)($p['status'] ?? 'pending');
$d = (string)($p['due_date'] ?? '');
if ($d === '' || $d >= $todayPu || !in_array($pst, ['pending', 'delayed'], true)) {
break;
}
if ($op === 'pending_plan_repaid') {
$p['status'] = 'paid';
$p['paid_at'] = date('Y-m-d');
} else {
$p['status'] = 'overdue';
$p['paid_at'] = null;
}
$p['updated_at'] = now();
$handled = true;
break;
}
unset($p);
}
if ($handled) {
$allPlans[(string)$archiveId] = $cfg;
saveRepaymentPlans($allPlans);
if ($op === 'pending_plan_repaid') {
recalcRemainingLimitByPlans($pdo, $archiveId, (array)$cfg['plans']);
}
flash('ok', $op === 'pending_plan_repaid' ? '已标记为已还款(按期还款)' : '已标记为已逾期(逾期中)');
} else {
flash('error', '未找到可处理的还款计划或状态已变更');
}
redirectBackOr($back);
}
flash('error', '不支持的操作');
redirectBackOr($back);
}
if ($action === 'download_import_template') {
requireAdmin();
$manualTemplatePath = __DIR__ . '/../data/import_template.xlsx';
if (is_file($manualTemplatePath)) {
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="' . rawurlencode('档案导入模板.xlsx') . '"');
readfile($manualTemplatePath);
exit;
}
$headers = ['一级目录', '二级目录', '三级目录', '档案名称', '贷款机构', '借款主体', '授信额度', '起止日', '到期日', '年利率', '担保类型', '担保人', '计息方式', '结息日', '是否有分期还款计划'];
$sample = ['银行贷款', '2026', 'XX银行', 'A公司流动资金贷款合同', 'XX银行', 'A公司', '5000000', '2026-01-15', '2027-01-14', '5.5', '抵押', 'B公司', '按月计息', '21', '否'];
if (class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx')) {
$filename = '档案导入模板.xlsx';
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue(
'A1',
'说明:第1行可写模板使用提示(导入时忽略);第2行为列标题勿改;从第3行起填数据。'
. '一级目录填「银行贷款」或「对外担保」;系统据此识别档案类型。对外担保导入时「贷款机构」「年利率」可留空;「档案名称」可留空,将按担保人+对外担保+授信万元自动生成。'
. '银行贷款必填至担保人(含档案名称列);选填:计息方式、结息日、是否有分期还款计划。'
);
$sheet->fromArray($headers, null, 'A2');
$sheet->fromArray([$sample], null, 'A3');
foreach (range('A', 'O') as $col) {
$sheet->getColumnDimension($col)->setWidth(20);
}
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save('php://output');
exit;
}
flash('error', '未找到可用xlsx模板。请安装PhpSpreadsheet或将模板放到 data/import_template.xlsx');
header('Location: index.php');
exit;
}
if ($action === 'import_archives') {
requireAdmin();
if (empty($_FILES['import_file']) || (int)$_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
flash('error', '请选择有效导入文件');
header('Location: index.php');
exit;
}
$name = (string)$_FILES['import_file']['name'];
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$rows = [];
if ($ext === 'csv') {
$fp = fopen((string)$_FILES['import_file']['tmp_name'], 'rb');
if ($fp) {
fgetcsv($fp);
$header = fgetcsv($fp);
while (($line = fgetcsv($fp)) !== false) {
$rows[] = $line;
}
fclose($fp);
}
} elseif (in_array($ext, ['xlsx', 'xls'], true) && class_exists('\PhpOffice\PhpSpreadsheet\IOFactory')) {
$sheet = \PhpOffice\PhpSpreadsheet\IOFactory::load((string)$_FILES['import_file']['tmp_name'])->getActiveSheet();
$arr = $sheet->toArray();
array_shift($arr);
$header = array_shift($arr);
$rows = $arr;
} else {
flash('error', '仅支持CSV,或安装PhpSpreadsheet后支持Excel');
header('Location: index.php');
exit;
}
$created = 0;
$skipped = 0;
$financeAll = loadFinanceConfigs();
$planAll = loadRepaymentPlans();
$seen = [];
foreach ($rows as $r) {
$lv1 = (string)($r[0] ?? '');
$lv2 = (string)($r[1] ?? '');
$lv3 = (string)($r[2] ?? '');
$archiveName = trim((string)($r[3] ?? ''));
$loanInstitution = trim((string)($r[4] ?? ''));
$borrower = trim((string)($r[5] ?? ''));
$creditRaw = trim((string)($r[6] ?? ''));
$startDate = trim((string)($r[7] ?? ''));
$endDate = trim((string)($r[8] ?? ''));
$annualRateRaw = trim((string)($r[9] ?? ''));
$guaranteeType = trim((string)($r[10] ?? ''));
$guarantor = trim((string)($r[11] ?? ''));
$importKind = trim($lv1) === '对外担保' ? ARCHIVE_KIND_EXTERNAL_GUARANTEE : ARCHIVE_KIND_BANK_LOAN;
if ($importKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE) {
if ($lv1 === '' || $lv2 === '' || $lv3 === '' || $borrower === '' || $creditRaw === '' || $startDate === '' || $endDate === '' || $guaranteeType === '' || $guarantor === '') {
continue;
}
$annualRate = $annualRateRaw === '' ? null : (float) $annualRateRaw;
} else {
if ($lv1 === '' || $lv2 === '' || $lv3 === '' || $archiveName === '' || $loanInstitution === '' || $borrower === '' || $creditRaw === '' || $startDate === '' || $endDate === '' || $annualRateRaw === '' || $guaranteeType === '' || $guarantor === '') {
continue;
}
$annualRate = (float) $annualRateRaw;
}
// 无论目录是否预先存在,都会自动按三级路径创建并返回三级目录ID。
$dirId = ensureDirectoryPath($pdo, $lv1, $lv2, $lv3);
$credit = (float) $creditRaw;
$remain = $credit;
if ($importKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE) {
$archiveName = autoExternalGuaranteeArchiveName($guarantor, $credit);
if ($archiveName === '') {
continue;
}
}
$fingerprint = implode('|', [
$importKind,
(string) $dirId,
mb_strtolower($archiveName),
mb_strtolower($loanInstitution),
mb_strtolower($borrower),
(string) $credit,
$startDate,
$endDate,
(string) ($annualRate ?? ''),
mb_strtolower($guaranteeType),
mb_strtolower($guarantor),
]);
if (isset($seen[$fingerprint])) {
$skipped++;
continue;
}
$seen[$fingerprint] = true;
$dupStmt = $pdo->prepare('SELECT COUNT(*) FROM archives WHERE directory_id=:d AND archive_kind=:ak AND name=:n AND COALESCE(loan_institution,\'\')=:li AND borrower=:b AND credit_limit=:cl AND start_date=:sd AND end_date=:ed AND COALESCE(annual_rate,-999999.99)=COALESCE(:ar,-999999.99) AND guarantee_type=:gt AND guarantor=:g');
$dupStmt->execute([
':d' => $dirId,
':ak' => $importKind,
':n' => $archiveName,
':li' => $loanInstitution,
':b' => $borrower,
':cl' => $credit,
':sd' => $startDate,
':ed' => $endDate,
':ar' => $annualRate,
':gt' => $guaranteeType,
':g' => $guarantor,
]);
if ((int)$dupStmt->fetchColumn() > 0) {
$skipped++;
continue;
}
$pdo->prepare('INSERT INTO archives(name,directory_id,archive_kind,loan_institution,borrower,credit_limit,remaining_limit,start_date,end_date,annual_rate,guarantee_type,guarantor,created_at,updated_at) VALUES(:name,:directory_id,:archive_kind,:loan_institution,:borrower,:credit_limit,:remaining_limit,:start_date,:end_date,:annual_rate,:guarantee_type,:guarantor,:created_at,:updated_at)')
->execute([
':name' => $archiveName,
':directory_id' => $dirId,
':archive_kind' => $importKind,
':loan_institution' => $loanInstitution,
':borrower' => $borrower,
':credit_limit' => $credit,
':remaining_limit' => $remain,
':start_date' => $startDate ?: null,
':end_date' => $endDate ?: null,
':annual_rate' => $annualRate,
':guarantee_type' => $guaranteeType,
':guarantor' => $guarantor,
':created_at' => now(),
':updated_at' => now(),
]);
$archiveId = (int)$pdo->lastInsertId();
$modeText = trim((string)($r[12] ?? ''));
$mode = $modeText === '按季付息' ? 'quarterly' : ($modeText === '到期一次性付清' ? 'bullet' : 'monthly');
$settlementRule = $mode === 'quarterly' ? 'quarterly_day' : ($mode === 'bullet' ? 'maturity_once' : 'monthly_day');
$settlementDay = $mode === 'bullet' ? 0 : max(1, min(28, (int)trim((string)($r[13] ?? '21'))));
$financeAll[(string)$archiveId] = [
'interest_mode' => $mode,
'settlement_rule' => $settlementRule,
'settlement_day' => $settlementDay,
'overdue_float_pct' => 50,
'manual_overdue' => false,
'manual_early_settled' => false,
'remark' => '',
'updated_at' => now(),
];
$planEnabledText = trim((string)($r[14] ?? '否'));
$planAll[(string)$archiveId] = ['enabled' => in_array($planEnabledText, ['是', 'yes', 'YES', '1'], true), 'plans' => []];
$created++;
}
saveFinanceConfigs($financeAll);
saveRepaymentPlans($planAll);
flash('ok', '导入完成:新增 ' . $created . ' 条,重复忽略 ' . $skipped . ' 条');
header('Location: index.php');
exit;
}
if ($action === 'request_delete_archive') {
requireAdmin();
$archiveId = (int)($_GET['id'] ?? 0);
if ($archiveId <= 0) {
flash('error', '参数错误');
header('Location: index.php');
exit;
}
$approvals = loadDeleteApprovals();
foreach ($approvals as $ap) {
if (($ap['target_type'] ?? '') === 'archive' && (int)($ap['target_id'] ?? 0) === $archiveId && ($ap['status'] ?? '') === 'pending') {
flash('error', '该档案已有待审核删除申请');
header('Location: index.php');
exit;
}
}
$approvals[] = [
'id' => uniqid('del_', true),
'target_type' => 'archive',
'target_id' => $archiveId,
'status' => 'pending',
'requested_by' => (int)$me['id'],
'requested_name' => (string)$me['username'],
'requested_at' => now(),
'approved_by' => null,
'approved_at' => null,
];
saveDeleteApprovals($approvals);
flash('ok', '已提交删除申请,需另一位管理员审核');
header('Location: index.php');
exit;
}
if ($action === 'request_delete_selected') {
requireAdmin();
$idsRaw = $_GET['ids'] ?? '';
if (is_array($idsRaw)) {
$ids = array_values(array_unique(array_filter(array_map('intval', $idsRaw))));
} else {
$ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string)$idsRaw)))));
}
if (empty($ids)) {
flash('error', '请先选择要删除的档案');
header('Location: index.php');
exit;
}
$approvals = loadDeleteApprovals();
$pendingSet = [];
foreach ($approvals as $ap) {
if (($ap['status'] ?? '') !== 'pending') {
continue;
}
if (($ap['target_type'] ?? '') === 'archive' && !empty($ap['target_id'])) {
$pendingSet[] = (string)$ap['target_id'];
}
if (($ap['target_type'] ?? '') === 'archive_batch' && !empty($ap['target_ids']) && is_array($ap['target_ids'])) {
foreach ($ap['target_ids'] as $x) {
$pendingSet[] = (string)(int)$x;
}
}
}
foreach ($ids as $id) {
if (in_array((string)$id, $pendingSet, true)) {
flash('error', '所选档案中存在待审核删除项,请先完成审核');
header('Location: index.php');
exit;
}
}
$approvals[] = [
'id' => uniqid('del_', true),
'target_type' => 'archive_batch',
'target_ids' => $ids,
'target_count' => count($ids),
'status' => 'pending',
'requested_by' => (int)$me['id'],
'requested_name' => (string)$me['username'],
'requested_at' => now(),
'approved_by' => null,
'approved_at' => null,
];
saveDeleteApprovals($approvals);
flash('ok', '已提交选中档案删除申请(' . count($ids) . ' 条),需另一位管理员审核');
header('Location: index.php');
exit;
}
if ($action === 'delete_approvals') {
requireAdmin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$approvalId = trim((string)($_POST['approval_id'] ?? ''));
$do = (string)($_POST['do'] ?? '');
$approvals = loadDeleteApprovals();
foreach ($approvals as &$ap) {
if (($ap['id'] ?? '') !== $approvalId || ($ap['status'] ?? '') !== 'pending') {
continue;
}
if ((int)$ap['requested_by'] === (int)$me['id']) {
flash('error', '不能由申请人本人审核');
break;
}
if ($do === 'approve' && ($ap['target_type'] ?? '') === 'archive') {
hardDeleteArchive($pdo, (int)$ap['target_id']);
$ap['status'] = 'approved';
$ap['approved_by'] = (int)$me['id'];
$ap['approved_at'] = now();
flash('ok', '删除已由第二管理员审核并执行');
break;
}
if ($do === 'approve' && ($ap['target_type'] ?? '') === 'archive_batch') {
$ids = array_values(array_unique(array_filter(array_map('intval', (array)($ap['target_ids'] ?? [])))));
foreach ($ids as $aid) {
hardDeleteArchive($pdo, $aid);
}
$ap['status'] = 'approved';
$ap['approved_by'] = (int)$me['id'];
$ap['approved_at'] = now();
flash('ok', '批量删除已审核并执行(' . count($ids) . ' 条)');
break;
}
if ($do === 'reject') {
$ap['status'] = 'rejected';
$ap['approved_by'] = (int)$me['id'];
$ap['approved_at'] = now();
flash('ok', '已驳回删除申请');
break;
}
}
unset($ap);
saveDeleteApprovals($approvals);
redirectBackOr('index.php?action=delete_approvals');
}
$approvals = array_reverse(loadDeleteApprovals());
$filterStatus = (string)($_GET['status'] ?? 'pending');
$filterTarget = trim((string)($_GET['target_id'] ?? ''));
$filterRequester = trim((string)($_GET['requester'] ?? ''));
$approvals = array_values(array_filter($approvals, function ($ap) use ($filterStatus, $filterTarget, $filterRequester) {
if ($filterStatus !== 'all' && ($ap['status'] ?? '') !== $filterStatus) {
return false;
}
if ($filterTarget !== '' && (string)($ap['target_id'] ?? '') !== $filterTarget) {
return false;
}
if ($filterRequester !== '' && mb_stripos((string)($ap['requested_name'] ?? ''), $filterRequester) === false) {
return false;
}
return true;
}));
$usersMap = [];
$archiveMap = fetchArchiveSimpleMap($pdo);
foreach ($pdo->query('SELECT id,username FROM users')->fetchAll() as $u) {
$usersMap[(int)$u['id']] = (string)$u['username'];
}
pageHeader($me); ?>
删除审核(双管理员)
| 申请时间 | 类型 | 目标 | 申请人 | 状态 | 审核人 | 审核时间 | 操作 |
| = e((string)$ap['requested_at']) ?> |
= e((string)$ap['target_type']) ?> |
= e(approvalTargetLabel($ap, $archiveMap)) ?> |
= e((string)$ap['requested_name']) ?> |
= e((string)$ap['status']) ?> |
= e($usersMap[(int)$ap['approved_by']]) ?>- |
= e((string)$ap['approved_at']) ?>- |
-
|
| 暂无符合条件的审核记录 |
prepare('SELECT * FROM files WHERE id=:id');
$stmt->execute([':id' => $id]);
$f = $stmt->fetch();
if ($f) {
$p = UPLOAD_DIR . '/' . $f['stored_name'];
if (is_file($p)) {
unlink($p);
}
$pdo->prepare('DELETE FROM files WHERE id=:id')->execute([':id' => $id]);
flash('ok', '文件已删除');
header('Location: index.php?action=archive_form&id=' . (int)$f['archive_id']);
exit;
}
flash('error', '文件不存在');
header('Location: index.php');
exit;
}
if ($action === 'rename_file') {
requireAdmin();
$fileId = (int)($_POST['file_id'] ?? 0);
$newNameRaw = trim((string)($_POST['new_name'] ?? ''));
if ($fileId <= 0 || $newNameRaw === '') {
flash('error', '重命名参数错误');
header('Location: index.php');
exit;
}
$stmt = $pdo->prepare('SELECT * FROM files WHERE id=:id');
$stmt->execute([':id' => $fileId]);
$f = $stmt->fetch();
if (!$f) {
flash('error', '文件不存在');
header('Location: index.php');
exit;
}
$currentStored = (string)$f['stored_name'];
$dirPart = dirname($currentStored);
$dirPart = $dirPart === '.' ? '' : $dirPart;
$prefix = pathinfo($currentStored, PATHINFO_FILENAME);
$dotPos = strpos($prefix, '_');
$uniqPrefix = $dotPos === false ? uniqid('f_', true) : substr($prefix, 0, $dotPos);
$ext = strtolower((string)$f['file_ext']);
$newOriginal = $newNameRaw;
if (strtolower((string)pathinfo($newOriginal, PATHINFO_EXTENSION)) !== $ext) {
$newOriginal .= '.' . $ext;
}
$normalized = normalizeFileName($newOriginal);
$newBase = $uniqPrefix . '_' . $normalized;
$newStored = ($dirPart !== '' ? $dirPart . '/' : '') . $newBase;
$oldPath = UPLOAD_DIR . '/' . $currentStored;
$newPath = UPLOAD_DIR . '/' . $newStored;
if (!is_file($oldPath)) {
flash('error', '原文件不存在');
header('Location: index.php?action=archive_form&id=' . (int)$f['archive_id']);
exit;
}
if (is_file($newPath)) {
$newStored = ($dirPart !== '' ? $dirPart . '/' : '') . uniqid('f_', true) . '_' . $normalized;
$newPath = UPLOAD_DIR . '/' . $newStored;
}
if (!rename($oldPath, $newPath)) {
flash('error', '文件重命名失败');
header('Location: index.php?action=archive_form&id=' . (int)$f['archive_id']);
exit;
}
$pdo->prepare('UPDATE files SET original_name=:o, stored_name=:s WHERE id=:id')
->execute([':o' => $newOriginal, ':s' => $newStored, ':id' => $fileId]);
flash('ok', '文件已重命名');
header('Location: index.php?action=archive_form&id=' . (int)$f['archive_id']);
exit;
}
if ($action === 'download_file') {
requireLogin();
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare('SELECT * FROM files WHERE id=:id');
$stmt->execute([':id' => $id]);
$f = $stmt->fetch();
if (!$f) {
http_response_code(404);
exit('Not found');
}
$p = UPLOAD_DIR . '/' . $f['stored_name'];
if (!is_file($p)) {
http_response_code(404);
exit('File missing');
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($f['original_name']) . '"');
readfile($p);
exit;
}
if ($action === 'preview_file') {
requireLogin();
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare('SELECT * FROM files WHERE id=:id');
$stmt->execute([':id' => $id]);
$f = $stmt->fetch();
if (!$f || !canPreview($f['file_ext'])) {
http_response_code(403);
exit('不支持预览');
}
$p = UPLOAD_DIR . '/' . $f['stored_name'];
if (!is_file($p)) {
http_response_code(404);
exit('文件不存在');
}
if ($f['file_ext'] === 'pdf') {
header('Content-Type: application/pdf');
} else {
header('Content-Type: image/' . ($f['file_ext'] === 'jpg' ? 'jpeg' : $f['file_ext']));
}
readfile($p);
exit;
}
if ($action === 'archive_detail') {
requireLogin();
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare('SELECT a.*, d.name AS dir_name FROM archives a JOIN directories d ON a.directory_id=d.id WHERE a.id=:id');
$stmt->execute([':id' => $id]);
$a = $stmt->fetch();
if (!$a) {
flash('error', '档案不存在');
header('Location: index.php');
exit;
}
$planAll = loadRepaymentPlans();
$planCfg = getArchiveRepaymentPlan($planAll, $id);
$repaymentPlans = $planCfg['plans'] ?? [];
$financeAll = loadFinanceConfigs();
$financeCfg = getArchiveFinanceConfig($financeAll, $id);
$status = archiveStatusWithConfig($a['end_date'], $financeCfg);
$detailKind = normalizeArchiveKind($a['archive_kind'] ?? '');
$interestPayable = calcCurrentInterest($a, $financeCfg, $repaymentPlans);
$settlementInterestPayable = $detailKind === ARCHIVE_KIND_BANK_LOAN
? calcSettlementPeriodInterest($a, $financeCfg, $repaymentPlans, date('Y-m-d'))
: 0.0;
usort($repaymentPlans, function ($x, $y) {
return strcmp((string)($x['due_date'] ?? ''), (string)($y['due_date'] ?? ''));
});
$files = $pdo->prepare('SELECT * FROM files WHERE archive_id=:a ORDER BY id DESC');
$files->execute([':a' => $id]);
pageHeader($me); ?>
= e($a['name']) ?> (= e($a['dir_name']) ?>)
状态:= e($status) ?>
类型:= e(archiveKindLabel($detailKind)) ?>
| = $detailKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE ? '债权人' : '贷款机构' ?> | = e((string)$a['loan_institution']) ?> | 借款主体 | = e((string)$a['borrower']) ?> |
| 授信额度 | = e(amount($a['credit_limit'] !== null ? (float)$a['credit_limit'] : null)) ?> | 剩余额度 | = e(amount($a['remaining_limit'] !== null ? (float)$a['remaining_limit'] : null)) ?> |
| 起止日 | = e((string)$a['start_date']) ?> | 到期日 | = e((string)$a['end_date']) ?> |
| 年利率 | = $detailKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE ? '—' : e($a['annual_rate'] === null ? '' : ((string)$a['annual_rate'] . '%')) ?> | 担保类型 | = e((string)$a['guarantee_type']) ?> |
| 担保人 | = e((string)$a['guarantor']) ?> |
| 结息方式 |
= e($financeCfg['interest_mode'] === 'quarterly' ? '按季计息' : ($financeCfg['interest_mode'] === 'bullet' ? '到期一次性付清' : '按月计息')) ?> |
结息日 |
|
| 逾期利率上浮 | = e(amount((float)$financeCfg['overdue_float_pct'])) ?>% | 当前应付利息 | = e(amount($interestPayable)) ?> 元 |
| 结息日应付利息 | = e(amount($settlementInterestPayable)) ?> 元 (本结息周期整段) |
| 备注 | = e($detailRemark) ?> |
分期还款计划
该档案未启用分期还款计划
| 还款日期 | 还款金额 | 状态 |
| = e((string)$p['due_date']) ?> |
= e(amount((float)($p['amount'] ?? 0))) ?> |
已还款';
} elseif ($st === 'delayed') {
echo '已延期';
} elseif ($st === 'overdue') {
echo '已逾期';
} else {
echo '待处理';
}
?>
|
还款计划状态和延期调整请在“编辑档案”页面操作。
文件列表
| 文件名 | 大小 | 操作 |
| = e($f['original_name']) ?> |
= number_format(((int)$f['file_size']) / 1024, 1) ?> KB |
|
0) {
$st = $pdo->prepare('SELECT * FROM archives WHERE id=:id');
$st->execute([':id' => $id]);
$editing = $st->fetch();
if (!$editing) {
flash('error', '档案不存在');
header('Location: index.php');
exit;
}
}
$dirs = fetchDirectories($pdo);
$loanInstitutions = loadLoanInstitutions();
$dirId = (int)($_GET['dir_id'] ?? ($editing['directory_id'] ?? 0));
$dirPath = dirPathMap($dirs);
$planAll = loadRepaymentPlans();
$planCfg = $editing ? getArchiveRepaymentPlan($planAll, (int)$editing['id']) : ['enabled' => false, 'plans' => []];
$financeAll = loadFinanceConfigs();
$financeCfg = $editing ? getArchiveFinanceConfig($financeAll, (int)$editing['id']) : [
'interest_mode' => '',
'settlement_rule' => 'monthly_day',
'settlement_day' => 21,
'overdue_float_pct' => 50,
'manual_overdue' => false,
'manual_early_settled' => false,
'remark' => '',
'manual_principal_repaid' => 0.0,
'manual_interest_paid' => 0.0,
];
$planRows = $planCfg['plans'] ?? [];
if (empty($planRows)) {
for ($i = 0; $i < 6; $i++) {
$planRows[] = ['id' => '', 'due_date' => '', 'amount' => ''];
}
}
$editArchiveId = (int)($editing['id'] ?? 0);
$editFiles = [];
if ($editArchiveId > 0) {
$fs = $pdo->prepare('SELECT * FROM files WHERE archive_id=:a ORDER BY id DESC');
$fs->execute([':a' => $editArchiveId]);
$editFiles = $fs->fetchAll();
}
pageHeader($me); ?>
= $editing ? '编辑档案' : '新建档案' ?>
0): ?>
文件管理(编辑页)
| 文件名 | 大小 | 重命名 | 操作 |
| = e((string)$f['original_name']) ?> |
= number_format(((int)$f['file_size']) / 1024, 1) ?> KB |
|
|
| 暂无文件 |
20) {
flash('error', '用户名或密码格式不正确');
} else {
try {
$pdo->prepare('INSERT INTO users(username,password_hash,role,created_at) VALUES(:u,:p,:r,:c)')
->execute([':u' => $u, ':p' => password_hash($p, PASSWORD_DEFAULT), ':r' => 'user', ':c' => now()]);
flash('ok', '用户已创建');
} catch (Throwable $e) {
flash('error', '用户名已存在');
}
}
} elseif ($do === 'role') {
$uid = (int)$_POST['uid'];
$role = $_POST['role'] === 'admin' ? 'admin' : 'user';
$pdo->prepare('UPDATE users SET role=:r WHERE id=:id')->execute([':r' => $role, ':id' => $uid]);
flash('ok', '角色已更新');
} elseif ($do === 'reset') {
$uid = (int)$_POST['uid'];
$np = (string)$_POST['new_password'];
if (strlen($np) < 6 || strlen($np) > 20) {
flash('error', '密码长度需为6-20');
} else {
$pdo->prepare('UPDATE users SET password_hash=:p WHERE id=:id')->execute([':p' => password_hash($np, PASSWORD_DEFAULT), ':id' => $uid]);
flash('ok', '密码已重置');
}
} elseif ($do === 'delete') {
$uid = (int)$_POST['uid'];
if ($uid === (int)$me['id']) {
flash('error', '不能删除当前登录账号');
} else {
$pdo->prepare('DELETE FROM users WHERE id=:id')->execute([':id' => $uid]);
flash('ok', '用户已删除');
}
}
header('Location: index.php?action=users');
exit;
}
$users = $pdo->query('SELECT id,username,role,created_at FROM users ORDER BY id ASC')->fetchAll();
pageHeader($me); ?>
用户管理
| ID | 用户名 | 角色 | 创建时间 | 操作 |
| = (int)$u['id'] ?> | = e(maskUsername((string)$u['username'])) ?> |
|
= e($u['created_at']) ?> |
|
prepare('SELECT * FROM users WHERE id=:id');
$stmt->execute([':id' => (int)$me['id']]);
$u = $stmt->fetch();
if (!$u || !password_verify($cur, $u['password_hash'])) {
flash('error', '当前密码错误');
} elseif (strlen($new) < 6 || strlen($new) > 20) {
flash('error', '新密码长度需为6-20位');
} elseif ($new !== $confirm) {
flash('error', '新密码与确认密码不一致');
} else {
$pdo->prepare('UPDATE users SET password_hash=:p WHERE id=:id')->execute([':p' => password_hash($new, PASSWORD_DEFAULT), ':id' => (int)$me['id']]);
flash('ok', '密码修改成功');
}
header('Location: index.php?action=profile');
exit;
}
pageHeader($me); ?>
便捷查询
= quickQueryMenuHtml() ?>
应付本息:统计履行中及逾期中银行贷款按机构当前应付利息、本结息周期整段结息日应付利息,以及当月还款计划金额。
对外担保:按主体名称检索“该主体作为担保人”的履行中及逾期中银行贷款与对外担保档案。
查询总金额:按选项汇总履行中及逾期中档案的授信总额与剩余额度合计。
利息计算器 / 承兑贴现计算器:本地浏览器计算,结果仅供参考。
历史还款记录:汇总分期计划中「已还款」本金及状态为「已完成」档案的授信本金规模。
返回首页
query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
$planAll = loadRepaymentPlans();
$financeAll = loadFinanceConfigs();
$hr = qqBuildRepaymentHistory($rowsAll, $planAll, $financeAll);
$planRows = $hr['plan_rows'];
$cb = $hr['completed_bank'];
$cg = $hr['completed_guar'];
pageHeader($me); ?>
历史还款记录查询
汇总
| 项目 | 笔数 | 金额合计(元) |
| 分期还款计划中「已还款」本金 |
= count($planRows) ?> |
= e(amount((float)$hr['plan_paid_total'])) ?> |
| 已完成 — 银行贷款(授信本金规模) |
= (int)$cb['count'] ?> |
= e(amount((float)$cb['sum_credit'])) ?> |
| 已完成 — 对外担保(授信本金规模) |
= (int)$cg['count'] ?> |
= e(amount((float)$cg['sum_credit'])) ?> |
「已完成」指合同状态为已完成;授信本金规模为档案授信额度合计,用于对应到期/结清贷款本金规模参考。分期已还与整笔结清可能同时存在,请勿简单相加避免重复理解。
分期计划已还款明细
| 档案ID | 档案名称 | 机构 | 借款主体 | 计划到期日 | 标记还款日 | 还款金额 | 操作 |
| = (int)$pr['archive_id'] ?> |
= e((string)$pr['archive_name']) ?> |
= e((string)$pr['loan_institution']) ?> |
= e((string)$pr['borrower']) ?> |
= e((string)$pr['due_date']) ?> |
= e((string)$pr['paid_at']) ?> |
= e(amount((float)$pr['amount'])) ?> |
0): ?>查看— |
| 暂无已标记还款的分期计划 |
query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
$financeAll = loadFinanceConfigs();
$tot = qqBuildTotalsQuery($rowsAll, $financeAll, $scope, $inst !== '' ? $inst : null);
$loanInstitutions = loadLoanInstitutions();
pageHeader($me); ?>
汇总结果
| 笔数(履行+逾期) | 授信总额合计 | 剩余额度合计 |
| = (int)($tot['count'] ?? 0) ?> |
= e(amount((float)($tot['sum_credit'] ?? 0))) ?> |
= e(amount((float)($tot['sum_remaining'] ?? 0))) ?> |
统计范围含状态为「履行中」与「逾期中」的档案。授信总额、剩余额度均来自各档案字段;剩余额度含分期已还与手动归还本金后的结果。
query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
$financeAll = loadFinanceConfigs();
$planAll = loadRepaymentPlans();
$quickStats = qqBuildPayableInterestData($rowsAll, $financeAll, $planAll);
$instRows = $quickStats['instRows'];
$planRows = $quickStats['planRows'];
$planTotal = (float)($quickStats['planTotal'] ?? 0);
if ($exportFormat === 'xlsx' && class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx')) {
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet1 = $spreadsheet->getActiveSheet();
$sheet1->setTitle('机构利息汇总');
$sheet1->fromArray(['机构', '笔数(履行+逾期)', '当前应付利息', '结息日', '结息日应付利息'], null, 'A1');
$sheet1->fromArray(array_map(fn($x) => [$x['loan_institution'], $x['loan_count'], amount((float)$x['current_interest']), (string)($x['settlement_date'] ?? ''), amount((float)$x['settlement_interest'])], $instRows), null, 'A2');
foreach (range('A', 'E') as $col) { $sheet1->getColumnDimension($col)->setWidth(24); }
$sheet2 = $spreadsheet->createSheet();
$sheet2->setTitle('当月还款计划');
$sheet2->fromArray(['机构', '档案名称', '借款主体', '到期日', '金额', '状态'], null, 'A1');
$sheet2->fromArray(array_map(fn($x) => [$x['loan_institution'], $x['archive_name'], $x['borrower'], $x['due_date'], amount((float)$x['amount']), $x['status']], $planRows), null, 'A2');
$sheet2->setCellValue('A' . (count($planRows) + 3), '合计');
$sheet2->setCellValue('E' . (count($planRows) + 3), amount($planTotal));
foreach (range('A', 'F') as $col) { $sheet2->getColumnDimension($col)->setWidth(22); }
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="' . rawurlencode('应付本息查询_' . date('Y-m-d') . '.xlsx') . '"');
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save('php://output');
exit;
}
pageHeader($me); ?>
一、银行贷款机构利息统计(履行中及逾期中)
| 机构 | 笔数 | 当前应付利息 | 结息日 | 结息日应付利息 |
| = e((string)$r['loan_institution']) ?> |
= (int)$r['loan_count'] ?> |
= e(amount((float)$r['current_interest'])) ?> |
= e((string)($r['settlement_date'] ?? '')) ?> |
= e(amount((float)$r['settlement_interest'])) ?> |
| 合计 | - | = e(amount($sumCur)) ?> | - | = e(amount($sumSettle)) ?> |
| 暂无数据 |
二、当月还款计划金额
| 机构 | 档案名称 | 借款主体 | 到期日 | 金额 | 状态 |
| = e((string)$r['loan_institution']) ?> |
= e((string)$r['archive_name']) ?> |
= e((string)$r['borrower']) ?> |
= e((string)$r['due_date']) ?> |
= e(amount((float)$r['amount'])) ?> |
= e((string)$r['status']) ?> |
| 合计 | - | = e(amount($planTotal)) ?> | - |
| 暂无当月还款计划 |
query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
$financeAll = loadFinanceConfigs();
$resultRows = array_values(array_filter(qqSearchGuarantorArchives($rows, $kw), function ($r) use ($financeAll) {
return archiveStatusIsOngoingAggregate((string)($r['end_date'] ?? ''), getArchiveFinanceConfig($financeAll, (int)$r['id']));
}));
if ($format === 'xlsx' && $kw !== '' && class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx')) {
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('对外担保查询');
$sheet->fromArray(['档案类型', '档案名称', '贷款机构/债权人', '借款主体', '担保人', '授信额度', '剩余额度', '到期日', '状态'], null, 'A1');
$sheet->fromArray(array_map(function ($r) use ($financeAll) {
return [
archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))),
(string)($r['name'] ?? ''),
(string)($r['loan_institution'] ?? ''),
(string)($r['borrower'] ?? ''),
(string)($r['guarantor'] ?? ''),
amount($r['credit_limit'] !== null ? (float)$r['credit_limit'] : null),
amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null),
(string)($r['end_date'] ?? ''),
archiveStatusWithConfig((string)($r['end_date'] ?? ''), getArchiveFinanceConfig($financeAll, (int)$r['id'])),
];
}, $resultRows), null, 'A2');
foreach (range('A', 'I') as $col) { $sheet->getColumnDimension($col)->setWidth(18); }
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="' . rawurlencode('对外担保查询_' . date('Y-m-d') . '.xlsx') . '"');
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save('php://output');
exit;
}
pageHeader($me); ?>
对外担保查询
仅列出状态为「履行中」或「逾期中」的档案。
| 档案类型 | 档案名称 | 贷款机构/债权人 | 借款主体 | 担保人 | 授信额度 | 剩余额度 | 到期日 | 状态 |
| = e(archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? '')))) ?> |
= e((string)($r['name'] ?? '')) ?> |
= e((string)($r['loan_institution'] ?? '')) ?> |
= e((string)($r['borrower'] ?? '')) ?> |
= e((string)($r['guarantor'] ?? '')) ?> |
= e(amount($r['credit_limit'] !== null ? (float)$r['credit_limit'] : null)) ?> |
= e(amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null)) ?> |
= e((string)($r['end_date'] ?? '')) ?> |
= e($st) ?> |
| = $kw === '' ? '请输入主体名称后查询' : '未命中履行中或逾期中的相关档案(已完成等已排除)' ?> |
0) {
$dirFilter = collectSubDirIds($dirs, $dirId);
}
$rows = $pdo->query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
$financeAll = loadFinanceConfigs();
$rows = array_values(array_filter($rows, function ($r) use ($mode, $selected, $dirFilter, $financeAll, $kindFilter) {
if ($dirFilter && !in_array((int)$r['directory_id'], $dirFilter, true)) {
return false;
}
$rowKind = normalizeArchiveKind((string)($r['archive_kind'] ?? ''));
if ($kindFilter !== 'all' && $rowKind !== $kindFilter) {
return false;
}
$status = archiveStatusWithConfig($r['end_date'], getArchiveFinanceConfig($financeAll, (int)$r['id']));
if ($mode === 'selected' && !in_array((int)$r['id'], $selected, true)) {
return false;
}
return true;
}));
$modeName = ['all' => '全部', 'selected' => '选中'][$mode] ?? '全部';
$headers = ['档案类型', '贷款机构', '借款主体', '授信额度', '剩余额度', '起止日', '到期日', '年利率', '担保类型', '担保人', '合同状态'];
$dataRows = [];
foreach ($rows as $r) {
$dataRows[] = [
archiveKindLabel(normalizeArchiveKind($r['archive_kind'] ?? '')),
$r['loan_institution'],
$r['borrower'],
amount($r['credit_limit'] !== null ? (float)$r['credit_limit'] : null),
amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null),
$r['start_date'],
$r['end_date'],
$r['annual_rate'] === null ? '' : ((string)$r['annual_rate'] . '%'),
$r['guarantee_type'],
$r['guarantor'],
archiveStatusWithConfig($r['end_date'], getArchiveFinanceConfig($financeAll, (int)$r['id'])),
];
}
$xlsxEnabled = class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx');
if ($format === 'xlsx' && $xlsxEnabled) {
$filename = '档案汇总_' . $modeName . '_' . date('Y-m-d') . '.xlsx';
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray($headers, null, 'A1');
$sheet->fromArray($dataRows, null, 'A2');
foreach (range('A', 'K') as $col) {
$sheet->getColumnDimension($col)->setWidth(16);
}
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save('php://output');
exit;
}
$filename = '档案汇总_' . $modeName . '_' . date('Y-m-d') . '.csv';
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'wb');
fputcsv($out, $headers);
foreach ($dataRows as $row) {
fputcsv($out, $row);
}
fclose($out);
exit;
}
$dirs = fetchDirectories($pdo);
$dirId = (int)($_GET['dir_id'] ?? 0);
$q = trim((string)($_GET['q'] ?? ''));
$searchField = $_GET['search_field'] ?? 'all';
$statusFilter = $_GET['status'] ?? 'not_done';
$kindFilter = (string)($_GET['kind'] ?? ARCHIVE_KIND_BANK_LOAN);
if (!in_array($kindFilter, ['all', ARCHIVE_KIND_BANK_LOAN, ARCHIVE_KIND_EXTERNAL_GUARANTEE], true)) {
$kindFilter = ARCHIVE_KIND_BANK_LOAN;
}
$sortBy = $_GET['sort_by'] ?? 'end_date';
$sortOrder = strtolower((string)($_GET['sort_order'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';
$rows = $pdo->query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
$financeAllGlobal = loadFinanceConfigs();
$dirPath = dirPathMap($dirs);
$dirScope = [];
if ($dirId > 0) {
$dirScope = collectSubDirIds($dirs, $dirId);
}
$archives = array_values(array_filter($rows, function ($r) use ($q, $statusFilter, $dirScope, $searchField, $financeAllGlobal, $kindFilter) {
if ($dirScope && !in_array((int)$r['directory_id'], $dirScope, true)) {
return false;
}
$rowKind = normalizeArchiveKind((string)($r['archive_kind'] ?? ''));
if ($kindFilter !== 'all' && $rowKind !== $kindFilter) {
return false;
}
$status = archiveStatusWithConfig($r['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$r['id']));
if ($statusFilter === 'not_done' && $status === '已完成') {
return false;
}
if (!in_array($statusFilter, ['all', 'not_done'], true) && $statusFilter !== $status) {
return false;
}
if ($q !== '') {
if (!in_array($searchField, ['all', 'borrower', 'guarantor'], true)) {
$searchField = 'all';
}
if ($searchField === 'borrower') {
$hay = mb_strtolower((string)$r['borrower']);
} elseif ($searchField === 'guarantor') {
$hay = mb_strtolower((string)$r['guarantor']);
} else {
$hay = mb_strtolower(implode(' ', [
$r['name'],
$r['loan_institution'],
$r['borrower'],
$r['guarantee_type'],
$r['guarantor'],
]));
}
if (mb_strpos($hay, mb_strtolower($q)) === false) {
return false;
}
}
return true;
}));
if (!in_array($sortBy, ['end_date', 'created_at', 'loan_institution'], true)) {
$sortBy = 'end_date';
}
usort($archives, function ($a, $b) use ($sortBy, $sortOrder, $financeAllGlobal) {
$sa = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id']));
$sb = archiveStatusWithConfig($b['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$b['id']));
$w = ['履行中' => 0, '逾期中' => 1, '待更新' => 2, '已完成' => 3];
$sw = ($w[$sa] ?? 9) <=> ($w[$sb] ?? 9);
if ($sw !== 0) {
return $sw;
}
$av = (string)($a[$sortBy] ?? '');
$bv = (string)($b[$sortBy] ?? '');
if ($sortBy === 'loan_institution') {
$av = mb_strtolower($av);
$bv = mb_strtolower($bv);
}
$cmp = $av <=> $bv;
if ($cmp === 0) {
$cmp = ((int)$a['id']) <=> ((int)$b['id']);
}
return $sortOrder === 'asc' ? $cmp : -$cmp;
});
$totalCount = count($archives);
$activeCount = 0;
$doneCount = 0;
foreach ($archives as $item) {
$status = archiveStatusWithConfig($item['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$item['id']));
if ($status === '履行中') {
$activeCount++;
} elseif ($status === '已完成') {
$doneCount++;
} elseif ($status === '逾期中') {
$activeCount++;
}
}
$today = date('Y-m-d');
$day60 = date('Y-m-d', strtotime('+60 days'));
$dueSoonLoans = array_values(array_filter($archives, function ($a) use ($today, $day60, $financeAllGlobal) {
if (empty($a['end_date'])) {
return false;
}
$status = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id']));
if ($status === '已完成') {
return false;
}
return $a['end_date'] >= $today && $a['end_date'] <= $day60;
}));
$dueSoonCount = count($dueSoonLoans);
$dueSoonLoans = array_slice($dueSoonLoans, 0, 8);
$overdueLoans = array_values(array_filter($archives, function ($a) use ($financeAllGlobal) {
$status = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id']));
return $status === '逾期中';
}));
$overdueLoanCount = count($overdueLoans);
$overdueLoans = array_slice($overdueLoans, 0, 8);
$repayPlanSoon = [];
$overduePlanRows = [];
$planAllForReminder = loadRepaymentPlans();
foreach ($planAllForReminder as $aid => $cfg) {
if (empty($cfg['enabled']) || empty($cfg['plans']) || !is_array($cfg['plans'])) {
continue;
}
$aidInt = (int)$aid;
$arc = null;
foreach ($archives as $tmpArc) {
if ((int)$tmpArc['id'] === $aidInt) {
$arc = $tmpArc;
break;
}
}
if (!$arc) {
continue;
}
$day30 = date('Y-m-d', strtotime('+30 days'));
foreach ($cfg['plans'] as $p) {
$status = (string)($p['status'] ?? 'pending');
$d = (string)($p['due_date'] ?? '');
if ($status === 'paid' || $d === '') {
continue;
}
if ($d >= $today && $d <= $day30) {
$repayPlanSoon[] = [
'archive_id' => $aidInt,
'plan_id' => (string)($p['id'] ?? ''),
'archive_kind' => normalizeArchiveKind((string)($arc['archive_kind'] ?? '')),
'loan_institution' => $arc['loan_institution'] ?? '',
'borrower' => $arc['borrower'] ?? '',
'amount' => (float)($p['amount'] ?? 0),
'due_date' => $d,
];
}
if ($status === 'overdue' && $d <= $today) {
$overduePlanRows[] = [
'archive_id' => $aidInt,
'plan_id' => (string)($p['id'] ?? ''),
'archive_kind' => normalizeArchiveKind((string)($arc['archive_kind'] ?? '')),
'loan_institution' => $arc['loan_institution'] ?? '',
'borrower' => $arc['borrower'] ?? '',
'amount' => (float)($p['amount'] ?? 0),
'due_date' => $d,
];
}
}
}
$repaySoonCount = count($repayPlanSoon);
$repayPlanSoon = array_slice($repayPlanSoon, 0, 8);
$overdueCount = $overdueLoanCount + count($overduePlanRows);
$pendingUpdateRows = [];
foreach ($archives as $a) {
$aid = (int)$a['id'];
$end = (string)($a['end_date'] ?? '');
if ($end === '' || $end >= $today) {
continue;
}
if (archiveStatusWithConfig($end, getArchiveFinanceConfig($financeAllGlobal, $aid)) !== '待更新') {
continue;
}
$pendingUpdateRows[] = [
'row_kind' => 'loan',
'archive_id' => $aid,
'type_label' => '贷款到期',
'loan_institution' => (string)($a['loan_institution'] ?? ''),
'borrower' => (string)($a['borrower'] ?? ''),
'guarantor' => (string)($a['guarantor'] ?? ''),
'amount' => $a['remaining_limit'] !== null ? (float)$a['remaining_limit'] : null,
'date' => $end,
'plan_id' => '',
];
}
$archiveIdsInScope = [];
foreach ($archives as $a) {
$archiveIdsInScope[(int)$a['id']] = true;
}
foreach ($planAllForReminder as $aidStr => $cfg) {
$aidInt = (int)$aidStr;
if (empty($archiveIdsInScope[$aidInt])) {
continue;
}
if (empty($cfg['enabled']) || empty($cfg['plans']) || !is_array($cfg['plans'])) {
continue;
}
$arc = null;
foreach ($archives as $tmp) {
if ((int)$tmp['id'] === $aidInt) {
$arc = $tmp;
break;
}
}
if (!$arc) {
continue;
}
foreach ($cfg['plans'] as $p) {
$pst = (string)($p['status'] ?? 'pending');
$d = (string)($p['due_date'] ?? '');
if ($d === '' || $d >= $today) {
continue;
}
if (!in_array($pst, ['pending', 'delayed'], true)) {
continue;
}
$pendingUpdateRows[] = [
'row_kind' => 'plan',
'archive_id' => $aidInt,
'type_label' => '还款计划到期',
'loan_institution' => (string)($arc['loan_institution'] ?? ''),
'borrower' => (string)($arc['borrower'] ?? ''),
'guarantor' => (string)($arc['guarantor'] ?? ''),
'amount' => (float)($p['amount'] ?? 0),
'date' => $d,
'plan_id' => (string)($p['id'] ?? ''),
];
}
}
usort($pendingUpdateRows, function ($x, $y) {
$c = strcmp((string)($x['date'] ?? ''), (string)($y['date'] ?? ''));
if ($c !== 0) {
return $c;
}
return ((int)($x['archive_id'] ?? 0)) <=> ((int)($y['archive_id'] ?? 0));
});
$pendingUpdateCount = count($pendingUpdateRows);
if ($action === 'pending_updates') {
requireLogin();
$puBack = 'index.php';
if ($dirId > 0) {
$puBack .= '?dir_id=' . $dirId;
}
pageHeader($me); ?>
待更新详情
以下为已过到期日且尚未在系统中标记结果的贷款与还款计划。处理后将从本列表移除。
← 返回首页
| 类型 | 贷款机构 | 借款主体 | 担保人 | 金额 | 日期 | 操作 |
| = e((string)($r['type_label'] ?? '')) ?> |
= e((string)($r['loan_institution'] ?? '')) ?> |
= e((string)($r['borrower'] ?? '')) ?> |
= e((string)($r['guarantor'] ?? '')) ?> |
= e(amount(isset($r['amount']) && $r['amount'] !== null ? (float)$r['amount'] : null)) ?> |
= e((string)($r['date'] ?? '')) ?> |
查看档案
|
| 暂无待更新项 |
'贷款到期(60天)', 'archive_id' => (int)$r['id'], 'plan_id' => '', 'archive_kind' => archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))), 'loan_institution' => (string)$r['loan_institution'], 'borrower' => (string)$r['borrower'], 'amount' => amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null), 'date' => (string)$r['end_date']];
}
foreach ($repayPlanSoon as $r) {
$allReminderRows[] = ['type' => '还款计划(30天)', 'archive_id' => (int)$r['archive_id'], 'plan_id' => (string)$r['plan_id'], 'archive_kind' => archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))), 'loan_institution' => (string)$r['loan_institution'], 'borrower' => (string)$r['borrower'], 'amount' => amount((float)$r['amount']), 'date' => (string)$r['due_date']];
}
foreach ($overdueLoans as $r) {
$allReminderRows[] = ['type' => '逾期中', 'archive_id' => (int)$r['id'], 'plan_id' => '', 'archive_kind' => archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))), 'loan_institution' => (string)$r['loan_institution'], 'borrower' => (string)$r['borrower'], 'amount' => amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null), 'date' => (string)$r['end_date']];
}
foreach ($overduePlanRows as $r) {
$allReminderRows[] = ['type' => '逾期中', 'archive_id' => (int)$r['archive_id'], 'plan_id' => (string)$r['plan_id'], 'archive_kind' => archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))), 'loan_institution' => (string)$r['loan_institution'], 'borrower' => (string)$r['borrower'], 'amount' => amount((float)$r['amount']), 'date' => (string)$r['due_date']];
}
$rows = array_values(array_filter($allReminderRows, function ($row) use ($reminderType) {
if ($reminderType === 'all') return true;
if ($reminderType === 'due') return $row['type'] === '贷款到期(60天)';
if ($reminderType === 'plan') return $row['type'] === '还款计划(30天)';
if ($reminderType === 'overdue') return $row['type'] === '逾期中';
return true;
}));
if ($exportFormat !== '') {
$headers = ['类型', '档案类型', '贷款机构', '借款主体', '金额', '日期'];
if ($exportFormat === 'xlsx' && class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx')) {
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray($headers, null, 'A1');
$sheet->fromArray(array_map(fn($x) => [$x['type'], $x['archive_kind'], $x['loan_institution'], $x['borrower'], $x['amount'], $x['date']], $rows), null, 'A2');
foreach (range('A', 'F') as $col) { $sheet->getColumnDimension($col)->setWidth(20); }
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="' . rawurlencode('提醒明细_' . date('Y-m-d') . '.xlsx') . '"');
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save('php://output');
exit;
}
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . rawurlencode('提醒明细_' . date('Y-m-d') . '.csv') . '"');
echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'wb');
fputcsv($out, $headers);
foreach ($rows as $r) { fputcsv($out, [$r['type'], $r['archive_kind'], $r['loan_institution'], $r['borrower'], $r['amount'], $r['date']]); }
fclose($out);
exit;
}
pageHeader($me); ?>
到期提醒详情
| 类型 | 档案类型 | 贷款机构 | 借款主体 | 金额 | 日期 | 操作 |
| = e($r['type']) ?> |
= e((string)($r['archive_kind'] ?? '')) ?> |
= e($r['loan_institution']) ?> |
= e($r['borrower']) ?> |
= e($r['amount']) ?> |
= e($r['date']) ?> |
0): ?>
查看档案
0 && $r['type'] === '贷款到期(60天)' && isAdmin()): ?>
0 && $r['type'] === '还款计划(30天)' && isAdmin()): ?>
0 && $r['type'] === '逾期中' && isAdmin()): ?>
-
|
| 暂无提醒内容 |
$totalPages) {
$currentPage = $totalPages;
}
$offset = ($currentPage - 1) * $perPage;
$archivesPage = array_slice($archives, $offset, $perPage);
$baseQuery = ['q' => $q, 'search_field' => $searchField, 'status' => $statusFilter, 'kind' => $kindFilter, 'per_page' => $perPage, 'sort_by' => $sortBy, 'sort_order' => $sortOrder];
if ($dirId) {
$baseQuery['dir_id'] = $dirId;
}
unset($baseQuery['page']);
$pathIds = collectAncestorIds($dirs, $dirId ?: null);
pageHeader($me);
?>
= buildTreeHtml($dirs, $dirId ?: null, isAdmin(), $pathIds) ?>
便捷查询
= quickQueryMenuHtml() ?>