feat: 新增前端支付模块

- 新增payment/支付管理(PaymentManager、微信/支付宝Handler)
- 新增wxapi/WXPayEntryActivity微信支付回调
- 扩展ApiService、PaymentModels、PaymentRepository

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
iammm0
2026-02-10 14:23:49 +08:00
parent 498277aac3
commit 0af0074e23
8 changed files with 679 additions and 4 deletions

View File

@@ -1,9 +1,12 @@
package com.huaga.life_echo.data.repository
import com.huaga.life_echo.network.ApiService
import com.huaga.life_echo.network.models.CreatePaymentOrderResponse
import com.huaga.life_echo.network.models.OrderDto
import com.huaga.life_echo.network.models.PaymentOrderStatusResponse
import com.huaga.life_echo.network.models.PlanDto
import com.huaga.life_echo.network.models.QuotaCheckDto
import com.huaga.life_echo.network.models.TestSubscriptionResponse
class PaymentRepository(
private val apiService: ApiService
@@ -31,4 +34,37 @@ class PaymentRepository(
suspend fun getOrderById(orderId: String): Result<OrderDto> {
return apiService.getOrderById(orderId)
}
// ==================== 支付订单(微信/支付宝) ====================
/**
* 创建支付订单(获取支付参数)
*/
suspend fun createPaymentOrder(
planId: String,
paymentMethod: String
): Result<CreatePaymentOrderResponse> {
return apiService.createPaymentOrder(planId, paymentMethod)
}
/**
* 查询支付订单状态
*/
suspend fun getPaymentOrderStatus(orderId: String): Result<PaymentOrderStatusResponse> {
return apiService.getPaymentOrderStatus(orderId)
}
/**
* 获取支付订单列表
*/
suspend fun getPaymentOrders(): Result<List<PaymentOrderStatusResponse>> {
return apiService.getPaymentOrders()
}
/**
* 测试订阅开关(仅当服务端开启时可用)
*/
suspend fun setTestSubscription(action: String, planId: String = "pro"): Result<TestSubscriptionResponse> {
return apiService.setTestSubscription(action, planId)
}
}

View File

@@ -237,7 +237,75 @@ class ApiService(
}
}
// ==================== 支付订单API微信/支付宝) ====================
/**
* 创建支付订单
* 返回微信支付参数或支付宝订单字符串
*/
suspend fun createPaymentOrder(
planId: String,
paymentMethod: String
): Result<CreatePaymentOrderResponse> {
return try {
val response = client.post("$BASE_URL/api/payment/create-order") {
contentType(ContentType.Application.Json)
setBody(CreatePaymentOrderRequest(planId, paymentMethod))
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* 查询支付订单状态
*/
suspend fun getPaymentOrderStatus(orderId: String): Result<PaymentOrderStatusResponse> {
return try {
val response = client.get("$BASE_URL/api/payment/order/$orderId/status") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* 获取支付订单列表(新接口)
*/
suspend fun getPaymentOrders(): Result<List<PaymentOrderStatusResponse>> {
return try {
val response = client.get("$BASE_URL/api/payment/orders") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
// ==================== 用户相关API ====================
/**
* 测试订阅开关(仅当服务端 ENABLE_TEST_SUBSCRIPTION=1 时可用)
* 用于微信支付审核通过前模拟付费通过,体验订阅额度。
*/
suspend fun setTestSubscription(
action: String,
planId: String = "pro"
): Result<TestSubscriptionResponse> {
return try {
val response = client.post("$BASE_URL/api/user/test-subscription") {
contentType(ContentType.Application.Json)
setBody(TestSubscriptionRequest(action = action, planId = planId))
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getUserProfile(): Result<UserProfileDto> {
return try {

View File

@@ -1,5 +1,6 @@
package com.huaga.life_echo.network.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
@@ -18,19 +19,22 @@ data class PlanBenefitDto(
val features: List<String> = emptyList() // 功能列表
)
// 套餐信息DTO
// 套餐信息DTO(与后端 /api/plans 返回字段对应)
@Serializable
data class PlanDto(
val id: String,
val name: String,
val displayName: String,
@SerialName("display_name") val displayName: String,
val price: Double,
val currency: String = "CNY",
val billingCycle: String, // "monthly", "yearly"
@SerialName("billing_cycle") val billingCycle: String? = null,
val benefits: List<PlanBenefitDto> = emptyList(),
val features: List<String> = emptyList(),
val isActive: Boolean = true,
val isCurrentPlan: Boolean = false
val isCurrentPlan: Boolean = false,
@SerialName("max_conversations") val maxConversations: Int? = null,
@SerialName("max_chapters") val maxChapters: Int? = null,
@SerialName("is_popular") val isPopular: Boolean = false
)
// 订单信息DTO
@@ -69,5 +73,84 @@ data class QuotaCheckDto(
val remainingWords: Int? = null,
val remainingChapters: Int? = null,
val remainingConversations: Int? = null,
// 已用量与上限(用于展示 "已用 X / 共 Y"
val usedConversations: Int? = null,
val usedChapters: Int? = null,
val maxConversations: Int? = null,
val maxChapters: Int? = null,
val message: String? = null
)
// ==================== 支付订单相关模型 ====================
/**
* 创建支付订单请求
*/
@Serializable
data class CreatePaymentOrderRequest(
@SerialName("plan_id") val planId: String,
@SerialName("payment_method") val paymentMethod: String // "wechat" / "alipay"
)
/**
* 创建支付订单响应
*/
@Serializable
data class CreatePaymentOrderResponse(
@SerialName("order_id") val orderId: String,
@SerialName("payment_method") val paymentMethod: String,
// 微信支付参数(仅当 paymentMethod == "wechat" 时有值)
@SerialName("wechat_params") val wechatParams: WeChatPayParamsDto? = null,
// 支付宝订单字符串(仅当 paymentMethod == "alipay" 时有值)
@SerialName("alipay_order_string") val alipayOrderString: String? = null
)
/**
* 微信支付参数 DTO
*/
@Serializable
data class WeChatPayParamsDto(
val appId: String = "",
val partnerId: String = "",
val prepayId: String = "",
val nonceStr: String = "",
val timeStamp: String = "",
val sign: String = "",
val packageValue: String = "Sign=WXPay"
)
/**
* 订单状态查询响应
*/
@Serializable
data class PaymentOrderStatusResponse(
@SerialName("order_id") val orderId: String,
@SerialName("plan_id") val planId: String,
@SerialName("plan_name") val planName: String,
val amount: Int, // 金额(分)
val currency: String,
@SerialName("payment_method") val paymentMethod: String,
val status: String, // pending / paid / failed / cancelled / refunded
@SerialName("trade_no") val tradeNo: String? = null,
@SerialName("created_at") val createdAt: String,
@SerialName("paid_at") val paidAt: String? = null
)
/**
* 测试订阅请求(仅开发/测试环境,微信支付审核通过前使用)
*/
@Serializable
data class TestSubscriptionRequest(
val action: String, // "activate" | "deactivate"
@SerialName("plan_id") val planId: String? = "pro"
)
/**
* 测试订阅响应
*/
@Serializable
data class TestSubscriptionResponse(
val success: Boolean,
val message: String,
@SerialName("subscription_type") val subscriptionType: String
)

View File

@@ -0,0 +1,106 @@
package com.huaga.life_echo.payment
import android.app.Activity
import android.util.Log
import com.alipay.sdk.app.PayTask
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* 支付宝支付处理器
*
* 负责:
* 1. 调起支付宝 APP 支付
* 2. 解析支付结果
*/
class AlipayHandler {
companion object {
private const val TAG = "AlipayHandler"
}
/**
* 发起支付宝支付
*
* 注意:此方法需要在协程中调用(内部会切换到 IO 线程)
*
* @param activity 当前 Activity
* @param orderString 从服务端获取的支付宝订单字符串
* @param orderId 内部订单号
* @return PaymentResult
*/
suspend fun startPay(
activity: Activity,
orderString: String,
orderId: String
): PaymentResult = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "发起支付宝支付: orderId=$orderId")
val payTask = PayTask(activity)
val result: Map<String, String> = payTask.payV2(orderString, true)
Log.d(TAG, "支付宝支付结果: $result")
parseResult(result, orderId)
} catch (e: Exception) {
Log.e(TAG, "支付宝支付异常", e)
PaymentResult.Failure(
orderId = orderId,
paymentMethod = "alipay",
errorCode = -1,
errorMessage = e.message ?: "支付异常"
)
}
}
/**
* 解析支付宝支付结果
*/
private fun parseResult(result: Map<String, String>, orderId: String): PaymentResult {
val resultStatus = result["resultStatus"] ?: ""
val memo = result["memo"] ?: ""
return when (resultStatus) {
"9000" -> {
// 支付成功
Log.d(TAG, "支付宝支付成功: orderId=$orderId")
PaymentResult.Success(orderId = orderId, paymentMethod = "alipay")
}
"6001" -> {
// 用户取消
Log.d(TAG, "支付宝支付取消: orderId=$orderId")
PaymentResult.Cancelled(orderId = orderId, paymentMethod = "alipay")
}
"6002" -> {
// 网络连接出错
Log.e(TAG, "支付宝支付网络错误: orderId=$orderId")
PaymentResult.Failure(
orderId = orderId,
paymentMethod = "alipay",
errorCode = 6002,
errorMessage = "网络连接异常,请检查网络后重试"
)
}
"4000" -> {
// 支付失败
Log.e(TAG, "支付宝支付失败: orderId=$orderId, memo=$memo")
PaymentResult.Failure(
orderId = orderId,
paymentMethod = "alipay",
errorCode = 4000,
errorMessage = memo.ifEmpty { "支付失败" }
)
}
else -> {
Log.e(TAG, "支付宝支付未知状态: resultStatus=$resultStatus, memo=$memo")
PaymentResult.Failure(
orderId = orderId,
paymentMethod = "alipay",
errorCode = resultStatus.toIntOrNull() ?: -1,
errorMessage = memo.ifEmpty { "支付异常($resultStatus)" }
)
}
}
}
}

View File

@@ -0,0 +1,82 @@
package com.huaga.life_echo.payment
import android.app.Activity
import android.content.Context
import android.util.Log
/**
* 统一支付管理器
*
* 提供统一的支付入口,屏蔽微信/支付宝底层差异
*
* 使用方式:
* ```
* val manager = PaymentManager(context, wechatAppId)
*
* // 微信支付
* manager.startWeChatPay(activity, params, orderId) { result -> ... }
*
* // 支付宝支付
* val result = manager.startAlipay(activity, orderString, orderId)
* ```
*/
class PaymentManager(
context: Context,
wechatAppId: String
) {
companion object {
private const val TAG = "PaymentManager"
}
private val weChatPayHandler = WeChatPayHandler(context.applicationContext, wechatAppId)
private val alipayHandler = AlipayHandler()
/**
* 检查微信是否已安装
*/
fun isWeChatInstalled(): Boolean {
return weChatPayHandler.isWeChatInstalled()
}
/**
* 发起微信支付
*
* @param params 从服务端获取的微信支付参数
* @param orderId 内部订单号
* @param callback 支付结果回调(在主线程)
*/
fun startWeChatPay(
params: WeChatPayParams,
orderId: String,
callback: (PaymentResult) -> Unit
) {
Log.d(TAG, "发起微信支付: orderId=$orderId")
weChatPayHandler.startPay(params, orderId, callback)
}
/**
* 发起支付宝支付
*
* 注意:此方法需要在协程中调用
*
* @param activity 当前 Activity
* @param orderString 从服务端获取的支付宝订单字符串
* @param orderId 内部订单号
* @return PaymentResult
*/
suspend fun startAlipay(
activity: Activity,
orderString: String,
orderId: String
): PaymentResult {
Log.d(TAG, "发起支付宝支付: orderId=$orderId")
return alipayHandler.startPay(activity, orderString, orderId)
}
/**
* 释放资源,在不需要时调用
*/
fun release() {
weChatPayHandler.release()
}
}

View File

@@ -0,0 +1,39 @@
package com.huaga.life_echo.payment
/**
* 支付结果封装
*/
sealed class PaymentResult {
/** 支付成功 */
data class Success(
val orderId: String,
val paymentMethod: String
) : PaymentResult()
/** 支付失败 */
data class Failure(
val orderId: String,
val paymentMethod: String,
val errorCode: Int = -1,
val errorMessage: String = "支付失败"
) : PaymentResult()
/** 用户取消支付 */
data class Cancelled(
val orderId: String,
val paymentMethod: String
) : PaymentResult()
}
/**
* 微信支付调起参数(从服务端获取)
*/
data class WeChatPayParams(
val appId: String,
val partnerId: String,
val prepayId: String,
val nonceStr: String,
val timeStamp: String,
val sign: String,
val packageValue: String = "Sign=WXPay"
)

View File

@@ -0,0 +1,187 @@
package com.huaga.life_echo.payment
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import com.tencent.mm.opensdk.modelpay.PayReq
import com.tencent.mm.opensdk.openapi.IWXAPI
import com.tencent.mm.opensdk.openapi.WXAPIFactory
/**
* 微信支付处理器
*
* 负责:
* 1. 注册微信 SDK
* 2. 调起微信支付
* 3. 接收支付结果(通过 WXPayEntryActivity 转发)
*/
class WeChatPayHandler(
private val context: Context,
private val appId: String
) {
companion object {
private const val TAG = "WeChatPayHandler"
/** 微信支付结果广播 Action */
const val ACTION_WECHAT_PAY_RESULT = "com.huaga.life_echo.WECHAT_PAY_RESULT"
const val EXTRA_ERROR_CODE = "error_code"
const val EXTRA_ERROR_MSG = "error_msg"
}
private val wxApi: IWXAPI by lazy {
WXAPIFactory.createWXAPI(context, appId, true).also {
it.registerApp(appId)
}
}
/** 当前等待支付结果的回调 */
private var pendingCallback: ((PaymentResult) -> Unit)? = null
private var pendingOrderId: String? = null
/** 广播接收器:接收来自 WXPayEntryActivity 的支付结果 */
private val payResultReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val errCode = intent?.getIntExtra(EXTRA_ERROR_CODE, -1) ?: -1
val errMsg = intent?.getStringExtra(EXTRA_ERROR_MSG) ?: ""
handlePayResult(errCode, errMsg)
}
}
private var isReceiverRegistered = false
/**
* 检查微信是否已安装
*/
fun isWeChatInstalled(): Boolean {
return wxApi.isWXAppInstalled
}
/**
* 发起微信支付
*
* @param params 从服务端获取的支付参数
* @param orderId 内部订单号
* @param callback 支付结果回调
*/
fun startPay(
params: WeChatPayParams,
orderId: String,
callback: (PaymentResult) -> Unit
) {
if (!isWeChatInstalled()) {
callback(
PaymentResult.Failure(
orderId = orderId,
paymentMethod = "wechat",
errorCode = -2,
errorMessage = "未安装微信客户端"
)
)
return
}
// 保存回调
pendingCallback = callback
pendingOrderId = orderId
// 注册广播接收器
registerReceiver()
// 构建支付请求
val req = PayReq().apply {
appId = params.appId
partnerId = params.partnerId
prepayId = params.prepayId
nonceStr = params.nonceStr
timeStamp = params.timeStamp
sign = params.sign
packageValue = params.packageValue
}
Log.d(TAG, "发起微信支付: orderId=$orderId")
val result = wxApi.sendReq(req)
if (!result) {
Log.e(TAG, "微信支付调起失败")
pendingCallback?.invoke(
PaymentResult.Failure(
orderId = orderId,
paymentMethod = "wechat",
errorCode = -3,
errorMessage = "微信支付调起失败"
)
)
pendingCallback = null
pendingOrderId = null
}
}
/**
* 处理微信支付结果
* 由广播接收器调用
*/
private fun handlePayResult(errCode: Int, errMsg: String) {
val orderId = pendingOrderId ?: ""
val callback = pendingCallback
// 清理状态
pendingCallback = null
pendingOrderId = null
val result = when (errCode) {
0 -> {
Log.d(TAG, "微信支付成功: orderId=$orderId")
PaymentResult.Success(orderId = orderId, paymentMethod = "wechat")
}
-1 -> {
Log.e(TAG, "微信支付失败: errCode=$errCode, errMsg=$errMsg")
PaymentResult.Failure(
orderId = orderId,
paymentMethod = "wechat",
errorCode = errCode,
errorMessage = errMsg.ifEmpty { "支付失败" }
)
}
-2 -> {
Log.d(TAG, "微信支付取消: orderId=$orderId")
PaymentResult.Cancelled(orderId = orderId, paymentMethod = "wechat")
}
else -> {
Log.e(TAG, "微信支付未知错误: errCode=$errCode")
PaymentResult.Failure(
orderId = orderId,
paymentMethod = "wechat",
errorCode = errCode,
errorMessage = "未知错误($errCode)"
)
}
}
callback?.invoke(result)
}
private fun registerReceiver() {
if (!isReceiverRegistered) {
val filter = IntentFilter(ACTION_WECHAT_PAY_RESULT)
context.registerReceiver(payResultReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
isReceiverRegistered = true
}
}
/**
* 释放资源
*/
fun release() {
if (isReceiverRegistered) {
try {
context.unregisterReceiver(payResultReceiver)
} catch (_: Exception) {
}
isReceiverRegistered = false
}
pendingCallback = null
pendingOrderId = null
wxApi.detach()
}
}

View File

@@ -0,0 +1,74 @@
package com.huaga.life_echo.wxapi
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import com.huaga.life_echo.payment.WeChatPayHandler
import com.tencent.mm.opensdk.constants.ConstantsAPI
import com.tencent.mm.opensdk.modelbase.BaseReq
import com.tencent.mm.opensdk.modelbase.BaseResp
import com.tencent.mm.opensdk.openapi.IWXAPI
import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler
import com.tencent.mm.opensdk.openapi.WXAPIFactory
import com.huaga.life_echo.config.AppConfig
/**
* 微信支付结果回调 Activity
*
* 重要说明:
* - 此类必须位于 {应用包名}.wxapi 目录下
* - 类名必须为 WXPayEntryActivity
* - 这是微信 SDK 的硬性要求,不可更改路径和类名
*
* 收到微信支付结果后,通过广播转发给 WeChatPayHandler
*/
class WXPayEntryActivity : Activity(), IWXAPIEventHandler {
companion object {
private const val TAG = "WXPayEntryActivity"
}
private lateinit var wxApi: IWXAPI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
wxApi = WXAPIFactory.createWXAPI(this, AppConfig.WECHAT_APP_ID)
wxApi.handleIntent(intent, this)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
wxApi.handleIntent(intent, this)
}
/**
* 微信发送请求到第三方应用时回调(支付场景不会触发)
*/
override fun onReq(req: BaseReq?) {
Log.d(TAG, "onReq: ${req?.type}")
}
/**
* 第三方应用发送到微信的请求处理后的响应结果回调
* 支付结果在这里接收
*/
override fun onResp(resp: BaseResp?) {
Log.d(TAG, "onResp: type=${resp?.type}, errCode=${resp?.errCode}, errStr=${resp?.errStr}")
if (resp?.type == ConstantsAPI.COMMAND_PAY_BY_WX) {
// 通过广播将结果转发给 WeChatPayHandler
val intent = Intent(WeChatPayHandler.ACTION_WECHAT_PAY_RESULT).apply {
setPackage(packageName)
putExtra(WeChatPayHandler.EXTRA_ERROR_CODE, resp.errCode)
putExtra(WeChatPayHandler.EXTRA_ERROR_MSG, resp.errStr ?: "")
}
sendBroadcast(intent)
}
// 关闭此透明 Activity
finish()
}
}