修复启动时序、重构详情页布局、对接取消订单接口

1. 启动时序修复:Launcher 从 ActivationActivity 改为 HomeActivity,
   已激活用户直接进首页,不再闪现激活页
2. 详情页布局重构:左右分栏设计(左侧海报 + 右侧信息/操作),
   全屏背景图 + 渐变遮罩,TV 端焦点导航
3. 对接后端取消订单接口 POST /api/jst/daycard/cancel/{orderNo},
   用户关闭支付弹窗时自动取消未支付订单
This commit is contained in:
freeboss
2026-08-06 13:43:29 +08:00
parent e1a088ef8e
commit 198022477a
31 changed files with 16212 additions and 147 deletions

View File

@@ -5,7 +5,7 @@ package com.pukka.mediaapp.config
*/
object DayCardConfig {
/** 天卡 API 基础地址 */
const val BASE_URL = "http://172.16.100.128:8084/"
const val BASE_URL = "http://58.220.83.58:8084/"
/** 激活码长度(管理后台生成的 5 位激活码) */
const val ACTIVATION_CODE_LENGTH = 5
@@ -37,6 +37,8 @@ object DayCardConfig {
const val DAYCARD_CREATE = "api/jst/daycard/create"
/** 查询订单状态 */
const val DAYCARD_STATUS = "api/jst/daycard/status/{orderNo}"
/** 取消订单(用户关闭支付弹窗时调用) */
const val DAYCARD_CANCEL = "api/jst/daycard/cancel/{orderNo}"
/** 心跳续期 */
const val HEARTBEAT = "api/jst/heartBeat"
/** 刷新 sign */

View File

@@ -232,3 +232,42 @@ data class TagItem(
val epg_type: Int = 0,
val subject_type: Int = 0
)
// ==================== 顶部功能键 (对应 GetTopButtonBean) ====================
data class TopButtonResponse(
val resultcode: String = "",
val data: List<TopButton>? = null
) {
data class TopButton(
val name: String = "",
val type: String = "",
val image: String = "",
val focusImage: String = "",
val param: String = "",
val positionid: String = "",
val url: String = ""
)
}
// ==================== 随心看/短视频 (对应 GetShortVideoBean) ====================
data class ShortVideoResponse(
val resultcode: String = "",
val result: List<ShortVideo>? = null
) {
data class ShortVideo(
val name: String = "",
val image: String = "",
val contentcode: String = "",
val playurl: String = "",
val duration: String = ""
)
}
// ==================== 搜索联想词 (对应 SuggestBean) ====================
data class SuggestResponse(
val resultcode: String = "",
val suggest: List<String> = emptyList()
)

View File

@@ -33,11 +33,13 @@ data class InitRequest(
/** init 响应(唯一不需要 sign 的接口) */
data class InitResponse(
val sign: String = "",
val data: InitData? = null
) : BaseResponse()
) : BaseResponse() {
val sign: String get() = data?.sign ?: ""
}
data class InitData(
val sign: String = "",
val device: DeviceInfo? = null
)

View File

@@ -1,5 +1,6 @@
package com.pukka.mediaapp.network
import com.migu.bean.GetTopButtonBean
import com.pukka.mediaapp.model.*
import retrofit2.http.Body
import retrofit2.http.POST
@@ -26,4 +27,8 @@ interface ApiService {
/** 栏目标签 */
@POST
suspend fun getTagsList(@Url url: String, @Body params: Map<String, String>): TagsListResponse
/** 顶部功能键(直连橘视厅,返回 libdata 的 GetTopButtonBean 供 TopView.bindData 使用) */
@POST
suspend fun getTopButton(@Url url: String, @Body params: Map<String, String>): GetTopButtonBean
}

View File

@@ -33,4 +33,8 @@ interface DayCardApiService {
/** 6. 刷新 sign(用当前 sign 换新 sign,拦截器自动注入当前 sign)。无请求体。 */
@POST("api/jst/refreshSign")
suspend fun refreshSign(): RefreshSignResponse
/** 7. 取消订单(用户关闭支付弹窗时调用)。sign 由拦截器自动注入。 */
@POST("api/jst/daycard/cancel/{orderNo}")
suspend fun cancelOrder(@Path("orderNo") orderNo: String): BaseResponse
}

View File

@@ -1,5 +1,6 @@
package com.pukka.mediaapp.network
import com.migu.bean.GetTopButtonBean
import com.pukka.mediaapp.config.ApiConfig
import com.pukka.mediaapp.model.*
import kotlinx.coroutines.Dispatchers
@@ -109,6 +110,18 @@ class MediaRepository {
}
}
/** 获取顶部功能键(直连橘视厅 topButton.do,返回 GetTopButtonBean 供 TopView.bindData 渲染) */
suspend fun getTopButton(): Result<GetTopButtonBean> = withContext(Dispatchers.IO) {
try {
val params = SignUtil.buildParams()
val url = ApiConfig.BASE_URL + ApiConfig.PATH_PREFIX + "/" + ApiConfig.Path.TOP_BUTTON
val response = api.getTopButton(url, params)
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
companion object {
@Volatile
private var instance: MediaRepository? = null

View File

@@ -26,13 +26,6 @@ class ActivationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 已激活则直接进首页
if (SignManager.isActivated() && SignManager.hasSign()) {
startActivity(Intent(this, HomeActivity::class.java))
finish()
return
}
binding = ActivityActivationBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -41,6 +34,23 @@ class ActivationActivity : AppCompatActivity() {
submitActivation(activateCode)
}
})
// 已激活则异步验证 sign 有效性(调后端 init,防止后台删除设备后 App 仍认为已激活)
if (SignManager.isActivated() && SignManager.hasSign()) {
binding.activationView.showActivateMessage("正在验证...")
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) { SignManager.doInit() }
FileLogger.log("ActivationActivity", "启动验证成功,进入首页")
startActivity(Intent(this@ActivationActivity, HomeActivity::class.java))
finish()
} catch (e: Exception) {
FileLogger.error("ActivationActivity", "启动验证失败,清除缓存重新激活: ${e.message}", e)
SignManager.clear()
binding.activationView.showActivateErrorMessage("设备信息已失效,请重新激活")
}
}
}
}
private fun submitActivation(code: String) {

View File

@@ -447,12 +447,14 @@ class DetailActivity : AppCompatActivity() {
// 关闭按钮
dialogBinding?.btnClose?.setOnClickListener {
paymentViewModel.cancelOrder()
dismissPaymentDialog()
paymentViewModel.reset()
}
// 保留兼容:确认/取消按钮(默认隐藏)
dialogBinding?.btnCancel?.setOnClickListener {
paymentViewModel.cancelOrder()
dismissPaymentDialog()
paymentViewModel.reset()
}
@@ -531,6 +533,10 @@ class DetailActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
// Activity 销毁时,若支付弹窗仍在(未支付),取消订单
if (paymentDialog?.isShowing == true) {
paymentViewModel.cancelOrder()
}
countDownTimer?.cancel()
countDownTimer = null
paymentViewModel.cancelPolling()

View File

@@ -124,6 +124,7 @@ class HomeActivity : AppCompatActivity() {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.navItems.collect { navAdapter.submitList(it, viewModel.selectedNavIndex.value) } }
launch { viewModel.sections.collect { sectionAdapter.submitList(it) } }
launch { viewModel.topButton.collect { it?.let { bean -> binding.topView.bindData(bean) } } }
}
}
}

