系统指南

Forthright Billing(Turo 车辆代管记账系统)的设计说明:整体架构、核心分成算法、各功能模块与重要约定。供日常使用与维护参考。

一、系统概览

本系统为 Turo 车辆代管业务的记账与分钱平台:车主把车托管给管理团队在 Turo 上运营,系统负责导入 Turo 结算数据、按每辆车配置的股权占比把收益拆分给车主与管理方、生成双方的月度分钱报告,并配套车辆档案、开支报销、保险、余额、验车、过路费对账等运营工具。

技术栈 Spring Boot 3.2.3(Java 17)单体应用 + 纯静态 HTML/JS 页面(Bootstrap 5 + jQuery + DataTables)+ MySQL。PDF 生成用 iText 5。无前端框架、无构建步骤,页面直接放在 src/main/resources/static/
架构形态 浏览器页面 → /api/** REST 接口(JPA/原生 SQL 查询)→ MySQL。两份月度分钱报表由后端 /api/reports/monthly 统一聚合计算(车主/管理两侧同一套口径),页面只负责渲染;分成结果(owner_earnings)由数据导入流水线批量算好落库,核心公式集中在 ProfitCalculator 并有单元测试锁定。
运行配置 开发:端口 8081,MySQL localhost:3309/forthright;生产(prod profile):端口 8082,MySQL localhost:3306,上传目录 /opt/forthrightBillingFront/uploads。全链路时区统一为 UTC。JPA ddl-auto=none,表结构由 SQL 脚本人工维护。
后台任务 全系统没有任何定时任务(无 @Scheduled)。所有批处理(导入、利润计算、分成)都在"数据导入"的 HTTP 请求内同步执行——导入即刷新。
安全 系统无登录鉴权(车辆/车主/行程/分成/余额等核心数据接口还标注了 @CrossOrigin * 开放跨域),依赖网络层(如内网/反向代理)做访问控制。部署时切勿直接暴露公网。
二、端到端数据流

从 Turo 导出数据到最终分钱,主线是一条"导入即全量重算"的流水线(在数据导入页上传后、同一个请求内按序执行):

上传数据(data-import.html) 上传 Turo 车辆 JSON(turo_cars.json,可选)与行程收益 CSV(可多个年份文件,必选),文件落盘在 uploads/。⚠️ 每次导入会 TRUNCATE tripsowner_earnings 全量重建,因此必须一次性上传所有历史年份的 CSV;cars 表按 VIN 增量更新、不清空。保护机制:文件解析不出任何有效行程时会在清空旧数据之前中止;流水线任一步失败都会明确报错(不再假装成功),完成后返回各步骤真实统计与未匹配车牌清单。
导入车辆(LoadCars,仅上传了 JSON 时) 按 VIN upsert 车辆档案:新车插入时写入 turo_id、年款、品牌、型号、车牌、图片;已存在的车只更新后五项,turo_id 不会更新。没有任何车主记录的车会被自动挂到 owner_id=31(DUMMY 占位车主)名下 100%,需人工到"车辆所有权管理"改派真实车主。
导入行程(LoadTrips) 逐行解析 CSV 写入 trips 表(44 个字段:租金、9 档折扣、各类费用、代收款、总收益等)。自动识别 Turo 新旧两种导出格式。
车辆匹配(UpdateTuroCarIds) 对 car_turo_id 为空的行程,从车辆名 "…(NJ #T91ULJ)" 中提取 # 后的车牌,按 cars.plate 精确匹配回填。车牌是行程与车辆关联的唯一纽带——匹配不上的行程不参与后续分成。
利润计算(UpdateTripProfits) 为每笔行程算出两个口径并回写:shareable_profit(可分配利润,车主分成基数)和 actual_earning(实际收益,管理月报收入口径)。公式见第三章。
上下架日期(UpdateCarDates) listing_date = 该车最早行程开始日;最后一次行程结束距今满 2 个完整月及以上的车写入 scrap_date(值为最后行程结束日,视为已下架),不足 2 个整月则置空。
车主分成落库(OwnerEarningsCalculator) 对每笔 completed 且 shareable_profit>0 的行程,按 car_owners.ownership_percentage 拆分写入 owner_earnings;并对近 24 个月中的每个月份,凡该月没有 completed 真实行程(起止完整落在当月)的车,生成一条该月的 D 开头占位(dummy)行程,并给该车每位车主各插一条 0.01 哨兵记录,保证报表里每辆车都可见。
报表消费(纯只读) 车主月报与管理月报调用 /api/reports/monthly,由后端按统一口径聚合(dummy 剔除、行程去重、无行程车辆的开支照常入表);具体账单页仍直接查询分成明细。资金实际收付再通过"车主余额管理"手工记账(与报表无自动联动)。
关键依赖:分成金额在导入时一次性算好落库(且只插不改)。修改了车主占比、行程数据修正后,必须重新执行一次数据导入才会反映到分成和报表中——报表端只查不算。
三、核心算法:利润计算与分成

这是全系统的核心。一笔 Turo 行程的钱按以下六步拆分:①–③ 在数据导入时由 ProfitCalculator 算好落库(有单元测试锁定口径);④⑤ 由后端 /api/reports/monthly 按月聚合;⑥ 在管理月报页面按系统设置比例展示。

① 可分配利润 shareable_profit —— 车主分成的基数
shareable_profit = trip_price + boost_price
  + 9 档时长折扣(three_day / one_week / two_week / three_week / one_month / two_month / three_month / early_bird / non_refundable,均为负数)
  + host_promo_credit + excess_distance_fee + cancellation_fee + additional_usage_fee + other_fees

只有租金类收入参与车主分成。以下项目不进该基数:delivery_fee(送车费)、extras_fee、late_fee(迟还费)、improper_return_fee、airport_fee、cleaning_fee(清洁费)、smoking_fee(吸烟费)、host_fines、gas_fee、sales_tax,以及代收代付类(过路费罚单、EV 充电、油费补贴)。折扣字段在库中以负数存储,公式做加法即为扣减。

② 实际收益 actual_earning —— 管理月报的收入口径
actual_earning = total_earnings − tolls_tickets − ontrip_ev_charging − posttrip_ev_charging − gas_reimbursement

从 Turo 总入账中剔除四类"代收转付"款(这些钱最终要还给实际垫付方,不是经营收益)。

③ 车主分成(导入时写入 owner_earnings)
车主分成 = shareable_profit × 该车主的 ownership_percentage ÷ 100
  • 只处理 trip_status = 'completed'shareable_profit > 0 的行程;取消/未完成/零利润行程不产生分成。
  • 占比在"车辆所有权管理"页按车配置(car_owners.ownership_percentage,页面默认 70,即车主 70% / 管理 30%);一辆车可有多个车主,各自按占比拿钱。系统不校验占比之和是否为 100。
  • 特例("加州车"规则):特例车主在按比例分成之外,额外返还 tolls_tickets + ontrip_ev_charging + posttrip_ev_charging + gas_reimbursement + cleaning_fee + smoking_fee。特例车主 ID 按分公司配置(系统设置键 california.owner.id,留空=无特例);总公司库未配置时沿用历史默认 owner_id=2,分公司库默认无特例。
  • 特例(硬编码):owner_id=31 是 DUMMY 占位车主,导入时无主车辆自动挂其名下 100%——它的"分成"不代表真实车主收益。
④ 管理分成(管理月报前端实时计算)
管理行程利润 = actual_earning − Σ该行程所有车主分成
页面显示的管理占比 = 100 − Σ车主占比

注意两个基数不同(车主按 shareable_profit 分,管理拿 actual_earning 的剩余),展开后等价于:

管理行程利润 = shareable_profit × (100 − Σ车主占比) ÷ 100 + (actual_earning − shareable_profit)

也就是说:管理方除了拿"剩余百分比",还独享两口径的差额——送车费、迟还费、机场费、清洁费、吸烟费、host_fines 等不参与车主分成的收入全归管理;反过来 Turo 平台抽成造成的差额(可能为负)也由管理方独自承担。(owner_id=2 特例车除外:其额外返还的过路费、清洁费、吸烟费等相当于从管理利润中扣出,上面的等价式对它不成立。)系统没有一个全局固定的"管理占总收益 X%",实际比例由每辆车的车主占比配置决定。

⑤ 叠加开支与补偿(ownerShare 标志分流)

"支出补偿管理"里的每笔记录有一个 ownerShare(页面叫"车主参与")标志,决定这笔钱进哪边的月报:

车主侧(ownerShare = 1 / true)
车主月报净利润 =
Σ车主分成 车主承担的开支 + 车主享有的补偿
管理侧(ownerShare = 0 / false)
每车管理总利润 =
Σ管理行程利润 管理开支 + 管理收入(补偿)
  • 报表侧对开支金额一律按 −|amount| 扣减,无论录入正负;补偿按录入原值直接累加(不取绝对值,录成负数会反向扣减)。
  • 保险模块自动写入的保费属于开支:该车有车主 → ownerShare=1(车主全额承担);无车主 → ownerShare=0(管理承担)。
  • 开支/补偿对应的车辆当月没有任何分成记录时,报表会为其生成一条零行程行,金额照常计入(不会丢失)。
⑥ 管理团队成员内部分钱
成员分成 = 当月管理总利润(Σ所有车的管理总利润) × 成员 percentage ÷ 100

成员名单与比例在"系统设置 → 管理团队成员"配置(存于 app_settingsmanagement.members,JSON 数组)。比例合计 ≠ 100% 时页面只给黄色警告、不做归一化。

附:月份归属与占位行程规则
  • 归属月:普通行程按行程结束日(trip_end)落月——跨月行程全额计入结束的那个月;D 开头 dummy 行程则要求起止日期完整落在查询区间内。
  • Dummy 占位行程:某车某月没有起止日期完整落在该月内的 completed 真实行程时,导入流水线会生成 reservation_id = "D{turoId}_{年}{月}" 的占位行程(guest_name = "No Revenue This Month",金额 0.01 哨兵值),并给该车每位车主各插一条 0.01 分成记录——目的是让无收入的车仍出现在月报中。两份月报均不将 dummy 计入行程数/天数/金额。注意:只有跨月行程的车该月仍会生成 dummy,报表中会与真实行程并存。
  • 使用率:utilization = 当月租借总天数 ÷ 当月天数 × 100%。
四、功能模块说明
报告管理
车主月度报告(monthly-report.html)
  • 选月份(近 24 个月,默认上月)+ 可选车主,按车汇总:行程数、租借天数、总利润(车主分成合计)、支出(ownerShare=1)、补偿、净利润、日均利润、使用率。
  • 顶部"总利润"卡片显示的实际是净利润(已扣支出、加补偿);另统计当月过路费与罚单总额(剔除 dummy、按行程去重)。
  • 数据来自 /api/reports/monthly?scope=owner 统一聚合;点击车辆行弹出该车当月行程 / 支出 / 补偿三张明细表;支持 CSV 导出;表尾为全量合计。
管理团队报告(management-monthly-report.html)
  • 按第三章 ④⑤⑥ 的公式核算管理方利润:数据来自 /api/reports/monthly?scope=management(按行程算管理分成 → 按车聚合 → 叠加 ownerShare=0 的开支/收入)→ 按 management.members 比例给成员分钱;表尾为全量合计。
  • 车辆表默认按管理总利润降序;点击行查看该车行程明细(含每笔的车主占比/分成、管理占比/分成)、管理开支、管理收入;各表可导出 CSV。
  • 未配置管理团队成员时,成员分成卡片会引导前往系统设置。
具体账单(owner-earnings.html)
  • 逐笔行程分成明细查询:按车主 / 车辆(级联)/ 日期范围过滤(基于行程结束日期),默认上月。
  • 行点击弹窗展示该行程的全部财务字段(价格、折扣、各类费用、代收款、车主占比与分成),支持 CSV 导出。
管理功能
车辆所有权管理(car-ownership-management.html)
  • 维护三份主数据:车辆档案(cars)、车主档案(owners)、车-车主占比(car_owners.ownership_percentage,分成算法的基础配置,默认 70)。
  • 编辑车辆弹窗内可为一辆车添加多个车主并各设占比;保存采用"先全删后重建"策略。⚠️ 把车主全部移除后保存不会生效(旧关系保留)——页面无法把车主清到零,彻底清空须直接调 DELETE /api/car-owners/{id} 接口或改数据库。
  • ⚠️ 改占比只影响下一次数据导入之后的分成计算(owner_earnings 已落库的金额不会自动刷新)。
车主余额管理(owner-balance-management.html)
  • 为每位车主维护资金账户(owners.balance)与流水(存入 / 提取 / 调整),每笔流水记录操作后余额快照。
  • 与月报无自动联动:月结打款、垫付等都要在此手工记账;建议在描述里统一格式(如"2026-06 分成结算")便于对账。
  • 提取不校验余额充足性(可为负);余额是存储值而非流水推导值,勿绕过此页面直接改库。
支出与补偿管理(expense-reimbursement-management.html)
  • 双 Tab 记录车辆开支(car_expense)与补偿收入(car_reim),Excel 式行内编辑,按月/按车前端筛选。
  • "车主参与"复选框 = ownerShare 标志:勾选进车主月报,不勾进管理月报(详见第三章 ⑤)。
  • 记录靠 car_turo_id 关联车辆,录错 ID 不报错但会被月报静默忽略。
车辆状态追踪(car-status-tracking.html)
  • 车辆异常工单:状态(维修 / 失踪 / 完成 / 等待赔付 / 其他)+ 描述 + 创建人,可标记解决(自动记录解决时间与备注)。
  • 统计卡片中"紧急处理" = 未解决且状态为"失踪"或"等待赔付"的记录数。与财务计算无关。
保险管理(insurance-management.html)
  • 每月为每辆车录入保险费:自动作为负数开支写入 car_expense(记账日期硬编码为上月 15 号,描述"保险费用 - yyyy年MM月"),并按月保存一份可"加载上次配置"的快照。
  • 费用归属全有或全无:有车主 → 车主全额承担(ownerShare=1);无车主 → 管理承担。
  • 同月重复提交是幂等的:会先删除本系统之前为该期生成的保费开支再按本次金额重建(手工录入的开支不受影响);系统不记录保单有效期、无到期提醒。
系统设置(settings.html)
  • 分公司级键值设置(app_settings):公司名称(company.name,改导航栏品牌)、检查表默认车主/检查员信息(host.* / inspector.*)、管理团队成员及分成比例(management.members)。
  • 页面顶部的"分公司管理"板块是全局功能:新建分公司(自动建库建表)、改名、删除(总公司 ID=1 不可删)。详见第五章。
工具功能
数据导入(data-import.html)
  • 整条计算流水线的入口(见第二章)。上传 Turo 车辆 JSON + 行程 CSV,同步执行导入与全部重算,完成后展示各步骤真实统计(含未匹配车牌清单);失败会明确报错。
  • 导入写入当前分公司的数据库(右上角切换器决定)。
  • ⚠️ 必须一次性上传所有历史年份 CSV(trips 全量重建);导入成功后自动清理已上传文件。
检查表生成(inspection-form.html / external-inspection.html)
  • 三步向导生成 Turo 年度维护检查表 PDF:选车(读系统 cars 表)→ 填车主/检查员信息(可一键填充系统设置里的默认值、上传签名图)→ 生成并预览。
  • 以 Turo 官方模板为底(运行时联网下载并缓存 24 小时),所有检查项自动勾选 PASS、轮胎花纹深度为 6–8/32 随机值、总体结果恒为 PASS——使用者需自行确认车辆实际状况,知悉合规责任。
  • 生成的 PDF 是临时文件:每次生成新检查表时会顺带清理超过 30 分钟的旧文件,且目录内超过 10 个文件时最旧的 5 个会被立即强删——生成后请立即下载。
  • 另有不在导航栏的外部版 external-inspection.html:第一步改为上传 Turo 导出的 cars.json(不入库),供未录入系统的车辆使用;可用 ?branch=N 参数指定分公司。该页无鉴权,分享链接需谨慎。
Toll 比对(toll-reconciliation.html)
  • 上传 EZPass 交易 CSV,按"车牌(或电子标签映射出的车牌)+ 交易时间落在行程起止之间"匹配行程,返回追加 MATCHED_RESERVATION_ID / MATCHED_VEHICLE / MAPPED_PLATE 三列的 CSV 下载。纯内存处理,不写库。
  • ⚠️ 电子标签(transponder)→ 车牌的映射表硬编码在 Java 源码里(39 条),新车辆/换标签需改代码重新部署;输出中 MAPPED_PLATE 为空即提示映射表缺条目。
  • 时间匹配无宽限期:还车之后才记账的过路费会匹配不到,需人工处理。
五、多分公司机制
  • 数据隔离:每个分公司的业务数据(车辆、车主、行程、报表、设置)存放在独立数据库:总公司 ID=1 用主库 forthright,其余为 forthright_br_<ID>。分公司注册表固定在主库 forthright.branches
  • 请求路由:navbar.js 把当前分公司 ID 存在浏览器 localStorage,并给所有同源请求自动加 X-Branch-Id 请求头;后端 BranchFilter 识别后按请求切换数据库(也支持 ?branch=N 查询参数,用于无法带请求头的场景)。右上角切换器换分公司后整页刷新。
  • 新建分公司:系统设置页在线创建——自动建库并按 branch-schema.sql(11 张业务表)建表。删除分公司只删注册记录,数据库保留在 MySQL 中(可人工恢复)。
  • 数据导入同样走分公司路由:导入流水线(DataImportPipeline)使用应用的路由数据源,"数据导入"页会写入当前分公司对应的数据库;新分公司库导入时会自动补建 DUMMY 占位车主(id=31)。
  • 同一浏览器多标签页共享分公司选择:在 A 标签页切换分公司后,B 标签页的后续请求也会指向新分公司,但页面内容仍是旧的,需手动刷新,注意别看错数据。
六、数据库表一览
作用与关键字段
trips行程主表(导入时全量重建)。reservation_id(唯一;D 开头 = dummy 占位)、car_turo_id(关联 cars.turo_id,由车牌匹配回填)、trip_status(仅 completed 参与分成)、trip_start/trip_end(归属月按 trip_end)、trip_price、boost_price、9 档折扣(负数)、各类费用与代收款、total_earnings、shareable_profitactual_earning
cars车辆档案(按 VIN 增量 upsert,不清空)。turo_id(业务关联键,字符串)、VIN、year/make/model、plate(车牌,行程匹配的纽带)、status、purchase_amount、listing_date、scrap_date、is_insured(当前无任何页面读写,属死字段)。
owners车主档案。name(唯一)、balance(账户余额,权威存储值)。特殊 ID:2=加州车(分成额外返还代收款)、31=DUMMY 占位车主。
car_owners车-车主多对多关系。ownership_percentage(0–100,车主分成比例,分成算法的核心配置;管理占比 = 100 − 合计)。
owner_earnings车主分成结果表(导入时全量重算;只插不改)。owner_id + trip_id 唯一、amount(0.01 为无收入哨兵值)。
owner_balance_transactions车主余额流水。amount(存入正 / 提取负)、balance_after(交易后余额快照)、type(DEPOSIT / WITHDRAWAL / ADJUSTMENT)。
car_expense车辆开支。car_turo_id、expense_date、amount(保险自动入账为负数;报表一律按 −|amount| 扣)、owner_share(1=车主月报,0=管理月报)。
car_reim车辆补偿/报销收入。字段同上,日期字段为 reim_date,金额正向累加。
insurance_tracking月度保险配置快照。period_key(yyyy-MM,同月覆盖)、car_insurance_amounts(每车金额 JSON)、expense_date(上月 15 号)。
car_status_tracking车辆状态工单。car_turo_id、status(页面固定五种:维修/失踪/完成/等待赔付/其他,数据库层无约束)、is_resolved、resolution_date/notes。
app_settings分公司级键值设置。company.name、host.*、inspector.*、management.members(管理团队成员 JSON)。
forthright.branches(仅主库)分公司注册表。id、name、region、schema_name(forthright_br_<id>)。

注:检查表(Inspection)不落库,仅生成临时 PDF;过路费对账纯内存处理,不写库。

七、重要约定速查
约定含义
reservation_idD 开头系统生成的 dummy 占位行程(格式 D{turoId}_{年}{月},月份不补零),代表"该车该月无收入"。金额用 0.01 哨兵值;车主月报的统计会剔除;过路费匹配等查询显式排除。新增手工行程切勿使用 D 前缀。
ownerShare("车主参与")开支/补偿的归属开关:1/true = 车主侧(进车主月报),0/false = 管理侧(进管理月报)。
车辆名含 (#车牌)行程与车辆匹配、以及各报表提取车牌都依赖车辆名中 # 与 ) 之间的车牌串——Turo 上的车辆命名必须保持该格式,且与 cars.plate 一致。
折扣为负数trips 表 9 档折扣按负数存储,利润公式做加法即扣减;导入若出现正数折扣会虚增利润。
"加州车"特例车主分成时额外返还过路费、EV 充电、油补、清洁费、吸烟费;按分公司配置(设置键 california.owner.id),总公司默认 owner_id=2,分公司默认无。
owner_id = 31硬编码 DUMMY 占位车主:导入时无主车辆自动挂其名下 100%,需人工改派。
占比默认 70所有权管理页添加车主时默认 70%,即"车主 70 / 管理 30"的常规约定;系统不强制占比合计 = 100。
归属月 = trip_end 所在月跨月行程全额算进结束月;月报按自然月(近 24 个月可选)。
金额符号开支在报表侧一律按 −|amount| 扣减;保险自动入账本身为负数;余额流水提取存负数。
分公司 ID = 1总公司,库名 forthright,不可删除;其余分公司库名 forthright_br_<ID>。
八、注意事项与已知限制
日常操作须知
  • 改占比 / 修数据后要重新导入:owner_earnings 只在导入时计算且只插不改,任何占比调整、行程修正都要重跑一次数据导入才生效。
  • 导入必须全量:trips 每次导入被清空重建,只传新年份的 CSV 会丢历史数据(传错文件时系统会在清空前中止并报错)。
  • 注意导入结果中的未匹配车牌清单:清单里的行程不参与分成,先到车辆管理补录车辆再重新导入。
  • 车主余额需手工记账:报表算出的应付分成不会自动过账到余额,月结时人工录入。
已知技术限制 / 风险点
  • 无登录鉴权:所有接口(含清空上传目录、改余额等敏感操作)都无访问控制,车辆/车主/行程/分成/余额等核心接口还开放了跨域(@CrossOrigin *);必须部署在受控网络内。
  • 所有权管理页"清空全部车主再保存"不生效(旧关系保留):保存流程只在车主列表非空时才先删后建,页面无法把某车的车主清到零,需直接调 DELETE /api/car-owners/{id} 接口。
  • ownership_percentage 是"当前值":报表中占比列实时取自 car_owners,与历史落库的分成金额可能对不上(占比改过之后)。
  • Toll 比对的电子标签映射表、DUMMY 车主 ID(31)仍硬编码在 Java 源码,调整需改代码重新部署(加州车主已改为系统设置 california.owner.id 可配置)。
  • 页面样式依赖公网 CDN(Bootstrap / Font Awesome / jQuery / DataTables),离线环境页面会失效。

本指南依据当前代码整理(2026-07,含月报后端化与导入流水线重构后的行为)。若代码有更新,以实际实现为准;核心算法出处:calc/ProfitCalculator.java(利润与分成公式)、OwnerEarningsCalculator.java(分成落库)、MonthlyReportService.java(月报聚合)。