'自家的', '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)。 * * 取的是所有账本里 kind='car' 的账目 —— 用户说"直接读取就可以"。 * 返回结构与自有车辆**同形**(id 用负数区分:`-账目id`,`from` 标来源), * 这样模板里画下拉、画配置列表都能用同一套代码。 */ 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; } /** * 选车下拉用的合并列表:财务的车子在前(用户已经录好了),自家车辆在后。 * * @return array 每项都带 `from` = 'ledger' | 'local' */ public static function pickable(int $familyId): array { $out = []; foreach (self::fromLedger($familyId) as $v) { $out[] = $v; } 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; } }