Files
TripLog/templates/nodes/_expenses.php
T
laoyang a79956758b 补齐未接入代码的功能域:参与人 / 评论 / 花销与分摊
数据库里早就建好了 comments、node_members + role_tags、expenses +
expense_splits 三组表,但一直没有代码接入。本次把这三块连上业务,
并新增「节点详情页」作为聚合入口。

新增
- src/Access.php          统一归属校验。此前每个控制器各写各的 family_id
                          条件,容易漏;现在 activity()/node()/target() 一处收敛,
                          requireAdmin() 统一管理员判断。
- src/Comments.php        两级平铺:parent_id 只允许指向顶层,写「回复的回复」
                          时规范化回顶层;删顶层会连同回复一起删。
- src/Members.php         参与人 + 角色标签 + 情绪 + 签到。情绪目录 17 项,
                          全部选用 Emoji 11.0(2018)及以前的码位,避免旧系统
                          字体缺字形显示成空白方块。
- src/Expenses.php        花销与分摊。分摊按「分」为单位 intdiv 平均,
                          余数归最后一人,保证 SUM(share_amount) = total_amount。
- templates/nodes/show.php 节点详情页:基本信息 + 参与人 + 花销 + 讨论 + 附件入口。
- 角色标签管理、评论区、参与人区、花销区各自的模板与控制器。

修复
- 越权:往别人活动的节点上插节点、把别家用户加成参与人,都只校验了
  「节点存在」,没校验「用户与节点同家庭」。现统一走 Access。
- 删活动时节点附件文件删不掉:附件在 /uploads/attachments/ 下,却调了
  只认 /uploads/covers/ 的删除函数,判定不通过 → 文件永久残留。改为
  统一的 deleteUploadFile(),两个目录都认。
- 删有引用记录的用户会 500(activities.created_by / attachments.uploader_id /
  comments.author_id / expenses.payer_user_id 这几处没有级联,也没做前置检查)。

其他
- 活动/节点列表补上附件、评论、参与人、花销四个计数(一次 GROUP BY 查完)。
- 首页布局新增一条迁移易踩的规避:新列上的索引必须排在 ALTER TABLE 之后。
- 新增 .gitignore:config.php(含数据库口令)与 uploads/ 下用户上传的
  头像封面不入库,只保留 uploads/.htaccess 那条禁止执行脚本的规则。
- 新增 config.example.php 作为新环境的配置模板。

本批不改数据库结构,不需要任何数据迁移。

本地与线上验证
- php -l 全绿(PHP 8.4 本地 / 线上 7.4.30 与 8.1.7 双版本)。
- 模板渲染检查 180 项全通过。
- 线上端到端 153 项全通过:登录、活动增删改查、节点流转、
  参与人/角色标签/情绪/签到、评论、花销与分摊、权限、级联清理、
  全站页面无 PHP 报错泄漏、跑完基线逐表核对且既有 id 集合逐字未变。
- 顺带写了 trip_sql_column_check.py:拿真实库表结构静态校验全项目 SQL 的
  列名引用。本次就是靠它之外的 e2e 抓到 travel_modes 并没有 line_name
  (该列属于 trip_nodes),一个列名写错导致节点详情页整页 Fatal。
2026-09-22 22:03:08 +08:00

