本文根据过去一年参与的支付与账户平台开发整理。为保护业务与合作方信息,项目名称、服务商、域名、内部接口和部署标识均已泛化。
过去一年,我持续迭代一套支付与账户平台。它面向个人用户和运营团队,覆盖账户与钱包、充值提现、跨境汇款、卡产品、清结算、身份认证和风险控制。
这一年的主线覆盖业务模块扩展和业务事实统一。账户、订单、余额、资金流水和运营动作逐步收拢到统一的业务事实中,每次资金变化都需要在同步请求、异步任务和外部回调之间保持一致、可追溯。
我持续负责需求分析、方案设计、数据库迁移、Laravel 管理后台、Go API、外部服务接入、异步任务、测试、部署和运行维护。本文记录的是这段开发经历中的业务边界、技术实现和工程取舍。
业务范围逐步形成完整生命周期
| 业务模块 | 主要能力 |
| 用户与安全 | 注册登录、Token、密码与支付密码、双因子认证、生物识别、设备管理和安全日志 |
| 身份认证 | KYC、证件与活体核验、认证状态、审核记录和补充材料 |
| 账户与钱包 | 用户余额、逻辑银行账户、收款账户、联系人、充值、提现、内部转账和交易记录 |
| 跨境汇款 | 多币种汇款、本地银行与区域支付方式、收款人、费用计算和状态查询 |
| 卡产品 | 产品配置、申请审核、配送、激活、冻结与解冻、PIN、限额、交易和调查 |
| 费率与分佣 | 固定费率、比例费率、代理关系、分佣、佣金冲正和头寸记录 |
| 运营管理 | 平台后台、角色权限、数据字典、通知模板、应用版本、导出和审计日志 |
把这些模块放到一条业务流程中,生命周期通常从身份认证与开户开始,经过交易发起、资格与风险检查、运营审核、外部处理,最后进入清结算、对账和审计。个人用户关心交易能否完成,运营团队需要判断哪些步骤可以审核、补充材料或重新处理,财务和技术人员则要确认余额、流水、订单与结算记录彼此一致。
这些角色关注的结果不同,但依赖的是同一组业务事实。平台的职责划分也围绕这组事实展开。
职责边界围绕业务事实划分
运营后台使用 Laravel。数据表格、筛选、权限、表单、导出和审核操作可以直接复用成熟组件,新增管理功能主要围绕业务字段和审核流程展开。
Go 侧负责复杂交易规则、资金状态变化、外部服务调用和用户端 API。管理后台通过 gRPC 调用这些能力,PHP 与 Go 复用同一套扣款、退款和卡片状态规则。
管理后台可以直接读取数据库完成查询、展示与配置管理,复杂资金操作则通过 gRPC 进入 Go 领域服务。查询与展示通过统一 Model 和 Repository 组织数据;配置修改执行字段校验,并记录操作人与变更内容;余额、订单、退款、结算和卡状态统一由领域服务处理。
Laravel 侧负责组织操作入口和审计上下文,Go 侧接收操作人、业务单号和动作类型,完成事务与状态推进。用户端和管理后台共享资金规则,两类入口仍使用各自独立的认证方式。这样可以减少跨入口的规则重复,代价是 PHP 与 Go 之间需要长期维护协议、字段和错误语义的一致性。
整套系统由三个运行单元组成:Laravel 管理后台提供平台运营与业务管理能力;Go API Server 同时提供 HTTP 与 gRPC 服务,承载用户和内部交易能力;Go Consumer 处理消息队列、延迟任务、状态同步和定时任务。
API Server 与 Consumer 共用一套 Go 代码,分别编译为独立二进制。领域代码和数据模型可以复用,在线请求与异步任务则可以分别部署和扩容。
管理后台使用 PHP 8.3、Laravel 12、Dcat Admin 定制版本、Vue 3 和 Vite。核心服务使用 Go、Gin、gRPC、GORM Gen、Google Wire 和 Viper。PostgreSQL 保存业务事实,Redis 承担缓存、Token、限流、短期去重和部分任务状态,RabbitMQ 与 Asynq 处理异步工作。
Go API Server 在同一进程内同时启动 HTTP 与 gRPC 服务,HTTP 面向用户端和内部管理接口,gRPC 供 Laravel 后台复用领域能力。Google Wire 分别维护 API Server 和 Consumer 的依赖图,两个二进制共享 Service、Repository 和生成式 DAO,但各自只加载需要的运行组件。
独立运行的 Consumer 让在线请求与后台任务可以分别配置资源和并发,也带来了最终一致性、消息重投和多运行单元运维等额外成本。交易正确性不能依赖部署边界,API Server 与 Consumer 必须通过相同的状态规则、幂等键和数据库约束处理业务记录。
一笔交易如何穿过系统
交易请求进入系统后,同步部分负责确认身份、校验请求并写入业务事实,耗时较长的外部交互与状态同步交给 Consumer。一条典型流程如下:
同步请求结束时,订单、余额和资金流水已经形成一致的数据库状态。队列消息只携带业务 ID 和必要的任务类型,Consumer 执行时重新读取数据库,避免长时间排队后仍使用旧快照。Webhook 也先完成签名校验并写入队列,再由同一套领域服务推进订单状态。
资金变化统一写进事务和流水
一笔交易往往同时涉及用户余额、卡账户余额、手续费、资金流水、结算记录和外部通道状态。账户表保存当前余额,业务订单描述交易目的,资金流水记录变动方向、金额、变动前后余额、关联订单和操作来源。三类数据各自承担一种职责。
平台没有把订单状态直接当作余额,也没有只保留当前余额。业务订单、余额和流水分开建模后,当前资金、交易目的和变化过程都能独立核对,代价是每次资金操作都必须在同一事务中维护多类记录。
核心资金服务要求调用方显式传入事务对象。余额更新、流水写入和业务订单状态变化都使用这个事务,事务提交后一起生效,回滚时也一起恢复到提交前状态。头寸账本沿用同样的接口约束,调用方无法绕开业务事务单独改余额。
begin transaction
load current order and account
verify source state
lock balance row for update
verify available balance
calculate balance_after
insert funds or position ledger
update balance and business order
assert affected rows
commit transaction
publish asynchronous work并发控制会按对象选择不同机制。账户与头寸余额通过 SELECT FOR UPDATE 串行计算 balance_after;充值和结算等订单使用 version 做乐观锁;状态推进则把原状态放进 WHERE 条件,并在更新后检查 RowsAffected。数据库还通过唯一约束和 CHECK 约束限制重复记录、资金方向与操作类型组合。
交易金额使用 int64 保存为最小货币单位,费率和需要更高精度的换算在边界层使用精确十进制类型。管理后台只在显示、录入、导出和统计时做单位换算,数据库与 Go 领域服务始终使用统一整数语义。
退款、冲正、佣金和头寸调整不会覆盖原流水,而是生成方向相反或类型独立的新记录。流水保存 balance_before、balance_after、关联订单和操作来源,财务与技术人员可以从当前余额逐笔回溯变化过程。
状态机与幂等共同约束交易生命周期
外部服务可以同步返回受理结果,也可以通过 Webhook 或主动查询更新后续状态。一笔订单因此可能跨越多次请求,状态机负责约束它从创建、处理到完成及退款的迁移顺序。
每个状态同时记录资金处理进度、业务审核进度和下一步动作。接口负责触发状态变化,状态机约束迁移顺序。稳定的业务幂等键贯穿提交、查询和回调处理,使同一订单在不同入口使用一致标识。
不同业务表可以保留各自的状态枚举,领域服务负责把外部状态映射为统一的交易结果。进入终态时,来源订单、用户交易记录、资金流水和头寸流水在同一事务内更新;已经处于终态的订单再次收到查询或回调时,直接返回当前结果。
幂等分为三个层次。入口层根据用户、请求方法、路径和请求体计算摘要,通过 Redis 的原子写入拦截短时间内的重复提交;任务层使用稳定业务 ID 作为幂等键,使队列重投和外部写请求指向同一笔交易;数据库层通过唯一约束与条件更新限制状态只推进一次。
这三层不会把分布式处理变成 Exactly Once。它们把重复请求、队列重投和回调重放转化为可以识别、拒绝或返回原结果的操作。任何缺少稳定业务 ID、唯一约束或条件更新的写请求,都不能只依赖 Redis 去重获得同样的保证。
状态更新同时接受 Webhook 和主动查询。Webhook 负责及时接收变化,详情查询与定时任务会继续确认仍处于 processing 的订单。两种入口最终调用同一状态处理器,避免各自维护一套订单迁移规则。
外部能力集中在适配层
支付、卡服务、短信、身份核验和对象存储使用不同协议。平台通过统一适配层管理请求结构、签名方式、状态映射和返回结果。
项目使用 Client Interface、Adapter、Provider 和 Strategy 组织外部能力。业务层依赖统一接口,具体实现由配置和依赖注入选择。消息服务可以按地区、优先级和可用性选择 Provider。
适配层减少了服务商协议进入领域代码的机会,也需要持续维护字段、错误码和状态语义的映射。统一接口只覆盖稳定的业务动作,服务商特有参数仍留在各自 Adapter 内,避免为了表面一致而丢失必要能力。
外部 Client 采用进程内单例并复用连接池,使所有调用共享底层 Transport、TCP 连接和 TLS 会话。业务 Service 通过 Interface 依赖 Client,单元测试可以注入 Mock,Provider 只负责实例选择和生命周期管理。
核心交易 Client 按接口设置独立的 context 超时预算。查询与写入分别使用不同的熔断器,各自统计调用结果和冷却时间。熔断器按照 closed、open、half-open 三个状态运行,冷却结束后只放行探测请求。
重试策略也按 HTTP 语义区分。查询请求可以对网络中断、限流和服务端状态做指数退避;写请求只有携带稳定幂等键时才进入重试。退避等待会响应 context 取消,领域服务只接收最终业务结果,不直接处理连接与重试细节。
异步处理换来伸缩性,也引入最终一致性
Consumer 单独承接 RabbitMQ、Asynq 和 Scheduler。RabbitMQ 用于事件路由,Asynq 用于可执行任务和定时调度,两者共享领域服务与 Repository,但处理的是不同的投递语义。
RabbitMQ 更适合按事件类型路由消息。通知、订单和超时任务进入不同的 Exchange 与 Queue,再通过 Routing Key 分发到具体处理器。队列使用持久化消息、手动确认和发布确认,Consumer 可以在处理完成后明确提交消费结果。
Asynq 负责可执行任务和 Scheduler,包括状态查询、周期结算、订单时效检查与外部提交。不同队列可以设置独立并发数和优先级,资金类任务与普通后台任务不必共享同一消费节奏。
长时间任务的 Payload 只保存业务记录 ID,Consumer 启动处理时再从 PostgreSQL 读取完整上下文。资金与状态类 Handler 会先检查当前状态,只有处于目标源状态的记录才继续处理。资金记录随数据库事务一起提交,通知、回调和统计等后续工作再进入消息队列,重复投递仍会得到相同结果。
发布确认和消费确认可以降低消息丢失风险,但不能消除重复投递。API 返回受理结果时,外部服务可能尚未完成处理,后续状态仍要由 Webhook、主动查询和定时任务共同确认。接口响应、状态查询和运营后台都需要明确区分「已受理」与「已完成」。
认证强度按操作风险组合
认证入口分为两类:个人用户使用 Token,内部管理接口使用独立签名。两类入口分别使用独立认证中间件和权限范围。
HTTP 全局中间件负责安全响应头、Request ID、访问日志、Metrics、Tracing 和多语言错误处理。进入具体 Route Group 后,再按接口用途组合 Token、限流、设备信息、短时去重、支付密码、双因子认证和资格检查。认证强度由路由声明直接体现,不需要在 Handler 内重复拼装。
交易接口还会叠加支付密码、双因子认证、设备指纹、用户资格、身份认证状态、风险评分、请求限流和防重复提交。不同操作按风险级别选择验证组合和强度。
认证能力按路由组合后,普通查询不必承担高风险资金操作的全部校验成本,敏感操作仍可以提高认证强度。代价是权限与中间件声明需要跟随业务风险变化持续检查,不能只依赖 Handler 内的临时判断。
设备信息用于识别新设备、常用地区和操作频率。风险计算拆成用户、金额和频率等独立 Evaluator,每个 Evaluator 只读取自己需要的 Repository,再由风险服务合并分数、风险因子和后续动作。规则阈值与权重来自配置,业务 Handler 只消费评估结果。
卡片敏感信息、身份材料和文件下载采用独立权限。完整卡号、验证码、证件文件与普通账户资料使用不同访问规则,访问动作写入审计记录,返回内容按用途脱敏或使用短期签名地址。
数据库、gRPC 与生成代码一起维护
数据库结构由 Laravel Migration 管理,Go 侧通过 GORM Gen 生成 Model 和 DAO。表结构变化后重新运行生成器,再把生成结果与业务代码一起验证。
PHP 与 Go 的内部调用使用 Protocol Buffers 和 gRPC。协议文件是两端共享的契约,字段调整后重新生成客户端与服务端代码。外部 HTTP API 使用 OpenAPI 描述,接口实现完成后同步更新文档。
一次字段变更通常会同时涉及数据库、Protocol Buffers、生成代码、业务 Service、管理页面和文档。这些内容放在同一轮修改中验证,使跨语言契约保持一致。
生成代码减少了手工维护 Model、DAO 和 Stub 的漂移,也扩大了字段变更的验证范围。Migration 或协议中的一个字段变化,可能同时影响 PHP、Go、管理页面、生成代码和外部文档,因此不能把重新生成成功当作变更完成。
Laravel Migration
-> migrate schema
-> GORM Gen: Model / DAO
-> Protocol Buffers: PHP / Go stubs
-> Wire: API / Consumer dependency graph
-> OpenAPI / Swagger
-> lint, test, build生成代码不做手工修改。数据库 Nullable、索引和字段类型由 GORM Gen 从 Schema 读取,DAO 同时生成基础查询接口;Protocol Buffers 生成 gRPC Client 与 Server Stub;构造函数变化则重新生成两套 Wire 依赖图。代码评审可以把注意力放在 Migration、协议定义和领域逻辑上。
测试围绕系统边界展开。领域服务重点验证事务、幂等和状态迁移;Handler 验证请求绑定、认证与响应契约;外部 Client 通过 Interface 替换为 Mock;涉及 PostgreSQL 和 Redis 的流程进入集成测试。Go 测试同时启用 Race Detector,构建阶段分别编译 API Server 与 Consumer,用来验证生成代码和依赖注入结果。
多环境共用一套代码
项目按开发、测试、生产和容器环境组织 YAML 配置,API Server 与 Consumer 分别维护运行参数。同一套代码通过独立配置连接数据库、缓存、消息队列和外部服务。
API Server 与 Consumer 分别构建为 Linux 二进制,并通过相同的配置目录和环境参数选择运行环境。在线请求与后台任务可以独立设置资源、并发和扩容策略,不需要拆分领域代码。
Docker、Kubernetes 配置和部署脚本覆盖构建、启动、健康检查、Metrics 与运行状态采集。普通环境配置、Secret、业务开关和外部服务参数分开管理,示例配置只保留结构和占位符。
这一年形成的方法
新增业务模块时,开发顺序逐渐固定下来:先定义订单与状态,再确定余额和流水语义,随后补充幂等键、异步任务、外部适配和运营入口。数据库约束、生成代码、接口契约和测试在同一轮修改中更新。
资金正确性也不再由某一个组件单独承担。数据库事务保证一次操作内的数据一致,流水保留变化过程,状态机限制迁移方向,稳定业务 ID 和数据库约束处理重复请求,Webhook 与主动查询共同确认外部结果。这些机制各自处理不同的失败方式。
同步请求与异步任务都以数据库中的当前业务状态为准。队列只传业务 ID,Consumer 重新加载记录并检查源状态,用户端、运营后台和定时任务最终调用相同的领域规则。这种做法增加了状态设计和测试成本,但减少了长任务使用旧数据以及不同入口各自推进状态的问题。
跨语言契约维护、异步状态滞后和外部服务的不确定性仍然存在。Laravel 与 Go 的分工、独立 Consumer 和适配层只是把这些成本放到清晰的边界内,后续迭代仍需要同时检查协议、状态、幂等、可观测性和运营处理方式。
延伸阅读
金融科技工程手册:继续了解资金表示、账本、幂等、对账、控制与测试。
跨境支付:支付网关与技术方案:从网关、动态路由、多币种清算和运营支撑角度展开系统设计。
跨境支付:合规风控与用户运营管理:补充 KYC、风险控制、争议处理和持续运营的业务边界。
参考资料
框架与基础设施:
数据一致性与幂等:
Redis 的 SET NX 与过期时间适合入口层短期去重,完整业务幂等仍然依赖稳定业务 ID、数据库唯一约束、条件更新和状态检查。
消息与任务:
RabbitMQ 的 Consumer Acknowledgement 与 Publisher Confirm 是两个独立机制,消息重投仍要求 Handler 保持幂等。Asynq 当前主版本仍为 v0.x,升级时需要检查 Public API 兼容性。
接口契约与生成代码:
Google Wire 用于说明现有系统的历史技术选型,其官方仓库已经归档并停止维护,不应直接作为新项目的默认推荐。