2026-10-05 13:26:49 +08:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* 车辆库(v24 第 16 批 用户第 8 条)。
|
|
|
|
|
*
|
|
|
|
|
* 用户原话:「增加一个设置按钮,用来设置车辆管理、出行方式、节点类型。
|
|
|
|
|
* 注意,我们在财务中,已经把车辆添加进去了,所以这里不用重复添加
|
|
|
|
|
* 那里面的车辆,直接读取就可以,但是可以添加其他的车辆,
|
|
|
|
|
* 比如租的车,借别人的车。」
|
|
|
|
|
*
|
|
|
|
|
* ⚠️ 车辆有**两个来源**,这里刻意**不合并**(合并 = 同一台车两处维护):
|
|
|
|
|
* ① 财务里的车子 —— `ledger_entries.kind='car'` 的那些**账目**。
|
|
|
|
|
* 它记的是"这台车值多少钱"(一台车的资产),**没有车牌、没有车型名**,
|
|
|
|
|
* 严格说不是"一辆能挑来出行的车"。用户说"直接读取就可以",
|
|
|
|
|
* 所以这里把它作为**只读候选**列出来(标成「来自财务」),
|
|
|
|
|
* 选中时只记一个名字,不往 vehicles 表里复制。
|
|
|
|
|
* ② vehicles 表 —— 真正"出行用的车":有车牌、车型名、是自家/租的/借的。
|
|
|
|
|
* 自家车在这里**可以**录(录了就干净、好挑),但**不强制**。
|
|
|
|
|
*
|
|
|
|
|
* 为什么不给两者加外键:账目随时可能被改名或删除(财务那边是它自己的节奏),
|
|
|
|
|
* 硬绑会让车辆记录跟着消失 —— 而出行的车不该因为"这台车在财务里被删了"就没了。
|
|
|
|
|
* 真要对上就靠车型名(人工),这是有意的松耦合。
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App;
|
|
|
|
|
|
|
|
|
|
final class Vehicle
|
|
|
|
|
{
|
|
|
|
|
/** 车辆来源:自家 / 租的 / 借别人的 */
|
|
|
|
|
public const TYPES = [
|
|
|
|
|
'own' => '自家的',
|
|
|
|
|
'rental' => '租的',
|
|
|
|
|
'borrowed' => '借的',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** 一个家的车(vehicles 表里的) */
|
|
|
|
|
public static function all(int $familyId): array
|
|
|
|
|
{
|
|
|
|
|
$st = Database::get()->prepare(
|
|
|
|
|
'SELECT * FROM vehicles WHERE family_id = ?
|
|
|
|
|
ORDER BY is_default DESC, vtype, id'
|
|
|
|
|
);
|
|
|
|
|
$st->execute([$familyId]);
|
|
|
|
|
return $st->fetchAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 财务里已登记的车子(**只读引用**,不复制进 vehicles)。
|
|
|
|
|
*
|
2026-10-05 16:45:10 +08:00
|
|
|
* 🔴 **自 v24 第 17 批(用户第 3 条)起,界面上已不再使用** ——
|
|
|
|
|
* 用户原话「车辆下拉框,不需要出现什么财务里登记的车,只把车辆列出来就行了」。
|
|
|
|
|
* 方法本身保留的两个理由:
|
|
|
|
|
* ① `vehicles.ledger_id` 是**软关联**(某台出行车在财务里对应哪条资产账目),
|
|
|
|
|
* 将来"按车统计油费/保养"要用它把两边的数字对起来;
|
|
|
|
|
* ② 删掉方法会让那层对应关系在代码里失去唯一的说明处(注释也跟着没了)。
|
|
|
|
|
* **不要再把它接回选车下拉** —— 那正是这一批要改掉的东西。
|
2026-10-05 13:26:49 +08:00
|
|
|
*/
|
|
|
|
|
public static function fromLedger(int $familyId): array
|
|
|
|
|
{
|
|
|
|
|
// ⚠️ 表可能还不存在(极老库 / 升级中途)→ 宽容返回空,别把"选车"整块炸掉
|
|
|
|
|
try {
|
|
|
|
|
$st = Database::get()->prepare(
|
|
|
|
|
"SELECT e.id, e.title, e.note FROM ledger_entries e
|
|
|
|
|
JOIN ledgers l ON l.id = e.ledger_id
|
|
|
|
|
WHERE l.family_id = ? AND e.kind = 'car'
|
|
|
|
|
ORDER BY e.id"
|
|
|
|
|
);
|
|
|
|
|
$st->execute([$familyId]);
|
|
|
|
|
} catch (\Throwable $ex) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
$out = [];
|
|
|
|
|
foreach ($st->fetchAll() as $r) {
|
|
|
|
|
$out[] = [
|
|
|
|
|
// 负数 id:与 vehicles 表的正数 id 天然不撞;选车表单提交负数即"来自财务"
|
|
|
|
|
'id' => -((int)$r['id']),
|
|
|
|
|
'ledger_id' => (int)$r['id'],
|
|
|
|
|
'name' => (string)$r['title'],
|
|
|
|
|
'plate' => '',
|
|
|
|
|
'vtype' => 'own',
|
|
|
|
|
'vendor' => null,
|
|
|
|
|
'order_no' => null,
|
|
|
|
|
'is_default'=> 0,
|
|
|
|
|
'note' => (string)($r['note'] ?? ''),
|
|
|
|
|
'from' => 'ledger', // 模板据此标「来自财务」并且不给改/删
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
return $out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-10-05 16:45:10 +08:00
|
|
|
* 选车下拉用的列表 —— **只有 vehicles 表里的车**。
|
2026-10-05 13:26:49 +08:00
|
|
|
*
|
2026-10-05 16:45:10 +08:00
|
|
|
* ⚠️ 用户 2026-10-05 第 17 批 第 3 条把上一批的做法**推翻**了:
|
|
|
|
|
* 「车辆下拉框,不需要出现什么财务里登记的车,只把车辆列出来就行了。」
|
|
|
|
|
* 第 16 批第 8 条当时说的是"财务里的车直接读取就可以",这一批明确不要了。
|
|
|
|
|
* 理由(用户的思路):**出行的车**与**财务里的车子账目**是两回事 ——
|
|
|
|
|
* 后者记的是"这台车值多少钱"(资产),没有车牌车型,混进选车列表里
|
|
|
|
|
* 只会让人在"大众朗逸"和"家里的老车"之间发懵。
|
|
|
|
|
* 所以现在选车只认 vehicles 表(自家 / 租的 / 借的都录在这里)。
|
|
|
|
|
*
|
|
|
|
|
* ⚠️ `fromLedger()` 方法**暂时保留但不再被任何界面调用**(见下方注释):
|
|
|
|
|
* 财务里那些车是真实存在的资产,将来若要"按车统计油费/保养"还会用到;
|
|
|
|
|
* 删掉它反而会丢掉那层对应关系(`vehicles.ledger_id` 软关联还指着它)。
|
|
|
|
|
*
|
|
|
|
|
* @return array 每项都带 `from` = 'local'
|
2026-10-05 13:26:49 +08:00
|
|
|
*/
|
|
|
|
|
public static function pickable(int $familyId): array
|
|
|
|
|
{
|
|
|
|
|
$out = [];
|
|
|
|
|
foreach (self::all($familyId) as $v) {
|
|
|
|
|
$v['from'] = 'local';
|
|
|
|
|
$out[] = $v;
|
|
|
|
|
}
|
|
|
|
|
return $out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function find(int $familyId, int $id): ?array
|
|
|
|
|
{
|
|
|
|
|
if ($id <= 0) {
|
|
|
|
|
return null; // 负数/0 是"来自财务的引用",不是本表的行
|
|
|
|
|
}
|
|
|
|
|
$st = Database::get()->prepare('SELECT * FROM vehicles WHERE id = ? AND family_id = ?');
|
|
|
|
|
$st->execute([$id, $familyId]);
|
|
|
|
|
$r = $st->fetch();
|
|
|
|
|
return $r ?: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 「走到哪都显示得出来」的一行字:车牌 + 车型 */
|
|
|
|
|
public static function label(array $v): string
|
|
|
|
|
{
|
|
|
|
|
$plate = trim((string)($v['plate'] ?? ''));
|
|
|
|
|
$name = trim((string)($v['name'] ?? ''));
|
|
|
|
|
if ($plate !== '' && $name !== '') {
|
|
|
|
|
return $plate . ' ' . $name;
|
|
|
|
|
}
|
|
|
|
|
return $plate !== '' ? $plate : $name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @return array{ok:bool,id?:int,error?:string} */
|
|
|
|
|
public static function save(int $familyId, array $d, ?int $id = null): array
|
|
|
|
|
{
|
|
|
|
|
$name = mb_substr(trim((string)($d['name'] ?? '')), 0, 100, 'UTF-8');
|
|
|
|
|
if ($name === '') {
|
|
|
|
|
return ['ok' => false, 'error' => '车型名不能为空'];
|
|
|
|
|
}
|
|
|
|
|
$vtype = isset(self::TYPES[(string)($d['vtype'] ?? '')]) ? (string)$d['vtype'] : 'own';
|
|
|
|
|
$row = [
|
|
|
|
|
mb_substr(trim((string)($d['plate'] ?? '')), 0, 20, 'UTF-8'),
|
|
|
|
|
$name,
|
|
|
|
|
$vtype,
|
|
|
|
|
self::txt($d['vendor'] ?? '', 100),
|
|
|
|
|
self::txt($d['order_no'] ?? '', 50),
|
|
|
|
|
(int)($d['is_default'] ?? 0) > 0 ? 1 : 0,
|
|
|
|
|
self::txt($d['note'] ?? '', 500),
|
|
|
|
|
];
|
|
|
|
|
$db = Database::get();
|
|
|
|
|
// ⚠️ 默认车**只能有一台**:把别的都清掉再设,否则"默认"就失去意义
|
|
|
|
|
// (新建行程时默认选中的是哪一台,取决于 SQL 的排序运气)。
|
|
|
|
|
if ($row[5] === 1) {
|
|
|
|
|
$db->prepare('UPDATE vehicles SET is_default = 0 WHERE family_id = ?')->execute([$familyId]);
|
|
|
|
|
}
|
|
|
|
|
if ($id !== null && $id > 0) {
|
|
|
|
|
if (!self::find($familyId, $id)) {
|
|
|
|
|
return ['ok' => false, 'error' => '找不到这辆车'];
|
|
|
|
|
}
|
|
|
|
|
$db->prepare('UPDATE vehicles SET plate=?, name=?, vtype=?, vendor=?, order_no=?,
|
|
|
|
|
is_default=?, note=? WHERE id=? AND family_id=?')
|
|
|
|
|
->execute([...$row, $id, $familyId]);
|
|
|
|
|
return ['ok' => true, 'id' => $id];
|
|
|
|
|
}
|
|
|
|
|
$db->prepare('INSERT INTO vehicles (family_id, plate, name, vtype, vendor, order_no,
|
|
|
|
|
is_default, note) VALUES (?,?,?,?,?,?,?,?)')
|
|
|
|
|
->execute([$familyId, ...$row]);
|
|
|
|
|
return ['ok' => true, 'id' => (int)$db->lastInsertId()];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删一辆车。
|
|
|
|
|
*
|
|
|
|
|
* ⚠️ 只用**本表**的行能被删(负数 id 是财务的引用,删它等于动财务数据 —— 不干)。
|
|
|
|
|
* ⚠️ 已被行程引用的车不许删:删了行程上的 `vehicle_id` 就成了悬空值
|
|
|
|
|
* (外键是 SET NULL,数据不丢,但用户会看到"我明明选过车,现在没了")。
|
|
|
|
|
* 给一句"还有 N 段行程在用",比静默清空好。
|
|
|
|
|
*/
|
|
|
|
|
public static function destroy(int $familyId, int $id): array
|
|
|
|
|
{
|
|
|
|
|
if ($id <= 0) {
|
|
|
|
|
return ['ok' => false, 'error' => '来自财务的车子请到财务里改'];
|
|
|
|
|
}
|
|
|
|
|
if (!self::find($familyId, $id)) {
|
|
|
|
|
return ['ok' => false, 'error' => '找不到这辆车'];
|
|
|
|
|
}
|
|
|
|
|
$st = Database::get()->prepare('SELECT COUNT(*) FROM trip_nodes WHERE vehicle_id = ?');
|
|
|
|
|
$st->execute([$id]);
|
|
|
|
|
$n = (int)$st->fetchColumn();
|
|
|
|
|
if ($n > 0) {
|
|
|
|
|
return ['ok' => false, 'error' => '还有 ' . $n . ' 段行程在用这辆车,先改掉它们再删'];
|
|
|
|
|
}
|
|
|
|
|
Database::get()->prepare('DELETE FROM vehicles WHERE id = ? AND family_id = ?')
|
|
|
|
|
->execute([$id, $familyId]);
|
|
|
|
|
return ['ok' => true];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function txt($v, int $max): ?string
|
|
|
|
|
{
|
|
|
|
|
$v = mb_substr(trim((string)$v), 0, $max, 'UTF-8');
|
|
|
|
|
return $v !== '' ? $v : null;
|
|
|
|
|
}
|
|
|
|
|
}
|