160 lines
6.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 节点花销面板
* 依赖外部变量:$node、$expenses、$expenseSummary、$familyUsers、
* $categories、$currencies(code=>中文名)、$expenseSymbols(code=>符号)、$currentUserId
*/
$expenses = $expenses ?? [];
$familyUsers = $familyUsers ?? [];
$categories = $categories ?? [];
$currencies = $currencies ?? [];
$expenseSymbols = $expenseSymbols ?? [];
$expenseSummary = $expenseSummary ?? ['budget' => 0.0, 'actual' => 0.0, 'count' => 0, 'diff' => 0.0];
$budget = (float)$expenseSummary['budget'];
$actual = (float)$expenseSummary['actual'];
$diff = (float)$expenseSummary['diff'];
$money = static function (float $v): string {
return number_format($v, 2);
};
?>
<section class="panel" id="expenses">
<h2 class="section-title">💰 花销 (<?= (int)$expenseSummary['count'] ?>)</h2>
<div class="expense-summary">
<div class="expense-stat">
<span class="muted">预算</span>
<strong>¥<?= $money($budget) ?></strong>
</div>
<div class="expense-stat">
<span class="muted">实际</span>
<strong>¥<?= $money($actual) ?></strong>
</div>
<div class="expense-stat">
<span class="muted"><?= $diff > 0 ? '超出预算' : '比预算省' ?></span>
<strong class="<?= $diff > 0 ? 'money-over' : 'money-under' ?>">¥<?= $money(abs($diff)) ?></strong>
</div>
</div>
<?php if (empty($expenses)): ?>
<p class="muted">还没有记账</p>
<?php else: ?>
<div class="expense-list">
<?php foreach ($expenses as $ex):
$sym = $expenseSymbols[$ex['currency']] ?? '';
$splits = [];
foreach (($ex['splits'] ?? []) as $sp) {
$sname = (string)($sp['user_name'] ?? '');
if ($sname === '') $sname = '已注销';
$splits[] = $sname . ' ' . $sym . $money((float)$sp['share_amount']);
}
?>
<div class="expense-row">
<div class="expense-main">
<span class="expense-cat"><?= e($ex['category']) ?></span>
<?php if ((int)$ex['is_budget'] === 1): ?>
<span class="expense-kind expense-kind-budget">预算</span>
<?php else: ?>
<span class="expense-kind expense-kind-actual">实际</span>
<?php endif; ?>
<span class="expense-desc"><?= e($ex['description'] ?: $ex['category']) ?></span>
<span class="expense-amount"><?= e($sym) ?><?= $money((float)$ex['total_amount']) ?></span>
</div>
<div class="expense-sub">
付款:<?= e($ex['payer_name'] ?: '未知') ?>
<?php if (!empty($splits)): ?>
· 分摊:<?= e(implode(' / ', $splits)) ?>
<?php endif; ?>
<form method="post" action="<?= url('expenses/destroy') ?>" style="display:inline"
onsubmit="return confirm('删除这笔花销?');">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="id" value="<?= (int)$ex['id'] ?>">
<button type="submit" class="comment-del">删除</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<details class="inline-panel">
<summary>+ 记一笔花销</summary>
<form method="post" action="<?= url('expenses/store') ?>" class="form form-plain">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="node_id" value="<?= (int)$node['id'] ?>">
<div class="form-row two-col">
<div>
<label>金额 *</label>
<input type="text" name="total_amount" required placeholder="例如:268.50">
</div>
<div>
<label>币种</label>
<select name="currency">
<?php foreach ($currencies as $code => $label): ?>
<option value="<?= e($code) ?>"><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-row two-col">
<div>
<label>类别</label>
<select name="category">
<?php foreach ($categories as $cat): ?>
<option value="<?= e($cat) ?>"><?= e($cat) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label>付款人 *</label>
<select name="payer_user_id" required>
<?php foreach ($familyUsers as $u): ?>
<option value="<?= (int)$u['id'] ?>"
<?= (int)$u['id'] === (int)$currentUserId ? ' selected' : '' ?>>
<?= e($u['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-row">
<label>说明</label>
<input type="text" name="description" maxlength="200" placeholder="例如:三人门票">
</div>
<div class="form-row">
<label>性质</label>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="is_budget" value="0" checked> 实际花费
</label>
<label class="radio-label">
<input type="radio" name="is_budget" value="1"> 预算
</label>
</div>
</div>
<div class="form-row">
<label>分摊人(默认全家平摊,可只勾选部分人)</label>
<div class="check-grid">
<?php foreach ($familyUsers as $u): ?>
<label class="checkbox-label">
<input type="checkbox" name="split_user_ids[]"
value="<?= (int)$u['id'] ?>" checked>
<span><?= e($u['name']) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">保存</button>
</div>
</form>
</details>
</section>