Initial commit: 金牛集团贷款档案管理系统
导入现有代码与文档,排除 data/uploads 运行时数据。
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
# 运行时数据与上传(勿入库)
|
||||
/data/
|
||||
/uploads/
|
||||
!/data/.gitkeep
|
||||
!/uploads/.gitkeep
|
||||
|
||||
# Composer
|
||||
/vendor/
|
||||
composer.lock
|
||||
|
||||
# 本地与 IDE
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
.idea/
|
||||
.vscode/
|
||||
*.log
|
||||
|
||||
# 临时与备份
|
||||
*.bak
|
||||
*.tmp
|
||||
/tmp/
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
function currentUser(): ?array
|
||||
{
|
||||
if (empty($_SESSION['uid'])) {
|
||||
return null;
|
||||
}
|
||||
$stmt = db()->prepare('SELECT id, username, role, created_at FROM users WHERE id = :id');
|
||||
$stmt->execute([':id' => (int)$_SESSION['uid']]);
|
||||
$u = $stmt->fetch();
|
||||
return $u ?: null;
|
||||
}
|
||||
|
||||
function isAdmin(): bool
|
||||
{
|
||||
$u = currentUser();
|
||||
return $u && $u['role'] === 'admin';
|
||||
}
|
||||
|
||||
function requireLogin(): void
|
||||
{
|
||||
if (!currentUser()) {
|
||||
header('Location: index.php?action=login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdmin(): void
|
||||
{
|
||||
requireLogin();
|
||||
if (!isAdmin()) {
|
||||
http_response_code(403);
|
||||
echo 'Forbidden';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
const DB_PATH = __DIR__ . '/../data/archive.sqlite';
|
||||
const UPLOAD_DIR = __DIR__ . '/../uploads';
|
||||
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
||||
const ALLOWED_EXTENSIONS = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx'];
|
||||
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
mb_internal_encoding('UTF-8');
|
||||
|
||||
if (!is_dir(UPLOAD_DIR)) {
|
||||
mkdir(UPLOAD_DIR, 0775, true);
|
||||
}
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
function db(): PDO
|
||||
{
|
||||
static $pdo = null;
|
||||
if ($pdo instanceof PDO) {
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
$pdo = new PDO('sqlite:' . DB_PATH);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$pdo->exec('PRAGMA foreign_keys = ON');
|
||||
|
||||
initSchema($pdo);
|
||||
seedAdmin($pdo);
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function initSchema(PDO $pdo): void
|
||||
{
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK(role IN ('admin','user')),
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
");
|
||||
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS directories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
parent_id INTEGER NULL,
|
||||
level INTEGER NOT NULL CHECK(level BETWEEN 1 AND 3),
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(parent_id) REFERENCES directories(id) ON DELETE CASCADE
|
||||
);
|
||||
");
|
||||
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS archives (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
directory_id INTEGER NOT NULL,
|
||||
loan_institution TEXT,
|
||||
borrower TEXT,
|
||||
credit_limit REAL,
|
||||
remaining_limit REAL,
|
||||
start_date TEXT,
|
||||
end_date TEXT,
|
||||
annual_rate REAL,
|
||||
guarantee_type TEXT,
|
||||
guarantor TEXT,
|
||||
archive_kind TEXT NOT NULL DEFAULT 'bank_loan',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(directory_id) REFERENCES directories(id) ON DELETE CASCADE
|
||||
);
|
||||
");
|
||||
|
||||
$hasArchiveKind = false;
|
||||
foreach ($pdo->query('PRAGMA table_info(archives)')->fetchAll(PDO::FETCH_ASSOC) as $col) {
|
||||
if (($col['name'] ?? '') === 'archive_kind') {
|
||||
$hasArchiveKind = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$hasArchiveKind) {
|
||||
$pdo->exec("ALTER TABLE archives ADD COLUMN archive_kind TEXT NOT NULL DEFAULT 'bank_loan'");
|
||||
}
|
||||
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
archive_id INTEGER NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
stored_name TEXT NOT NULL,
|
||||
file_ext TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(archive_id) REFERENCES archives(id) ON DELETE CASCADE
|
||||
);
|
||||
");
|
||||
|
||||
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_archives_end_date ON archives(end_date)");
|
||||
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_archives_loan_inst ON archives(loan_institution)");
|
||||
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_archives_borrower ON archives(borrower)");
|
||||
}
|
||||
|
||||
function seedAdmin(PDO $pdo): void
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE username = :username');
|
||||
$stmt->execute([':username' => 'admin']);
|
||||
if ((int)$stmt->fetchColumn() > 0) {
|
||||
return;
|
||||
}
|
||||
$stmt = $pdo->prepare('INSERT INTO users(username,password_hash,role,created_at) VALUES(:u,:p,:r,:c)');
|
||||
$stmt->execute([
|
||||
':u' => 'admin',
|
||||
':p' => password_hash('jinniu123', PASSWORD_DEFAULT),
|
||||
':r' => 'admin',
|
||||
':c' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function e(string $v): string
|
||||
{
|
||||
return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function now(): string
|
||||
{
|
||||
return date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
function flash(string $type, string $msg): void
|
||||
{
|
||||
$_SESSION['flash'] = ['type' => $type, 'msg' => $msg];
|
||||
}
|
||||
|
||||
function getFlash(): ?array
|
||||
{
|
||||
if (!isset($_SESSION['flash'])) {
|
||||
return null;
|
||||
}
|
||||
$f = $_SESSION['flash'];
|
||||
unset($_SESSION['flash']);
|
||||
return $f;
|
||||
}
|
||||
|
||||
function normalizeFileName(string $name): string
|
||||
{
|
||||
$name = strtolower($name);
|
||||
$name = preg_replace('/[^a-z0-9\.]+/', '_', $name);
|
||||
return trim((string)$name, '_');
|
||||
}
|
||||
|
||||
function archiveStatus(?string $endDate): string
|
||||
{
|
||||
if (!$endDate) {
|
||||
return '待更新';
|
||||
}
|
||||
$today = date('Y-m-d');
|
||||
if ($endDate >= $today) {
|
||||
return '履行中';
|
||||
}
|
||||
return '待更新';
|
||||
}
|
||||
|
||||
function canPreview(string $ext): bool
|
||||
{
|
||||
return in_array(strtolower($ext), ['pdf', 'jpg', 'jpeg', 'png', 'gif'], true);
|
||||
}
|
||||
|
||||
function amount(?float $v): string
|
||||
{
|
||||
if ($v === null) {
|
||||
return '';
|
||||
}
|
||||
return number_format($v, 2, '.', '');
|
||||
}
|
||||
|
||||
function dirPathMap(array $dirs): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($dirs as $d) {
|
||||
$map[(int)$d['id']] = $d;
|
||||
}
|
||||
$path = [];
|
||||
foreach ($dirs as $d) {
|
||||
$id = (int)$d['id'];
|
||||
$parts = [$d['name']];
|
||||
$p = $d['parent_id'];
|
||||
while ($p && isset($map[(int)$p])) {
|
||||
$parts[] = $map[(int)$p]['name'];
|
||||
$p = $map[(int)$p]['parent_id'];
|
||||
}
|
||||
$path[$id] = implode(' / ', array_reverse($parts));
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
function collectSubDirIds(array $dirs, int $rootId): array
|
||||
{
|
||||
$children = [];
|
||||
foreach ($dirs as $d) {
|
||||
$pid = $d['parent_id'] === null ? null : (int)$d['parent_id'];
|
||||
if ($pid !== null) {
|
||||
$children[$pid][] = (int)$d['id'];
|
||||
}
|
||||
}
|
||||
$result = [$rootId];
|
||||
$queue = [$rootId];
|
||||
while ($queue) {
|
||||
$cur = array_shift($queue);
|
||||
foreach ($children[$cur] ?? [] as $cid) {
|
||||
$result[] = $cid;
|
||||
$queue[] = $cid;
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($result));
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function qqBuildPayableInterestData(array $rowsAll, array $financeAll, array $planAll): array
|
||||
{
|
||||
$todayQ = date('Y-m-d');
|
||||
$monthStart = date('Y-m-01');
|
||||
$monthEnd = date('Y-m-t');
|
||||
$archiveMap = [];
|
||||
foreach ($rowsAll as $row) {
|
||||
$archiveMap[(int)($row['id'] ?? 0)] = $row;
|
||||
}
|
||||
|
||||
$bankActive = array_values(array_filter($rowsAll, function ($a) use ($financeAll) {
|
||||
$kind = normalizeArchiveKind((string)($a['archive_kind'] ?? ''));
|
||||
if ($kind !== ARCHIVE_KIND_BANK_LOAN) {
|
||||
return false;
|
||||
}
|
||||
return archiveStatusIsOngoingAggregate((string)($a['end_date'] ?? ''), getArchiveFinanceConfig($financeAll, (int)$a['id']));
|
||||
}));
|
||||
|
||||
$instStats = [];
|
||||
foreach ($bankActive as $a) {
|
||||
$aid = (int)$a['id'];
|
||||
$inst = (string)($a['loan_institution'] ?? '');
|
||||
if ($inst === '') {
|
||||
$inst = '未填写机构';
|
||||
}
|
||||
if (!isset($instStats[$inst])) {
|
||||
$instStats[$inst] = ['loan_count' => 0, 'current_interest' => 0.0, 'settlement_interest' => 0.0, 'settlement_dates' => []];
|
||||
}
|
||||
$cfg = getArchiveFinanceConfig($financeAll, $aid);
|
||||
$plans = (array)((getArchiveRepaymentPlan($planAll, $aid))['plans'] ?? []);
|
||||
$mode = (string)($cfg['interest_mode'] ?? 'monthly');
|
||||
$nextSettle = $mode === 'bullet'
|
||||
? ((string)($a['end_date'] ?? '') >= $todayQ ? (string)$a['end_date'] : null)
|
||||
: nextSettlementDate($todayQ, $mode, (int)($cfg['settlement_day'] ?? 21));
|
||||
$instStats[$inst]['loan_count']++;
|
||||
$instStats[$inst]['current_interest'] += calcCurrentInterest($a, $cfg, $plans);
|
||||
if (!empty($nextSettle)) {
|
||||
$instStats[$inst]['settlement_interest'] += calcSettlementPeriodInterest($a, $cfg, $plans, $todayQ);
|
||||
$instStats[$inst]['settlement_dates'][(string)$nextSettle] = true;
|
||||
}
|
||||
}
|
||||
ksort($instStats);
|
||||
$instRows = [];
|
||||
foreach ($instStats as $inst => $s) {
|
||||
$instRows[] = [
|
||||
'loan_institution' => $inst,
|
||||
'loan_count' => (int)$s['loan_count'],
|
||||
'current_interest' => round((float)$s['current_interest'], 2),
|
||||
'settlement_interest' => round((float)$s['settlement_interest'], 2),
|
||||
'settlement_date' => implode(' / ', array_keys((array)$s['settlement_dates'])),
|
||||
];
|
||||
}
|
||||
|
||||
$planRows = [];
|
||||
$planTotal = 0.0;
|
||||
foreach ($planAll as $aid => $cfg) {
|
||||
if (empty($cfg['enabled']) || empty($cfg['plans']) || !is_array($cfg['plans'])) {
|
||||
continue;
|
||||
}
|
||||
$aidInt = (int)$aid;
|
||||
$arc = $archiveMap[$aidInt] ?? null;
|
||||
if (!$arc) {
|
||||
continue;
|
||||
}
|
||||
foreach ((array)$cfg['plans'] as $p) {
|
||||
$status = (string)($p['status'] ?? 'pending');
|
||||
$due = (string)($p['due_date'] ?? '');
|
||||
if ($due === '' || $due < $monthStart || $due > $monthEnd || $status === 'paid') {
|
||||
continue;
|
||||
}
|
||||
$inst = (string)($arc['loan_institution'] ?? '');
|
||||
if ($inst === '') {
|
||||
$inst = '未填写机构';
|
||||
}
|
||||
$amt = (float)($p['amount'] ?? 0);
|
||||
$planRows[] = [
|
||||
'loan_institution' => $inst,
|
||||
'archive_name' => (string)($arc['name'] ?? ''),
|
||||
'borrower' => (string)($arc['borrower'] ?? ''),
|
||||
'due_date' => $due,
|
||||
'amount' => $amt,
|
||||
'status' => $status,
|
||||
];
|
||||
$planTotal += $amt;
|
||||
}
|
||||
}
|
||||
usort($planRows, function ($a, $b) {
|
||||
$cmp = strcmp((string)$a['due_date'], (string)$b['due_date']);
|
||||
if ($cmp !== 0) {
|
||||
return $cmp;
|
||||
}
|
||||
return strcmp((string)$a['loan_institution'], (string)$b['loan_institution']);
|
||||
});
|
||||
return ['instRows' => $instRows, 'planRows' => $planRows, 'planTotal' => round($planTotal, 2)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $scope all_bank|all_guar|bank_inst|guar_inst
|
||||
* @param string|null $inst 与档案「贷款机构/债权人」完全一致;后两项 scope 必填
|
||||
* @return array{count:int,sum_credit:float,sum_remaining:float}
|
||||
*/
|
||||
function qqBuildTotalsQuery(array $rowsAll, array $financeAll, string $scope, ?string $inst): array
|
||||
{
|
||||
if (($scope === 'bank_inst' || $scope === 'guar_inst') && ($inst === null || trim($inst) === '')) {
|
||||
return ['count' => 0, 'sum_credit' => 0.0, 'sum_remaining' => 0.0];
|
||||
}
|
||||
$instNorm = $inst !== null ? trim($inst) : '';
|
||||
$filtered = array_filter($rowsAll, function ($a) use ($financeAll, $scope, $instNorm): bool {
|
||||
if (!archiveStatusIsOngoingAggregate((string)($a['end_date'] ?? ''), getArchiveFinanceConfig($financeAll, (int)$a['id']))) {
|
||||
return false;
|
||||
}
|
||||
$kind = normalizeArchiveKind((string)($a['archive_kind'] ?? ''));
|
||||
if ($scope === 'all_bank') {
|
||||
return $kind === ARCHIVE_KIND_BANK_LOAN;
|
||||
}
|
||||
if ($scope === 'all_guar') {
|
||||
return $kind === ARCHIVE_KIND_EXTERNAL_GUARANTEE;
|
||||
}
|
||||
$rowInst = (string)($a['loan_institution'] ?? '');
|
||||
if ($scope === 'bank_inst') {
|
||||
return $kind === ARCHIVE_KIND_BANK_LOAN && $rowInst === $instNorm;
|
||||
}
|
||||
if ($scope === 'guar_inst') {
|
||||
return $kind === ARCHIVE_KIND_EXTERNAL_GUARANTEE && $rowInst === $instNorm;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
$sumCredit = 0.0;
|
||||
$sumRemaining = 0.0;
|
||||
$count = 0;
|
||||
foreach ($filtered as $a) {
|
||||
$count++;
|
||||
if (isset($a['credit_limit']) && $a['credit_limit'] !== null && $a['credit_limit'] !== '') {
|
||||
$sumCredit += (float)$a['credit_limit'];
|
||||
}
|
||||
if (isset($a['remaining_limit']) && $a['remaining_limit'] !== null && $a['remaining_limit'] !== '') {
|
||||
$sumRemaining += (float)$a['remaining_limit'];
|
||||
}
|
||||
}
|
||||
return [
|
||||
'count' => $count,
|
||||
'sum_credit' => round($sumCredit, 2),
|
||||
'sum_remaining' => round($sumRemaining, 2),
|
||||
];
|
||||
}
|
||||
|
||||
function qqSearchGuarantorArchives(array $rows, string $kw): array
|
||||
{
|
||||
return array_values(array_filter($rows, function ($r) use ($kw) {
|
||||
if ($kw === '') {
|
||||
return false;
|
||||
}
|
||||
return mb_stripos((string)($r['guarantor'] ?? ''), $kw) !== false;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史还款:分期计划已还本金明细 + 已完成档案授信合计(按类型)
|
||||
*
|
||||
* @return array{plan_rows: list<array>, plan_paid_total: float, completed_bank: array{count:int,sum_credit:float}, completed_guar: array{count:int,sum_credit:float}}
|
||||
*/
|
||||
function qqBuildRepaymentHistory(array $rowsAll, array $planAll, array $financeAll): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($rowsAll as $r) {
|
||||
$map[(int)($r['id'] ?? 0)] = $r;
|
||||
}
|
||||
$planRows = [];
|
||||
$planPaidTotal = 0.0;
|
||||
foreach ($planAll as $aidStr => $cfg) {
|
||||
$aid = (int)$aidStr;
|
||||
if (empty($cfg['enabled']) || empty($cfg['plans']) || !is_array($cfg['plans'])) {
|
||||
continue;
|
||||
}
|
||||
$arc = $map[$aid] ?? null;
|
||||
foreach ($cfg['plans'] as $p) {
|
||||
if ((string)($p['status'] ?? '') !== 'paid') {
|
||||
continue;
|
||||
}
|
||||
$amt = (float)($p['amount'] ?? 0);
|
||||
$planPaidTotal += $amt;
|
||||
$planRows[] = [
|
||||
'archive_id' => $aid,
|
||||
'archive_name' => $arc ? (string)($arc['name'] ?? '') : '(档案不存在)',
|
||||
'loan_institution' => $arc ? (string)($arc['loan_institution'] ?? '') : '',
|
||||
'borrower' => $arc ? (string)($arc['borrower'] ?? '') : '',
|
||||
'due_date' => (string)($p['due_date'] ?? ''),
|
||||
'paid_at' => (string)($p['paid_at'] ?? ''),
|
||||
'amount' => $amt,
|
||||
];
|
||||
}
|
||||
}
|
||||
usort($planRows, static function ($a, $b): int {
|
||||
$c = strcmp((string)($b['paid_at'] ?? ''), (string)($a['paid_at'] ?? ''));
|
||||
if ($c !== 0) {
|
||||
return $c;
|
||||
}
|
||||
return ((int)($b['archive_id'] ?? 0)) <=> ((int)($a['archive_id'] ?? 0));
|
||||
});
|
||||
|
||||
$completedBank = ['count' => 0, 'sum_credit' => 0.0];
|
||||
$completedGuar = ['count' => 0, 'sum_credit' => 0.0];
|
||||
foreach ($rowsAll as $a) {
|
||||
$st = archiveStatusWithConfig((string)($a['end_date'] ?? ''), getArchiveFinanceConfig($financeAll, (int)$a['id']));
|
||||
if ($st !== '已完成') {
|
||||
continue;
|
||||
}
|
||||
$kind = normalizeArchiveKind((string)($a['archive_kind'] ?? ''));
|
||||
$credit = $a['credit_limit'] !== null && $a['credit_limit'] !== '' ? (float)$a['credit_limit'] : 0.0;
|
||||
if ($kind === ARCHIVE_KIND_BANK_LOAN) {
|
||||
$completedBank['count']++;
|
||||
$completedBank['sum_credit'] += $credit;
|
||||
} elseif ($kind === ARCHIVE_KIND_EXTERNAL_GUARANTEE) {
|
||||
$completedGuar['count']++;
|
||||
$completedGuar['sum_credit'] += $credit;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'plan_rows' => $planRows,
|
||||
'plan_paid_total' => round($planPaidTotal, 2),
|
||||
'completed_bank' => [
|
||||
'count' => (int)$completedBank['count'],
|
||||
'sum_credit' => round((float)$completedBank['sum_credit'], 2),
|
||||
],
|
||||
'completed_guar' => [
|
||||
'count' => (int)$completedGuar['count'],
|
||||
'sum_credit' => round((float)$completedGuar['sum_credit'], 2),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** @var array $me */
|
||||
?>
|
||||
<div class="card fin-tool-page">
|
||||
<h3 class="section-title">承兑贴现计算器</h3>
|
||||
<div class="quick-links-col" style="margin-bottom:14px">
|
||||
<a class="tree-link" href="index.php?action=quick_queries">← 返回便捷查询</a>
|
||||
</div>
|
||||
<div class="fin-two-col">
|
||||
<div class="fin-panel">
|
||||
<h4>票据信息</h4>
|
||||
<div class="fin-row">
|
||||
<label>票据类型</label>
|
||||
<div>
|
||||
<label class="inline small"><input type="radio" name="dc_bill" value="bank" checked> 银行承兑汇票</label>
|
||||
<label class="inline small" style="margin-left:12px"><input type="radio" name="dc_bill" value="commercial"> 商业承兑汇票</label>
|
||||
<p class="fin-muted" id="dc_risk_hint" style="margin:4px 0 0">银承信用风险相对较低;商承请结合承兑人资信评估。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label for="dc_face">票面金额(元)</label>
|
||||
<div>
|
||||
<input id="dc_face" type="text" inputmode="decimal" style="width:100%;max-width:280px">
|
||||
<div id="dc_face_fmt" class="fin-muted"></div>
|
||||
<div id="dc_face_cap" class="fin-cn-cap"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label for="dc_issue">出票日期</label>
|
||||
<input id="dc_issue" type="date">
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label>到期日</label>
|
||||
<div>
|
||||
<label class="inline small"><input type="radio" name="dc_mat_mode" value="date" checked> 指定日期</label>
|
||||
<label class="inline small" style="margin-left:10px"><input type="radio" name="dc_mat_mode" value="after"> 见票后定期</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row" id="dc_row_mat_date">
|
||||
<label for="dc_maturity">到期日期</label>
|
||||
<input id="dc_maturity" type="date">
|
||||
</div>
|
||||
<div class="fin-row" id="dc_row_mat_after" style="display:none">
|
||||
<label for="dc_months">期限(月)</label>
|
||||
<div>
|
||||
<input id="dc_months" type="number" min="1" max="12" step="1" value="6" style="width:80px">
|
||||
<button type="button" class="toolbar-btn" id="dc_apply_months" style="margin-left:8px">推算到期日</button>
|
||||
<span class="fin-muted" id="dc_mat_preview"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label for="dc_discount">贴现日期</label>
|
||||
<input id="dc_discount" type="date">
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label class="inline"><input type="checkbox" id="dc_holiday"> 到期日遇双休日/法定节假日顺延至下一工作日</label>
|
||||
</div>
|
||||
<h4 style="margin:16px 0 10px">贴现参数</h4>
|
||||
<div class="fin-row">
|
||||
<label for="dc_rate">贴现年利率 %</label>
|
||||
<div>
|
||||
<input id="dc_rate" type="text" inputmode="decimal" style="width:100px">
|
||||
<button type="button" class="toolbar-btn" data-ref="1.85" style="margin-left:6px">国股参考</button>
|
||||
<button type="button" class="toolbar-btn" data-ref="2.15">城商参考</button>
|
||||
<button type="button" class="toolbar-btn" data-ref="2.45">农商参考</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label>计息基准</label>
|
||||
<span>360 天/年(票据贴现常用)</span>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label class="inline"><input type="checkbox" id="dc_remote"> 异地(+3 天在途)</label>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label for="dc_day_rule">计息方式</label>
|
||||
<select id="dc_day_rule">
|
||||
<option value="exclusive" selected>记首不记尾(贴现日至到期前一日)</option>
|
||||
<option value="inclusive">记首记尾(对比用)</option>
|
||||
</select>
|
||||
</div>
|
||||
<h4 style="margin:16px 0 10px">反向推算(选填其一)</h4>
|
||||
<div class="fin-row">
|
||||
<label for="dc_net_in">实付金额(元)</label>
|
||||
<input id="dc_net_in" type="text" inputmode="decimal" placeholder="反推利率">
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label for="dc_per10">每10万扣息(元)</label>
|
||||
<input id="dc_per10" type="text" inputmode="decimal" placeholder="反推利率">
|
||||
</div>
|
||||
<div class="fin-actions">
|
||||
<button type="button" class="toolbar-btn primary" id="dc_calc">计算</button>
|
||||
<button type="button" class="toolbar-btn" id="dc_copy">复制结果</button>
|
||||
<button type="button" class="toolbar-btn" id="dc_save">保存记录</button>
|
||||
<button type="button" class="btn-gray" id="dc_reset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fin-result-card" id="dc_result">
|
||||
<h4 style="margin:0 0 10px">计算结果</h4>
|
||||
<p id="dc_result_placeholder" class="fin-muted" style="margin:0">填写左侧并点击「计算」</p>
|
||||
<div id="dc_result_body" style="display:none;font-size:13px;line-height:1.65">
|
||||
<p style="margin:0"><strong>贴现天数</strong>:<span id="dc_out_days">—</span> <span id="dc_out_day_note" class="fin-muted"></span></p>
|
||||
<p style="margin:8px 0 0"><strong>贴现利息</strong>:<span class="fin-out-num" style="font-size:18px" id="dc_out_int">—</span> 元</p>
|
||||
<p style="margin:8px 0 0"><strong>实付金额</strong>:<span id="dc_out_net">—</span> 元</p>
|
||||
<p style="margin:8px 0 0"><strong>年化成本率</strong>:<span id="dc_out_ann">—</span></p>
|
||||
<p style="margin:8px 0 0"><strong>每10万扣息</strong>:<span id="dc_out_p10">—</span> 元</p>
|
||||
<p style="margin:8px 0 0" id="dc_rev_line" class="fin-muted"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-panel" style="margin-top:12px">
|
||||
<h4 style="margin:0 0 8px">本地历史记录</h4>
|
||||
<p class="fin-muted" style="margin:0 0 8px">保存在本浏览器,可对比多次试算。</p>
|
||||
<ul id="dc_hist_list" style="margin:0;padding-left:18px;font-size:12px;max-height:220px;overflow:auto"></ul>
|
||||
<button type="button" class="btn-gray" id="dc_hist_clear" style="margin-top:8px">清空历史</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var H2025=['2025-01-01','2025-01-28','2025-01-29','2025-01-30','2025-01-31','2025-02-01','2025-02-02','2025-02-03','2025-02-04','2025-04-04','2025-04-05','2025-04-06','2025-05-01','2025-05-02','2025-05-03','2025-05-04','2025-05-05','2025-05-31','2025-06-01','2025-06-02','2025-10-01','2025-10-02','2025-10-03','2025-10-04','2025-10-05','2025-10-06','2025-10-07','2025-10-08'];
|
||||
var H2026=['2026-01-01','2026-01-02','2026-01-03','2026-02-15','2026-02-16','2026-02-17','2026-02-18','2026-02-19','2026-02-20','2026-02-21','2026-02-22','2026-02-23','2026-04-04','2026-04-05','2026-04-06','2026-05-01','2026-05-02','2026-05-03','2026-05-04','2026-05-05','2026-06-19','2026-06-20','2026-06-21','2026-09-25','2026-09-26','2026-09-27','2026-10-01','2026-10-02','2026-10-03','2026-10-04','2026-10-05','2026-10-06','2026-10-07'];
|
||||
var HSET={}; [].concat(H2025,H2026).forEach(function(x){HSET[x]=1;});
|
||||
function pad(n){return (n<10?'0':'')+n;}
|
||||
function ymd(d){return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate());}
|
||||
function parseYmd(s){
|
||||
var p=s.split('-'); if(p.length!==3) return null;
|
||||
var d=new Date(+p[0],+p[1]-1,+p[2]); return isNaN(d.getTime())?null:d;
|
||||
}
|
||||
function isWeekend(d){var w=d.getDay(); return w===0||w===6;}
|
||||
function isHoliday(d){return !!HSET[ymd(d)];}
|
||||
function isNonWork(d){return isWeekend(d)||isHoliday(d);}
|
||||
function nextWorkdayFrom(ymdStr){
|
||||
var d=parseYmd(ymdStr); if(!d) return ymdStr;
|
||||
for(var i=0;i<20;i++){
|
||||
if(!isNonWork(d)) return ymd(d);
|
||||
d.setDate(d.getDate()+1);
|
||||
}
|
||||
return ymd(d);
|
||||
}
|
||||
function addMonthsKeepDom(y,m,day,addM){
|
||||
var nm=m-1+addM, ny=y+Math.floor(nm/12), nmo=((nm%12)+12)%12;
|
||||
var last=new Date(ny,nmo+1,0).getDate();
|
||||
var nd=Math.min(day,last);
|
||||
return new Date(ny,nmo,nd);
|
||||
}
|
||||
function syncBillHint(){
|
||||
var v=document.querySelector('input[name="dc_bill"]:checked').value;
|
||||
document.getElementById('dc_risk_hint').textContent=v==='bank'?'银承:流通性较好,贴现利率通常低于商承。':'商承:请重点评估承兑人信用;贴现利率通常高于银承。';
|
||||
}
|
||||
document.querySelectorAll('input[name="dc_bill"]').forEach(function(r){r.addEventListener('change',syncBillHint);});
|
||||
syncBillHint();
|
||||
function parseNum(s){s=String(s||'').replace(/,/g,'').trim(); if(s==='')return NaN; var v=parseFloat(s); return isFinite(v)?v:NaN;}
|
||||
function fmtMoney(n){if(!isFinite(n))return''; var p=(Math.round(n*100)/100).toFixed(2).split('.'); p[0]=p[0].replace(/\B(?=(\d{3})+(?!\d))/g,','); return p.join('.');}
|
||||
var DIG='零壹贰叁肆伍陆柒捌玖', UNIT=['','拾','佰','仟'];
|
||||
function sectionFour(n){var s='',str=('0000'+n).slice(-4); for(var i=0;i<4;i++){var d=+str[i]; if(d===0){s+=DIG[0];continue;} s=s.replace(/零+$/,''); s+=DIG[d]+UNIT[3-i];} return (s.replace(/零+/g,'零').replace(/零$/,''))||DIG[0];}
|
||||
function toRmbUpper(x){if(!isFinite(x)||x<=0)return''; var yi=Math.floor(x/1e8),wan=Math.floor((x%1e8)/1e4),ge=Math.floor(x%1e4); var out=''; if(yi>0)out+=sectionFour(yi)+'亿'; if(wan>0||(yi>0&&ge>0))out+=sectionFour(wan)+'万'; return (out+sectionFour(ge)+'元整').replace(/^壹拾/g,'拾');}
|
||||
function syncFace(){var v=parseNum(document.getElementById('dc_face').value); document.getElementById('dc_face_fmt').textContent=isFinite(v)&&v>0?('格式化:'+fmtMoney(v)):'';
|
||||
document.getElementById('dc_face_cap').textContent=isFinite(v)&&v>0?('大写:'+toRmbUpper(Math.floor(v))):'';}
|
||||
document.getElementById('dc_face').addEventListener('input',syncFace);
|
||||
document.querySelectorAll('input[name="dc_mat_mode"]').forEach(function(r){
|
||||
r.addEventListener('change',function(){
|
||||
var m=document.querySelector('input[name="dc_mat_mode"]:checked').value;
|
||||
document.getElementById('dc_row_mat_date').style.display=m==='date'?'':'none';
|
||||
document.getElementById('dc_row_mat_after').style.display=m==='after'?'':'none';
|
||||
});
|
||||
});
|
||||
document.getElementById('dc_apply_months').addEventListener('click',function(){
|
||||
var iss=document.getElementById('dc_issue').value; if(!iss){alert('请先填出票日');return;}
|
||||
var mo=parseInt(document.getElementById('dc_months').value,10); if(!(mo>=1&&mo<=12)){alert('期限月数须为1-12');return;}
|
||||
var p=iss.split('-').map(Number); var d=addMonthsKeepDom(p[0],p[1],p[2],mo);
|
||||
document.getElementById('dc_maturity').value=ymd(d);
|
||||
document.getElementById('dc_mat_preview').textContent='推算到期:'+ymd(d);
|
||||
});
|
||||
document.querySelectorAll('[data-ref]').forEach(function(btn){
|
||||
btn.addEventListener('click',function(){ document.getElementById('dc_rate').value=this.getAttribute('data-ref'); });
|
||||
});
|
||||
if(!document.getElementById('dc_discount').value){
|
||||
var t=new Date(); document.getElementById('dc_discount').value=ymd(t);
|
||||
}
|
||||
function effectiveMaturity(){
|
||||
var m=document.querySelector('input[name="dc_mat_mode"]:checked').value;
|
||||
var mat=document.getElementById('dc_maturity').value;
|
||||
if(m==='after' && !mat){ alert('请先「推算到期日」或改选指定到期日'); return null; }
|
||||
if(!mat){ alert('请填写到期日'); return null; }
|
||||
if(document.getElementById('dc_holiday').checked) return nextWorkdayFrom(mat);
|
||||
return mat;
|
||||
}
|
||||
function discountDays(dis, matEff, rule, remote){
|
||||
var d0=parseYmd(dis), d1=parseYmd(matEff);
|
||||
if(!d0||!d1||d1<=d0) return NaN;
|
||||
var diff=Math.round((d1-d0)/86400000);
|
||||
var base=rule==='inclusive'?diff+1:diff;
|
||||
if(base<0) return NaN;
|
||||
var extra=remote?3:0;
|
||||
return base+extra;
|
||||
}
|
||||
function calcForward(face,ratePct,days){
|
||||
return face*(ratePct/100)*(days/360);
|
||||
}
|
||||
function rateFromNet(face,net,days){
|
||||
if(!(face>0)||!(net>0)||!(days>0)||net>=face) return NaN;
|
||||
return (face-net)*360/(face*days)*100;
|
||||
}
|
||||
function calc(){
|
||||
document.getElementById('dc_rev_line').textContent='';
|
||||
var face=parseNum(document.getElementById('dc_face').value);
|
||||
var dis=document.getElementById('dc_discount').value;
|
||||
var matEff=effectiveMaturity();
|
||||
var rule=document.getElementById('dc_day_rule').value;
|
||||
var remote=document.getElementById('dc_remote').checked;
|
||||
var days=discountDays(dis,matEff,rule,remote);
|
||||
if(!(face>0)){ alert('票面金额须大于0'); return; }
|
||||
if(!dis||!matEff){ alert('请完善贴现日与到期日'); return; }
|
||||
if(!isFinite(days)||days<=0){ alert('贴现日须早于到期日,且计息天数须为正'); return; }
|
||||
var netIn=parseNum(document.getElementById('dc_net_in').value);
|
||||
var per10=parseNum(document.getElementById('dc_per10').value);
|
||||
var rate=parseNum(document.getElementById('dc_rate').value);
|
||||
var intr;
|
||||
if(isFinite(netIn)&&netIn>0&&netIn<face){
|
||||
rate=rateFromNet(face,netIn,days);
|
||||
if(!isFinite(rate)||!(rate>0)){ alert('无法根据实付金额反推利率,请检查天数与金额'); return; }
|
||||
intr=face-netIn;
|
||||
document.getElementById('dc_rate').value=rate.toFixed(4);
|
||||
document.getElementById('dc_rev_line').textContent='已按实付金额反推贴现年利率:'+rate.toFixed(4)+'%';
|
||||
} else if(isFinite(per10)&&per10>0){
|
||||
intr=per10*(face/100000);
|
||||
rate=(intr*360/(face*days))*100;
|
||||
if(!isFinite(rate)||!(rate>0)){ alert('无法根据每10万扣息反推利率'); return; }
|
||||
document.getElementById('dc_rate').value=rate.toFixed(4);
|
||||
document.getElementById('dc_rev_line').textContent='已按每10万扣息反推贴现年利率:'+rate.toFixed(4)+'%';
|
||||
} else {
|
||||
if(!(rate>0)){ alert('请输入贴现年利率,或填写反推字段'); return; }
|
||||
intr=calcForward(face,rate,days);
|
||||
}
|
||||
var net=face-intr;
|
||||
var ann=net>0 ? (intr/net)*(360/days)*100 : NaN;
|
||||
var p10=face>0? intr/(face/100000):NaN;
|
||||
document.getElementById('dc_out_days').textContent=String(days);
|
||||
document.getElementById('dc_out_day_note').textContent=(remote?'(含异地+3天)':'')+(document.getElementById('dc_holiday').checked?';到期日已按工作日顺延':'');
|
||||
document.getElementById('dc_out_int').textContent=fmtMoney(intr);
|
||||
document.getElementById('dc_out_net').textContent=fmtMoney(net);
|
||||
document.getElementById('dc_out_ann').textContent=isFinite(ann)?(ann.toFixed(4)+'%'):'—';
|
||||
document.getElementById('dc_out_p10').textContent=isFinite(p10)?fmtMoney(p10):'—';
|
||||
document.getElementById('dc_result_placeholder').style.display='none';
|
||||
document.getElementById('dc_result_body').style.display='block';
|
||||
window._dc_last={face:face,intr:intr,net:net,days:days,w:face/10000};
|
||||
}
|
||||
function copyRes(){
|
||||
if(!window._dc_last){ alert('请先计算'); return; }
|
||||
var w=window._dc_last.w, t='票面'+w.toFixed(2)+'万 | 贴现息'+fmtMoney(window._dc_last.intr)+'元 | 实付'+fmtMoney(window._dc_last.net)+'元 | 天数'+window._dc_last.days+'天';
|
||||
if(navigator.clipboard) navigator.clipboard.writeText(t).then(function(){alert('已复制');},function(){alert(t);});
|
||||
else alert(t);
|
||||
}
|
||||
function saveHist(){
|
||||
if(!window._dc_last){ alert('请先计算'); return; }
|
||||
var key='dc_hist_v1', arr=JSON.parse(localStorage.getItem(key)||'[]');
|
||||
arr.unshift({t:Date.now(),...window._dc_last,rate:document.getElementById('dc_rate').value});
|
||||
arr=arr.slice(0,30); localStorage.setItem(key,JSON.stringify(arr)); renderHist();
|
||||
}
|
||||
function renderHist(){
|
||||
var arr=JSON.parse(localStorage.getItem('dc_hist_v1')||'[]');
|
||||
var ul=document.getElementById('dc_hist_list'); ul.innerHTML='';
|
||||
arr.forEach(function(r,i){
|
||||
var li=document.createElement('li');
|
||||
li.textContent=new Date(r.t).toLocaleString()+' — 实付 '+fmtMoney(r.net)+' / 息 '+fmtMoney(r.intr)+' / '+r.days+'天 / 利率'+r.rate+'%';
|
||||
ul.appendChild(li);
|
||||
});
|
||||
}
|
||||
document.getElementById('dc_calc').addEventListener('click',calc);
|
||||
document.getElementById('dc_copy').addEventListener('click',copyRes);
|
||||
document.getElementById('dc_save').addEventListener('click',saveHist);
|
||||
document.getElementById('dc_hist_clear').addEventListener('click',function(){ localStorage.removeItem('dc_hist_v1'); renderHist(); });
|
||||
document.getElementById('dc_reset').addEventListener('click',function(){
|
||||
document.getElementById('dc_face').value=''; document.getElementById('dc_rate').value='';
|
||||
document.getElementById('dc_issue').value=''; document.getElementById('dc_maturity').value='';
|
||||
document.getElementById('dc_net_in').value=''; document.getElementById('dc_per10').value='';
|
||||
syncFace(); document.getElementById('dc_result_body').style.display='none';
|
||||
document.getElementById('dc_result_placeholder').style.display='block';
|
||||
});
|
||||
renderHist();
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** @var array $me */
|
||||
?>
|
||||
<div class="card fin-tool-page">
|
||||
<h3 class="section-title">利息计算器</h3>
|
||||
<div class="quick-links-col" style="margin-bottom:14px">
|
||||
<a class="tree-link" href="index.php?action=quick_queries">← 返回便捷查询</a>
|
||||
</div>
|
||||
<div class="fin-two-col">
|
||||
<div class="fin-panel">
|
||||
<h4>输入</h4>
|
||||
<div class="fin-row">
|
||||
<label for="ic_principal">本金(元)</label>
|
||||
<div>
|
||||
<input id="ic_principal" type="text" inputmode="decimal" autocomplete="off" placeholder="如 1000000" style="width:100%;max-width:320px">
|
||||
<div id="ic_principal_fmt" class="fin-muted"></div>
|
||||
<div id="ic_principal_cap" class="fin-cn-cap"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label for="ic_rate">利率数值</label>
|
||||
<div>
|
||||
<input id="ic_rate" type="text" inputmode="decimal" style="width:140px">
|
||||
<select id="ic_rate_kind" style="margin-left:8px">
|
||||
<option value="annual">年利率 %</option>
|
||||
<option value="monthly">月利率 %</option>
|
||||
<option value="daily">日利率 %</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label>计息天数</label>
|
||||
<div>
|
||||
<label class="inline small"><input type="radio" name="ic_day_mode" value="manual" checked> 模式A:直接输入天数</label>
|
||||
<label class="inline small" style="margin-left:12px"><input type="radio" name="ic_day_mode" value="range"> 模式B:起止日期</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row" id="ic_row_manual">
|
||||
<label for="ic_days">天数</label>
|
||||
<div>
|
||||
<input id="ic_days" type="number" min="0" step="1" style="width:120px" placeholder="正整数">
|
||||
<span class="fin-muted" id="ic_days_hint"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row" id="ic_row_range" style="display:none">
|
||||
<label>起止日期</label>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center">
|
||||
<input id="ic_d1" type="date">
|
||||
<span>至</span>
|
||||
<input id="ic_d2" type="date">
|
||||
<span class="fin-muted" id="ic_range_days_label"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label for="ic_count_mode">结息方式</label>
|
||||
<select id="ic_count_mode">
|
||||
<option value="inclusive">记首记尾(算头又算尾,天数+1)</option>
|
||||
<option value="exclusive" selected>记首不记尾(算头不算尾)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fin-row">
|
||||
<label for="ic_basis">计息基准</label>
|
||||
<select id="ic_basis">
|
||||
<option value="360" selected>360 天/年(银行常用)</option>
|
||||
<option value="365">365 天/年</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fin-actions">
|
||||
<button type="button" class="toolbar-btn primary" id="ic_calc">计算</button>
|
||||
<button type="button" class="toolbar-btn" id="ic_copy">一键复制结果</button>
|
||||
<button type="button" class="btn-gray" id="ic_reset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-result-card" id="ic_result_wrap">
|
||||
<h4 style="margin:0 0 10px">计算结果</h4>
|
||||
<p class="fin-muted" style="margin:0 0 8px" id="ic_placeholder">请先填写左侧数据并点击「计算」</p>
|
||||
<div id="ic_result_body" style="display:none">
|
||||
<p class="small muted" style="margin:0">应付利息</p>
|
||||
<div class="fin-out-num" id="ic_out_interest">—</div>
|
||||
<p class="small muted" style="margin:12px 0 0">本息合计</p>
|
||||
<div class="fin-out-num" id="ic_out_total" style="font-size:18px">—</div>
|
||||
<p style="margin:12px 0 4px;font-size:13px">实际计息天数:<strong id="ic_out_days">—</strong></p>
|
||||
<p style="margin:0;font-size:13px">参考日利率:<strong id="ic_out_daily">—</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var DIG='零壹贰叁肆伍陆柒捌玖', UNIT=['','拾','佰','仟'], GRP=['','万','亿','兆'];
|
||||
function sectionFour(n){
|
||||
var s='', str=('0000'+n).slice(-4);
|
||||
for(var i=0;i<4;i++){
|
||||
var d=+str[i]; if(d===0){ s+=DIG[0]; continue; }
|
||||
s=s.replace(/零+$/,'');
|
||||
s+=DIG[d]+UNIT[3-i];
|
||||
}
|
||||
s=s.replace(/零+/g,'零').replace(/零$/,'');
|
||||
return s||DIG[0];
|
||||
}
|
||||
function toRmbUpper(x){
|
||||
if(!isFinite(x)||x<=0) return '';
|
||||
var yi=Math.floor(x/1e8), wan=Math.floor((x%1e8)/1e4), ge=Math.floor(x%1e4);
|
||||
var out='';
|
||||
if(yi>0) out+=sectionFour(yi)+'亿';
|
||||
if(wan>0||(yi>0&&ge>0)) out+=sectionFour(wan)+'万';
|
||||
out+=sectionFour(ge)+'元整';
|
||||
return out.replace(/^壹拾/g,'拾');
|
||||
}
|
||||
function parseNum(s){
|
||||
s=String(s||'').replace(/,/g,'').trim();
|
||||
if(s==='') return NaN;
|
||||
var v=parseFloat(s);
|
||||
return isFinite(v)?v:NaN;
|
||||
}
|
||||
function fmtMoney(n){
|
||||
if(!isFinite(n)) return '';
|
||||
var p=(Math.round(n*100)/100).toFixed(2).split('.');
|
||||
p[0]=p[0].replace(/\B(?=(\d{3})+(?!\d))/g,',');
|
||||
return p.join('.');
|
||||
}
|
||||
function syncPrincipalFmt(){
|
||||
var raw=document.getElementById('ic_principal').value;
|
||||
var v=parseNum(raw);
|
||||
document.getElementById('ic_principal_fmt').textContent=isFinite(v)&&v>0?('格式化:'+fmtMoney(v)+' 元'):'';
|
||||
document.getElementById('ic_principal_cap').textContent=isFinite(v)&&v>0?('大写:'+toRmbUpper(Math.floor(v))):'';
|
||||
}
|
||||
function daysFromRange(d1,d2,mode){
|
||||
if(!d1||!d2) return NaN;
|
||||
var t0=new Date(d1+'T12:00:00').getTime(), t1=new Date(d2+'T12:00:00').getTime();
|
||||
if(t1<t0) return NaN;
|
||||
var diff=Math.round((t1-t0)/86400000);
|
||||
return mode==='inclusive'?diff+1:diff;
|
||||
}
|
||||
function getEffectiveDays(){
|
||||
var mode=document.querySelector('input[name="ic_day_mode"]:checked').value;
|
||||
var cm=document.getElementById('ic_count_mode').value;
|
||||
if(mode==='manual'){
|
||||
var n=parseInt(document.getElementById('ic_days').value,10);
|
||||
return isFinite(n)&&n>=0?n:NaN;
|
||||
}
|
||||
return daysFromRange(document.getElementById('ic_d1').value,document.getElementById('ic_d2').value,cm);
|
||||
}
|
||||
function dailyRef(P,Rk,rate,basis){
|
||||
var b=+basis;
|
||||
if(Rk==='annual') return (rate/100)/b;
|
||||
if(Rk==='monthly') return (rate/100)/30;
|
||||
return rate/100;
|
||||
}
|
||||
function interestAmount(P,Rk,rate,days,basis){
|
||||
if(!(P>0)||!(days>=0)||!(rate>0)) return NaN;
|
||||
var b=+basis;
|
||||
if(Rk==='annual') return P*(rate/100)*(days/b);
|
||||
if(Rk==='monthly') return P*(rate/100)*(days/30);
|
||||
return P*(rate/100)*days;
|
||||
}
|
||||
function calc(){
|
||||
var P=parseNum(document.getElementById('ic_principal').value);
|
||||
var rate=parseNum(document.getElementById('ic_rate').value);
|
||||
var Rk=document.getElementById('ic_rate_kind').value;
|
||||
var basis=document.getElementById('ic_basis').value;
|
||||
var days=getEffectiveDays();
|
||||
if(!(P>0)){ alert('请输入有效本金'); return; }
|
||||
if(!(rate>0)){ alert('请输入有效利率'); return; }
|
||||
if(!isFinite(days)||days<0){ alert('请检查计息天数或起止日期'); return; }
|
||||
var intr=interestAmount(P,Rk,rate,days,basis);
|
||||
var dref=dailyRef(P,Rk,rate,basis);
|
||||
document.getElementById('ic_out_interest').textContent=fmtMoney(intr)+' 元';
|
||||
document.getElementById('ic_out_total').textContent=fmtMoney(P+intr)+' 元';
|
||||
document.getElementById('ic_out_days').textContent=String(days);
|
||||
document.getElementById('ic_out_daily').textContent=(dref*100).toFixed(6)+'%(按当前利率种类换算,仅供参考)';
|
||||
document.getElementById('ic_result_body').style.display='block';
|
||||
var ph=document.getElementById('ic_placeholder'); if(ph) ph.style.display='none';
|
||||
}
|
||||
function copyTxt(){
|
||||
var t='利息 '+document.getElementById('ic_out_interest').textContent+' | 本息合计 '+document.getElementById('ic_out_total').textContent+' | 天数 '+document.getElementById('ic_out_days').textContent;
|
||||
if(navigator.clipboard) navigator.clipboard.writeText(t).then(function(){alert('已复制');},function(){alert(t);});
|
||||
else alert(t);
|
||||
}
|
||||
function resetAll(){
|
||||
document.getElementById('ic_principal').value='';
|
||||
document.getElementById('ic_rate').value='';
|
||||
document.getElementById('ic_days').value='';
|
||||
document.getElementById('ic_d1').value='';
|
||||
document.getElementById('ic_d2').value='';
|
||||
syncPrincipalFmt();
|
||||
document.getElementById('ic_result_body').style.display='none';
|
||||
var ph=document.getElementById('ic_placeholder'); if(ph) ph.style.display='block';
|
||||
}
|
||||
document.getElementById('ic_principal').addEventListener('input',syncPrincipalFmt);
|
||||
document.getElementById('ic_principal').addEventListener('paste',function(){setTimeout(syncPrincipalFmt,0);});
|
||||
document.querySelectorAll('input[name="ic_day_mode"]').forEach(function(r){
|
||||
r.addEventListener('change',function(){
|
||||
var m=document.querySelector('input[name="ic_day_mode"]:checked').value;
|
||||
document.getElementById('ic_row_manual').style.display=m==='manual'?'':'none';
|
||||
document.getElementById('ic_row_range').style.display=m==='range'?'':'none';
|
||||
});
|
||||
});
|
||||
function syncRangeLabel(){
|
||||
var cm=document.getElementById('ic_count_mode').value;
|
||||
var d=daysFromRange(document.getElementById('ic_d1').value,document.getElementById('ic_d2').value,cm);
|
||||
document.getElementById('ic_range_days_label').textContent=isFinite(d)?('→ 计息天数 '+d):'';
|
||||
}
|
||||
['ic_d1','ic_d2','ic_count_mode'].forEach(function(id){
|
||||
var el=document.getElementById(id);
|
||||
if(el) el.addEventListener('change',syncRangeLabel);
|
||||
});
|
||||
document.getElementById('ic_days').addEventListener('input',function(){
|
||||
if(this.value && document.getElementById('ic_d1').value){ document.getElementById('ic_d1').value=''; document.getElementById('ic_d2').value=''; document.getElementById('ic_range_days_label').textContent=''; }
|
||||
});
|
||||
document.getElementById('ic_d1').addEventListener('change',function(){ if(this.value) document.getElementById('ic_days').value=''; });
|
||||
document.getElementById('ic_d2').addEventListener('change',function(){ if(this.value) document.getElementById('ic_days').value=''; });
|
||||
document.getElementById('ic_calc').addEventListener('click',calc);
|
||||
document.getElementById('ic_copy').addEventListener('click',copyTxt);
|
||||
document.getElementById('ic_reset').addEventListener('click',resetAll);
|
||||
})();
|
||||
</script>
|
||||
Executable
BIN
Binary file not shown.
Executable
+4063
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
1、在编辑档案页面增加分期还款计划功能,增加是否有分期还款计划选项,选择否则忽略,选择是则录入,默认显示6期,用户可自行增加,内容包含(还款日期、还款金额),还款计划约定日期到期后用户选择是否按期还款,若还款,剩余额度根据还款计划的还款金额自动减少,若未按约定还款,则选择延期日期,同时还款计划根据延期日期自动更新;
|
||||
2、增加到期日提前提醒功能,在档案首页状态栏增加60天内贷款到期的信息,包含(贷款机构、借款主体、剩余额度、到期日4个信息),还款计划到期日30天内提醒,包含(贷款机构、借款主体、还款额度、还款日期4个信息)
|
||||
3、档案编辑页面增加结息方式选项,可选择计息方式(按月计息、按季计息、利随本清),结息日期(每月几号、每季几号、到期一次性还息),逾期利息(可选择上浮百分比),履行中的档案详情查看页面,根据剩余额度、年利率、结息方式计算当前应付的利息
|
||||
4、默认贷款到期日后状态自动设为已完成,增加手动选项,用户可在档案编辑页面手动选择贷款已逾期,若贷款逾期,利息则根据逾期利息上浮后的利率计算,状态也调整为逾期中,颜色用红色
|
||||
——比如A公司授信额度为1000万元,贷款日期为2025年10月9日,年利率为5%,计息方式为按月计息,结息日为每月20日,根据还款计划2026年3月9日需归还本金50万,2026年3月10号后,管理员选择已按约定还款,剩余本金自动变为950万,2026年3月20日用户打开A公司档案详情页面,显示当前应付利息为自2026年2月20日至2026年3月9日1000万元本金计算的利息加2026年3月9日至2026年3月20日950万本金计算的利息,共计38125元。
|
||||
5、档案列表筛选增加全部、借款主体、担保人选项,可进一步筛选搜索信息
|
||||
6、档案列表显示取消目录路径,增加授信额度、剩余额度、起止日、到期日、年利率
|
||||
7、不显示用户名全称,显示用户名首字母及尾字母,中间用***代替,admin和user用中文管理员、普通用户代替
|
||||
8、、删除需双管理员操作,避免单管理员误操作
|
||||
备注:以上是我的个人想法,若有功能性冲突或矛盾,或难以实现的功能请提前告知我,避免网页崩溃,其他功能不要改变,目前我的数据库已有内容,更新尽量不影响我已录入的信息。
|
||||
|
||||
|
||||
本次暂不更新的功能:增加导入功能,可导入标准格式的excel自动识别归类档案基本信息,提供导入模板下载
|
||||
@@ -0,0 +1,190 @@
|
||||
# 金牛集团贷款档案管理系统 - 功能说明(当前版本)
|
||||
|
||||
## 系统概述
|
||||
|
||||
本系统面向贷款档案管理场景,提供目录管理、档案管理、文件管理、分期还款计划、结息与逾期管理、提醒中心、模板导入导出、用户与权限、双管理员删除审批等能力。
|
||||
|
||||
---
|
||||
|
||||
## 1. 用户与权限
|
||||
|
||||
### 1.1 登录
|
||||
|
||||
- 用户名 + 密码登录
|
||||
- 默认管理员账号:`admin / jinniu123`
|
||||
|
||||
### 1.2 角色
|
||||
|
||||
- 管理员:可新建/编辑/导入/上传/提交删除申请/审核删除
|
||||
- 普通用户:可查看、搜索、筛选、导出、下载文件
|
||||
|
||||
### 1.3 展示优化
|
||||
|
||||
- 顶部用户名脱敏显示(首字母+***+尾字母)
|
||||
- 角色显示中文(管理员 / 普通用户)
|
||||
|
||||
---
|
||||
|
||||
## 2. 目录管理(三级)
|
||||
|
||||
- 仅支持三级目录
|
||||
- 档案仅允许挂在三级目录
|
||||
- 目录树支持折叠与路径高亮
|
||||
- 管理员可在节点后执行:编辑 / 新增子目录(三级不显示)/ 删除(仅空目录)
|
||||
|
||||
---
|
||||
|
||||
## 3. 档案管理
|
||||
|
||||
### 3.1 基础字段
|
||||
|
||||
- 档案名称
|
||||
- 所属目录(三级)
|
||||
- 贷款机构、借款主体
|
||||
- 授信额度、剩余额度(自动计算)
|
||||
- 起止日、到期日
|
||||
- 年利率、担保类型、担保人
|
||||
|
||||
### 3.2 页面结构
|
||||
|
||||
- 首页默认显示档案列表
|
||||
- 新建档案使用独立页面
|
||||
- 编辑档案使用独立页面
|
||||
- 查看档案页面为只读
|
||||
|
||||
### 3.3 列表能力
|
||||
|
||||
- 搜索字段选择:全部 / 借款主体 / 担保人
|
||||
- 状态筛选:履行中 / 逾期中 / 已完成 / 未设置
|
||||
- 排序:按创建时间 / 到期日 / 贷款机构
|
||||
- 分页:10 / 15 / 30 / 50
|
||||
|
||||
---
|
||||
|
||||
## 4. 文件管理
|
||||
|
||||
### 4.1 上传入口
|
||||
|
||||
- 新建档案页面支持直接选择文件,创建后自动关联上传
|
||||
- 编辑档案页面下方“文件管理(编辑页)”支持上传并刷新列表
|
||||
|
||||
### 4.2 查看页面限制
|
||||
|
||||
- 查看页仅保留预览/下载,不提供上传/删除
|
||||
|
||||
### 4.3 编辑页文件操作
|
||||
|
||||
- 上传、预览、下载、删除
|
||||
- 支持重命名文件:数据库与 `uploads` 真实文件名同步更新
|
||||
|
||||
### 4.4 存储规则
|
||||
|
||||
- 文件按目录层级自动分文件夹存储,不混放到根目录
|
||||
|
||||
---
|
||||
|
||||
## 5. 分期还款计划
|
||||
|
||||
- 编辑页可选“是否有分期还款计划”
|
||||
- 选“是”后展开录入区(默认6期,可增删)
|
||||
- 每期字段:还款日期、还款金额、状态(未还款/提前还款或按期还款/延期)
|
||||
- 到期前后状态联动:
|
||||
- 到期前:可提前还款,不可延期
|
||||
- 到期后:可按期还款或延期
|
||||
- 剩余额度由“授信额度 - 已还款计划总额”自动计算
|
||||
|
||||
---
|
||||
|
||||
## 6. 结息与逾期
|
||||
|
||||
### 6.1 结息方式
|
||||
|
||||
- 按月计息
|
||||
- 按季计息
|
||||
- 到期一次性付清
|
||||
|
||||
### 6.2 结息日
|
||||
|
||||
- 当结息方式为按月/按季时:输入1-28日
|
||||
- 当结息方式为到期一次性付清时:结息日自动禁用并显示一次性付息语义
|
||||
|
||||
### 6.3 逾期判断
|
||||
|
||||
- 是否逾期默认“否”
|
||||
- 若当前日期未超过到期日:不可手动选择逾期
|
||||
- 若超过到期日:可手动选择是否逾期
|
||||
- 支持逾期利率上浮(默认 50%)
|
||||
|
||||
### 6.4 状态与利息
|
||||
|
||||
- 状态:履行中 / 逾期中(红色)/ 已完成 / 未设置
|
||||
- 详情页显示“当前应付利息”
|
||||
- 利息计算支持分段本金(结合已还款计划)
|
||||
|
||||
---
|
||||
|
||||
## 7. 提醒中心
|
||||
|
||||
### 7.1 首页看板(仅数量)
|
||||
|
||||
- 当前结果总数
|
||||
- 履行中
|
||||
- 已完成
|
||||
- 未设置
|
||||
- 贷款到期
|
||||
- 还款计划
|
||||
- 贷款逾期
|
||||
|
||||
### 7.2 提醒详情页
|
||||
|
||||
- 独立页面展示具体提醒内容
|
||||
- 可按类型筛选:贷款到期 / 还款计划 / 贷款逾期
|
||||
- 支持导出当前筛选结果(xlsx/csv)
|
||||
|
||||
---
|
||||
|
||||
## 8. 导出
|
||||
|
||||
- 导出全部:可选 xlsx / csv
|
||||
- 导出选中:可选 xlsx / csv
|
||||
- 普通用户可导出
|
||||
|
||||
---
|
||||
|
||||
## 9. 模板导入
|
||||
|
||||
### 9.1 模板下载
|
||||
|
||||
- 支持下载 xlsx 模板
|
||||
- 若存在 `data/import_template.xlsx`,优先下载该手工模板
|
||||
|
||||
### 9.2 导入能力
|
||||
|
||||
- 支持 csv/xlsx(xlsx 需安装 PhpSpreadsheet)
|
||||
- 导入时可自动创建目录并批量创建档案
|
||||
- 必填与选填规则以模板内说明为准
|
||||
|
||||
---
|
||||
|
||||
## 10. 双管理员删除流程
|
||||
|
||||
- 第一步:管理员A提交删除申请
|
||||
- 第二步:管理员B在“删除审核”页面审批
|
||||
- 限制:申请人不能审核自己的申请
|
||||
- 审核通过后执行真实删除
|
||||
|
||||
---
|
||||
|
||||
## 11. 当前技术实现说明
|
||||
|
||||
- 主数据:SQLite
|
||||
- 扩展业务配置:`data/*.json`(免迁移兼容方案)
|
||||
- 前后端:PHP 单体应用
|
||||
|
||||
---
|
||||
|
||||
## 12. 注意事项
|
||||
|
||||
- 若启用 xlsx 导入/导出,建议安装 `PhpSpreadsheet`
|
||||
- 上线后请立即修改默认管理员密码
|
||||
- 建议定期备份 `data/` 与 `uploads/`
|
||||
@@ -0,0 +1,79 @@
|
||||
1.便捷查询下的功能改为折叠显示,默认折叠,点击后打开菜单
|
||||
2.便捷查询增加利息计算器功能,具体要求如下:
|
||||
## 核心功能
|
||||
1. **本金输入**:支持输入本金金额,带千分位格式化和大写金额回显(如:壹拾万元整)。
|
||||
2. **利率输入**:支持输入年利率(%),可选是否区分年利率/月利率/日利率。
|
||||
3. **计息天数**:提供两种输入模式,可切换:
|
||||
- 模式A:直接手动输入天数(正整数)。
|
||||
- 模式B:输入起止日期(YYYY-MM-DD),自动计算并回显天数。
|
||||
4. **结息方式**:下拉选择:
|
||||
- "记首记尾"(算头又算尾,天数+1)
|
||||
- "记首不记尾"(算头不算尾,实际天数)
|
||||
5. **计息基准**:可选 360天/年 或 365天/年(银行常用360,实际天数用365)。
|
||||
6. **计算结果**:
|
||||
- 显示利息金额(元,保留2位小数)
|
||||
- 显示本息合计
|
||||
- 显示实际计息天数
|
||||
- 显示利率换算后的日利率参考值
|
||||
|
||||
## 交互细节
|
||||
- 切换日期输入模式时,已填数据尽量保留或联动更新。
|
||||
- 起止日期选择后,天数自动计算并显示在输入框旁。
|
||||
- 手动输入天数时,如果起止日期已填,自动清空或提示不一致。
|
||||
- 所有金额输入框支持粘贴纯数字自动格式化。
|
||||
- 增加"一键复制结果"按钮。
|
||||
- 增加"重置"按钮清空所有数据。
|
||||
|
||||
## 界面风格
|
||||
- 简洁的财务工具风格,类似银行网银的贷款计算器。
|
||||
- 结果区域用卡片高亮显示。
|
||||
3.增加承兑贴现计算器,具体功能如下:
|
||||
## 核心功能
|
||||
|
||||
### 1. 票据信息
|
||||
- **票据类型**:单选【银行承兑汇票 / 商业承兑汇票】(影响风险提示和默认利率)。
|
||||
- **票面金额**:输入框,支持千分位格式化,联动大写金额回显。
|
||||
- **出票日期**:日期选择器(YYYY-MM-DD)。
|
||||
- **到期日期**:日期选择器,或选择"见票后定期付款"模式(输入月数自动推算到期日)。
|
||||
- **贴现日期**:日期选择器,默认当天。
|
||||
|
||||
### 2. 到期日推算逻辑(关键)
|
||||
- 支持"月对月"推算:输入出票日期 + 期限(如6个月),自动计算到期日。
|
||||
- 月末规则:出票日为1月31日,期限6个月,到期日应为7月31日;若到期月无对应日期(如8月31日→2月),则取到期月最后一天(2月28/29日)。
|
||||
- 节假日顺延(可选):到期日遇法定节假日自动顺延至下一工作日(需内置常见节假日或允许手动标记)。
|
||||
|
||||
### 3. 贴现参数
|
||||
- **贴现利率**:输入年利率(%),支持快捷选择【6个月国股利率 / 城商利率 / 农商利率】作为参考填入。
|
||||
- **计息基准**:固定 360天/年(票据贴现行业标准)。
|
||||
- **是否异地**:勾选框,勾选后贴现天数自动 +3天(在途时间)。
|
||||
- **计息方式**:固定"记首不记尾"(贴现日至到期前一日,行业标准),但允许切换为"记首记尾"用于对比。
|
||||
|
||||
### 4. 计算结果
|
||||
- **贴现天数**:自动显示(含异地+3天说明)。
|
||||
- **贴现利息**:票面金额 × 贴现天数 × 贴现利率 / 360。
|
||||
- **实付金额**:票面金额 - 贴现利息。
|
||||
- **年化成本率**:(贴现利息 / 实付金额)×(360 / 贴现天数),用于评估实际资金成本。
|
||||
- **每10万扣息**:方便票据行业常用的"每十万扣多少"报价方式反推。
|
||||
|
||||
### 5. 反向计算(高级功能)
|
||||
- 已知【实付金额】反推【贴现利率】。
|
||||
- 已知【每10万扣息】反推【贴现利率】。
|
||||
|
||||
## 交互细节
|
||||
- 输入出票日+期限月数后,到期日自动计算并回显。
|
||||
- 贴现日期不能晚于到期日,否则提示错误。
|
||||
- 切换"异地"选项时,天数和利息实时重算。
|
||||
- 增加"复制结果"按钮,格式为:票面XX万 | 贴现息XX元 | 实付XX元 | 天数XX天。
|
||||
- 增加"保存记录"按钮,本地存储历史计算,支持对比列表。
|
||||
|
||||
## 界面风格
|
||||
- 财务工具风格,左右分栏:左侧输入,右侧结果卡片高亮。
|
||||
- 响应式,支持手机端使用。
|
||||
|
||||
## 校验规则
|
||||
- 票面金额 > 0
|
||||
- 贴现日 < 到期日
|
||||
- 利率 > 0
|
||||
- 期限月数为正整数(1-12)
|
||||
|
||||
4.增加历史还款记录查询,统计所有已归还的还本计划本金及到期的贷款本金
|
||||
@@ -0,0 +1,104 @@
|
||||
# 金牛集团贷款档案管理系统 - 部署说明
|
||||
|
||||
## 1. 环境要求
|
||||
|
||||
- 宝塔面板(Linux)
|
||||
- Nginx 或 Apache
|
||||
- PHP 8.0+(建议 8.2)
|
||||
- 必需扩展:`pdo_sqlite`、`sqlite3`、`mbstring`、`fileinfo`
|
||||
- 推荐扩展:`zip`、`xml`(用于 xlsx 导出/导入)
|
||||
|
||||
## 2. 目录结构
|
||||
|
||||
- `public/`:网站入口目录(宝塔运行目录设置到这里)
|
||||
- `app/`:配置与基础逻辑
|
||||
- `data/`:业务数据(SQLite + JSON配置)
|
||||
- `uploads/`:上传文件目录(按目录层级自动分文件夹)
|
||||
|
||||
### `data/` 下的关键文件
|
||||
|
||||
- `archive.sqlite`:主业务数据库
|
||||
- `repayment_plans.json`:分期还款计划(免迁移方案)
|
||||
- `archive_finance.json`:结息/逾期配置(免迁移方案)
|
||||
- `delete_approvals.json`:双管理员删除审批记录
|
||||
- `import_template.xlsx`:可选,自定义导入模板(手工放置)
|
||||
|
||||
## 3. 宝塔部署步骤
|
||||
|
||||
1. 新建 PHP 站点
|
||||
2. 上传项目到站点目录
|
||||
3. 网站运行目录设置为:`/public`
|
||||
4. 在 PHP 扩展中启用上面的必需扩展
|
||||
5. 给 `data/` 与 `uploads/` 赋予可写权限
|
||||
6. 访问域名初始化系统
|
||||
|
||||
## 4. 默认账号
|
||||
|
||||
- 用户名:`admin`
|
||||
- 密码:`jinniu123`
|
||||
|
||||
> 登录后建议立即修改默认密码。
|
||||
|
||||
## 5. 导出说明
|
||||
|
||||
- 导出支持:`xlsx` / `csv`
|
||||
- 页面可自由选择格式
|
||||
- 若服务器安装了 `PhpSpreadsheet`,`xlsx` 可直接使用:
|
||||
|
||||
```bash
|
||||
composer require phpoffice/phpspreadsheet
|
||||
```
|
||||
|
||||
## 6. 导入说明
|
||||
|
||||
### 6.1 下载模板
|
||||
|
||||
- 点击页面“模板下载”
|
||||
- 系统优先下载:`data/import_template.xlsx`(如果你手工放了该文件)
|
||||
- 若该文件不存在,系统会动态生成模板(需要 `PhpSpreadsheet`)
|
||||
|
||||
### 6.2 模板字段规则
|
||||
|
||||
#### 必填字段
|
||||
|
||||
- 一级目录
|
||||
- 二级目录
|
||||
- 三级目录
|
||||
- 档案名称
|
||||
- 贷款机构
|
||||
- 借款主体
|
||||
- 授信额度
|
||||
- 起止日
|
||||
- 到期日
|
||||
- 年利率
|
||||
- 担保类型
|
||||
- 担保人
|
||||
|
||||
#### 选填字段
|
||||
|
||||
- 计息方式(按月计息 / 按季付息 / 到期一次性付清)
|
||||
- 结息日(1-28)
|
||||
- 是否有分期还款计划(是/否)
|
||||
|
||||
## 7. 文件上传与存储
|
||||
|
||||
文件上传后按“目录层级 + 档案”自动存储,例如:
|
||||
|
||||
`uploads/一级目录/二级目录/三级目录/archive_档案ID_档案名/文件`
|
||||
|
||||
## 8. 删除审批(双管理员)
|
||||
|
||||
- 管理员A点击删除 -> 仅提交申请
|
||||
- 管理员B在“删除审核”页面审批通过后才执行真实删除
|
||||
- 申请人本人不能审批自己的删除申请
|
||||
|
||||
## 9. 试运行建议
|
||||
|
||||
上线前建议至少验证:
|
||||
|
||||
1. 新建/编辑/查看档案
|
||||
2. 新建页上传文件、编辑页文件管理(上传/重命名/删除)
|
||||
3. 分期计划与剩余额度联动
|
||||
4. 到期提醒详情页筛选与导出
|
||||
5. 模板导入(先用 2-3 条测试数据)
|
||||
6. 双管理员删除闭环流程
|
||||
Reference in New Issue
Block a user