'自家的', '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)。 * * 🔴 **自 v24 第 17 批(用户第 3 条)起,界面上已不再使用** —— * 用户原话「车辆下拉框,不需要出现什么财务里登记的车,只把车辆列出来就行了」。 * 方法本身保留的两个理由: * ① `vehicles.ledger_id` 是**软关联**(某台出行车在财务里对应哪条资产账目), * 将来"按车统计油费/保养"要用它把两边的数字对起来; * ② 删掉方法会让那层对应关系在代码里失去唯一的说明处(注释也跟着没了)。 * **不要再把它接回选车下拉** —— 那正是这一批要改掉的东西。 */ 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; } /** * 选车下拉用的列表 —— **只有 vehicles 表里的车**。 * * ⚠️ 用户 2026-10-05 第 17 批 第 3 条把上一批的做法**推翻**了: * 「车辆下拉框,不需要出现什么财务里登记的车,只把车辆列出来就行了。」 * 第 16 批第 8 条当时说的是"财务里的车直接读取就可以",这一批明确不要了。 * 理由(用户的思路):**出行的车**与**财务里的车子账目**是两回事 —— * 后者记的是"这台车值多少钱"(资产),没有车牌车型,混进选车列表里 * 只会让人在"大众朗逸"和"家里的老车"之间发懵。 * 所以现在选车只认 vehicles 表(自家 / 租的 / 借的都录在这里)。 * * ⚠️ `fromLedger()` 方法**暂时保留但不再被任何界面调用**(见下方注释): * 财务里那些车是真实存在的资产,将来若要"按车统计油费/保养"还会用到; * 删掉它反而会丢掉那层对应关系(`vehicles.ledger_id` 软关联还指着它)。 * * @return array 每项都带 `from` = 'local' */ 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; } }