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 导出数据到最终分钱,主线是一条"导入即全量重算"的流水线(在数据导入页上传后、同一个请求内按序执行):
TRUNCATE trips 和 owner_earnings 全量重建,因此必须一次性上传所有历史年份的 CSV;cars 表按 VIN 增量更新、不清空。保护机制:文件解析不出任何有效行程时会在清空旧数据之前中止;流水线任一步失败都会明确报错(不再假装成功),完成后返回各步骤真实统计与未匹配车牌清单。
owner_id=31(DUMMY 占位车主)名下 100%,需人工到"车辆所有权管理"改派真实车主。
"…(NJ #T91ULJ)" 中提取 # 后的车牌,按 cars.plate 精确匹配回填。车牌是行程与车辆关联的唯一纽带——匹配不上的行程不参与后续分成。
shareable_profit(可分配利润,车主分成基数)和 actual_earning(实际收益,管理月报收入口径)。公式见第三章。
car_owners.ownership_percentage 拆分写入 owner_earnings;并对近 24 个月中的每个月份,凡该月没有 completed 真实行程(起止完整落在当月)的车,生成一条该月的 D 开头占位(dummy)行程,并给该车每位车主各插一条 0.01 哨兵记录,保证报表里每辆车都可见。
/api/reports/monthly,由后端按统一口径聚合(dummy 剔除、行程去重、无行程车辆的开支照常入表);具体账单页仍直接查询分成明细。资金实际收付再通过"车主余额管理"手工记账(与报表无自动联动)。
这是全系统的核心。一笔 Turo 行程的钱按以下六步拆分:①–③ 在数据导入时由 ProfitCalculator 算好落库(有单元测试锁定口径);④⑤ 由后端 /api/reports/monthly 按月聚合;⑥ 在管理月报页面按系统设置比例展示。
即只有租金类收入参与车主分成。以下项目不进该基数:delivery_fee(送车费)、extras_fee、late_fee(迟还费)、improper_return_fee、airport_fee、cleaning_fee(清洁费)、smoking_fee(吸烟费)、host_fines、gas_fee、sales_tax,以及代收代付类(过路费罚单、EV 充电、油费补贴)。折扣字段在库中以负数存储,公式做加法即为扣减。
从 Turo 总入账中剔除四类"代收转付"款(这些钱最终要还给实际垫付方,不是经营收益)。
trip_status = 'completed' 且 shareable_profit > 0 的行程;取消/未完成/零利润行程不产生分成。car_owners.ownership_percentage,页面默认 70,即车主 70% / 管理 30%);一辆车可有多个车主,各自按占比拿钱。系统不校验占比之和是否为 100。california.owner.id,留空=无特例);总公司库未配置时沿用历史默认 owner_id=2,分公司库默认无特例。owner_id=31 是 DUMMY 占位车主,导入时无主车辆自动挂其名下 100%——它的"分成"不代表真实车主收益。注意两个基数不同(车主按 shareable_profit 分,管理拿 actual_earning 的剩余),展开后等价于:
也就是说:管理方除了拿"剩余百分比",还独享两口径的差额——送车费、迟还费、机场费、清洁费、吸烟费、host_fines 等不参与车主分成的收入全归管理;反过来 Turo 平台抽成造成的差额(可能为负)也由管理方独自承担。(owner_id=2 特例车除外:其额外返还的过路费、清洁费、吸烟费等相当于从管理利润中扣出,上面的等价式对它不成立。)系统没有一个全局固定的"管理占总收益 X%",实际比例由每辆车的车主占比配置决定。
"支出补偿管理"里的每笔记录有一个 ownerShare(页面叫"车主参与")标志,决定这笔钱进哪边的月报:
−|amount| 扣减,无论录入正负;补偿按录入原值直接累加(不取绝对值,录成负数会反向扣减)。成员名单与比例在"系统设置 → 管理团队成员"配置(存于 app_settings 的 management.members,JSON 数组)。比例合计 ≠ 100% 时页面只给黄色警告、不做归一化。
reservation_id = "D{turoId}_{年}{月}" 的占位行程(guest_name = "No Revenue This Month",金额 0.01 哨兵值),并给该车每位车主各插一条 0.01 分成记录——目的是让无收入的车仍出现在月报中。两份月报均不将 dummy 计入行程数/天数/金额。注意:只有跨月行程的车该月仍会生成 dummy,报表中会与真实行程并存。/api/reports/monthly?scope=owner 统一聚合;点击车辆行弹出该车当月行程 / 支出 / 补偿三张明细表;支持 CSV 导出;表尾为全量合计。/api/reports/monthly?scope=management(按行程算管理分成 → 按车聚合 → 叠加 ownerShare=0 的开支/收入)→ 按 management.members 比例给成员分钱;表尾为全量合计。DELETE /api/car-owners/{id} 接口或改数据库。?branch=N 参数指定分公司。该页无鉴权,分享链接需谨慎。forthright,其余为 forthright_br_<ID>。分公司注册表固定在主库 forthright.branches。X-Branch-Id 请求头;后端 BranchFilter 识别后按请求切换数据库(也支持 ?branch=N 查询参数,用于无法带请求头的场景)。右上角切换器换分公司后整页刷新。branch-schema.sql(11 张业务表)建表。删除分公司只删注册记录,数据库保留在 MySQL 中(可人工恢复)。| 表 | 作用与关键字段 |
|---|---|
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_profit、actual_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_id 以 D 开头 | 系统生成的 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>。 |
本指南依据当前代码整理(2026-07,含月报后端化与导入流水线重构后的行为)。若代码有更新,以实际实现为准;核心算法出处:calc/ProfitCalculator.java(利润与分成公式)、OwnerEarningsCalculator.java(分成落库)、MonthlyReportService.java(月报聚合)。