task manager 读取
This commit is contained in:
parent
9ce2bda2bc
commit
1424f72256
|
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"task_module_config": {
|
||||
"basic_info": {
|
||||
"module_name": "任务中心",
|
||||
"page_path": "底部第二个页签",
|
||||
"layout_style": "上下滑动长页面",
|
||||
"currency_display": ["金币账户", "巴西雷亚尔现金账户"]
|
||||
},
|
||||
"task_categories": [
|
||||
{
|
||||
"category_id": "newbie_task",
|
||||
"category_name": "新手任务(Missão Para Iniciantes)",
|
||||
"valid_period": "注册后7天内",
|
||||
"display_priority": 1,
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "newbie_first_withdraw",
|
||||
"task_name": "完成第一次提现",
|
||||
"task_desc": "完成首次提现操作(0.1 BRL门槛),即可获得100金币奖励",
|
||||
"target_action": "用户完成首次提现流程(含绑定Pix账户、观看广告)",
|
||||
"reward_type": "金币",
|
||||
"reward_value": 100,
|
||||
"is_one_time": true,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"task_id": "newbie_push_notify",
|
||||
"task_name": "开启消息推送",
|
||||
"task_desc": "启用APP消息推送权限,实时获取任务更新与奖励通知",
|
||||
"target_action": "用户授权APP系统推送通知",
|
||||
"reward_type": "金币",
|
||||
"reward_value": 300,
|
||||
"is_one_time": true,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"task_id": "newbie_join_discord",
|
||||
"task_name": "加入Discord社区",
|
||||
"task_desc": "通过APP内链接加入官方Discord社区,获取专属活动与客服支持",
|
||||
"target_action": "用户通过指定链接成功加入Discord社区",
|
||||
"reward_type": "金币",
|
||||
"reward_value": 200,
|
||||
"is_one_time": true,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"task_id": "newbie_7day_checkin",
|
||||
"task_name": "7日连续签到",
|
||||
"task_desc": "连续7天完成签到,奖励逐级递增,第7天最高可得800金币",
|
||||
"target_action": "用户每日完成签到操作(支持广告补签)",
|
||||
"reward_type": "金币",
|
||||
"reward_details": [
|
||||
{"day": 1, "value": 100},
|
||||
{"day": 2, "value": 300},
|
||||
{"day": 3, "value": 300},
|
||||
{"day": 4, "value": 500},
|
||||
{"day": 5, "value": 300},
|
||||
{"day": 6, "value": 300},
|
||||
{"day": 7, "value": 800}
|
||||
],
|
||||
"support_makeup": true,
|
||||
"makeup_method": "观看15-30秒激励视频",
|
||||
"is_one_time": true,
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"category_id": "daily_task",
|
||||
"category_name": "日常任务(Missão Diária)",
|
||||
"valid_period": "每日凌晨00:00重置",
|
||||
"display_priority": 2,
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "daily_checkin",
|
||||
"task_name": "每日签到",
|
||||
"task_desc": "每日签到可获金币,7天为一周期,奖励循环递增(支持双倍广告奖励)",
|
||||
"target_action": "用户当日完成签到操作",
|
||||
"reward_type": "金币",
|
||||
"reward_details": [
|
||||
{"day": 1, "value": 100},
|
||||
{"day": 2, "value": 300},
|
||||
{"day": 3, "value": 300},
|
||||
{"day": 4, "value": 500},
|
||||
{"day": 5, "value": 300},
|
||||
{"day": 6, "value": 300},
|
||||
{"day": 7, "value": 500}
|
||||
],
|
||||
"double_reward_method": "观看15-30秒激励视频",
|
||||
"support_makeup": true,
|
||||
"makeup_method": "观看15-30秒激励视频",
|
||||
"is_one_time": false,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"task_id": "daily_video_ladder",
|
||||
"task_name": "阶梯观看视频",
|
||||
"task_desc": "累计观看指定数量短视频,分3档领取奖励(与基础观看收益叠加)",
|
||||
"target_action": "用户累计观看短视频",
|
||||
"reward_type": "金币",
|
||||
"reward_details": [
|
||||
{"target_count": 10, "value": 100},
|
||||
{"target_count": 20, "value": 150},
|
||||
{"target_count": 30, "value": 200}
|
||||
],
|
||||
"reward_stackable": true,
|
||||
"is_one_time": false,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"task_id": "daily_ad_ladder",
|
||||
"task_name": "阶梯观看激励视频",
|
||||
"task_desc": "累计观看指定数量激励视频,分3档领取奖励(每日上限10次)",
|
||||
"target_action": "用户累计观看激励视频",
|
||||
"reward_type": "金币",
|
||||
"reward_details": [
|
||||
{"target_count": 1, "value": 100},
|
||||
{"target_count": 5, "value": 150},
|
||||
{"target_count": 10, "value": 200}
|
||||
],
|
||||
"daily_limit": 10,
|
||||
"reward_stackable": true,
|
||||
"is_one_time": false,
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,23 @@
|
|||
package com.gamedog.vididin
|
||||
|
||||
import com.ama.core.architecture.BaseApp
|
||||
import com.gamedog.vididin.manager.TaskManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
|
||||
@HiltAndroidApp
|
||||
class VidiDinApp : BaseApp() {
|
||||
|
||||
init {
|
||||
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initManagers()
|
||||
}
|
||||
|
||||
private fun initManagers() {
|
||||
TaskManager.instance()
|
||||
}
|
||||
}
|
||||
|
|
@ -6,14 +6,18 @@ import androidx.core.view.ViewCompat
|
|||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.ama.core.architecture.appBase.AppViewsFragment
|
||||
import com.ama.core.architecture.appBase.OnFragmentBackgroundListener
|
||||
import com.ama.core.architecture.util.SpUtil
|
||||
import com.ama.core.architecture.util.setOnClickBatch
|
||||
import com.ama.core.architecture.util.setStatusBarDarkFont
|
||||
import com.gamedog.vididin.main.fragments.task.DailySignDialog
|
||||
import com.gamedog.vididin.main.fragments.task.TaskBean
|
||||
import com.gamedog.vididin.main.interfaces.OnTabClickAgainListener
|
||||
import com.gamedog.vididin.manager.TaskManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.getValue
|
||||
import com.gamedog.vididin.databinding.VididinappFeatureMessageFragmentMessageBinding as ViewBinding
|
||||
import com.gamedog.vididin.main.fragments.home.YoutubeViewModel as ViewModel
|
||||
|
|
@ -24,6 +28,7 @@ import com.gamedog.vididin.main.fragments.home.YoutubeUiState as UiState
|
|||
@AndroidEntryPoint
|
||||
class TasksFragment : AppViewsFragment<ViewBinding, UiState, ViewModel>(), OnTabClickAgainListener, OnFragmentBackgroundListener {
|
||||
|
||||
private var mTaskConfig: TaskBean? = null
|
||||
override val mViewModel: ViewModel by viewModels()
|
||||
override var isBackgroundBright: Boolean = true
|
||||
|
||||
|
|
@ -42,6 +47,7 @@ class TasksFragment : AppViewsFragment<ViewBinding, UiState, ViewModel>(), OnTab
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override fun ViewBinding.initViews() {
|
||||
with(binding) {
|
||||
setOnClickBatch(ivGotoDailySign) {
|
||||
|
|
@ -52,6 +58,10 @@ class TasksFragment : AppViewsFragment<ViewBinding, UiState, ViewModel>(), OnTab
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
mTaskConfig = TaskManager.instance().getTaskConfig()
|
||||
}
|
||||
}
|
||||
|
||||
override fun ViewBinding.initListeners() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package com.gamedog.vididin.main.fragments.task
|
||||
|
||||
data class TaskBean(
|
||||
val task_module_config: TaskModuleConfig
|
||||
)
|
||||
|
||||
data class TaskModuleConfig(
|
||||
val basic_info: BasicInfo,
|
||||
val task_categories: List<TaskCategory>
|
||||
)
|
||||
|
||||
data class BasicInfo(
|
||||
val currency_display: List<String>,
|
||||
val layout_style: String,
|
||||
val module_name: String,
|
||||
val page_path: String
|
||||
)
|
||||
|
||||
data class TaskCategory(
|
||||
val category_id: String,
|
||||
val category_name: String,
|
||||
val display_priority: Int,
|
||||
val tasks: List<Task>,
|
||||
val valid_period: String
|
||||
)
|
||||
|
||||
data class Task(
|
||||
val daily_limit: Int,
|
||||
val double_reward_method: String,
|
||||
val is_one_time: Boolean,
|
||||
val makeup_method: String,
|
||||
val reward_details: List<RewardDetail>,
|
||||
val reward_stackable: Boolean,
|
||||
val reward_type: String,
|
||||
val reward_value: Int,
|
||||
val status: String,
|
||||
val support_makeup: Boolean,
|
||||
val target_action: String,
|
||||
val task_desc: String,
|
||||
val task_id: String,
|
||||
val task_name: String
|
||||
)
|
||||
|
||||
data class RewardDetail(
|
||||
val day: Int,
|
||||
val target_count: Int,
|
||||
val value: Int
|
||||
)
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.gamedog.vididin.manager
|
||||
|
||||
import com.ama.core.architecture.util.FileUtil
|
||||
import com.gamedog.vididin.main.fragments.task.TaskBean
|
||||
import com.google.gson.GsonBuilder
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class TaskManager private constructor() {
|
||||
companion object {
|
||||
@Volatile
|
||||
private var instance: TaskManager? = null
|
||||
|
||||
fun instance(): TaskManager {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: TaskManager().also {
|
||||
instance = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
@Volatile
|
||||
private var initialized = false
|
||||
private var mTaskConfigBean: TaskBean? = null
|
||||
private val mGson = GsonBuilder().create()
|
||||
|
||||
|
||||
init {
|
||||
loadTaskConfigAsync()
|
||||
}
|
||||
|
||||
|
||||
suspend fun getTaskConfig(): TaskBean? = mutex.withLock {
|
||||
if (!initialized) {
|
||||
loadTaskConfigFromAsset()
|
||||
}
|
||||
return mTaskConfigBean
|
||||
}
|
||||
|
||||
private fun loadTaskConfigAsync() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
mutex.withLock {
|
||||
if (!initialized) {
|
||||
loadTaskConfigFromAsset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadTaskConfigFromAsset() {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val configStr = FileUtil.readAssetsFile("taskConfig.json")
|
||||
mTaskConfigBean = mGson.fromJson(configStr, TaskBean::class.java)
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package com.ama.core.architecture.util
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object AsyncUtil {
|
||||
private val jobMap = ConcurrentHashMap<String, Job>()
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
|
||||
fun <T> doGlobalAsync(
|
||||
backgroundWork: suspend () -> T,
|
||||
onStart: (() -> Unit)? = null,
|
||||
onComplete: ((T) -> Unit)? = null,
|
||||
onError: ((Exception) -> Unit)? = null
|
||||
): Job {
|
||||
return scope.launch {
|
||||
onStart?.invoke()
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) { backgroundWork() }
|
||||
onComplete?.invoke(result)
|
||||
} catch (e: Exception) {
|
||||
onError?.invoke(e)
|
||||
}
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion { cause ->
|
||||
if (cause != null) Log.d("AsyncTaskUtil", "Global task completed with error: $cause")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定组件生命周期的异步任务
|
||||
*/
|
||||
fun <T> doLifecycleAwareAsync(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
backgroundWork: suspend () -> T,
|
||||
onStart: (() -> Unit)? = null,
|
||||
onComplete: ((T) -> Unit)? = null,
|
||||
onError: ((Exception) -> Unit)? = null
|
||||
) {
|
||||
val key = lifecycleOwner::class.java.simpleName + System.identityHashCode(lifecycleOwner)
|
||||
|
||||
// 清理现有任务
|
||||
jobMap[key]?.cancel()
|
||||
|
||||
val job = scope.launch {
|
||||
onStart?.invoke()
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) { backgroundWork() }
|
||||
if (isActive) onComplete?.invoke(result)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
if (isActive) onError?.invoke(e)
|
||||
}
|
||||
}.also { jobMap[key] = it }
|
||||
|
||||
// 绑定生命周期
|
||||
lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_DESTROY -> {
|
||||
job.cancel()
|
||||
jobMap.remove(key)
|
||||
lifecycleOwner.lifecycle.removeObserver(this)
|
||||
Log.d("AsyncTaskUtil", "Lifecycle task cleaned up: $key")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有任务(用于应用退出)
|
||||
*/
|
||||
fun shutdown() {
|
||||
scope.cancel()
|
||||
jobMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|
||||
// 1 全局任务 ----------------------
|
||||
val job = AsyncTaskUtil.doGlobalAsync(
|
||||
backgroundWork = {
|
||||
// 在IO线程执行
|
||||
simulateNetworkRequest()
|
||||
},
|
||||
onStart = { showProgressBar() },
|
||||
onComplete = { result ->
|
||||
// 在主线程执行
|
||||
updateUI(result)
|
||||
},
|
||||
onError = { e ->
|
||||
showError(e.message ?: "Unknown error")
|
||||
}
|
||||
)
|
||||
|
||||
// 手动取消(如果需要)
|
||||
job.cancel()
|
||||
|
||||
|
||||
|
||||
|
||||
// 2 生命周期绑定任务 ----------------------
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// 任务自动绑定到Activity生命周期
|
||||
AsyncTaskUtil.doLifecycleAwareAsync(
|
||||
lifecycleOwner = this,
|
||||
backgroundWork = { loadUserData() },
|
||||
onComplete = { data -> updateUserInterface(data) },
|
||||
onError = { e -> showErrorMessage(e) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 3 ViewModel中 ----------------------
|
||||
class MyViewModel : ViewModel() {
|
||||
fun loadData() {
|
||||
// 使用viewModelScope而非全局Scope
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val data = withContext(Dispatchers.IO) { fetchData() }
|
||||
_uiState.value = SuccessState(data)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = ErrorState(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.ama.core.architecture.util
|
||||
|
||||
import android.content.Context
|
||||
import com.ama.core.architecture.BaseApp
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
|
||||
|
||||
class FileUtil private constructor() {
|
||||
companion object {
|
||||
|
||||
|
||||
fun readAssetsFile(fileName: String, encoding: String = "UTF-8"): String? {
|
||||
return try {
|
||||
// 使用use关键字自动关闭资源
|
||||
BaseApp.appContext().assets.open(fileName).use { inputStream ->
|
||||
InputStreamReader(inputStream, encoding).use { reader ->
|
||||
val buffer = CharArray(1024)
|
||||
val result = StringBuilder()
|
||||
while (true) {
|
||||
val bytesRead = reader.read(buffer)
|
||||
if (bytesRead == -1) break
|
||||
result.append(buffer, 0, bytesRead)
|
||||
}
|
||||
result.toString()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐行读取文本文件(适合处理大文件或需要按行处理的场景)
|
||||
*/
|
||||
fun readTextFileLineByLine(context: Context, fileName: String, encoding: String = "UTF-8"): List<String> {
|
||||
return try {
|
||||
context.assets.open(fileName).use { inputStream ->
|
||||
InputStreamReader(inputStream, encoding).use { reader ->
|
||||
BufferedReader(reader).useLines { lines ->
|
||||
lines.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.ama.core.architecture.util
|
||||
|
||||
class ResUtil private constructor() {
|
||||
companion object {
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue