feat: 08 订单与权益落地(金币解锁 / 权益判定生效 / 补发回收 / 对账)
为什么: 1) 第一阶段要能真正收到钱,而收钱方式是金币解锁分集。本提交把钱包、金币流水、 权益、订单四样打通,客户端可以解锁并立刻播放,不再只是展示。 2) 权益判定此前只是端口占位(一律视为未解锁)。交易模块用 @Primary 提供实现后, 播放入口与下载入口的付费判定同时生效 —— 两条链路一行没改。 3) 钱的事必须可追溯、可幂等:所有金币变动都带幂等键并写 coin_ledger, 余额与流水同事务;订单每次状态流转都写 order_event(月分区),支付争议可回溯。 4) 后台要能兜住线上问题:补发(重复提交幂等)、退款(回收权益 + 扣回金币, 余额不足时留人工跟进备注)、人工补币(先校验用户存在,避免给不存在的用户建钱包)、 手动对账(默认每天 04:20 自动跑前一天)。 5) 09 商店支付按约束只做端口预留:StoreVerifier 未接入时明确报"尚未配置", 宁可不收钱也不产生假订单。
This commit is contained in:
1 parent
af1dc48494
commit
7e730fbbca
37 files changed
+1467
-5
No files matched your search
+63
@@ -0,0 +1,63 @@
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fruitshorts.commerce.dal.entity.Orders;
|
||||
import com.fruitshorts.commerce.service.OrderService;
|
||||
import com.fruitshorts.commerce.service.UnlockService;
|
||||
import com.fruitshorts.commerce.service.WalletService;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 客户端金币与订阅。
|
||||
*
|
||||
* 金币解锁是第一阶段真实的收费方式;真实商店购买走 {@code /app/orders/verify},
|
||||
* 本轮只预留接口(商店未接入时返回明确业务提示,不会产生假订单)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/app")
|
||||
@RequiredArgsConstructor
|
||||
public class AppCommerceController {
|
||||
|
||||
private final WalletService walletService;
|
||||
private final UnlockService unlockService;
|
||||
private final OrderService orderService;
|
||||
|
||||
/** 我的钱包:余额 + 最近流水,客户端一处渲染。 */
|
||||
@GetMapping("/wallet")
|
||||
public Result<Map<String, Object>> wallet() {
|
||||
Long userId = SecurityUtils.getAppUserId();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("wallet", walletService.getOrCreate(userId));
|
||||
data.put("ledger", walletService.recentLedger(userId, 20));
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
/** 金币解锁分集:幂等,重复点击不会重复扣币。 */
|
||||
@PostMapping("/episodes/{episodeId}/unlock")
|
||||
public Result<Map<String, Object>> unlock(@PathVariable Long episodeId) {
|
||||
return Result.ok(unlockService.unlock(SecurityUtils.getAppUserId(), episodeId));
|
||||
}
|
||||
|
||||
@GetMapping("/orders")
|
||||
public Result<List<Orders>> orders() {
|
||||
return Result.ok(orderService.listByUser(SecurityUtils.getAppUserId()));
|
||||
}
|
||||
|
||||
/** 提交商店票据。第一阶段商店未接入,会返回明确的"尚未配置"。 */
|
||||
@PostMapping("/orders/verify")
|
||||
public Result<Orders> verify(@RequestBody Map<String, String> body) {
|
||||
return Result.ok(orderService.verifyAndDeliver(SecurityUtils.getAppUserId(),
|
||||
body.getOrDefault("platform", ""), body.getOrDefault("receipt", "")));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user