Files
2026-09-15 15:44:13 +07:00

171 lines
6.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import '../../routers/jump_router.dart';
import '../../tools_base/loading/loading_alert_widget.dart';
import '../../tools_base/net/base_resp_bean.dart';
import '../../tools_base/net/net_code.dart';
import '../api_service/buy_service.dart';
/// 下单商品类型,对应 /product/buy 的 productType 字段。
/// 只收单品下单用到的类型;VIP 卡的 productType 由服务端随卡下发(新人卡 5、预售卡 21 等),
/// 走 [PayManager.buyVip] 的 int
enum ProductType {
/// 1 单个作品:金币视频、黄游(SEED_LINK)、社区帖子
media(1),
/// 19 整本/整部:漫画整本、动漫整部、短剧(买单集时靠 contentID 指定哪一集)
mediaAll(19),
/// 25 漫画单集
mediaChapter(25),
/// 101 群聊
group(101);
const ProductType(this.value);
/// 传给接口的原始值
final int value;
}
/// 支付管理:统一下单流程。
/// 内部包掉 loadingshow → cancel)、余额不足(code 8000)跳充值,
/// 外部只透传成功/失败回调,并可带来源页面标识 [source]。
class PayManager {
PayManager._();
static final PayManager _instance = PayManager._();
factory PayManager() => _instance;
/// 下单购买(普通商品:视频/漫画/帖子/群聊/短剧单集等)
/// [productID] 商品ID [productType] 商品类型,见 [ProductType]
/// [source] 来源页面标识
/// [contentID] 子内容ID(短剧买单集时传这一集的 id)——短剧归因就靠它和 productID(剧 id),不另发字段
/// [checkoutContextId] 付费墙下发的结算上下文
/// [requestId] 幂等键,一笔业务拆成多次请求时复用同一个 id;不传则自动生成,见 BuyService._idempotent
/// [jsonTransformation] 要拿模型而不是原始 json 时传,结果在 onSuccess 的 data.data 里
/// [jumpWalletOnInsufficient] 余额不足(code 8000)是否自动跳充值页,默认 true;
/// 已在充值页、或要就地弹金币支付的场景传 false(仅跳过跳转;失败提示由网络层统一弹)
/// [onSuccess] 支付成功回调,各页面自行处理副作用(Get.back / 刷新钱包等)
/// [onFailure] 业务失败回调,可选(失败提示已由网络层统一弹)
Future<void> buy(
String? productID,
ProductType productType, {
String? source,
String? couponId,
int? goldVideoCouponNum,
String? serviceId,
String? contentID,
String? checkoutContextId,
String? requestId,
Function(Map<String, dynamic> json)? jsonTransformation,
bool jumpWalletOnInsufficient = true,
required void Function(BaseRespBean data) onSuccess,
void Function(BaseRespBean? data)? onFailure,
}) {
return _request(
() => BuyService.buyVideo(
productID,
productType.value,
source: source,
couponId: couponId,
goldVideoCouponNum: goldVideoCouponNum,
serviceId: serviceId,
contentID: contentID,
checkoutContextId: checkoutContextId,
requestId: requestId,
jsonTransformation: jsonTransformation,
),
jumpWalletOnInsufficient: jumpWalletOnInsufficient,
//一次性商品「已经买过了」等价于买到手,见 _request
repeatAsSuccess: true,
onSuccess: onSuccess,
onFailure: onFailure,
);
}
/// 金币购买 VIP(含预售尾款)
/// [productType] VIP 卡类型由服务端随卡下发(新人卡 5、预售卡 21 等),不是 [ProductType],保持原样透传
/// [finalPayStatus] 预售业务:true 付尾款 / false 付预定款;null 非预售
/// [requestId] 幂等键,同 [buy]
/// [experimentId]/[experimentVariant]/[sessionId] VIP 卡皮 A/B:与 /mine/topay 对齐回传
/// [mediaId]/[contentId]/[checkoutContextId] 短剧付费墙开卡归因,同 [buy]
/// 其余同 [buy]
Future<void> buyVip(
int? productType,
String? productID,
String? productName,
int? discountedPrice, {
String? source,
String? couponId = "",
bool? finalPayStatus,
String? experimentId,
String? experimentVariant,
String? sessionId,
String? mediaId,
String? contentId,
String? checkoutContextId,
String? requestId,
bool jumpWalletOnInsufficient = true,
required void Function(BaseRespBean data) onSuccess,
void Function(BaseRespBean? data)? onFailure,
}) {
return _request(
() => BuyService.buyVip(
productType,
productID,
productName,
discountedPrice,
source: source,
couponId: couponId,
finalPayStatus: finalPayStatus,
experimentId: experimentId,
experimentVariant: experimentVariant,
sessionId: sessionId,
mediaId: mediaId,
contentId: contentId,
checkoutContextId: checkoutContextId,
requestId: requestId,
),
jumpWalletOnInsufficient: jumpWalletOnInsufficient,
onSuccess: onSuccess,
onFailure: onFailure,
);
}
/// 统一下单流程:loading + 余额不足(code 8000)跳充值页。
/// 有响应的失败提示由 HttpResponseInterceptor 全局弹;网络无响应(超时/断网)不额外处理。
/// [repeatAsSuccess] 8005 重复购买是否当成功。
/// 只有一次性商品(视频/漫画/帖子/群聊/短剧单集)能开——「已经买过了」就是已拥有;
/// VIP 不能开:会员是可叠加续费的,8005 不代表用户拿到了东西,
/// 当成功会误报购买埋点、弹「购买成功」并把充值页关掉
Future<void> _request(
Future<BaseRespBean> Function() request, {
bool jumpWalletOnInsufficient = true,
bool repeatAsSuccess = false,
required void Function(BaseRespBean data) onSuccess,
void Function(BaseRespBean? data)? onFailure,
}) async {
LoadingAlertWidget.show();
final BaseRespBean data;
try {
data = await request();
} finally {
// 先关 loading 再回调,避免 onSuccess 里的 Get.back 误弹到 loading 弹窗。
// 走 finally 是因为请求前的签名(generateRequestOption→_sign)会抛,
// 抛出去就没人关这层全屏 loading 了,用户只能杀进程
LoadingAlertWidget.cancel();
}
// 注意 8005 走进来时 data.data 是错误体不是订单结果,onSuccess 里取字段要判类型
if (data.isSuccess || (repeatAsSuccess && data.code == Code.REPEAT_BUY)) {
onSuccess(data);
return;
}
// 余额不足:拦截器只弹提示不跳转,这里补跳充值页(已在充值页则传 false 跳过)
if (data.code == Code.NOT_ENOUGH_MONEY && jumpWalletOnInsufficient) {
pushToWalletPage(tabPosition: 1);
}
onFailure?.call(data);
}
}