'餐饮', 'ticket' => '门票', 'traffic' => '交通', 'stay' => '住宿', 'shop' => '购物', 'other' => '其他', ]; /** 一段行程的花销(按时间正序) */ public static function ofNode(int $nodeId): array { $st = Database::get()->prepare( 'SELECT e.*, p.name AS person_name, p.avatar, p.avatar_color FROM trip_expenses e LEFT JOIN persons p ON p.id = e.person_id WHERE e.node_id = ? ORDER BY e.created_at, e.id' ); $st->execute([$nodeId]); return $st->fetchAll(); } /** * 一段行程的花销合计(单位:分)。 * * ⚠️ COALESCE 不能省:没有任何花销时 SUM 返回 NULL,直接拿去 number_format * 会在 PHP 8 上报"传 null 给 float 参数"的弃用告警(页面上会冒一行 Warning)。 */ public static function sumOfNode(int $nodeId): int { $st = Database::get()->prepare( 'SELECT COALESCE(SUM(amount_cents), 0) FROM trip_expenses WHERE node_id = ?' ); $st->execute([$nodeId]); return (int)$st->fetchColumn(); } /** 一段旅程(活动)的总花销:它下面**所有**行程之和(用户第 2 条) */ public static function sumOfEvent(int $eventId): int { $st = Database::get()->prepare( 'SELECT COALESCE(SUM(e.amount_cents), 0) FROM trip_expenses e JOIN trip_nodes n ON n.id = e.node_id WHERE n.event_id = ?' ); $st->execute([$eventId]); return (int)$st->fetchColumn(); } /** * 给一批行程批量补"这一段花了多少"(列表/详情用;N+1 在几十段时很明显)。 * * @param array $rows 行程数组(会被就地补上 `spent_cents`) */ public static function attachSums(array &$rows): void { $ids = []; foreach ($rows as $r) { $id = (int)($r['id'] ?? 0); if ($id > 0) { $ids[] = $id; } } $sum = []; foreach ($ids as $id) { $sum[$id] = 0; } if ($ids) { $in = implode(',', array_fill(0, count($ids), '?')); $st = Database::get()->prepare( "SELECT node_id, COALESCE(SUM(amount_cents),0) AS s FROM trip_expenses WHERE node_id IN ($in) GROUP BY node_id" ); $st->execute($ids); foreach ($st->fetchAll() as $r) { $sum[(int)$r['node_id']] = (int)$r['s']; } } foreach ($rows as &$r) { $r['spent_cents'] = $sum[(int)$r['id']] ?? 0; } unset($r); } /** @return array{ok:bool,id?:int,error?:string} */ public static function add(int $familyId, int $nodeId, array $d): array { $cents = self::parseAmount((string)($d['amount'] ?? '')); if ($cents === null) { return ['ok' => false, 'error' => '金额填得不对(例如 128 或 128.50)']; } $title = mb_substr(trim((string)($d['title'] ?? '')), 0, 200, 'UTF-8'); $cat = isset(self::CATS[(string)($d['category'] ?? '')]) ? (string)$d['category'] : 'other'; $pid = (int)($d['person_id'] ?? 0); if ($pid > 0) { // 只认本家的人(表单可以改,不能凭它把别人家的人在页面上引出来) $st = Database::get()->prepare('SELECT id FROM persons WHERE id = ? AND family_id = ?'); $st->execute([$pid, $familyId]); if ($st->fetchColumn() === false) { $pid = 0; } } $db = Database::get(); $db->prepare( 'INSERT INTO trip_expenses (family_id, node_id, person_id, amount_cents, category, title) VALUES (?,?,?,?,?,?)' )->execute([$familyId, $nodeId, $pid > 0 ? $pid : null, $cents, $cat, $title !== '' ? $title : null]); return ['ok' => true, 'id' => (int)$db->lastInsertId()]; } /** 删一笔(校验它确实属于这个家的某段行程) */ public static function destroy(int $familyId, int $id): bool { $st = Database::get()->prepare('SELECT id FROM trip_expenses WHERE id = ? AND family_id = ?'); $st->execute([$id, $familyId]); if ($st->fetchColumn() === false) { return false; } Database::get()->prepare('DELETE FROM trip_expenses WHERE id = ?')->execute([$id]); return true; } // ---------------- 展示 ---------------- /** 分 → 人话(¥1,280 / ¥12.50;金额为零显示 ¥0) */ public static function money(int $cents): string { $neg = $cents < 0; $v = abs($cents) / 100; // 整数金额不带小数尾巴(¥1280 比 ¥1,280.00 好读);有角分才显示两位 $s = (fmod($v, 1.0) === 0.0) ? number_format($v) : number_format($v, 2); return ($neg ? '-¥' : '¥') . $s; } /** * 表单里那个金额字符串 → 分。 * * ⚠️ 不用 `(float)` 直接乘 100:`128.55 * 100` 在浮点下是 12854.999…, * `(int)` 一截就变成 12854(差一分钱)。这里走**字符串**拆小数点, * 整数部分与小数部分分开算,没有浮点参与。 */ public static function parseAmount(string $raw): ?int { $raw = trim(str_replace([',', ',', '¥', '¥', ' '], '', $raw)); if ($raw === '') { return null; } if (!preg_match('/^(-?)(\d+)(?:\.(\d{1,2}))?$/', $raw, $m)) { return null; } $yuan = (int)$m[2]; $cent = isset($m[3]) ? (int)str_pad($m[3], 2, '0') : 0; $total = $yuan * 100 + $cent; return $m[1] === '-' ? -$total : $total; } }