在 Android App 与 Spring 后端中实现支付系统
明确结论
在没有注册公司的阶段,可以先完成支付流程的技术集成和业务验证。不过,此时通常只能使用支付服务商提供的 Sandbox/Test Mode,或者自行搭建一个不处理真实资金的模拟支付网关。
如果业务需要通过银行卡收取真实款项,再把资金结算给提供服务的用户,同时由平台收取手续费,这类模式通常属于平台型支付、Marketplace Payment 或 Split Payment。正式上线时,支付服务商一般会要求平台主体和收款用户完成 KYC/KYB、签署协议,并遵守当地的支付、税务和反洗钱规定。能否以个人身份申请生产账户,要看所在国家或地区以及支付服务商的政策,不能默认一定可行。
比较稳妥的实施顺序是:
- 使用测试账户验证完整的技术链路。
- 通过内部账本验证分账、退款、争议和异常处理。
- 确认产品可行后,再注册经营主体并申请生产环境。
- 不要为绕过审核而借用他人的商户账户,也不要自行代收、保管或转付真实资金。
应当选择哪类支付产品
这个业务并不是普通的”用户向平台购买商品”,而是涉及三方的资金关系:
User B 支付
↓
支付服务商处理银行卡交易
↓
User A 获得服务收入
Platform 获得手续费
选择的支付产品需要明确支持以下功能:
- 银行卡支付和安全的卡信息令牌化
- 平台或 Marketplace 模式
- Connected Account、Sub-merchant 或类似的子商户机制
- 平台手续费
- 分账或延迟结算
- 收款用户身份验证
- 退款、拒付和争议处理
- Webhook 事件通知
- Sandbox/Test Mode
各家服务商对这些功能的命名不同,可能称为 Connect、Platforms、MarketPay、Split Payments 或 Multiparty Payments。Stripe Connect、Adyen for Platforms 等都属于这一类,但它们的开户条件、适用国家、结算方式和责任划分并不相同。具体要求应以目标地区当时有效的官方文档和合同为准。
普通的单商户支付接口通常不适合这种业务。即使技术上可以先把全部款项收进平台账户,再转账给 User A,也可能让平台承担资金托管、支付牌照、税务或其他合规责任。
推荐的验证方案
第一阶段:完全模拟支付
先定义一套与具体支付服务商无关的接口,避免业务逻辑直接依赖某一家服务商的 SDK:
public interface PaymentGateway {
PaymentSession createPayment(CreatePaymentCommand command);
RefundResult refund(String providerPaymentId, long amountMinor);
PaymentStatus getStatus(String providerPaymentId);
}
开发阶段可以使用内存或数据库实现:
@Service
@Profile("local")
public class FakePaymentGateway implements PaymentGateway {
private final Map<String, PaymentStatus> payments =
new ConcurrentHashMap<>();
@Override
public PaymentSession createPayment(CreatePaymentCommand command) {
String paymentId = UUID.randomUUID().toString();
payments.put(paymentId, PaymentStatus.PENDING);
return new PaymentSession(
paymentId,
"fake-client-secret-" + paymentId
);
}
@Override
public RefundResult refund(String providerPaymentId, long amountMinor) {
PaymentStatus status = payments.get(providerPaymentId);
if (status != PaymentStatus.SUCCEEDED) {
throw new IllegalStateException("Only succeeded payments can be refunded");
}
payments.put(providerPaymentId, PaymentStatus.REFUNDED);
return new RefundResult(providerPaymentId, amountMinor);
}
@Override
public PaymentStatus getStatus(String providerPaymentId) {
return payments.getOrDefault(providerPaymentId, PaymentStatus.NOT_FOUND);
}
}
这个实现无法验证真实的银行卡网络,但足以测试订单状态机、手续费计算、通知、退款和失败恢复等核心逻辑。
第二阶段:接入支付服务商的测试环境
注册开发者账户或测试账户后,使用服务商提供的测试卡号和测试凭据。测试交易不会产生真实扣款,通常也不需要事先完成生产环境的全部审核。
测试至少应覆盖以下场景:
- 支付成功
- 余额不足或银行卡被拒
- 需要额外身份验证,例如 3-D Secure
- 用户中途取消
- 请求超时,但实际支付成功
- Webhook 重复发送
- Webhook 乱序到达
- 全额退款和部分退款
- 平台手续费计算
- 收款账户尚未完成验证
- 结算失败
- 拒付或争议
测试账户只能证明系统集成和业务状态流转是否正确,不能保证生产商户申请一定会通过审核。
Spring 后端的基本设计
创建支付订单
金额必须由后端计算,不能采用 Android 客户端传入的最终金额或手续费。
public record CreateOrderRequest(
UUID serviceOrderId,
UUID payerId
) {
}
@Service
public class PaymentService {
private final PaymentGateway paymentGateway;
private final OrderRepository orderRepository;
public PaymentService(
PaymentGateway paymentGateway,
OrderRepository orderRepository
) {
this.paymentGateway = paymentGateway;
this.orderRepository = orderRepository;
}
@Transactional
public PaymentSession createPayment(CreateOrderRequest request) {
Order order = orderRepository.findById(request.serviceOrderId())
.orElseThrow(() -> new IllegalArgumentException("Order not found"));
if (!order.getBuyerId().equals(request.payerId())) {
throw new IllegalArgumentException("Payer does not own this order");
}
long grossAmount = order.getAmountMinor();
long platformFee = calculatePlatformFee(grossAmount);
CreatePaymentCommand command = new CreatePaymentCommand(
order.getId().toString(),
grossAmount,
platformFee,
order.getSellerAccountId(),
order.getCurrency()
);
return paymentGateway.createPayment(command);
}
private long calculatePlatformFee(long grossAmount) {
// 示例:5%,实际规则应明确处理舍入方式和最低手续费。
return Math.multiplyExact(grossAmount, 5) / 100;
}
}
金额最好使用货币最小单位的整数,例如分:
100.00 CNY → 10000
29.99 USD → 2999
不要使用 double 保存金额。如果业务必须使用小数,应使用 BigDecimal,并明确指定 RoundingMode。
处理 Webhook
支付的最终状态应由服务商发送到服务器的通知确认,不能以 Android 客户端声称”支付成功”作为依据。
@RestController
@RequestMapping("/webhooks/payments")
public class PaymentWebhookController {
private final PaymentWebhookVerifier verifier;
private final PaymentEventService eventService;
public PaymentWebhookController(
PaymentWebhookVerifier verifier,
PaymentEventService eventService
) {
this.verifier = verifier;
this.eventService = eventService;
}
@PostMapping
public ResponseEntity<Void> receive(
@RequestBody byte[] payload,
@RequestHeader("Payment-Signature") String signature
) {
VerifiedPaymentEvent event = verifier.verify(payload, signature);
eventService.process(event);
return ResponseEntity.ok().build();
}
}
处理事件时必须保证幂等:
@Transactional
public void process(VerifiedPaymentEvent event) {
if (processedEventRepository.existsByProviderEventId(event.id())) {
return;
}
switch (event.type()) {
case PAYMENT_SUCCEEDED -> markPaymentSucceeded(event);
case PAYMENT_FAILED -> markPaymentFailed(event);
case PAYMENT_REFUNDED -> recordRefund(event);
case DISPUTE_CREATED -> freezeRelatedBalance(event);
default -> {
// 记录未知或暂不处理的事件。
}
}
processedEventRepository.save(
new ProcessedEvent(event.id(), Instant.now())
);
}
验证签名时,必须使用支付服务商官方提供的算法或 SDK。请求头名称、签名格式和重放保护规则因服务商而异,不要自行猜测或设计。
Android 端的职责
Android App 只负责以下工作:
- 请求 Spring 后端创建支付会话。
- 使用支付服务商的官方 Android SDK 展示银行卡输入界面或支付页面。
- 将后端返回的
clientSecret、session ID 或其他短期凭据交给 SDK。 - 显示支付处理中、成功或失败等状态。
- 再向后端查询订单的最终状态。
流程可以设计为:
Android
→ POST /api/orders/{id}/payment-session
Spring
→ 支付服务商:创建测试支付
支付服务商
→ Spring:返回 clientSecret
Spring
→ Android:返回 clientSecret
Android
→ 官方 SDK:确认银行卡支付
支付服务商
→ Spring Webhook:payment_succeeded
Spring
→ 更新订单和内部账本
Android
→ GET /api/orders/{id}
银行卡号、CVV 等敏感信息不应经过自己的 Spring API,也不能写入日志或数据库。它们应通过支付服务商的 SDK、托管页面或令牌化组件直接提交给服务商,否则可能大幅扩大 PCI DSS 合规范围。
内部账本要单独设计
支付服务商的交易记录不能替代业务账本。账本至少需要记录:
- User B 支付的总金额
- User A 应得金额
- 平台手续费
- 服务商处理费
- 已退款金额
- 可结算余额
- 已结算金额
- 冻结或争议金额
例如:
订单总额: 10,000
平台手续费: 500
User A 应收: 9,500
以上金额均使用货币最小单位。支付服务商的处理费由哪一方承担,需要写入业务规则和合同,不能默认从平台手续费中扣除。
账本应采用追加记录的方式,不要只维护一个可以直接覆盖的余额字段:
public record LedgerEntry(
UUID accountId,
UUID orderId,
LedgerEntryType type,
long amountMinor,
String currency,
Instant createdAt
) {
}
这种设计才能正确处理退款、拒付、手续费调整和对账差异。
上线前必须确认的事项
测试完成后,切换到生产环境通常还需要确认以下问题:
- 平台应以个人、个体经营者还是公司身份签约
- User A 是否被视为子商户或服务提供者
- 谁是法律意义上的收款方
- 谁向 User B 开具发票或收据
- 平台能否暂时持有用户资金
- 退款和拒付损失由谁承担
- 如何收集 User A 的 KYC 和税务信息
- 是否允许跨境收款和不同币种结算
- 银行卡数据处理是否符合 PCI DSS
- 用户协议、隐私政策和服务商协议是否覆盖这种业务模式
具体要求会受到地区、行业和交易结构的影响。金融、医疗、博彩、数字资产以及某些按需服务可能还有额外限制。确定运营地区和支付服务商后,应请当地专业人士确认法律和税务结构。
不建议采用的做法
不要通过以下方式处理真实交易:
- 借用朋友或其他公司的商户账户
- 把所有款项收进个人银行卡,再人工转给 User A
- 将交易伪装成与实际业务不同的商品或服务
- 在服务器中保存银行卡号或 CVV
- 只根据 Android 客户端的回调确认订单已付款
- 在 User A 尚未完成必要验证时承诺可以提现
- 忽略退款、拒付和负余额场景
这些做法可能造成账户冻结、资金延迟结算或合同违约,也可能带来支付和资金托管方面的法律风险。
可执行的落地顺序
先用抽象的 PaymentGateway 和内部账本完成业务开发,再接入一家支持平台支付的服务商测试环境。使用测试卡跑通支付、抽成、退款、拒付和结算状态,并让支付接口与具体服务商保持隔离。
这样可以在不处理真实资金、通常也不需要立即注册公司的情况下,验证产品的核心技术和业务逻辑。测试结果确认方案可行后,再根据运营地区选择主体形式,完成支付服务商审核并切换到生产密钥。
测试环境回答的是”系统能否工作”,公司注册和商户审核解决的是”能否合法处理真实资金”。这两个阶段需要分开完成。
备注:内容仅供参考。
