多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

TREK 旅行预算与费用分摊完全指南:多币种记账、成员分摊与自动结算(Costs / Budget)

TREK 旅行预算与费用分摊完全指南:多币种记账、成员分摊与自动结算(Costs / Budget) TREK 旅行预算与费用分摊完全指南多币种记账、成员分摊与自动结算Costs / Budget【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK本文基于 TREK 开源仓库的 wiki/Budget-Tracking.md 官方文档展开深入讲解自托管旅行规划器 TREK 的Costs费用跟踪模块——如何按分类记录行程开销、在多成员之间分摊费用、借助多币种换算与自动结算计算器核销债务并结合仓库源码server/src/services/budgetService.ts、server/src/nest/budget/budget.controller.ts、client/src/components/Budget/CostsPanel.tsx 等剖析其底层实现。读完本文你将掌握 Costs 的完整使用流程、三种费用分摊模式、汇率冻结机制与结算算法的设计原理并能直接在自己的 TREK 实例中落地这套预算方案。Costs 是什么一个从 Budget 更名而来的行程记账模块Costs 是 TREK 行程规划器内的费用跟踪功能按分类记录每笔支出、在多成员之间拆分费用并以卡片、图表等形式可视化整体花费。值得注意的是一次命名变更v3.3.0对应 issue #1464该功能现在在界面中统一叫 Costs——规划器的标签页显示为Costs在 Admin → Addons插件管理中也以Costs列出其内部 addon id 仍是budget因此权限名为budget_editMCP 作用域为budget:read/budget:write。也就是说你在界面上看到的是 Costs而在权限配置、API 路径与 MCP 授权中见到的是 budget两者指向同一个模块。在哪里找到 Costs打开行程规划器中的Costs标签页即可。该标签页仅在Costs 插件addon启用后才显示。管理员注意Costs 是一个 addon。需要在 Admin-Addons 中启用它。从源码看addon 启停由addons表驱动server/src/db/schema.ts 中addons表以enabled字段控制启用后前端规划器才会渲染 Costs 标签页。货币体系三种货币各司其职Costs 是多币种的issue #551。涉及三个设置它们承担完全不同的职责设置位置作用行程货币trip currencyTrip → Edit trip编辑行程行程的记账基准accounting base所有余额与结算都在该货币下计算每笔费用的币种费用弹窗中挑选按收据原样录入卢布行程中的 $100 晚餐记作100 USD在保存时冻结汇率换算为行程货币因此已结算的债务不会因市场波动而重新开口显示货币display currencySettings → General设置 → 通用只影响你读到的金额——总额、图表、余额——统一换算成一种货币。它不改变任何已存储的数据。保持默认的Trip currency时每个行程用其自身货币展示系统支持165 种货币汇率来自 Frankfurter无需 API key。当某条目的币种与显示货币不同时弹窗中会同时展示换算金额与汇率1 {from} in {to}账本行也会双币显示例如$100.00 → 7 668,71 ₽。更完整的说明请阅读 Currencies三种货币如何交互、切换行程货币时会发生什么、公开分享链接以哪种货币展示。底层原理汇率冻结与行程货币换锚“保存时冻结汇率”并非一句空话源码中有明确的实现server/src/services/budgetService.ts 的freezeForeignRate存储的exchange_rate含义是“每 1 单位行程货币可兑换多少单位条目/显示货币”结算时通过amount / rate完成换算只有当币种是外币且调用方未显式给出汇率时才冻结如果币种没有变化不会重新冻结避免无关编辑移动资金见 issue #1335 / #1445汇率拉取失败时优雅降级为实时汇率不抛异常。汇率数据由 server/src/services/exchangeRateService.ts 提供请求api.frankfurter.devv2 接口按基准币种做6 小时内存缓存并合并并发请求失败时回退到上次缓存仍失败则按恒等换算处理。当行程的基础货币被切换时rebaseTripCurrencyissue #1543会在切换前把所有currency NULL含义为“行程自身货币”的行固定到旧币种并按新币种重新冻结每行的汇率同时给有价格的地点places补上币种。已存储的金额数字不会被改写——每笔支出保留用户输入的币种与数字其真实价值得以保全。该函数必须在同步的行程更新之前运行better-sqlite3 事务无法 await因此 create/update 保持同步这一约束在源码注释中有明确说明。分类Categories支出按分类分组每个分类带一个小的彩色方块指示器随着分类增多颜色在12 色调色板中循环。需要指出的是新版 Costs 面板在 client/src/components/Budget/costsCategories.tsx 中定义了12 个固定分类accommodation 住宿、food 餐饮、groceries 杂货、transport 交通、flights 航班、activities 活动、sightseeing 观光、shopping 购物、fees 手续费、health 健康、tips 小费、other 其他每个分类配有 Lucide 图标与专属颜色旧版自由文本分类如Flight、Train、Hotel等会通过LEGACY_CATEGORY_MAP映射到固定 key避免历史数据落回other。从工具栏可以对分类执行添加分类——输入名称后点击按钮或按 Enter重命名分类——点击分类标题旁的铅笔图标重排分类——拖动分类标题左侧的拖拽手柄删除分类——点击分类标题中的垃圾桶图标。这会删除其中全部费用条目。费用条目Expense items每个分类下是一张条目表包含以下列列说明Name名称可内联编辑关联到预订时只读Total总额该条目的总花费Persons人数人数多成员行程中显示成员 chipsDays天数天数Per Person人均计算值Total ÷ PersonsPer Day每日计算值Total ÷ DaysPer Person/Day人·日计算值Total ÷ (Persons × Days)Date日期可选的支出日期Note备注自由文本备注点击任意可编辑单元格即可内联编辑拖动 grip 手柄可在分类内重排条目每个分类表格底部有内联add row添加行用于新增条目。在数据层面条目的存储表为budget_itemsserver/src/db/schema.ts包含trip_id、category、name、total_price、persons、days、note、sort_order、currency、exchange_rate、expense_date、reservation_id等字段关联成员与付款人分别存放在budget_item_members与budget_item_payers表分类顺序保存在budget_category_order表。createBudgetItem会为每个新分类自动分配下一个sort_order保证顺序稳定。分摊费用Splitting costsPersons列在不同行程类型下行为不同单人行程——直接输入人数多成员行程——出现成员 chip 选择器。点击编辑按钮打开费用弹窗可以选择三种分摊模式平均分摊Equally把费用在所选成员间平均拆分。因舍入产生的余数分币remainder cents会被确定性分配并按条目 ID 轮转splitEqualShares先把总额转为分base floor(totalCents / n)余数totalCents % n分给从itemId % n起始的成员保证整个行程下来每个人的承担额完全均衡。该函数在前端 client/src/components/Budget/CostsPanel.tsx 与服务端 server/src/services/budgetService.ts 中各有实现逻辑一致。自定义分摊Custom为每位旅行者输入具体金额。所有自定义分摊额之和必须精确等于总价。前端用payersBalanced/rebalancePayersclient/src/components/Budget/CostsPanel.helpers.ts做校验与再平衡splitCents按整分把差额摊给未固定的成员payersBalanced以“分”为单位判断是否与总额一致不一致则禁止保存——因为服务端会从 payer 总和重新推导total_price不平衡的 payer 列表会静默改写费用总额。逐项分摊Ticket构建一份明细清单例如苹果 $10、蛋糕 $50、牛奶 $40并为每个明细项指定分摊的行程参与者。单项份额按“分”精确计算费用总价自动求和明细分摊清单在多次编辑间保存/恢复。前端calculateTicketShares对每个明细项做整数分拆分后累加total为各明细项价格之和服务端将明细 JSON 存放在条目的note字段以TICKETJSON:前缀标识CSV 导出时会排除这类机器备注。点击已分配的成员 chip 可再次标记为已付款chip 显示绿色圆环对应服务端toggleMemberPaid写入budget_item_members.paid。结算计算器Settlement calculator当多个成员被分配到支出且成员之间存在未结清债务时总额卡片内会出现一个可折叠的Settlement结算区域点击标题展开后展示转账流向transfer flows谁付给谁、付多少净余额net balances每个成员的总体盈余或亏空。计算使用贪心匹配算法server/src/services/budgetService.ts 的calculateSettlement先按“该付的与应收的”把成员分为债务方debtor与债权方creditor各自按金额降序排序然后用双指针每次取两端较小值产生一笔转账直到全部抵消。最终得到的是清偿所有债务所需的最少转账次数。关于币种与汇率结算有两个关键设计余额始终以行程货币净额计算最后才一次性换算成显示货币——避免每次费用各自按不断变动的实时汇率舍入导致贪心简化器把余额重排成虚构的三方微转账issue #1382一笔已记录的转账payment也携带自己的币种用欧元转账清偿卢布债务是完全正常的因此转账弹窗带币种选择器记录时同样冻结汇率issue #1445。用其他币种做的转账在账本中双币显示$30.00 → 27,00 €。服务端createSettlement/updateSettlement都会先调用freezeForeignRate更新时传入既有转账的存储币种保证不改币种的编辑不会因实时汇率漂移而重新打开已结算的账目。已记录的转账持久化在budget_settlements表server/src/db/schema.ts可在账本中内联编辑与撤销undo即界面上完整的结算历史记录。Costs 汇总Costs summary右侧栏包含两个组件Total card总额卡片——以大字号展示总计多成员行程中还展示带比例条proportional bar的人均明细。桌面端上方另有四张汇总卡片You owe你欠款、Youre owed别人欠你、Outstanding未结清即已记总额但无人付款的条目、Total trip spend行程总花费其中 Total 卡片底部同时给出你的份额your share与已付金额you paidclient/src/components/Budget/CostsPanel.tsx 的totals计算逻辑。Donut chart环形图——按分类统计支出。每个扇区使用该分类的颜色图例始终展示每个分类的金额与百分比悬停图例行会高亮对应扇区。新版实现CategoryBreakdown以横向条形图按金额降序排列分类条长相对最贵分类缩放便于阅读排名。导出Exporting点击工具栏的Export CSV可将全部支出下载为电子表格v3.3.0 恢复该功能issue #1500。实现细节client/src/components/Budget/CostsPanel.tsx 的handleExportCsv文件以分号;分隔带UTF-8 BOMExcel 可直接打开不乱码行按日期排序无日期的排前文件命名为costs-trip.csv行程名中的非法字符会被清洗列结构为Date, Name, Category, Amount, Currency, Amount (显示货币), Note——每笔支出同时给出其原始币种金额与换算后的显示币种金额对包含分隔符、引号或换行的单元格做双引号转义。权限Permissions所有写操作增/改/删条目与分类以及设置条目的币种都需要budget_edit权限。行程货币归属行程本身因此修改它需要trip_edit权限。服务端在 server/src/nest/budget/budget.service.ts 中通过checkPermission(budget_edit, ...)校验控制器层对无权限请求返回 403server/src/nest/budget/budget.controller.ts。API 与实时同步Costs 的 REST 接口统一挂在/api/trips/:tripId/budget下JWT 保护server/src/nest/budget/budget.controller.ts每次变更都会通过 WebSocket 向行程广播事件实现多人实时协作方法路径事件GET/budget—GET/budget/summary/per-person—GET/POST/budget/settlement、/budget/settlementsbudget:settlement-created/-updated/-deletedPOST/budgetbudget:createdPUT/budget/reorder/items、/budget/reorder/categoriesbudget:reorderedPUT/budget/:idbudget:updatedPUT/budget/:id/membersbudget:members-updatedPUT/budget/:id/payersbudget:updatedPUT/budget/:id/members/:userId/paidbudget:member-paid-updatedDELETE/budget/:idbudget:deleted值得注意的一个联动当价格关联到预订的费用条目total_price变化时syncReservationPrice会把新价格写回预订reservation的 metadata 并广播reservation:updated失败不会阻断预算更新非致命。同时预订相关分类如酒店、航班可通过linkBudgetItemToReservation自动生成费用条目这也是文档See also中关联 Reservations-and-Bookings 的原因。参见CurrenciesAdmin-AddonsReservations-and-BookingsTrip-Planner-Overview【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表