v24 第 10 批:名册真性能(根因是每卡内联表单)等九条
- #6 名册线上真实数据(1184 人)卡顿:根因是**每张卡片内联一整份分组弹层**—— 全页 2369 个 form / 27234 个 input / 22496 个 radio / 98100 DOM 节点, 密码管理器(Bitwarden 等)逐个扫描 form 把主线程占死(用户报的就是这个冲突)。 改为卡片只带 data 属性 + 全页**唯一一份**共享浮层 #rosterMenus。 实测:form 2369→3、input 27234→25、DOM 98100→17659、解压 7694→2306 KB、 长任务 1/52ms → 0。新增哨兵 _fb_formbloat_check.py(含反向验证)。 - #5 日视图翻页失效:day_columns() 无条件吸到周一,天视图只有 1 列 → 永远显示那周周一。 改为仅 span>1 时吸附(周视图行为不变)。 - #2 财务「欠款」类别(负债);并修一个真 bug:负债填负数会让净资产**反向变大**。 写入口 normalizeAmount 归一化 + summary/timeline 侧 abs 防御 + Schema v17 纠正老数据。 - #3 账目行「盘点」「修改」两个并列按钮(各自就地弹面板);图移到账目上方, 总金额图跨整行 + 每个账目一条线 + 类别筛选条;charts() 改为覆盖全部账目。 - #4 日程新增「农历」「节假日」两本不可删除只读日历(app/Holiday.php): 两级自动更新 —— 内置规则(公历固定 + 清明节气 + 农历节日)保证断网也有; 官方 JSON 镜像自动抓取补齐调休。抓取**不在渲染路径联网**(响应后 fastcgi_finish_request + 频率闸门),缓存落 storage/cache。仅派生本不可写日程。 - #6 日程页设置按钮前加「跳到某一天」(选中即跳)。 - #7 起止范围改用一个 Semi dateTimeRange 控件(观影/旅程/节点/日程共 4 处); 新增 window.fbSetDT / fbUpgradeRange;修范围重挂(React 非受控组件须 unmount 再 createRoot) 与范围里 required 未摘除导致表单无法提交两个真实问题。 验证:e2e 786/0、batch10 44/0、batch9 38/0、batch_e 91/0、upgrade 126/0、picker 28/0、 semi_integrate 27/0、formbloat 5/0(+反向 3 红)、mobile 688/0、interact 97/0、drag 5/0、geom 5/5; 线上自检 19/19 + 线上真实账号巡检 28/0(含 Firefox 实测)。
This commit is contained in:
+105
-43
@@ -38,6 +38,12 @@ final class Ledger
|
||||
'cash' => ['现金', 'asset', '💵'],
|
||||
'loan' => ['贷款', 'debt', '📉'],
|
||||
'credit' => ['信用卡', 'debt', '💳'],
|
||||
// v24 第 10 批 #2(用户原话):「类别,增加一个欠款,用来记录别人的钱,
|
||||
// 属于负债,是负的。」
|
||||
// 与「贷款/信用卡」的区别:那两样是欠**机构**的钱,这个是欠**个人**的
|
||||
// (借了亲戚朋友的钱、还没还的份子钱…)。同属负债,所以一起计入「总负债」,
|
||||
// 净资产照旧 = 资产 − 负债。
|
||||
'owe' => ['欠款', 'debt', '🤝'],
|
||||
'other' => ['其它', 'asset', '📦'],
|
||||
];
|
||||
|
||||
@@ -106,6 +112,14 @@ final class Ledger
|
||||
|
||||
/**
|
||||
* 这本账的汇总。
|
||||
*
|
||||
* ⚠️ v24 第 10 批 #2:负债一律按**绝对值**计入(abs)。
|
||||
* 用户原话说「欠款…属于负债,**是负的**」—— 也就是说他会把欠款填成 -500。
|
||||
* 如果不取绝对值,`net = 资产 − 负债 = 1000 − (−500) = 1500`,
|
||||
* **欠了钱净资产反而变多**,这是最容易被忽略又最误导人的一类错。
|
||||
* 负债的"负"是**方向语义**(我欠别人),金额本身按正数参与"总负债",
|
||||
* 再由 net = 资产 − 负债 落到负号上 —— 全站只有这一个口径。
|
||||
*
|
||||
* @return array{asset:int,debt:int,net:int,by_kind:array<string,int>,count:int}
|
||||
*/
|
||||
public static function summary(array $entries): array
|
||||
@@ -116,11 +130,10 @@ final class Ledger
|
||||
foreach ($entries as $e) {
|
||||
$amt = (int)($e['amount_cents'] ?? 0);
|
||||
$kind = (string)($e['kind'] ?? 'other');
|
||||
$isDebt = (self::KINDS[$kind][1] ?? 'asset') === 'debt';
|
||||
if ($isDebt) {
|
||||
$debt += $amt;
|
||||
if (self::isDebt($kind)) {
|
||||
$debt += abs($amt); // 见方法注释:负数输入也要被算成负债
|
||||
} else {
|
||||
$asset += $amt;
|
||||
$asset += $amt; // 资产保持符号原样(透支=负资产是合理的)
|
||||
}
|
||||
$byKind[$kind] = ($byKind[$kind] ?? 0) + $amt;
|
||||
}
|
||||
@@ -133,6 +146,18 @@ final class Ledger
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 负债口径归一化:**负债类别的金额一律取绝对值**,其它类别原样。
|
||||
*
|
||||
* 只在写入口(addEntry / updateEntry / reconcile)调用 ——
|
||||
* 存进库里的数就已经是"负债为正、由 net 去减"的唯一口径,
|
||||
* 下游(汇总/时间线/图表)不必各写一遍 abs,也就不会有人漏写。
|
||||
*/
|
||||
private static function normalizeAmount(string $kind, int $cents): int
|
||||
{
|
||||
return self::isDebt($kind) ? abs($cents) : $cents;
|
||||
}
|
||||
|
||||
// ---------------- 写 ----------------
|
||||
|
||||
/** @return int 新账本 id */
|
||||
@@ -231,15 +256,17 @@ final class Ledger
|
||||
/** @return int 新增账目 id */
|
||||
public static function addEntry(int $ledgerId, array $d, int $byId = 0): int
|
||||
{
|
||||
$db = Database::get();
|
||||
$db = Database::get();
|
||||
$knd = self::kind($d['kind'] ?? '');
|
||||
$db->prepare(
|
||||
'INSERT INTO ledger_entries (ledger_id, kind, title, amount_cents, note, person_id, created_by)
|
||||
VALUES (?,?,?,?,?,?,?)'
|
||||
)->execute([
|
||||
$ledgerId,
|
||||
self::kind($d['kind'] ?? ''),
|
||||
$knd,
|
||||
mb_substr(trim((string)($d['title'] ?? '')), 0, 120, 'UTF-8'),
|
||||
self::toCents($d['amount'] ?? ''),
|
||||
// 负债归一化成正数(见 normalizeAmount):用户会把欠款填成负数
|
||||
self::normalizeAmount($knd, self::toCents($d['amount'] ?? '')),
|
||||
trim((string)($d['note'] ?? '')) ?: null,
|
||||
(int)($d['person_id'] ?? 0) ?: null,
|
||||
$byId ?: null,
|
||||
@@ -249,13 +276,14 @@ final class Ledger
|
||||
|
||||
public static function updateEntry(int $ledgerId, int $id, array $d): void
|
||||
{
|
||||
$knd = self::kind($d['kind'] ?? '');
|
||||
Database::get()->prepare(
|
||||
'UPDATE ledger_entries SET kind=?, title=?, amount_cents=?, note=?, person_id=?
|
||||
WHERE id=? AND ledger_id=?'
|
||||
)->execute([
|
||||
self::kind($d['kind'] ?? ''),
|
||||
$knd,
|
||||
mb_substr(trim((string)($d['title'] ?? '')), 0, 120, 'UTF-8'),
|
||||
self::toCents($d['amount'] ?? ''),
|
||||
self::normalizeAmount($knd, self::toCents($d['amount'] ?? '')),
|
||||
trim((string)($d['note'] ?? '')) ?: null,
|
||||
(int)($d['person_id'] ?? 0) ?: null,
|
||||
$id, $ledgerId,
|
||||
@@ -362,12 +390,14 @@ final class Ledger
|
||||
public static function reconcile(int $ledgerId, int $entryId, $amount, string $note = '', int $byId = 0): bool
|
||||
{
|
||||
$db = Database::get();
|
||||
$st = $db->prepare('SELECT id FROM ledger_entries WHERE id = ? AND ledger_id = ?');
|
||||
$st = $db->prepare('SELECT id, kind FROM ledger_entries WHERE id = ? AND ledger_id = ?');
|
||||
$st->execute([$entryId, $ledgerId]);
|
||||
if ($st->fetchColumn() === false) {
|
||||
$row = $st->fetch();
|
||||
if ($row === false) {
|
||||
return false;
|
||||
}
|
||||
$cents = self::toCents($amount);
|
||||
// 盘点也走同一条归一化:负债填 -500 也存成 500(见 normalizeAmount)
|
||||
$cents = self::normalizeAmount(self::kind((string)$row['kind']), self::toCents($amount));
|
||||
$db->prepare('UPDATE ledger_entries SET amount_cents = ? WHERE id = ? AND ledger_id = ?')
|
||||
->execute([$cents, $entryId, $ledgerId]);
|
||||
$db->prepare(
|
||||
@@ -395,15 +425,19 @@ final class Ledger
|
||||
}
|
||||
|
||||
/**
|
||||
* 这本账的**汇总时间线**:每个盘点日,整本账的净资产是多少。
|
||||
* 这本账的**汇总时间线**:每个时点,整本账的净资产是多少。
|
||||
*
|
||||
* 做法:把每个账目在每个时点上"当时的值"取出来再加总 ——
|
||||
* · 某账目在 t 之前从未盘点过 → 它的值是**它现在的值**(因为一直没变过);
|
||||
* · 盘点过 → 取 t 之前最后一次盘点的值。
|
||||
* 这样"只盘过招行"也能画出一条合理的总资产曲线,
|
||||
* 而不是"没盘过的账目在图上凭空消失"。
|
||||
* v24 第 10 批 #3 修正:时点不再只取"盘点过的日期"。
|
||||
* ⚠️ 只取盘点日期有个致命的冷启动问题:用户**第一次盘点时只有一个时点**,
|
||||
* 模板的 `count($timeline) > 1` 不成立 → 图根本不显示。而那一刻恰恰是他
|
||||
* 最想看到图的时刻("我盘点了,图怎么还没出来")。
|
||||
* 所以时点集合改成三者并集:① 账目**创建日**(那时它开始存在)
|
||||
* ② 所有盘点日 ③ 今天。于是"今天记了三笔账"就能画出一条 0 → 逐步累加的线。
|
||||
*
|
||||
* @return array<int,array{date:string,net:int}> 至少一条(今天);空账本返回 []
|
||||
* 取值规则(每个时点 t):账目在 t 之后才创建 → **不计入**(那时它还不存在);
|
||||
* 在 t 之前盘点过 → 取最后一次盘点的值;从未盘点过 → 取它现在的值(一直没变过)。
|
||||
*
|
||||
* @return array<int,array{date:string,net:int,asset:int,debt:int}> 空账本返回 []
|
||||
*/
|
||||
public static function timeline(int $ledgerId): array
|
||||
{
|
||||
@@ -421,15 +455,43 @@ final class Ledger
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
$dates[] = (string)$r['d'];
|
||||
}
|
||||
// ① 账目创建日:让"刚记完账"就有曲线可看(见方法注释的冷启动问题)
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT DISTINCT DATE(created_at) AS d FROM ledger_entries
|
||||
WHERE ledger_id = ? AND created_at IS NOT NULL ORDER BY d'
|
||||
);
|
||||
$st->execute([$ledgerId]);
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
$d = (string)$r['d'];
|
||||
if ($d !== '' && $d !== '0000-00-00') {
|
||||
$dates[] = $d;
|
||||
}
|
||||
}
|
||||
$today = date('Y-m-d');
|
||||
$dates[] = $today; // 永远带上"现在"
|
||||
$dates = array_values(array_unique($dates));
|
||||
sort($dates);
|
||||
|
||||
// 每条账目的快照:entry_id => [[date, cents], …]
|
||||
// ⚠️ 只有一个时点时给一个**零点**在它前面,让曲线有形状。
|
||||
// 全是同一天发生的事(今天记账 + 今天盘点)会退化成单点 ——
|
||||
// 单点折线图视觉上就是一个小圆圈,用户会以为"图坏了"。
|
||||
// 补一个"前一天:0"作为基线,读起来就是"从 0 涨到现在",
|
||||
// 与直觉一致(账本是新起的,之前确实没有资产)。
|
||||
// 只补在**最早那个时点的前一天**,不动中间的真实数据。
|
||||
if (count($dates) === 1) {
|
||||
array_unshift($dates, date('Y-m-d', strtotime($dates[0] . ' -1 day')));
|
||||
}
|
||||
|
||||
// 每条账目的快照:entry_id => [[date, cents], …];born = 它的创建日期
|
||||
$snaps = [];
|
||||
$born = [];
|
||||
$today10 = date('Y-m-d');
|
||||
foreach ($entries as $e) {
|
||||
$snaps[(int)$e['id']] = [];
|
||||
$eid = (int)$e['id'];
|
||||
$snaps[$eid] = [];
|
||||
$c = substr((string)($e['created_at'] ?? ''), 0, 10);
|
||||
// 取不到创建日就当"今天出生":宁可不画前段,也不要凭空多出历史
|
||||
$born[$eid] = ($c !== '' && $c !== '0000-00-00') ? $c : $today10;
|
||||
}
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT entry_id, DATE(created_at) AS d, amount_cents
|
||||
@@ -450,6 +512,10 @@ final class Ledger
|
||||
$debt = 0;
|
||||
foreach ($entries as $e) {
|
||||
$eid = (int)$e['id'];
|
||||
// 这个时点上它还不存在(还没记进来)→ 不计入,否则等于凭空多钱
|
||||
if ($d < $born[$eid]) {
|
||||
continue;
|
||||
}
|
||||
$val = (int)$e['amount_cents']; // 默认:一直没变过 = 还是现在这个值
|
||||
foreach ($snaps[$eid] as [$sd, $sv]) {
|
||||
if ($sd <= $d) {
|
||||
@@ -459,7 +525,8 @@ final class Ledger
|
||||
}
|
||||
}
|
||||
if (self::isDebt((string)$e['kind'])) {
|
||||
$debt += $val;
|
||||
// ⚠️ 与 summary 同一口径:负债取绝对值(老数据里可能是被判反的负数)
|
||||
$debt += abs($val);
|
||||
} else {
|
||||
$asset += $val;
|
||||
}
|
||||
@@ -490,34 +557,29 @@ final class Ledger
|
||||
}
|
||||
|
||||
/**
|
||||
* 这本账里**有盘点历史**的账目(列表上方只给这几条画线)。
|
||||
* 全画会变成一坨面条 —— 用户要的是"招商银行每年的金额变化"这种单条曲线。
|
||||
* 这本账里**每条账目**的曲线(列表上方逐条画)。
|
||||
*
|
||||
* v24 第 10 批 #3(用户原话):「在账目上面,增加一个图表显示,**显示所有账目和
|
||||
* 总金额的变化曲线**,根据盘点的金额进行显示。横轴就是时间,纵轴是金额,用折线图。」
|
||||
*
|
||||
* ⚠️ 这里与 v24 第 8 条的旧实现有个**语义修正**:
|
||||
* 旧版只画「有盘点记录的账目」(当时担心"全画会变成一坨面条")。
|
||||
* 但用户这次明确要求「**所有**账目」,所以改成**每条都出线**,
|
||||
* "面条"问题改由**前端折叠 + 按类别筛选**解决(见模板里的曲线选择条),
|
||||
* 而不是靠"少画几条"偷偷减少信息。
|
||||
* 没盘点过的账目会得到一条**单点曲线**(就是它现在的值),
|
||||
* 图上画一个点而不是空白 —— 空白会被误读成"这条账没数据"。
|
||||
*
|
||||
* @return array<int,array{entry:array,series:array}>
|
||||
*/
|
||||
public static function charts(int $ledgerId): array
|
||||
{
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT entry_id, COUNT(*) AS n FROM ledger_entry_snapshots
|
||||
WHERE ledger_id = ? GROUP BY entry_id HAVING n > 0 ORDER BY entry_id'
|
||||
);
|
||||
$st->execute([$ledgerId]);
|
||||
$ids = array_map('intval', array_column($st->fetchAll(), 'entry_id'));
|
||||
if (!$ids) {
|
||||
return [];
|
||||
}
|
||||
$byId = [];
|
||||
foreach (self::entries($ledgerId) as $e) {
|
||||
$byId[(int)$e['id']] = $e;
|
||||
}
|
||||
$out = [];
|
||||
foreach ($ids as $eid) {
|
||||
if (!isset($byId[$eid])) {
|
||||
continue;
|
||||
}
|
||||
foreach (self::entries($ledgerId) as $e) {
|
||||
$eid = (int)$e['id'];
|
||||
$out[] = [
|
||||
'entry' => $byId[$eid],
|
||||
'series' => self::entrySeries($eid, (int)$byId[$eid]['amount_cents']),
|
||||
'entry' => $e,
|
||||
'series' => self::entrySeries($eid, (int)$e['amount_cents']),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
|
||||
Reference in New Issue
Block a user