Files
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

112 lines
4.2 KiB
PHP
Raw Permalink 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 $isEdit = $vehicle !== null; ?>
<div class="page-header">
<div>
<h1><?= $isEdit ? '编辑车辆' : '新增车辆' ?></h1>
<?php if ($isEdit): ?>
<p class="muted">ID:<?= (int)$vehicle['id'] ?></p>
<?php endif; ?>
</div>
<a href="<?= url('vehicles/index') ?>" class="btn">返回列表</a>
</div>
<form method="post" action="<?= url($isEdit ? 'vehicles/update' : 'vehicles/store') ?>" class="form">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<?php if ($isEdit): ?>
<input type="hidden" name="id" value="<?= (int)$vehicle['id'] ?>">
<?php endif; ?>
<div class="form-row">
<label>车牌号 *</label>
<input type="text" name="plate_number" required maxlength="20"
value="<?= e($vehicle['plate_number'] ?? '') ?>"
placeholder="例如:京A·88888 或 沪C12345"
style="text-transform:uppercase;">
<p class="muted" style="font-size:12px; margin-top:6px;">同一家庭内车牌号不可重复</p>
</div>
<div class="form-row">
<label>车型名称 *</label>
<input type="text" name="model_name" required maxlength="100"
value="<?= e($vehicle['model_name'] ?? '') ?>"
placeholder="例如:特斯拉 Model Y、大众 帕萨特">
</div>
<div class="form-row">
<label>车辆类型 *</label>
<div class="radio-group">
<?php foreach ($typeLabels as $key => $label): ?>
<label class="radio-label">
<input type="radio" name="vehicle_type" value="<?= e($key) ?>"
<?= ($vehicle['vehicle_type'] ?? 'own') === $key ? 'checked' : '' ?>>
<span><?= e($label) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<!-- 租车信息(仅租车类型显示) -->
<div class="rental-only">
<div class="form-row">
<label>租车公司</label>
<input type="text" name="rental_company" maxlength="100"
value="<?= e($vehicle['rental_company'] ?? '') ?>"
placeholder="例如:神州租车、一嗨租车">
</div>
<div class="form-row">
<label>租车订单号</label>
<input type="text" name="rental_order_no" maxlength="50"
value="<?= e($vehicle['rental_order_no'] ?? '') ?>"
placeholder="例如:RENT20260801001">
</div>
</div>
<!-- 默认自有车(仅自有车类型显示) -->
<div class="own-only">
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" name="is_default" value="1"
<?= !empty($vehicle['is_default']) ? 'checked' : '' ?>>
<span>设为默认自有车(同一家庭只能有一辆)</span>
</label>
<p class="muted" style="font-size:12px; margin-top:6px;">
选择后,其他自有车的默认标记会被自动取消。
</p>
</div>
</div>
<div class="form-row">
<label>备注</label>
<textarea name="notes" rows="3" placeholder="其他补充说明"><?= e($vehicle['notes'] ?? '') ?></textarea>
</div>
<div class="form-actions">
<a href="<?= url('vehicles/index') ?>" class="btn">取消</a>
<button type="submit" class="btn-primary">保存</button>
</div>
</form>
<script>
(function () {
var radios = document.querySelectorAll('input[name="vehicle_type"]');
if (!radios.length) return;
var rentalBlocks = document.querySelectorAll('.rental-only');
var ownBlocks = document.querySelectorAll('.own-only');
function refresh() {
var val = 'own';
radios.forEach(function (r) { if (r.checked) val = r.value; });
rentalBlocks.forEach(function (el) {
el.style.display = (val === 'rental') ? '' : 'none';
});
ownBlocks.forEach(function (el) {
el.style.display = (val === 'own') ? '' : 'none';
});
}
radios.forEach(function (r) { r.addEventListener('change', refresh); });
refresh();
})();
</script>