新增用户类型缓存及会话列表过滤

- 新增`TrtcUserTypeRepository`,用于缓存用户是否为AI账号。
- 实现三级缓存策略(内存、Room数据库、网络),以优化`trtcId`对应的用户类型(是否为AI)的获取性能。
- 在`Agent`和`Friend`聊天列表中,根据缓存的用户类型对会话进行过滤,确保正确分类。
- 在消息列表加载时,增加用户类型缓存的预热机制,提升进入会话列表的加载速度。
- 为`UserService`和`RiderProAPI`添加通过`trtcUserIds`批量获取用户信息的接口。
- 为`PointService`新增积分定价规则的解析和缓存功能。
- 在项目构建配置中,添加`Room`数据库和`KSP`的相关依赖。
This commit is contained in:
2025-11-11 11:41:37 +08:00
parent 0540293bff
commit 9f2dcffe90
11 changed files with 441 additions and 69 deletions

View File

@@ -4,6 +4,8 @@ plugins {
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("com.google.firebase.firebase-perf")
id("org.jetbrains.kotlin.kapt")
id("com.google.devtools.ksp") version "1.9.10-1.0.13"
}
android {
@@ -133,5 +135,10 @@ dependencies {
implementation(libs.androidx.camera.view)
implementation(libs.mlkit.barcode.scanning)
// Room 持久化
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
}

View File

@@ -2,9 +2,13 @@ package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.DictItem
import com.aiosman.ravenow.data.api.PointsBalance
import com.aiosman.ravenow.data.api.PointsChangeLog
import com.aiosman.ravenow.data.api.PointsChangeLogsResponse
import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -13,37 +17,121 @@ import kotlinx.coroutines.withContext
/**
* 积分服务
*
*
* 提供积分余额查询和积分变更日志查询功能
*/
object PointService {
// ========== 定价表(字典 points_rules相关 ==========
private val dictService: DictService = DictServiceImpl()
private val gson = Gson()
sealed class RuleAmount {
data class Fixed(val value: Int) : RuleAmount()
data class Range(val min: Int, val max: Int) : RuleAmount()
}
data class PointsRules(
val add: Map<String, RuleAmount>,
val sub: Map<String, RuleAmount>
)
private val _pointsRules = MutableStateFlow<PointsRules?>(null)
val pointsRules: StateFlow<PointsRules?> = _pointsRules.asStateFlow()
suspend fun refreshPointsRules(key: String = "points_rules") {
withContext(Dispatchers.IO) {
try {
val dict = dictService.getDictByKey(key)
val rules = parsePointsRules(dict)
_pointsRules.value = rules
} catch (_: Exception) {
_pointsRules.value = null
}
}
}
private fun parsePointsRules(dict: DictItem): PointsRules? {
val raw = dict.value
val jsonStr = when (raw) {
is String -> raw
else -> gson.toJson(raw)
}
return try {
val root = JsonParser.parseString(jsonStr).asJsonObject
fun parseBlock(block: JsonObject?): Map<String, RuleAmount> {
if (block == null) return emptyMap()
return block.entrySet().associate { entry ->
val key = entry.key
val v: JsonElement = entry.value
val amount: RuleAmount = when {
v.isJsonPrimitive && v.asJsonPrimitive.isNumber -> {
RuleAmount.Fixed(v.asInt)
}
v.isJsonPrimitive && v.asJsonPrimitive.isString -> {
val s = v.asString.trim()
if (s.contains("-")) {
val parts = s.split("-")
.map { it.trim() }
.filter { it.isNotEmpty() }
val min = parts.getOrNull(0)?.toIntOrNull()
val max = parts.getOrNull(1)?.toIntOrNull()
when {
min != null && max != null -> RuleAmount.Range(min, max)
min != null -> RuleAmount.Fixed(min)
else -> RuleAmount.Fixed(0)
}
} else {
RuleAmount.Fixed(s.toIntOrNull() ?: 0)
}
}
v.isJsonObject -> {
val obj = v.asJsonObject
val min = obj.get("min")?.takeIf { it.isJsonPrimitive }?.asInt
val max = obj.get("max")?.takeIf { it.isJsonPrimitive }?.asInt
val value = obj.get("value")?.takeIf { it.isJsonPrimitive }?.asInt
when {
min != null && max != null -> RuleAmount.Range(min, max)
value != null -> RuleAmount.Fixed(value)
else -> RuleAmount.Fixed(0)
}
}
else -> RuleAmount.Fixed(0)
}
key to amount
}
}
val addObj = root.getAsJsonObject("add")
val subObj = root.getAsJsonObject("sub")
PointsRules(
add = parseBlock(addObj),
sub = parseBlock(subObj)
)
} catch (_: Exception) {
null
}
}
// 全局可观察的积分余额(仅内存,不落盘)
private val _pointsBalance = MutableStateFlow<PointsBalance?>(null)
val pointsBalance: StateFlow<PointsBalance?> = _pointsBalance.asStateFlow()
// 当前已加载的用户ID用于处理用户切换
@Volatile
private var currentUserId: Int? = null
/**
* 设置当前用户ID当用户切换时会清空旧的积分数据以避免串号
*/
@Volatile private var currentUserId: Int? = null
/** 设置当前用户ID当用户切换时会清空旧的积分数据以避免串号 */
fun setCurrentUser(userId: Int?) {
if (currentUserId != userId) {
currentUserId = userId
_pointsBalance.value = null
}
}
/**
* 清空内存中的积分状态(用于登出或用户切换)
*/
/** 清空内存中的积分状态(用于登出或用户切换) */
fun clear() {
_pointsBalance.value = null
currentUserId = null
}
/**
* 刷新当前用户的积分余额(进入应用并完成登录态初始化后调用)
* - 若为游客或无 token则清空并返回
@@ -58,14 +146,14 @@ object PointService {
_pointsBalance.value = balance
}
}
/**
* 获取当前用户积分余额
*
*
* @param includeStatistics 是否包含统计信息(累计获得和累计消费),默认 true
* @return 积分余额信息,包含当前余额和可选的统计数据
* @throws Exception 网络请求失败或服务器返回错误
*
*
* 示例:
* ```kotlin
* try {
@@ -74,7 +162,7 @@ object PointService {
* println("当前余额: ${balance.balance}")
* println("累计获得: ${balance.totalEarned}")
* println("累计消费: ${balance.totalSpent}")
*
*
* // 仅获取当前余额
* val simpleBalance = PointService.getMyPointsBalance(includeStatistics = false)
* println("当前余额: ${simpleBalance.balance}")
@@ -93,10 +181,10 @@ object PointService {
}
}
}
/**
* 获取当前用户积分变更日志列表
*
*
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 20
* @param changeType 变更类型筛选("add": 增加, "subtract": 减少, "adjust": 调整null 表示不筛选
@@ -104,7 +192,7 @@ object PointService {
* @param endTime 结束时间格式YYYY-MM-DDnull 表示不限制
* @return 积分变更日志列表响应,包含日志列表和分页信息
* @throws Exception 网络请求失败或服务器返回错误
*
*
* 示例:
* ```kotlin
* try {
@@ -114,10 +202,10 @@ object PointService {
* logs.list.forEach { log ->
* println("${log.createdAt}: ${log.changeType} ${log.amount} (${log.reason})")
* }
*
*
* // 筛选积分增加记录
* val earnLogs = PointService.getMyPointsChangeLogs(changeType = "add")
*
*
* // 查询指定时间范围的记录
* val rangeLogs = PointService.getMyPointsChangeLogs(
* startTime = "2024-01-01",
@@ -129,20 +217,21 @@ object PointService {
* ```
*/
suspend fun getMyPointsChangeLogs(
page: Int = 1,
pageSize: Int = 20,
changeType: String? = null,
startTime: String? = null,
endTime: String? = null
page: Int = 1,
pageSize: Int = 20,
changeType: String? = null,
startTime: String? = null,
endTime: String? = null
): PointsChangeLogsResponse {
return withContext(Dispatchers.IO) {
val response = ApiClient.api.getMyPointsChangeLogs(
page = page,
pageSize = pageSize,
changeType = changeType,
startTime = startTime,
endTime = endTime
)
val response =
ApiClient.api.getMyPointsChangeLogs(
page = page,
pageSize = pageSize,
changeType = changeType,
startTime = startTime,
endTime = endTime
)
if (response.isSuccessful) {
response.body() ?: throw Exception("响应数据为空")
} else {
@@ -150,10 +239,8 @@ object PointService {
}
}
}
/**
* 积分变更类型常量
*/
/** 积分变更类型常量 */
object ChangeType {
/** 积分增加 */
const val ADD = "add"
@@ -162,10 +249,8 @@ object PointService {
/** 积分调整 */
const val ADJUST = "adjust"
}
/**
* 积分变更原因常量
*/
/** 积分变更原因常量 */
object ChangeReason {
// 获得积分类型
/** 新用户注册奖励 */
@@ -178,7 +263,7 @@ object PointService {
const val EARN_INVITE = "earn_invite"
/** 充值获得 */
const val EARN_RECHARGE = "earn_recharge"
// 消费积分类型
/** 创建群聊 */
const val SPEND_GROUP_CREATE = "spend_group_create"
@@ -195,10 +280,10 @@ object PointService {
/** 定时事件解锁 */
const val SPEND_SCHEDULE_EVENT = "spend_schedule_event"
}
/**
* 获取变更原因的中文描述
*
*
* @param reason 变更原因代码
* @return 中文描述
*/
@@ -209,7 +294,6 @@ object PointService {
ChangeReason.EARN_TASK -> "任务完成奖励"
ChangeReason.EARN_INVITE -> "邀请好友奖励"
ChangeReason.EARN_RECHARGE -> "充值获得"
ChangeReason.SPEND_GROUP_CREATE -> "创建群聊"
ChangeReason.SPEND_GROUP_EXPAND -> "扩容群聊"
ChangeReason.SPEND_AGENT_PRIVATE -> "Agent 私密模式"
@@ -217,14 +301,13 @@ object PointService {
ChangeReason.SPEND_ROOM_MEMORY -> "房间记忆添加"
ChangeReason.SPEND_CHAT_BACKGROUND -> "自定义聊天背景"
ChangeReason.SPEND_SCHEDULE_EVENT -> "定时事件解锁"
else -> reason // 未知原因,返回原始代码
}
}
/**
* 获取变更类型的中文描述
*
*
* @param changeType 变更类型代码
* @return 中文描述
*/
@@ -237,4 +320,3 @@ object PointService {
}
}
}

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.BatchTrtcUserIdRequestBody
import com.aiosman.ravenow.entity.AccountProfileEntity
data class UserAuth(
@@ -67,6 +68,16 @@ interface UserService {
suspend fun getUserProfileByOpenId(id: String):AccountProfileEntity
/**
* 批量通过 TRTC 用户ID 获取用户信息列表
* @param ids TRTC 用户ID列表最多100个
* @param includeAI 是否包含AI账号默认 false
* @return 用户信息实体列表
*/
suspend fun getUserProfilesByTrtcUserIds(
ids: List<String>,
includeAI: Boolean = false
): List<AccountProfileEntity>
}
class UserServiceImpl : UserService {
@@ -122,4 +133,18 @@ class UserServiceImpl : UserService {
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body.data.toAccountProfileEntity()
}
override suspend fun getUserProfilesByTrtcUserIds(
ids: List<String>,
includeAI: Boolean
): List<AccountProfileEntity> {
val resp = ApiClient.api.getAccountProfilesByTrtcBatch(
BatchTrtcUserIdRequestBody(
trtcUserIds = ids,
includeAI = includeAI
)
)
val body = resp.body() ?: throw ServiceException("Failed to get accounts")
return body.data.map { it.toAccountProfileEntity() }
}
}

View File

@@ -87,6 +87,13 @@ data class JoinGroupChatRequestBody(
val roomId: Int? = null,
)
data class BatchTrtcUserIdRequestBody(
@SerializedName("trtcUserIds")
val trtcUserIds: List<String>,
@SerializedName("includeAI")
val includeAI: Boolean? = null,
)
data class LoginUserRequestBody(
@SerializedName("username")
val username: String? = null,
@@ -1258,6 +1265,11 @@ interface RaveNowAPI {
@Path("id") id: String
): Response<DataContainer<AccountProfile>>
@POST("profile/trtc/batch")
suspend fun getAccountProfilesByTrtcBatch(
@Body body: BatchTrtcUserIdRequestBody
): Response<DataContainer<List<AccountProfile>>>
@POST("user/{id}/follow")
suspend fun followUser(
@Path("id") id: Int

View File

@@ -0,0 +1,58 @@
package com.aiosman.ravenow.data.db
import android.content.Context
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.Upsert
@Entity(tableName = "trtc_participant_cache")
data class TrtcParticipantCache(
@PrimaryKey val trtcId: String,
val isAI: Boolean,
val updatedAt: Long
)
@Dao
interface TrtcParticipantCacheDao {
@Query("SELECT * FROM trtc_participant_cache WHERE trtcId = :trtcId LIMIT 1")
suspend fun get(trtcId: String): TrtcParticipantCache?
@Query("SELECT * FROM trtc_participant_cache WHERE trtcId IN (:ids)")
suspend fun getMany(ids: List<String>): List<TrtcParticipantCache>
@Upsert
suspend fun upsertAll(items: List<TrtcParticipantCache>)
}
@Database(
entities = [TrtcParticipantCache::class],
version = 1,
exportSchema = false
)
abstract class MessageCacheDatabase : RoomDatabase() {
abstract fun trtcParticipantCacheDao(): TrtcParticipantCacheDao
companion object {
@Volatile
private var INSTANCE: MessageCacheDatabase? = null
fun getInstance(context: Context): MessageCacheDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
MessageCacheDatabase::class.java,
"message_cache.db"
).fallbackToDestructiveMigration()
.build()
.also { INSTANCE = it }
}
}
}
}

View File

@@ -0,0 +1,133 @@
package com.aiosman.ravenow.data.repo
import android.content.Context
import android.util.Log
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.data.db.MessageCacheDatabase
import com.aiosman.ravenow.data.db.TrtcParticipantCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
object TrtcUserTypeRepository {
private const val TAG = "TrtcUserTypeRepo"
private const val MAX_BATCH = 100
private const val TTL_MS: Long = 5L * 24 * 60 * 60 * 1000 // 5 天
private val memoryCache = ConcurrentHashMap<String, CacheEntry>()
private var initialized = false
private lateinit var userService: UserService
private lateinit var db: com.aiosman.ravenow.data.db.MessageCacheDatabase
data class CacheEntry(
val isAI: Boolean,
val updatedAt: Long
) {
fun isExpired(now: Long): Boolean = now - updatedAt > TTL_MS
}
private fun ensureInit(context: Context) {
if (!initialized) {
db = MessageCacheDatabase.getInstance(context)
userService = UserServiceImpl()
initialized = true
}
}
fun getCachedType(trtcId: String): Boolean? {
val entry = memoryCache[trtcId] ?: return null
return if (!entry.isExpired(System.currentTimeMillis())) entry.isAI else null
}
suspend fun getType(context: Context, trtcId: String): Boolean? {
ensureInit(context)
val now = System.currentTimeMillis()
val mem = memoryCache[trtcId]
if (mem != null && !mem.isExpired(now)) return mem.isAI
return withContext(Dispatchers.IO) {
val dao = db.trtcParticipantCacheDao()
val entity = dao.get(trtcId)
if (entity != null && now - entity.updatedAt <= TTL_MS) {
memoryCache[trtcId] = CacheEntry(entity.isAI, entity.updatedAt)
entity.isAI
} else {
null
}
}
}
suspend fun ensureTypes(context: Context, trtcIds: List<String>) {
ensureInit(context)
if (trtcIds.isEmpty()) return
val now = System.currentTimeMillis()
// 从内存/本地命中
val toFetch = withContext(Dispatchers.IO) {
val dao = db.trtcParticipantCacheDao()
val result = mutableSetOf<String>()
val needFromDb = mutableListOf<String>()
trtcIds.forEach { id ->
val mem = memoryCache[id]
if (mem == null || mem.isExpired(now)) {
needFromDb.add(id)
}
}
if (needFromDb.isNotEmpty()) {
val entities = dao.getMany(needFromDb)
val fromDbSet = entities.toMutableList()
// 写回内存并决定是否需要网络
val dbValid = mutableSetOf<String>()
fromDbSet.forEach { e ->
if (now - e.updatedAt <= TTL_MS) {
memoryCache[e.trtcId] = CacheEntry(e.isAI, e.updatedAt)
dbValid.add(e.trtcId)
}
}
needFromDb.forEach { id ->
if (!dbValid.contains(id)) {
result.add(id)
}
}
}
result.toList()
}
if (toFetch.isEmpty()) return
// 批量分片请求
val chunks = toFetch.chunked(MAX_BATCH)
for (chunk in chunks) {
try {
val profiles = withContext(Dispatchers.IO) {
userService.getUserProfilesByTrtcUserIds(chunk, includeAI = true)
}
// 将返回的 profile 按 trtcUserId -> isAI 映射
val upserts = profiles.mapNotNull { profile ->
val id = profile.trtcUserId
if (id.isNullOrEmpty()) null
else TrtcParticipantCache(
trtcId = id,
isAI = profile.aiAccount,
updatedAt = System.currentTimeMillis()
)
}
// 落库 + 内存
withContext(Dispatchers.IO) {
db.trtcParticipantCacheDao().upsertAll(upserts)
}
upserts.forEach { e ->
memoryCache[e.trtcId] = CacheEntry(e.isAI, e.updatedAt)
}
Log.d(TAG, "Fetched types: size=${upserts.size}, batch=${chunk.size}")
} catch (e: Exception) {
Log.w(TAG, "ensureTypes fetch failed: ${e.message}")
}
}
}
}

View File

@@ -30,6 +30,7 @@ import io.openim.android.sdk.models.ConversationInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.aiosman.ravenow.data.repo.TrtcUserTypeRepository
data class Conversation(
val id: String,
@@ -76,6 +77,13 @@ object MessageListViewModel : ViewModel() {
// noticeInfo = info
//
// isLoading = false
if (loadChat && com.aiosman.ravenow.AppState.enableChat) {
// 预热 trtcId -> isAI 缓存,避免首次进入 Agent/Friends 列表时阻塞
try {
prewarmChatTypes(context)
} catch (_: Exception) {
}
}
}
@@ -163,5 +171,23 @@ object MessageListViewModel : ViewModel() {
}
}
private suspend fun prewarmChatTypes(context: Context) {
val result = suspendCoroutine { continuation ->
OpenIMClient.getInstance().conversationManager.getAllConversationList(
object : OnBase<List<ConversationInfo>> {
override fun onSuccess(data: List<ConversationInfo>?) {
continuation.resumeWith(Result.success(data ?: emptyList()))
}
override fun onError(code: Int, error: String?) {
continuation.resumeWith(Result.failure(Exception("Error $code: $error")))
}
}
)
}
val trtcIds = result.filter { it.conversationType == 1 }
.mapNotNull { it.userID }
.distinct()
TrtcUserTypeRepository.ensureTypes(context, trtcIds)
}
}

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.index.tabs.message.tab
import android.content.Context
import android.icu.util.Calendar
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -28,6 +29,7 @@ import io.openim.android.sdk.models.Message
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.aiosman.ravenow.utils.MessageParser
import com.aiosman.ravenow.data.repo.TrtcUserTypeRepository
data class AgentConversation(
val id: String,
@@ -127,15 +129,7 @@ object AgentChatListViewModel : ViewModel() {
OpenIMClient.getInstance().conversationManager.getAllConversationList(
object : OnBase<List<ConversationInfo>> {
override fun onSuccess(data: List<ConversationInfo>?) {
// 过滤出智能体会话(单聊类型,且可能有特定标识)
val agentConversations = data?.filter { conversation ->
// 这里需要根据实际业务逻辑来过滤智能体会话
// 可能通过会话类型、用户ID前缀、或其他标识来判断
conversation.conversationType == 1 // 1 表示单聊
// 可以添加更多过滤条件,比如:
// && conversation.userID?.startsWith("ai_") == true
} ?: emptyList()
continuation.resumeWith(Result.success(agentConversations))
continuation.resumeWith(Result.success(data ?: emptyList()))
}
override fun onError(code: Int, error: String?) {
@@ -145,7 +139,22 @@ object AgentChatListViewModel : ViewModel() {
)
}
agentChatList = result.map { conversation ->
// 仅单聊
val singleChats = result.filter { it.conversationType == 1 }
val trtcIds = singleChats.mapNotNull { it.userID }.distinct()
// 预热缓存包含AI
try {
TrtcUserTypeRepository.ensureTypes(context, trtcIds)
} catch (e: Exception) {
Log.w("AgentChatListViewModel", "ensureTypes failed: ${e.message}")
}
// 过滤出 AI 会话
val filtered = singleChats.filter { conv ->
val id = conv.userID ?: return@filter false
TrtcUserTypeRepository.getCachedType(id) == true
}
agentChatList = filtered.map { conversation ->
AgentConversation.convertToAgentConversation(conversation, context)
}
}

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.index.tabs.message.tab
import android.content.Context
import android.icu.util.Calendar
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -25,6 +26,7 @@ import io.openim.android.sdk.models.Message
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.aiosman.ravenow.utils.MessageParser
import com.aiosman.ravenow.data.repo.TrtcUserTypeRepository
data class FriendConversation(
val id: String,
@@ -135,13 +137,19 @@ object FriendChatListViewModel : ViewModel() {
)
}
// 过滤出朋友会话(单聊类型,且排除 AI 智能体)
val filteredConversations = result.filter { conversation ->
// 1 表示单聊,排除 AI 智能体会话
conversation.conversationType == 1 &&
// 可以根据实际业务逻辑添加更多过滤条件
// 比如排除 AI 智能体的 userID 前缀或标识
!(conversation.userID?.startsWith("ai_") == true)
// 仅单聊
val singleChats = result.filter { it.conversationType == 1 }
val trtcIds = singleChats.mapNotNull { it.userID }.distinct()
// 预热缓存包含AI
try {
TrtcUserTypeRepository.ensureTypes(context, trtcIds)
} catch (e: Exception) {
Log.w("FriendChatListViewModel", "ensureTypes failed: ${e.message}")
}
// 过滤出普通人会话(未知也归入普通人以回退)
val filteredConversations = singleChats.filter { conversation ->
val id = conversation.userID ?: return@filter true
TrtcUserTypeRepository.getCachedType(id) != true
}
friendChatList = filteredConversations.map { conversation ->

View File

@@ -23,6 +23,14 @@ object PointsViewModel : ViewModel() {
viewModelScope.launch {
try {
loading = true
// 并行预加载积分定价表不影响UI
launch {
try {
PointService.refreshPointsRules()
} catch (e: Exception) {
Log.e("PointsViewModel", "refresh rules error", e)
}
}
if (!AppStore.isGuest) {
PointService.refreshMyPointsBalance()
}