View File

@@ -3,6 +3,7 @@ package com.pukka.mediaapp.viewmodel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.migu.bean.GetTopButtonBean
import com.pukka.mediaapp.model.LayoutSection
import com.pukka.mediaapp.model.NavItem
import com.pukka.mediaapp.network.MediaRepository
@@ -41,6 +42,7 @@ class HomeViewModel : ViewModel() {
fun loadNav() {
if (hasLoadedNav) return
viewModelScope.launch {
loadTopButton()
_loading.value = true
repository.getLayoutNav()
.onSuccess { response ->
@@ -91,6 +93,19 @@ class HomeViewModel : ViewModel() {
}
}
/** 顶部功能键数据(供 TopView.bindData 渲染) */
private val _topButton = MutableStateFlow<GetTopButtonBean?>(null)
val topButton: StateFlow<GetTopButtonBean?> = _topButton.asStateFlow()
/** 加载顶部功能键 */
fun loadTopButton() {
viewModelScope.launch {
repository.getTopButton()
.onSuccess { _topButton.value = it }
.onFailure { FileLogger.log("HomeVM", "顶部按钮加载失败: ${it.message}") }
}
}
/** 更新选中的导航索引 */
fun selectNav(index: Int) {
_selectedNavIndex.value = index

View File

@@ -229,6 +229,23 @@ class PaymentViewModel : ViewModel() {
pollJob = null
}
/**
* 取消未支付订单(用户关闭支付弹窗时调用)。
* fire-and-forget:不阻塞 UI,失败仅记录日志。
*/
fun cancelOrder() {
val orderNo = _orderNo.value
if (orderNo.isEmpty()) return
viewModelScope.launch {
try {
val resp = api.cancelOrder(orderNo)
FileLogger.log("PaymentVM", "cancelOrder: orderNo=$orderNo code=${resp.code} msg=${resp.msg}")
} catch (e: Exception) {
FileLogger.error("PaymentVM", "cancelOrder 失败: orderNo=$orderNo ${e.message}", e)
}
}
}
private fun cancelAllJobs() {
pollJob?.cancel()
pollJob = null