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; } /// 支付管理:统一下单流程。 /// 内部包掉 loading(show → 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 buy( String? productID, ProductType productType, { String? source, String? couponId, int? goldVideoCouponNum, String? serviceId, String? contentID, String? checkoutContextId, String? requestId, Function(Map 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 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 _request( Future 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); } }