这个提交包含在:
徐勤民 2026-03-27 18:45:21 +08:00
父节点 6e44428e8a
当前提交 0314acc18e
共有 77 个文件被更改,包括 3812 次插入399 次删除

2
.gitignore vendored
查看文件

@ -1,6 +1,7 @@
.DS_Store
.idea/
.m2/
.yarn/
.gradle/
.kotlin/
build/
@ -8,6 +9,7 @@ target/
node_modules/
dist/
coverage/
.pnp.*
*.iml
*.log
AndroidLibs/.gradle-home/

查看文件

@ -4,8 +4,9 @@
## 模块结构
- `commonsdk-core`: SDK 核心,承载网络、共享缓存、插件管理、App 更新、设备信息与时间工具。
- `commonsdk-compose`: Compose 扩展组件。
- `commonsdk-core`: SDK 核心,承载网络、下载、文件、权限、缓存、日志、全局弹窗、设备与时间等基础能力。
- `commonsdk-compose`: Compose 扩展组件,提供下拉刷新、折叠面板、滑动操作、WebView、图片等业务可复用组件。
- `commonsdk-update`: 更新 SDK,独立承载 APK 更新、插件更新、版本比较与安装编排。
- `lib-szyx`: 项目专属 SDK,承载真实登录接口、签名、业务 Header 与会话管理。
- `sample-app`: 示例宿主应用。
- `plugins/plugin-ui`: UI 演示插件,可独立运行,也可被宿主拉起。
@ -48,6 +49,10 @@ nexus.password=your-password
- `commonsdk-core` 提供:
- `HttpManager / RetrofitManager`
- `SharedCacheManager / SharedCacheProvider`
- `DownloadManager / FileHelper`
- `PermissionManager / GlobalCache / Logger / DialogCenter`
- `commonsdk-update` 提供:
- `UpdateSDK`
- `PluginPackageManager`
- `AppUpdater`
- `lib-szyx` 提供:

查看文件

@ -32,8 +32,11 @@ dependencies {
api(project(":commonsdk-core"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.webkit)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.compose)
implementation(libs.coil.compose)
debugImplementation(libs.bundles.compose.debug)
}

查看文件

@ -1,3 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="com.xuqm.sdk.compose.activity.WebViewPageActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="false" />
</application>
</manifest>

查看文件

@ -0,0 +1,42 @@
package com.xuqm.sdk.compose.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.xuqm.sdk.compose.components.webview.CommonWebView
import com.xuqm.sdk.compose.components.webview.WebViewConfig
import androidx.compose.material3.MaterialTheme
class WebViewPageActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val url = intent.getStringExtra(EXTRA_URL).orEmpty()
val title = intent.getStringExtra(EXTRA_TITLE)
setContent {
MaterialTheme {
CommonWebView(
url = url,
config = WebViewConfig(title = title),
)
}
}
}
companion object {
private const val EXTRA_URL = "extra_url"
private const val EXTRA_TITLE = "extra_title"
fun createIntent(
context: Context,
url: String,
title: String? = null,
): Intent {
return Intent(context, WebViewPageActivity::class.java).apply {
putExtra(EXTRA_URL, url)
putExtra(EXTRA_TITLE, title)
}
}
}
}

查看文件

@ -0,0 +1,97 @@
package com.xuqm.sdk.compose.components.accordion
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ExpandLess
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.xuqm.sdk.compose.components.refresh.RefreshableLazyColumn
data class AccordionItem<T>(
val id: String,
val title: String,
val data: T,
val initiallyExpanded: Boolean = false,
)
@Composable
fun AccordionPanel(
title: String,
modifier: Modifier = Modifier,
initiallyExpanded: Boolean = false,
headerBackground: Color = MaterialTheme.colorScheme.surfaceVariant,
content: @Composable () -> Unit,
) {
var expanded by remember { mutableStateOf(initiallyExpanded) }
Card(modifier = modifier.fillMaxWidth()) {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.background(headerBackground)
.clickable { expanded = !expanded }
.padding(horizontal = 16.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(title, style = MaterialTheme.typography.titleMedium)
Icon(
imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
contentDescription = null,
)
}
AnimatedVisibility(expanded) {
Box(modifier = Modifier.padding(16.dp)) {
content()
}
}
}
}
}
@Composable
fun <T> RefreshableAccordionList(
items: List<AccordionItem<T>>,
isRefreshing: Boolean,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
itemContent: @Composable (AccordionItem<T>) -> Unit,
) {
RefreshableLazyColumn(
items = items,
isRefreshing = isRefreshing,
onRefresh = onRefresh,
modifier = modifier,
listState = rememberLazyListState(),
itemContent = { _, item ->
AccordionPanel(
title = item.title,
initiallyExpanded = item.initiallyExpanded,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
) {
itemContent(item)
}
},
)
}

查看文件

@ -0,0 +1,103 @@
package com.xuqm.sdk.compose.components.image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
enum class ImageAdaptMode {
WIDTH_FIXED,
HEIGHT_FIXED,
BOTH_FIXED,
FILL_WIDTH,
}
@Composable
fun AdaptiveImage(
model: Any?,
modifier: Modifier = Modifier,
adaptMode: ImageAdaptMode = ImageAdaptMode.FILL_WIDTH,
width: Dp? = null,
height: Dp? = null,
aspectRatio: Float = 1f,
shape: Shape = RoundedCornerShape(16.dp),
contentScale: ContentScale = ContentScale.Crop,
contentDescription: String? = null,
) {
val imageModifier = when (adaptMode) {
ImageAdaptMode.WIDTH_FIXED -> Modifier.width(width ?: 160.dp).aspectRatio(aspectRatio)
ImageAdaptMode.HEIGHT_FIXED -> Modifier.height(height ?: 160.dp).aspectRatio(aspectRatio)
ImageAdaptMode.BOTH_FIXED -> Modifier.width(width ?: 160.dp).height(height ?: 160.dp)
ImageAdaptMode.FILL_WIDTH -> Modifier.fillMaxWidth().aspectRatio(aspectRatio)
}
AsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(model).crossfade(true).build(),
contentDescription = contentDescription,
modifier = modifier.then(imageModifier).clip(shape),
contentScale = contentScale,
)
}
@Composable
fun CircleImage(
model: Any?,
modifier: Modifier = Modifier,
size: Dp = 72.dp,
borderWidth: Dp = 0.dp,
borderColor: Color = Color.Transparent,
maskColor: Color = Color.Transparent,
overlayBrush: Brush? = null,
contentDescription: String? = null,
) {
Box(
modifier = modifier
.width(size)
.height(size)
.clip(CircleShape)
.border(borderWidth, borderColor, CircleShape)
.background(borderColor),
contentAlignment = Alignment.Center,
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(model).crossfade(true).build(),
contentDescription = contentDescription,
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant),
contentScale = ContentScale.Crop,
)
if (maskColor != Color.Transparent || overlayBrush != null) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
.background(overlayBrush ?: Brush.verticalGradient(listOf(maskColor, maskColor))),
)
}
}
}

查看文件

@ -0,0 +1,85 @@
package com.xuqm.sdk.compose.components.refresh
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwipeRefreshContainer(
isRefreshing: Boolean,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
val refreshState = rememberPullToRefreshState()
PullToRefreshBox(
state = refreshState,
isRefreshing = isRefreshing,
onRefresh = onRefresh,
modifier = modifier.fillMaxSize(),
content = content,
)
}
@Composable
fun <T> RefreshableLazyColumn(
items: List<T>,
isRefreshing: Boolean,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
listState: LazyListState = rememberLazyListState(),
enableLoadMore: Boolean = false,
isLoadingMore: Boolean = false,
hasMore: Boolean = true,
onLoadMore: () -> Unit = {},
emptyContent: (@Composable BoxScope.() -> Unit)? = null,
itemContent: @Composable (index: Int, item: T) -> Unit,
) {
val shouldLoadMore by remember(items, enableLoadMore, isLoadingMore, hasMore, listState) {
derivedStateOf {
if (!enableLoadMore || isLoadingMore || !hasMore) return@derivedStateOf false
val layoutInfo = listState.layoutInfo
val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
lastVisible >= items.lastIndex - 1 && items.isNotEmpty()
}
}
LaunchedEffect(shouldLoadMore) {
if (shouldLoadMore) {
onLoadMore()
}
}
SwipeRefreshContainer(
isRefreshing = isRefreshing,
onRefresh = onRefresh,
modifier = modifier,
) {
if (items.isEmpty() && emptyContent != null) {
Box(modifier = Modifier.fillMaxSize(), content = emptyContent)
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
) {
itemsIndexed(items) { index, item ->
itemContent(index, item)
}
}
}
}
}

查看文件

@ -0,0 +1,112 @@
package com.xuqm.sdk.compose.components.swipe
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
data class SwipeAction(
val label: String,
val backgroundColor: Color,
val contentColor: Color = Color.White,
val onClick: () -> Unit,
val content: (@Composable () -> Unit)? = null,
)
@Composable
fun SwipeActionItem(
modifier: Modifier = Modifier,
actions: List<SwipeAction>,
actionWidthDp: Int = 88,
content: @Composable () -> Unit,
) {
val scope = rememberCoroutineScope()
val density = LocalDensity.current
var offsetX by remember { mutableFloatStateOf(0f) }
val maxOffset = with(density) { -(actions.size * actionWidthDp.dp.toPx()) }
Box(
modifier = modifier
.fillMaxWidth()
.height(IntrinsicSize.Min),
) {
Row(
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxHeight(),
horizontalArrangement = Arrangement.End,
) {
actions.forEach { action ->
Box(
modifier = Modifier
.width(actionWidthDp.dp)
.fillMaxHeight()
.background(action.backgroundColor)
.clickable {
scope.launch { offsetX = 0f }
action.onClick()
},
contentAlignment = Alignment.Center,
) {
if (action.content != null) {
action.content.invoke()
} else {
Text(
text = action.label,
color = action.contentColor,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(8.dp),
)
}
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.offset { IntOffset(offsetX.roundToInt(), 0) }
.pointerInput(actions) {
detectHorizontalDragGestures(
onHorizontalDrag = { _, dragAmount ->
offsetX = (offsetX + dragAmount).coerceIn(maxOffset, 0f)
},
onDragEnd = {
scope.launch {
offsetX = if (offsetX < maxOffset / 2) maxOffset else 0f
}
},
)
}
.background(MaterialTheme.colorScheme.surface, RoundedCornerShape(16.dp)),
) {
content()
}
}
}

查看文件

@ -0,0 +1,114 @@
package com.xuqm.sdk.compose.components.webview
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
data class WebViewConfig(
val title: String? = null,
val javaScriptEnabled: Boolean = true,
val domStorageEnabled: Boolean = true,
val userAgentString: String? = null,
)
interface WebViewCallback {
fun onPageStarted(url: String?) {}
fun onPageFinished(url: String?) {}
fun onProgressChanged(progress: Int) {}
fun onReceivedTitle(title: String?) {}
fun shouldOverrideUrlLoading(url: String?): Boolean = false
}
@SuppressLint("SetJavaScriptEnabled")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CommonWebView(
url: String,
modifier: Modifier = Modifier,
config: WebViewConfig = WebViewConfig(),
callback: WebViewCallback? = null,
onClose: (() -> Unit)? = null,
) {
var webView: WebView? by remember { mutableStateOf(null) }
var title by remember { mutableStateOf(config.title ?: "") }
var progress by remember { mutableIntStateOf(0) }
BackHandler(enabled = webView?.canGoBack() == true) {
webView?.goBack()
}
DisposableEffect(Unit) {
onDispose { webView?.destroy() }
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(if (title.isBlank()) url else title) },
)
},
) { padding ->
AndroidView(
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = config.javaScriptEnabled
settings.domStorageEnabled = config.domStorageEnabled
settings.cacheMode = WebSettings.LOAD_DEFAULT
config.userAgentString?.let { settings.userAgentString = it }
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
callback?.onPageStarted(url)
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return callback?.shouldOverrideUrlLoading(request?.url?.toString()) ?: false
}
override fun onPageFinished(view: WebView?, url: String?) {
callback?.onPageFinished(url)
}
}
webChromeClient = object : WebChromeClient() {
override fun onReceivedTitle(view: WebView?, newTitle: String?) {
title = newTitle ?: config.title.orEmpty()
callback?.onReceivedTitle(newTitle)
}
override fun onProgressChanged(view: WebView?, newProgress: Int) {
progress = newProgress
callback?.onProgressChanged(newProgress)
}
}
loadUrl(url)
webView = this
}
},
modifier = modifier
.fillMaxSize(),
update = { view ->
if (view.url != url) {
view.loadUrl(url)
}
},
)
}
}

查看文件

@ -25,6 +25,7 @@ kotlin {
dependencies {
api(libs.androidx.core.ktx)
api(libs.androidx.activity.ktx)
api(libs.bundles.network)
api(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.android)

查看文件

@ -1,17 +1,24 @@
package com.xuqm.sdk
import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Bundle
import com.xuqm.sdk.cache.GlobalCache
import com.xuqm.sdk.network.HttpConfig
import com.xuqm.sdk.network.HttpManager
import com.xuqm.sdk.plugin.PluginPackageManager
import com.xuqm.sdk.update.AppUpdater
import com.xuqm.sdk.ui.DialogCenter
import com.xuqm.sdk.ui.ToastCenter
import com.xuqm.sdk.update.DownloadManager
import com.xuqm.sdk.utils.DeviceUtils
import com.xuqm.sdk.utils.FileHelper
import com.xuqm.sdk.utils.Logger
import java.lang.ref.WeakReference
object CoreSDK {
private var appContext: Context? = null
private var config: SDKConfig = SDKConfig()
private var topActivityRef: WeakReference<Activity>? = null
data class SDKConfig(
val debugMode: Boolean = false,
@ -20,20 +27,66 @@ object CoreSDK {
fun init(context: Context, config: SDKConfig = SDKConfig()) {
if (appContext != null) return
appContext = context.applicationContext
val applicationContext = context.applicationContext
appContext = applicationContext
this.config = config
HttpManager.init(HttpConfig(debugMode = config.debugMode))
Logger.init(config.debugMode)
GlobalCache.init(applicationContext)
ToastCenter.init(applicationContext)
DialogCenter.init(applicationContext)
FileHelper.init(applicationContext)
registerLifecycleCallbacks(applicationContext)
}
fun context(): Context = requireNotNull(appContext) { "CoreSDK not initialized" }
fun pluginPackageManager(): PluginPackageManager = PluginPackageManager.getInstance(context())
fun config(): SDKConfig = config
fun currentActivity(): Activity? = topActivityRef?.get()
fun downloadManager(): DownloadManager = DownloadManager.getInstance(context())
fun appUpdater(): AppUpdater = AppUpdater.getInstance(context())
fun fileHelper(): FileHelper = FileHelper
fun cache(): GlobalCache = GlobalCache
fun logger(): Logger = Logger
fun dialogCenter(): DialogCenter = DialogCenter
fun deviceId(): String = DeviceUtils.getDeviceId(context())
fun deviceInfo() = DeviceUtils.getDeviceInfo(context())
private fun registerLifecycleCallbacks(context: Context) {
val application = context as? Application ?: return
application.registerActivityLifecycleCallbacks(
object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
topActivityRef = WeakReference(activity)
}
override fun onActivityStarted(activity: Activity) {
topActivityRef = WeakReference(activity)
}
override fun onActivityResumed(activity: Activity) {
topActivityRef = WeakReference(activity)
}
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivityStopped(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
override fun onActivityDestroyed(activity: Activity) {
if (topActivityRef?.get() === activity) {
topActivityRef = null
}
}
},
)
}
}

查看文件

@ -0,0 +1,72 @@
package com.xuqm.sdk.cache
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
object GlobalCache {
private const val DEFAULT_TTL = 24 * 60 * 60 * 1000L
private var cacheManager: SharedCacheManager? = null
internal fun init(context: Context) {
if (cacheManager == null) {
cacheManager = SharedCacheManager.getInstance(context.applicationContext)
}
}
fun putString(key: String, value: String, ttl: Long = DEFAULT_TTL) {
requireManager().put(key, value, ttl)
}
fun getString(key: String, defaultValue: String? = null): String? {
return requireManager().getSync(key) ?: defaultValue
}
fun putBoolean(key: String, value: Boolean, ttl: Long = DEFAULT_TTL) {
putString(key, value.toString(), ttl)
}
fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
return getString(key)?.toBooleanStrictOrNull() ?: defaultValue
}
fun putInt(key: String, value: Int, ttl: Long = DEFAULT_TTL) {
putString(key, value.toString(), ttl)
}
fun getInt(key: String, defaultValue: Int = 0): Int {
return getString(key)?.toIntOrNull() ?: defaultValue
}
fun putLong(key: String, value: Long, ttl: Long = DEFAULT_TTL) {
putString(key, value.toString(), ttl)
}
fun getLong(key: String, defaultValue: Long = 0L): Long {
return getString(key)?.toLongOrNull() ?: defaultValue
}
fun putJson(key: String, value: JSONObject, ttl: Long = DEFAULT_TTL) {
putString(key, value.toString(), ttl)
}
fun getJson(key: String): JSONObject? {
return getString(key)?.let { runCatching { JSONObject(it) }.getOrNull() }
}
fun putJsonArray(key: String, value: JSONArray, ttl: Long = DEFAULT_TTL) {
putString(key, value.toString(), ttl)
}
fun getJsonArray(key: String): JSONArray? {
return getString(key)?.let { runCatching { JSONArray(it) }.getOrNull() }
}
fun remove(key: String) {
requireManager().remove(key)
}
private fun requireManager(): SharedCacheManager {
return requireNotNull(cacheManager) { "GlobalCache not initialized. Call CoreSDK.init() first." }
}
}

查看文件

@ -0,0 +1,56 @@
package com.xuqm.sdk.communication
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.util.UUID
data class PluginMessage(
val id: String = UUID.randomUUID().toString(),
val from: String,
val to: String? = null,
val topic: String,
val payload: String? = null,
val timestamp: Long = System.currentTimeMillis(),
)
object PluginMessenger {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val bus = MutableSharedFlow<PluginMessage>(extraBufferCapacity = 64)
fun send(
from: String,
topic: String,
payload: String? = null,
to: String? = null,
) {
bus.tryEmit(
PluginMessage(
from = from,
to = to,
topic = topic,
payload = payload,
),
)
}
fun subscribe(
owner: String,
topic: String? = null,
onReceive: (PluginMessage) -> Unit,
): Job {
return bus
.filter { message ->
val topicMatches = topic == null || topic == message.topic
val targetMatches = message.to.isNullOrBlank() || message.to == owner
topicMatches && targetMatches
}
.onEach(onReceive)
.launchIn(scope)
}
}

查看文件

@ -29,7 +29,7 @@ class RetrofitManager private constructor() {
private val serviceCache = ConcurrentHashMap<String, Any>()
private var globalConfig: HttpConfig = HttpConfig()
fun init(config: HttpConfig = HttpConfig()) {
internal fun init(config: HttpConfig = HttpConfig()) {
globalConfig = config
}
@ -75,7 +75,7 @@ class RetrofitManager private constructor() {
}
object HttpManager {
fun init(config: HttpConfig = HttpConfig()) {
internal fun init(config: HttpConfig = HttpConfig()) {
RetrofitManager.getInstance().init(config)
}
@ -83,4 +83,3 @@ object HttpManager {
return RetrofitManager.getInstance().getService(baseUrl, serviceClass, config)
}
}

查看文件

@ -0,0 +1,81 @@
package com.xuqm.sdk.permission
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.xuqm.sdk.ui.DialogCenter
data class PermissionRequest(
val permission: String,
val title: String,
val description: String,
)
data class PermissionResult(
val granted: List<String>,
val denied: List<String>,
) {
fun allGranted(): Boolean = denied.isEmpty()
}
class PermissionManager private constructor(
private val launcher: ActivityResultLauncher<Array<String>>,
private val callback: (PermissionResult) -> Unit,
) {
private var pendingRequests: List<PermissionRequest> = emptyList()
fun request(
requests: List<PermissionRequest>,
confirmTitle: String = "权限申请说明",
confirmButton: String = "继续申请",
cancelButton: String = "取消",
onCancelled: (() -> Unit)? = null,
) {
if (requests.isEmpty()) {
callback(PermissionResult(emptyList(), emptyList()))
return
}
pendingRequests = requests
val message = buildString {
appendLine("应用将申请以下权限:")
requests.forEach {
appendLine()
append("")
append(it.title)
append("")
append(it.description)
}
}.trim()
val shown = DialogCenter.showConfirm(
title = confirmTitle,
message = message,
confirmText = confirmButton,
cancelText = cancelButton,
cancelable = true,
canceledOnTouchOutside = false,
onConfirm = {
launcher.launch(requests.map { it.permission }.toTypedArray())
},
onCancel = onCancelled,
)
if (!shown) {
onCancelled?.invoke()
}
}
companion object {
fun register(
caller: ActivityResultCaller,
onResult: (PermissionResult) -> Unit,
): PermissionManager {
lateinit var manager: PermissionManager
val launcher = caller.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
val granted = manager.pendingRequests.map { it.permission }.filter { result[it] == true }
val denied = manager.pendingRequests.map { it.permission }.filterNot { result[it] == true }
onResult(PermissionResult(granted = granted, denied = denied))
}
manager = PermissionManager(launcher = launcher, callback = onResult)
return manager
}
}
}

查看文件

@ -0,0 +1,102 @@
package com.xuqm.sdk.ui
import android.app.Activity
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import com.xuqm.sdk.CoreSDK
object DialogCenter {
data class LoadingConfig(
val message: String = "加载中...",
val cancelable: Boolean = false,
val canceledOnTouchOutside: Boolean = false,
val onUserCancel: (() -> Unit)? = null,
)
private var appContext: Context? = null
private var currentDialog: AlertDialog? = null
private var loadingDialog: Dialog? = null
internal fun init(context: Context) {
appContext = context.applicationContext
}
fun showConfirm(
title: String,
message: String,
confirmText: String = "确定",
cancelText: String = "取消",
cancelable: Boolean = true,
canceledOnTouchOutside: Boolean = true,
onConfirm: () -> Unit,
onCancel: (() -> Unit)? = null,
): Boolean {
val activity = currentActivity() ?: return false
activity.runOnUiThread {
dismissCurrentDialog()
currentDialog = AlertDialog.Builder(activity)
.setTitle(title)
.setMessage(message)
.setPositiveButton(confirmText) { dialog, _ ->
dialog.dismiss()
onConfirm()
}
.setNegativeButton(cancelText) { dialog, _ ->
dialog.dismiss()
onCancel?.invoke()
}
.create()
.apply {
setCancelable(cancelable)
setCanceledOnTouchOutside(canceledOnTouchOutside)
show()
}
}
return true
}
fun showLoading(config: LoadingConfig = LoadingConfig()): Boolean {
val activity = currentActivity() ?: return false
activity.runOnUiThread {
dismissLoading()
loadingDialog = Dialog(activity).apply {
setCancelable(config.cancelable)
setCanceledOnTouchOutside(config.canceledOnTouchOutside)
setOnCancelListener { config.onUserCancel?.invoke() }
setContentView(
LinearLayout(activity).apply {
orientation = LinearLayout.HORIZONTAL
val padding = 48
setPadding(padding, padding, padding, padding)
addView(ProgressBar(activity))
addView(
TextView(activity).apply {
text = config.message
textSize = 16f
setPadding(24, 0, 0, 0)
},
)
},
)
show()
}
}
return true
}
fun dismissLoading() {
loadingDialog?.dismiss()
loadingDialog = null
}
fun dismissCurrentDialog() {
currentDialog?.dismiss()
currentDialog = null
}
private fun currentActivity(): Activity? = CoreSDK.currentActivity()
}

查看文件

@ -9,7 +9,7 @@ object ToastCenter {
private val handler = Handler(Looper.getMainLooper())
private var appContext: Context? = null
fun init(context: Context) {
internal fun init(context: Context) {
appContext = context.applicationContext
}

查看文件

@ -1,11 +1,8 @@
package com.xuqm.sdk.update
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.core.content.FileProvider
import com.xuqm.sdk.utils.FileHelper
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -159,20 +156,7 @@ class DownloadManager private constructor(private val context: Context) {
}
fun installApk(file: File): Boolean {
return runCatching {
val authority = "${context.packageName}.sdk.fileprovider"
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(context, authority, file)
} else {
Uri.fromFile(file)
}
context.startActivity(
Intent(Intent.ACTION_VIEW).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
setDataAndType(uri, "application/vnd.android.package-archive")
},
)
}.isSuccess
return FileHelper.installApk(file)
}
private fun resolvePath(storagePath: StoragePath, customPath: String?): String {

查看文件

@ -16,5 +16,38 @@ object DateTimeUtils {
}
fun now(pattern: String = "yyyy-MM-dd HH:mm:ss"): String = format(System.currentTimeMillis(), pattern)
}
fun nowMillis(): Long = System.currentTimeMillis()
fun parse(
value: String,
pattern: String = "yyyy-MM-dd HH:mm:ss",
timeZone: TimeZone = TimeZone.getDefault(),
locale: Locale = Locale.getDefault(),
): Long? {
return runCatching {
SimpleDateFormat(pattern, locale).apply { this.timeZone = timeZone }.parse(value)?.time
}.getOrNull()
}
fun isToday(timeMillis: Long): Boolean {
val today = format(System.currentTimeMillis(), "yyyy-MM-dd")
return format(timeMillis, "yyyy-MM-dd") == today
}
fun addDays(timeMillis: Long, days: Int): Long = timeMillis + days * 24L * 60L * 60L * 1000L
fun addHours(timeMillis: Long, hours: Int): Long = timeMillis + hours * 60L * 60L * 1000L
fun between(startMillis: Long, endMillis: Long): Long = endMillis - startMillis
fun toFriendlyText(timeMillis: Long): String {
val delta = System.currentTimeMillis() - timeMillis
return when {
delta < 60_000L -> "刚刚"
delta < 60L * 60L * 1000L -> "${delta / 60_000L}分钟前"
delta < 24L * 60L * 60L * 1000L -> "${delta / (60L * 60L * 1000L)}小时前"
else -> format(timeMillis, "yyyy-MM-dd")
}
}
}

查看文件

@ -0,0 +1,109 @@
package com.xuqm.sdk.utils
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object FileHelper {
private var appContext: Context? = null
internal fun init(context: Context) {
appContext = context.applicationContext
}
fun installApk(file: File): Boolean {
val context = requireContext()
return runCatching {
val uri = toUri(file)
context.startActivity(
Intent(Intent.ACTION_VIEW).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
setDataAndType(uri, "application/vnd.android.package-archive")
},
)
}.isSuccess
}
fun openFile(file: File, mimeType: String = "*/*"): Boolean {
val context = requireContext()
return runCatching {
context.startActivity(
Intent(Intent.ACTION_VIEW).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
setDataAndType(toUri(file), mimeType)
},
)
}.isSuccess
}
fun createTempImageFile(prefix: String = "IMG"): File {
val context = requireContext()
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val directory = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: context.cacheDir
return File(directory, "${prefix}_${timeStamp}.jpg").apply {
parentFile?.mkdirs()
}
}
fun toUri(file: File): Uri {
val context = requireContext()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(context, "${context.packageName}.sdk.fileprovider", file)
} else {
Uri.fromFile(file)
}
}
fun registerFilePicker(
caller: ActivityResultCaller,
onResult: (Uri?) -> Unit,
): ActivityResultLauncher<Array<String>> {
return caller.registerForActivityResult(ActivityResultContracts.OpenDocument(), onResult)
}
fun registerImagePicker(
caller: ActivityResultCaller,
onResult: (Uri?) -> Unit,
): ActivityResultLauncher<PickVisualMediaRequest> {
return caller.registerForActivityResult(ActivityResultContracts.PickVisualMedia(), onResult)
}
fun registerCameraCapture(
caller: ActivityResultCaller,
outputFileProvider: () -> File = { createTempImageFile() },
onResult: (Boolean, Uri?) -> Unit,
): Pair<ActivityResultLauncher<Uri>, () -> Uri> {
var pendingUri: Uri? = null
val launcher = caller.registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
onResult(success, pendingUri)
}
val uriProvider = {
pendingUri = toUri(outputFileProvider())
requireNotNull(pendingUri)
}
return launcher to uriProvider
}
fun createImageCaptureIntent(outputUri: Uri): Intent {
return Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
private fun requireContext(): Context {
return requireNotNull(appContext) { "FileHelper not initialized. Call CoreSDK.init() first." }
}
}

查看文件

@ -0,0 +1,49 @@
package com.xuqm.sdk.utils
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
object Logger {
private var debugEnabled: Boolean = false
private const val DEFAULT_TAG = "CoreSDK"
internal fun init(debug: Boolean) {
debugEnabled = debug
}
fun d(message: Any?, tag: String = DEFAULT_TAG) {
if (debugEnabled) {
Log.d(tag, stringify(message))
}
}
fun i(message: Any?, tag: String = DEFAULT_TAG) {
Log.i(tag, stringify(message))
}
fun w(message: Any?, tag: String = DEFAULT_TAG) {
Log.w(tag, stringify(message))
}
fun e(message: Any?, throwable: Throwable? = null, tag: String = DEFAULT_TAG) {
Log.e(tag, stringify(message), throwable)
}
fun json(json: String, tag: String = DEFAULT_TAG) {
val pretty = runCatching {
when {
json.trim().startsWith("{") -> JSONObject(json).toString(2)
json.trim().startsWith("[") -> JSONArray(json).toString(2)
else -> json
}
}.getOrDefault(json)
Log.d(tag, pretty)
}
private fun stringify(message: Any?): String = when (message) {
null -> "null"
is Throwable -> message.stackTraceToString()
else -> message.toString()
}
}

查看文件

@ -0,0 +1,31 @@
plugins {
alias(libs.plugins.android.library)
}
apply(from = rootProject.file("gradle/publishing.gradle.kts"))
android {
namespace = "com.xuqm.sdk.updatekit"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
kotlin {
jvmToolchain(21)
}
dependencies {
api(project(":commonsdk-core"))
implementation(libs.kotlinx.coroutines.android)
testImplementation(libs.junit4)
}

查看文件

@ -0,0 +1 @@

查看文件

@ -3,11 +3,9 @@ package com.xuqm.sdk.plugin
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.core.content.FileProvider
import com.xuqm.sdk.cache.CacheKeys
import com.xuqm.sdk.cache.SharedCacheManager
import com.xuqm.sdk.cache.GlobalCache
import com.xuqm.sdk.update.DownloadDecision
import com.xuqm.sdk.update.DownloadManager
import com.xuqm.sdk.update.DownloadRequest
@ -16,6 +14,7 @@ import com.xuqm.sdk.update.VersionCheckResult
import com.xuqm.sdk.update.VersionCheckStrategy
import com.xuqm.sdk.update.VersionComparator
import com.xuqm.sdk.update.VersionInfo
import com.xuqm.sdk.utils.FileHelper
import org.json.JSONObject
import java.io.File
@ -41,7 +40,6 @@ class PluginPackageManager private constructor(private val context: Context) {
}
}
private val cacheManager = SharedCacheManager.getInstance(context)
private val downloadManager = DownloadManager.getInstance(context)
fun cacheCurrentUser(
@ -57,11 +55,15 @@ class PluginPackageManager private constructor(private val context: Context) {
put("timestamp", System.currentTimeMillis())
extraData.forEach { (key, value) -> put(key, value) }
}
cacheManager.put(CacheKeys.CURRENT_USER, json.toString(), 10 * 60 * 1000)
GlobalCache.putString(CacheKeys.CURRENT_USER, json.toString(), 10 * 60 * 1000)
}
fun getCachedUser(appPackageName: String? = null): String? {
return cacheManager.getSync(CacheKeys.CURRENT_USER, appPackageName)
return if (appPackageName.isNullOrBlank() || appPackageName == context.packageName) {
GlobalCache.getString(CacheKeys.CURRENT_USER)
} else {
com.xuqm.sdk.cache.SharedCacheManager.getInstance(context).getSync(CacheKeys.CURRENT_USER, appPackageName)
}
}
fun isPluginInstalled(packageName: String): Boolean {
@ -92,11 +94,6 @@ class PluginPackageManager private constructor(private val context: Context) {
}.getOrNull()
}
fun compareVersion(packageName: String, remoteVersionCode: Long): Int {
val local = getLocalPluginInfo(packageName) ?: return 1
return remoteVersionCode.compareTo(local.versionCode)
}
fun shouldDownloadPlugin(
packageName: String,
remoteVersionCode: Long? = null,
@ -207,22 +204,7 @@ class PluginPackageManager private constructor(private val context: Context) {
return runCatching { context.startActivity(launchIntent) }.isSuccess
}
fun installPlugin(apkFile: File): Boolean {
return runCatching {
val authority = "${context.packageName}.sdk.fileprovider"
val uri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(context, authority, apkFile)
} else {
Uri.fromFile(apkFile)
}
context.startActivity(
Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
},
)
}.isSuccess
}
fun installPlugin(apkFile: File): Boolean = FileHelper.installApk(apkFile)
fun loadPlugin(apkFile: File): Boolean = installPlugin(apkFile)

查看文件

@ -3,6 +3,7 @@ package com.xuqm.sdk.update
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import com.xuqm.sdk.utils.FileHelper
import kotlinx.coroutines.flow.StateFlow
data class UpdateInfo(
@ -119,7 +120,7 @@ class AppUpdater private constructor(private val context: Context) {
fun clear(taskId: String): Boolean = downloadManager.clear(taskId)
fun installApk(file: java.io.File): Boolean = downloadManager.installApk(file)
fun installApk(file: java.io.File): Boolean = FileHelper.installApk(file)
fun installFromTask(taskId: String): Boolean {
val file = downloadManager.getDownloadedFile(taskId) ?: return false

查看文件

@ -0,0 +1,14 @@
package com.xuqm.sdk.updatekit
import android.content.Context
import com.xuqm.sdk.CoreSDK
import com.xuqm.sdk.plugin.PluginPackageManager
import com.xuqm.sdk.update.AppUpdater
object UpdateSDK {
fun appUpdater(context: Context = CoreSDK.context()): AppUpdater = AppUpdater.getInstance(context)
fun pluginPackageManager(context: Context = CoreSDK.context()): PluginPackageManager {
return PluginPackageManager.getInstance(context)
}
}

查看文件

@ -7,6 +7,7 @@ minSdk = "24"
coreKtx = "1.18.0"
lifecycle = "2.10.0"
activityCompose = "1.13.0"
activityKtx = "1.13.0"
composeBom = "2026.03.00"
coroutines = "1.10.2"
datastore = "1.1.7"
@ -14,6 +15,8 @@ retrofit = "3.0.0"
okhttp = "5.3.2"
gson = "2.13.2"
jserialization = "1.9.0"
webkit = "1.14.0"
coil = "2.7.0"
junit4 = "4.13.2"
androidxJunit = "1.3.0"
espresso = "3.7.0"
@ -23,6 +26,7 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref =
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
@ -34,6 +38,8 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jserialization" }
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }

查看文件

@ -31,7 +31,6 @@ class SzyxLoginActivity : ComponentActivity() {
if (!SzyxSDK.isInitialized()) {
SzyxSDK.init(this)
}
ToastCenter.init(this)
setContent { MaterialTheme { LoginScreen { finish() } } }
}
}

查看文件

@ -17,7 +17,7 @@ android {
manifestPlaceholders["authProviderAuthority"] = "com.xuqm.sample.auth"
manifestPlaceholders["sharedCacheAuthority"] = "com.xuqm.sample.sdk.cache.provider"
manifestPlaceholders["coreFileProviderAuthority"] = "com.xuqm.sample.sdk.fileprovider"
buildConfigField("String", "UPDATE_SERVER_BASE_URL", "\"http://192.168.116.9:3000/\"")
buildConfigField("String", "UPDATE_SERVER_BASE_URL", "\"http://192.168.113.162:8080/\"")
}
buildTypes {
@ -48,6 +48,7 @@ kotlin {
dependencies {
implementation(project(":commonsdk-core"))
implementation(project(":commonsdk-compose"))
implementation(project(":commonsdk-update"))
implementation(project(":lib-szyx"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)

查看文件

@ -49,6 +49,7 @@ import com.xuqm.sdk.update.StoragePath
import com.xuqm.sdk.update.UpdateInfo
import com.xuqm.sdk.update.VersionCheckResult
import com.xuqm.sdk.update.VersionCheckStrategy
import com.xuqm.sdk.updatekit.UpdateSDK
import com.xuqm.sdk.utils.DateTimeUtils
import com.xuqm.sample.update.UpdateRepository
import com.xuqm.szyx.SzyxSDK
@ -89,7 +90,7 @@ class MainActivity : ComponentActivity() {
if (intent.action == Intent.ACTION_PACKAGE_ADDED && reloadPluginAfterInstall) {
reloadPluginAfterInstall = false
val pluginUpdateInfo = currentPluginUpdateInfo
CoreSDK.pluginPackageManager().reloadPlugin(
UpdateSDK.pluginPackageManager().reloadPlugin(
packageName = PLUGIN_PACKAGE_NAME,
entryActivity = pluginUpdateInfo?.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
extras = pluginUpdateInfo?.extras ?: mapOf("hostPackageName" to this@MainActivity.packageName),
@ -117,7 +118,6 @@ class MainActivity : ComponentActivity() {
debugMode = true,
),
)
ToastCenter.init(this)
refreshState()
ensureLoginOnLaunch()
registerPackageChangeReceiver()
@ -170,7 +170,7 @@ class MainActivity : ComponentActivity() {
private fun refreshState() {
refreshSession()
pluginInstalledState.value = CoreSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
pluginInstalledState.value = UpdateSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
}
private fun openLogin() {
@ -199,7 +199,7 @@ class MainActivity : ComponentActivity() {
return
}
val pluginManager = CoreSDK.pluginPackageManager()
val pluginManager = UpdateSDK.pluginPackageManager()
pluginManager.cacheCurrentUser(
userId = session.loginModel.userId,
sessionId = session.loginModel.sessionId,
@ -224,7 +224,7 @@ class MainActivity : ComponentActivity() {
entryActivity = remoteUpdate.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
extras = mapOf("hostPackageName" to packageName),
)
val checkResult = CoreSDK.pluginPackageManager().checkPluginUpdate(
val checkResult = UpdateSDK.pluginPackageManager().checkPluginUpdate(
packageName = pluginUpdateInfo.packageName,
remoteVersionCode = pluginUpdateInfo.versionCode,
remoteVersionName = pluginUpdateInfo.versionName,
@ -235,7 +235,7 @@ class MainActivity : ComponentActivity() {
return@onSuccess
}
currentPluginUpdateInfo = pluginUpdateInfo
val taskId = CoreSDK.pluginPackageManager().downloadPlugin(
val taskId = UpdateSDK.pluginPackageManager().downloadPlugin(
updateInfo = pluginUpdateInfo,
fileName = "plugin-ui-release.apk",
storagePath = StoragePath.EXTERNAL_FILES,
@ -259,7 +259,7 @@ class MainActivity : ComponentActivity() {
updateRepository.fetchLatestAppUpdate(packageName)
.onSuccess { updateInfo ->
when (
val checkResult = CoreSDK.appUpdater().checkUpdate(
val checkResult = UpdateSDK.appUpdater().checkUpdate(
updateInfo = updateInfo,
strategy = VersionCheckStrategy.VERSION_CODE_OR_NAME,
)
@ -281,7 +281,7 @@ class MainActivity : ComponentActivity() {
private fun confirmDownloadAppUpdate() {
val updateInfo = pendingAppUpdateState.value ?: return
pendingAppUpdateState.value = null
val taskId = CoreSDK.appUpdater().downloadUpdate(
val taskId = UpdateSDK.appUpdater().downloadUpdate(
updateInfo = updateInfo,
fileName = "sample-app-update.apk",
storagePath = StoragePath.EXTERNAL_FILES,
@ -308,7 +308,7 @@ class MainActivity : ComponentActivity() {
is DownloadState.Success -> {
ToastCenter.show("插件下载完成,准备重新加载")
reloadPluginAfterInstall = true
if (!CoreSDK.pluginPackageManager().loadPlugin(state.file)) {
if (!UpdateSDK.pluginPackageManager().loadPlugin(state.file)) {
reloadPluginAfterInstall = false
ToastCenter.show("插件加载拉起失败")
}
@ -343,7 +343,7 @@ class MainActivity : ComponentActivity() {
when (state) {
is DownloadState.Success -> {
ToastCenter.show("安装包下载完成,准备安装")
if (!CoreSDK.appUpdater().installApk(state.file)) {
if (!UpdateSDK.appUpdater().installApk(state.file)) {
ToastCenter.show("应用安装拉起失败")
}
CoreSDK.downloadManager().clear(taskId)

查看文件

@ -20,6 +20,7 @@ rootProject.name = "AndroidLibs"
include(":commonsdk-core")
include(":commonsdk-compose")
include(":commonsdk-update")
include(":lib-szyx")
include(":sample-app")
include(":plugins:plugin-ui")

35
doc/01-project-overview.md 普通文件
查看文件

@ -0,0 +1,35 @@
# 01 项目概览
## 项目目标
本项目用于承载 Android SDK、示例宿主应用、插件能力、运营平台、管理平台以及配套的版本管理服务端。
当前目标重点:
- 保持 Android 端 `sample-app``plugin-ui` 的版本查询能力可继续使用
- 增加运营平台与管理平台两个 Vue 3 前端
- 服务端升级为 Spring Boot 微服务形态,便于后续继续扩展即时通讯、推送、版本管理等模块
- 首期优先落地版本管理、插件化开关、灰度发布与用户钩子能力
## 目录结构
```text
AndroidLibsGroup/
├── AndroidLibs/ Android SDK、示例宿主、插件工程
├── frontend/ Yarn workspace 前端工程
│ ├── ops-platform/ 运营平台
│ └── admin-platform/ 管理平台
├── server/ Spring Boot 服务端与旧版 Node 示例服务
│ ├── version-management-service/
│ └── version-service/
└── doc/ 项目文档
```
## 当前已实现内容
- Android SDK 与示例应用基础工程
- Vue 3 运营平台与管理平台基础页面
- Spring Boot 版本管理服务
- MySQL 持久化
- Redis 缓存灰度用户列表
- Android 更新兼容接口

37
doc/02-architecture.md 普通文件
查看文件

@ -0,0 +1,37 @@
# 02 架构设计
## 总体分层
### Android 侧
- `commonsdk-core`:网络、下载、文件、权限、缓存、日志、全局弹窗、时间等基础能力
- `commonsdk-compose`下拉刷新、折叠面板、左滑操作、WebView、图片等 Compose 组件扩展
- `commonsdk-update`:独立更新 SDK,负责宿主 APK / 插件版本比较、下载编排与安装拉起
- `lib-szyx`:业务登录与会话管理
- `sample-app`:宿主示例
- `plugins/plugin-ui`:插件示例
### 前端侧
- `ops-platform`:运营使用,开放注册、版本上传、发布、灰度圈选
- `admin-platform`:平台治理使用,审核主账户、禁用账户、管理子账户权限
### 服务端
- `version-management-service`:当前主服务
- 后续可继续拆分为账号中心、推送中心、IM 服务、文件服务等独立微服务
## 当前服务端结构
- `controller`:接口层
- `service`:业务编排层
- `persistence/entity`JPA 实体
- `persistence/repository`JPA 仓储
- `config`CORS、Redis、初始化数据等配置
## 演进方向
- 账号体系独立成 `account-service`
- 版本包上传独立成文件存储服务,对接 MinIO 或对象存储
- 灰度用户钩子改为对接真实用户平台 API,而不是初始化数据
- 增加统一鉴权与租户隔离

65
doc/03-frontend.md 普通文件
查看文件

@ -0,0 +1,65 @@
# 03 前端说明
## 技术栈
- Vue 3
- Vue Router 4
- Pinia
- Vite 6
- TypeScript 5
- Yarn workspace
## 包管理
前端统一使用 Yarn,不再使用 npm。
根目录:
```bash
cd frontend
yarn install
```
开发命令:
```bash
yarn dev:ops
yarn dev:admin
```
构建命令:
```bash
yarn build
```
## 项目职责
### 运营平台 `ops-platform`
- 主账户开放注册
- 版本管理
- 插件化开关
- 版本包上传
- 全量发布
- 灰度发布
- 灰度用户圈选
### 管理平台 `admin-platform`
- 查看运营主账户
- 审核主账户
- 禁用账户
- 查看子账户
- 管理子账户权限
## 环境变量
- `VITE_API_BASE_URL`:服务端接口地址,默认 `http://127.0.0.1:8080`
## 后续规划
- 引入统一 UI 组件体系
- 增加登录鉴权与路由守卫
- 加入页面级权限控制
- 加入文件上传组件和上传进度状态

56
doc/04-backend.md 普通文件
查看文件

@ -0,0 +1,56 @@
# 04 服务端说明
## 当前主服务
`server/version-management-service`
## 技术基线
- JDK 21
- Spring Boot 3.4.4
- Spring Web
- Spring Data JPA
- Spring Data Redis
- MySQL 8+
## 已实现接口能力
### 公开接口
- 运营平台主账户注册
### 管理平台接口
- 账户列表
- 账户审核/禁用
- 子账户权限更新
### 运营平台接口
- 应用列表
- 插件化开关
- 版本上传
- 版本发布
- 灰度用户列表
- 分组列表
- 快速选择列表
### Android 兼容接口
- `GET /api/v1/updates/app/latest`
- `GET /api/v1/updates/plugin/latest`
## 启动
```bash
cd server
mvn -pl version-management-service spring-boot:run
```
## 后续演进建议
- 增加 Spring Security + JWT
- 引入 Flyway 管理表结构
- 加入 OpenAPI/Swagger
- 增加单元测试与集成测试
- 版本包下载地址改为文件服务生成

60
doc/05-infrastructure.md 普通文件
查看文件

@ -0,0 +1,60 @@
# 05 数据库与缓存
## MySQL
- Host: `xuqinmin.com`
- Port: `3306`
- Database: `androidLibsServer`
- Username: `androidLibsServer`
当前通过 Spring JPA 自动建表,后续建议切换到 Flyway 管理。
当前 `version-management-service` 的数据库与 Redis 配置风格参考了
`/Users/xuqinmin/Projects/TrustProjects/AppManager/AppManagerWeb/RuoYi-Vue`
项目,重点对齐了:
- MySQL JDBC 参数格式
- Redis 基础连接项
- Redis Lettuce 连接池参数
- 数据库连接池的超时与空闲参数
支持通过环境变量覆盖:
- `DB_URL`
- `DB_USERNAME`
- `DB_PASSWORD`
## Redis
- Host: `redisdev.xuqinmin.com`
- Port: `6379`
- Database: `0`
支持通过环境变量覆盖:
- `REDIS_HOST`
- `REDIS_PORT`
- `REDIS_DATABASE`
- `REDIS_PASSWORD`
## 当前数据用途
### MySQL
- 运营主账户
- 子账户
- 应用配置
- 版本记录
- 灰度用户基础数据
- 用户分组
- 快速选择配置
### Redis
- 灰度用户列表查询缓存
- 分组与快速选择组合查询结果缓存
## 注意事项
- 当前数据库与 Redis 密码已直接写入配置文件,仅适合当前内部开发阶段
- 后续应迁移到环境变量、配置中心或密钥管理服务

查看文件

@ -0,0 +1,55 @@
# 06 版本管理与灰度发布
## 业务范围
当前版本管理覆盖:
- App 版本管理
- 插件版本管理
- 插件化开关
- 版本包上传登记
- 全量发布
- 灰度发布
## 灰度发布规则
灰度发布基于用户平台钩子能力,当前支持三种圈选方式:
- 分组选择
- 快速选择
- 单选用户
系统返回的用户字段包含:
- 用户 ID
- 昵称
- 手机号
- 邮箱
- 地区
- 分组信息
其中 ID、昵称、手机号、邮箱在前台展示时进行脱敏。
## Android 拉取版本逻辑
### 全量版本
所有用户都可拉取到最新已发布版本。
### 灰度版本
只有命中灰度规则的用户才能拉取到灰度版本。
支持命中条件:
- 命中指定用户 ID
- 命中指定用户分组
- 命中指定快速选择集合
## 后续升级项
- 支持灰度比例发布
- 支持按渠道、设备、地区、版本范围灰度
- 支持灰度回滚
- 支持发布时间窗口
- 支持上传真实 APK/AAB/插件包并持久化元数据

查看文件

@ -0,0 +1,42 @@
# 07 开发与交付流程
## 本地开发
### Android
`AndroidLibs/` 下使用 Gradle 进行开发与构建。
```bash
cd AndroidLibs
./gradlew :sample-app:assembleDebug :plugins:plugin-ui:assembleDebug
```
### 前端
```bash
cd frontend
yarn install
yarn dev:ops
yarn dev:admin
```
### 服务端
```bash
cd server
mvn -pl version-management-service spring-boot:run
```
## 提交流程
- 功能开发完成后,同步更新 `doc/` 文档
- 变更接口时,同时更新前端调用与服务端说明
- 变更基础设施配置时,同时更新 `05-infrastructure.md`
- 变更版本管理逻辑时,同时更新 `06-version-management.md`
- 变更 Android SDK 模块结构或公共组件时,同时更新 `08-android-sdk.md`
## 持续维护要求
- `doc/` 为项目正式文档目录,后续新增能力必须持续补充
- 若引入新微服务,需要新增单独文档章节
- 若引入 CI/CD、容器化部署、测试基线,需要补充新的专题文档

54
doc/08-android-sdk.md 普通文件
查看文件

@ -0,0 +1,54 @@
# 08 Android SDK 说明
## 当前模块拆分
- `commonsdk-core`
- 网络能力:`HttpManager`、`RetrofitManager`
- 下载能力:`DownloadManager`
- 文件能力:`FileHelper`
- 权限能力:`PermissionManager`
- 缓存能力:`GlobalCache`、`SharedCacheManager`
- 通信能力:`PluginMessenger`
- 通用 UI 能力:`ToastCenter`、`DialogCenter`
- 工具能力:`Logger`、`DateTimeUtils`
- `commonsdk-compose`
- 下拉刷新:`SwipeRefreshContainer`、`RefreshableLazyColumn`
- 折叠面板:`AccordionPanel`、`RefreshableAccordionList`
- 左滑操作:`SwipeActionItem`
- WebView`CommonWebView`、`WebViewPageActivity`
- 图片:`AdaptiveImage`、`CircleImage`
- `commonsdk-update`
- 宿主更新:`AppUpdater`
- 插件更新:`PluginPackageManager`
- SDK 门面:`UpdateSDK`
- 版本比较:`VersionComparator`
## 设计原则
- 下载与文件安装归 `core`,保证后续任意业务 SDK 都能复用。
- 宿主更新和插件更新归 `commonsdk-update`,避免核心模块承担业务升级策略。
- Compose 组件保持业务无关,App 可直接引用或二次封装。
## 接入方式
### 宿主应用
- 初始化基础 SDK`CoreSDK.init(context, CoreSDK.SDKConfig(...))`
- 使用更新能力:`UpdateSDK.appUpdater()`、`UpdateSDK.pluginPackageManager()`
- 使用公共能力:`CoreSDK.fileHelper()`、`CoreSDK.cache()`、`CoreSDK.dialogCenter()`
### WebView 页面
- 内嵌组件:直接使用 `CommonWebView(url = "...")`
- 独立页面:通过 `WebViewPageActivity.createIntent(context, url, title)` 跳转
### 权限申请
- 通过 `PermissionManager.register(...)` 注册权限请求器
- 请求时传入权限标题和描述,框架会先弹出确认说明,再真正调用系统权限弹窗
## 当前验证
- 2026-03-27 已执行:
- `./gradlew --no-daemon --no-configuration-cache :sample-app:assembleDebug :plugins:plugin-ui:assembleDebug`
- 结果:构建通过

20
doc/README.md 普通文件
查看文件

@ -0,0 +1,20 @@
# AndroidLibsGroup 文档
当前目录用于沉淀项目的长期文档,后续新增模块、接口、部署说明、发布流程都持续更新在这里。
## 文档索引
- [01-项目概览](./01-project-overview.md)
- [02-架构设计](./02-architecture.md)
- [03-前端说明](./03-frontend.md)
- [04-服务端说明](./04-backend.md)
- [05-数据库与缓存](./05-infrastructure.md)
- [06-版本管理与灰度发布](./06-version-management.md)
- [07-开发与交付流程](./07-development-workflow.md)
- [08-Android SDK 说明](./08-android-sdk.md)
## 维护约定
- 新增模块时,同步更新对应章节。
- 接口或数据结构发生变化时,同步更新服务端、前端和版本管理文档。
- 部署地址、账号、依赖版本变更时,优先更新 `04-05` 章节。

查看文件

@ -1,10 +1,17 @@
# frontend
当前目录新增两个 Vue 3 前端项目:
当前目录新增两个 Vue 3 前端项目,并统一使用 Yarn workspace 管理
- `ops-platform`:运营平台,提供开放注册、版本管理、插件化开关、全量/灰度发布。
- `admin-platform`:管理平台,负责审核运营账户、禁用账户、管理子账户权限。
## 安装依赖
```bash
cd frontend
yarn install
```
## 启动方式
先启动后端:
@ -17,15 +24,20 @@ mvn -pl version-management-service spring-boot:run
再分别启动前端:
```bash
cd frontend/ops-platform
npm install
npm run dev
cd frontend
yarn dev:ops
```
```bash
cd frontend/admin-platform
npm install
npm run dev
cd frontend
yarn dev:admin
```
## 构建
```bash
cd frontend
yarn build
```
前端默认请求 `http://127.0.0.1:8080`,如需调整可通过 `VITE_API_BASE_URL` 覆盖。

查看文件

@ -2,6 +2,7 @@
"name": "admin-platform",
"version": "0.1.0",
"private": true,
"packageManager": "yarn@1.22.22",
"type": "module",
"scripts": {
"dev": "vite",

查看文件

@ -2,6 +2,7 @@
"name": "ops-platform",
"version": "0.1.0",
"private": true,
"packageManager": "yarn@1.22.22",
"type": "module",
"scripts": {
"dev": "vite",

查看文件

@ -1,22 +1,7 @@
<template>
<div class="shell">
<aside class="sidebar">
<div>
<p class="eyebrow">Vue3 · 运营平台</p>
<h1>版本运营工作台</h1>
<p class="muted">开放注册子账户版本管理灰度发布都集中在这里</p>
</div>
<nav class="nav">
<RouterLink to="/register">平台注册</RouterLink>
<RouterLink to="/versions">版本管理</RouterLink>
</nav>
</aside>
<main class="content">
<RouterView />
</main>
</div>
</template>
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { RouterView } from 'vue-router'
</script>

查看文件

@ -18,15 +18,36 @@ export interface Account {
createdAt: string
}
export interface LoginResult {
account: Account
tenantAccountId: string
}
export interface PluginConfig {
id: string
appId: string
name: string
packageName: string
entryActivity?: string | null
description?: string | null
enabled: boolean
}
export interface ReleaseRecord {
id: string
appId: string
pluginId?: string | null
packageType: 'APP' | 'PLUGIN'
packageName: string
versionCode: number
versionName: string
title: string
changelog: string
downloadUrl: string
uploadedFileName: string
entryActivity?: string | null
minHostVersionCode?: number | null
minHostVersionName?: string | null
status: 'DRAFT' | 'PUBLISHED' | 'GRAYSCALE'
publishStrategy: string
grayRule?: {
@ -42,10 +63,12 @@ export interface ApplicationDetail {
id: string
name: string
packageName: string
pluginPackageName: string
pluginPackageName?: string | null
description?: string | null
pluginManagementEnabled: boolean
businessModules: string[]
}
plugins: PluginConfig[]
releases: ReleaseRecord[]
}
@ -92,29 +115,80 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
}
export const api = {
registerAccount(payload: Pick<Account, 'accountName' | 'contactName' | 'email' | 'phone'>) {
registerAccount(payload: {
accountName: string
contactName: string
email: string
phone: string
password: string
}) {
return request<Account>('/api/v1/open/accounts/register', {
method: 'POST',
body: JSON.stringify(payload),
})
},
listApplications() {
return request<ApplicationDetail[]>('/api/v1/ops/version/applications')
},
togglePluginManagement(appId: string, enabled: boolean) {
return request(`/api/v1/ops/version/applications/${appId}/plugin-management`, {
method: 'PUT',
body: JSON.stringify({ enabled }),
})
},
uploadRelease(appId: string, payload: Record<string, unknown>) {
return request<ReleaseRecord>(`/api/v1/ops/version/applications/${appId}/releases/upload`, {
login(payload: { email: string; password: string }) {
return request<LoginResult>('/api/v1/open/accounts/login', {
method: 'POST',
body: JSON.stringify(payload),
})
},
publishRelease(appId: string, releaseId: string, payload: Record<string, unknown>) {
return request<ReleaseRecord>(`/api/v1/ops/version/applications/${appId}/releases/${releaseId}/publish`, {
listApplications() {
return request<ApplicationDetail[]>('/api/v1/ops/apps')
},
createApplication(payload: Record<string, unknown>) {
return request('/api/v1/ops/apps', {
method: 'POST',
body: JSON.stringify(payload),
})
},
updateApplication(appId: string, payload: Record<string, unknown>) {
return request(`/api/v1/ops/apps/${appId}`, {
method: 'PUT',
body: JSON.stringify(payload),
})
},
togglePluginManagement(appId: string, enabled: boolean) {
return request(`/api/v1/ops/apps/${appId}/plugin-management`, {
method: 'PUT',
body: JSON.stringify({ enabled }),
})
},
listAppPackages(appId: string) {
return request<ReleaseRecord[]>(`/api/v1/ops/apps/${appId}/packages`)
},
uploadAppPackage(appId: string, payload: Record<string, unknown>) {
return request<ReleaseRecord>(`/api/v1/ops/apps/${appId}/packages`, {
method: 'POST',
body: JSON.stringify(payload),
})
},
listPlugins(appId: string) {
return request<PluginConfig[]>(`/api/v1/ops/apps/${appId}/plugins`)
},
createPlugin(appId: string, payload: Record<string, unknown>) {
return request<PluginConfig>(`/api/v1/ops/apps/${appId}/plugins`, {
method: 'POST',
body: JSON.stringify(payload),
})
},
updatePlugin(pluginId: string, payload: Record<string, unknown>) {
return request<PluginConfig>(`/api/v1/ops/plugins/${pluginId}`, {
method: 'PUT',
body: JSON.stringify(payload),
})
},
listPluginPackages(pluginId: string) {
return request<ReleaseRecord[]>(`/api/v1/ops/plugins/${pluginId}/packages`)
},
uploadPluginPackage(pluginId: string, payload: Record<string, unknown>) {
return request<ReleaseRecord>(`/api/v1/ops/plugins/${pluginId}/packages`, {
method: 'POST',
body: JSON.stringify(payload),
})
},
publishPackage(releaseId: string, payload: Record<string, unknown>) {
return request<ReleaseRecord>(`/api/v1/ops/packages/${releaseId}/publish`, {
method: 'POST',
body: JSON.stringify(payload),
})

查看文件

@ -1,12 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
import RegisterView from '../views/RegisterView.vue'
import VersionManagementView from '../views/VersionManagementView.vue'
import LoginView from '../views/LoginView.vue'
import OpsShellView from '../views/OpsShellView.vue'
import ModulePlaceholderView from '../views/ModulePlaceholderView.vue'
import AppManagementView from '../views/AppManagementView.vue'
export default createRouter({
history: createWebHistory(),
routes: [
{ path: '/', redirect: '/register' },
{ path: '/register', component: RegisterView },
{ path: '/versions', component: VersionManagementView },
{ path: '/', redirect: '/login' },
{ path: '/login', component: LoginView },
{
path: '/console',
component: OpsShellView,
children: [
{ path: '', redirect: '/console/apps' },
{ path: 'im', component: ModulePlaceholderView, props: { title: 'IM 模块', description: '预留即时通讯管理控制台。' } },
{ path: 'push', component: ModulePlaceholderView, props: { title: 'Push 模块', description: '预留推送策略、模板与发布能力。' } },
{ path: 'apps', component: AppManagementView },
],
},
],
})

查看文件

@ -66,6 +66,13 @@ select {
gap: 20px;
}
.auth-layout {
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
}
.stack {
grid-template-columns: 1.25fr 1fr;
align-items: start;
@ -167,6 +174,38 @@ button {
flex-wrap: wrap;
}
.tab-row,
.list-grid {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 16px;
}
.list-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.list-card {
text-align: left;
display: grid;
gap: 6px;
background: #eef5fb;
color: #163454;
}
.list-card[data-active='true'] {
background: linear-gradient(135deg, #0d72ff, #11b8a5);
color: white;
}
.sub-panel {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid rgba(16, 35, 61, 0.08);
}
.chips span,
.chip-button {
padding: 6px 10px;

查看文件

@ -0,0 +1,297 @@
<template>
<section class="page stack">
<div class="panel">
<div class="section-head">
<div>
<p class="section-tag">App 管理</p>
<h2>应用列表</h2>
</div>
<button class="primary" @click="showCreateApp = !showCreateApp">创建 App</button>
</div>
<form v-if="showCreateApp" class="form-grid" @submit.prevent="createApp">
<label><span>名称</span><input v-model="appForm.name" required /></label>
<label><span>包名</span><input v-model="appForm.packageName" required /></label>
<label><span>插件包名前缀</span><input v-model="appForm.pluginPackageName" /></label>
<label class="full"><span>说明</span><input v-model="appForm.description" /></label>
<label class="toggle"><input type="checkbox" v-model="appForm.pluginManagementEnabled" />支持插件化</label>
<button class="primary">保存 App</button>
</form>
<div class="list-grid">
<button
v-for="item in applications"
:key="item.application.id"
class="list-card"
:data-active="selectedApp?.application.id === item.application.id"
@click="selectApp(item)"
>
<strong>{{ item.application.name }}</strong>
<span>{{ item.application.packageName }}</span>
<span>{{ item.application.pluginManagementEnabled ? '支持插件化' : '仅宿主' }}</span>
</button>
</div>
</div>
<div class="panel" v-if="selectedApp">
<div class="section-head">
<div>
<p class="section-tag">当前应用</p>
<h2>{{ selectedApp.application.name }}</h2>
</div>
<button class="secondary" @click="loadAll">刷新</button>
</div>
<form class="form-grid" @submit.prevent="saveApp">
<label><span>名称</span><input v-model="appEdit.name" required /></label>
<label><span>包名</span><input v-model="appEdit.packageName" required /></label>
<label><span>插件包名前缀</span><input v-model="appEdit.pluginPackageName" /></label>
<label class="full"><span>说明</span><input v-model="appEdit.description" /></label>
<label class="toggle"><input type="checkbox" v-model="appEdit.pluginManagementEnabled" />支持插件化</label>
<button class="primary">修改 App 信息</button>
</form>
<div class="tab-row">
<button :class="tab === 'packages' ? 'primary' : 'secondary'" @click="tab = 'packages'">安装包</button>
<button
v-if="selectedApp.application.pluginManagementEnabled"
:class="tab === 'plugins' ? 'primary' : 'secondary'"
@click="tab = 'plugins'"
>
插件列表
</button>
</div>
<template v-if="tab === 'packages'">
<form class="form-grid" @submit.prevent="uploadAppPackage">
<label><span>包名</span><input v-model="appPackageForm.packageName" required /></label>
<label><span>版本名</span><input v-model="appPackageForm.versionName" required /></label>
<label><span>版本码</span><input v-model.number="appPackageForm.versionCode" type="number" required /></label>
<label><span>文件名</span><input v-model="appPackageForm.uploadedFileName" required /></label>
<label class="full"><span>下载地址</span><input v-model="appPackageForm.downloadUrl" required /></label>
<label class="full"><span>标题</span><input v-model="appPackageForm.title" required /></label>
<label class="full"><span>更新说明</span><textarea v-model="appPackageForm.changelog" rows="3" /></label>
<button class="primary">上传安装包</button>
</form>
<table class="table">
<thead><tr><th>版本</th><th>状态</th><th>操作</th></tr></thead>
<tbody>
<tr v-for="release in appPackages" :key="release.id">
<td>{{ release.versionName }} ({{ release.versionCode }})</td>
<td>{{ release.status }}</td>
<td class="actions">
<button class="ghost" @click="publishFull(release.id)">发布当前安装包</button>
<button class="ghost" @click="prepareGray(release.id)">配置灰度</button>
</td>
</tr>
</tbody>
</table>
</template>
<template v-else>
<form class="form-grid" @submit.prevent="createPlugin">
<label><span>插件名</span><input v-model="pluginForm.name" required /></label>
<label><span>插件包名</span><input v-model="pluginForm.packageName" required /></label>
<label class="full"><span>入口 Activity</span><input v-model="pluginForm.entryActivity" /></label>
<label class="full"><span>说明</span><input v-model="pluginForm.description" /></label>
<button class="primary">新建插件</button>
</form>
<div class="list-grid">
<button
v-for="plugin in plugins"
:key="plugin.id"
class="list-card"
:data-active="selectedPlugin?.id === plugin.id"
@click="selectPlugin(plugin)"
>
<strong>{{ plugin.name }}</strong>
<span>{{ plugin.packageName }}</span>
<span>{{ plugin.enabled ? '已启用' : '未启用' }}</span>
</button>
</div>
<div v-if="selectedPlugin" class="sub-panel">
<h3>{{ selectedPlugin.name }} 安装包</h3>
<form class="form-grid" @submit.prevent="uploadPluginPackage">
<label><span>版本名</span><input v-model="pluginPackageForm.versionName" required /></label>
<label><span>版本码</span><input v-model.number="pluginPackageForm.versionCode" type="number" required /></label>
<label><span>宿主最低版本码</span><input v-model.number="pluginPackageForm.minHostVersionCode" type="number" /></label>
<label><span>宿主最低版本名</span><input v-model="pluginPackageForm.minHostVersionName" /></label>
<label><span>文件名</span><input v-model="pluginPackageForm.uploadedFileName" required /></label>
<label class="full"><span>下载地址</span><input v-model="pluginPackageForm.downloadUrl" required /></label>
<label class="full"><span>标题</span><input v-model="pluginPackageForm.title" required /></label>
<label class="full"><span>更新说明</span><textarea v-model="pluginPackageForm.changelog" rows="3" /></label>
<button class="primary">上传插件安装包</button>
</form>
<table class="table">
<thead><tr><th>版本</th><th>宿主最低版本</th><th>状态</th><th>操作</th></tr></thead>
<tbody>
<tr v-for="release in pluginPackages" :key="release.id">
<td>{{ release.versionName }} ({{ release.versionCode }})</td>
<td>{{ release.minHostVersionName || '-' }}</td>
<td>{{ release.status }}</td>
<td class="actions">
<button class="ghost" @click="publishFull(release.id)">发版</button>
<button class="ghost" @click="prepareGray(release.id)">灰度</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<div v-if="grayReleaseId" class="sub-panel">
<h3>灰度信息配置</h3>
<div class="filters">
<label><span>分组</span>
<select v-model="filters.groupCode" @change="loadUsers">
<option value="">全部</option>
<option v-for="group in groups" :key="group.code" :value="group.code">{{ group.name }}</option>
</select>
</label>
<label><span>快选</span>
<select v-model="filters.quickSelectionCode" @change="loadUsers">
<option value="">全部</option>
<option v-for="item in quickSelections" :key="item.code" :value="item.code">{{ item.name }}</option>
</select>
</label>
<label class="grow"><span>搜索</span><input v-model="filters.keyword" @input="loadUsers" /></label>
</div>
<table class="table">
<thead><tr><th></th><th>ID</th><th>昵称</th><th>地区</th><th>分组</th></tr></thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td><input type="checkbox" :checked="selectedUsers.includes(user.id)" @change="toggleUser(user.id)" /></td>
<td>{{ user.id }}</td>
<td>{{ user.nickname }}</td>
<td>{{ user.region }}</td>
<td>{{ user.groupName }}</td>
</tr>
</tbody>
</table>
<div class="actions">
<button class="primary" @click="publishGray">确认灰度发布</button>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { api, type ApplicationDetail, type AudienceGroup, type AudienceUser, type PluginConfig, type QuickSelection, type ReleaseRecord } from '../api/client'
const applications = ref<ApplicationDetail[]>([])
const selectedApp = ref<ApplicationDetail | null>(null)
const selectedPlugin = ref<PluginConfig | null>(null)
const appPackages = ref<ReleaseRecord[]>([])
const pluginPackages = ref<ReleaseRecord[]>([])
const plugins = ref<PluginConfig[]>([])
const users = ref<AudienceUser[]>([])
const groups = ref<AudienceGroup[]>([])
const quickSelections = ref<QuickSelection[]>([])
const selectedUsers = ref<string[]>([])
const grayReleaseId = ref('')
const showCreateApp = ref(false)
const tab = ref<'packages' | 'plugins'>('packages')
const filters = reactive({ keyword: '', groupCode: '', quickSelectionCode: '' })
const appForm = reactive({ name: '', packageName: '', pluginPackageName: '', description: '', pluginManagementEnabled: true, businessModules: ['IM', 'PUSH', 'VERSION'] })
const appEdit = reactive({ name: '', packageName: '', pluginPackageName: '', description: '', pluginManagementEnabled: true, businessModules: ['IM', 'PUSH', 'VERSION'] })
const pluginForm = reactive({ name: '', packageName: '', entryActivity: '', description: '' })
const appPackageForm = reactive({ packageName: '', versionCode: 1, versionName: '', title: '发现新版本', changelog: '', downloadUrl: '', uploadedFileName: '', entryActivity: '', forceUpdate: false })
const pluginPackageForm = reactive({ versionCode: 1, versionName: '', title: '插件更新', changelog: '', downloadUrl: '', uploadedFileName: '', entryActivity: '', minHostVersionCode: 0, minHostVersionName: '' })
async function loadAll() {
applications.value = await api.listApplications()
const [groupList, quickList] = await Promise.all([api.listAudienceGroups(), api.listQuickSelections()])
groups.value = groupList
quickSelections.value = quickList
if (applications.value.length > 0 && !selectedApp.value) {
await selectApp(applications.value[0])
}
}
async function selectApp(item: ApplicationDetail) {
selectedApp.value = item
selectedPlugin.value = null
tab.value = 'packages'
Object.assign(appEdit, item.application)
appPackageForm.packageName = item.application.packageName
appPackages.value = await api.listAppPackages(item.application.id)
plugins.value = await api.listPlugins(item.application.id)
}
async function createApp() {
await api.createApplication(appForm)
showCreateApp.value = false
await loadAll()
}
async function saveApp() {
if (!selectedApp.value) return
await api.updateApplication(selectedApp.value.application.id, appEdit)
await loadAll()
}
async function uploadAppPackage() {
if (!selectedApp.value) return
await api.uploadAppPackage(selectedApp.value.application.id, appPackageForm)
appPackages.value = await api.listAppPackages(selectedApp.value.application.id)
}
async function createPlugin() {
if (!selectedApp.value) return
await api.createPlugin(selectedApp.value.application.id, pluginForm)
plugins.value = await api.listPlugins(selectedApp.value.application.id)
}
async function selectPlugin(plugin: PluginConfig) {
selectedPlugin.value = plugin
pluginPackages.value = await api.listPluginPackages(plugin.id)
}
async function uploadPluginPackage() {
if (!selectedPlugin.value) return
await api.uploadPluginPackage(selectedPlugin.value.id, pluginPackageForm)
pluginPackages.value = await api.listPluginPackages(selectedPlugin.value.id)
}
async function publishFull(releaseId: string) {
await api.publishPackage(releaseId, { grayPublish: false })
if (selectedApp.value) appPackages.value = await api.listAppPackages(selectedApp.value.application.id)
if (selectedPlugin.value) pluginPackages.value = await api.listPluginPackages(selectedPlugin.value.id)
}
function prepareGray(releaseId: string) {
grayReleaseId.value = releaseId
void loadUsers()
}
async function loadUsers() {
users.value = await api.listAudienceUsers(filters)
}
function toggleUser(userId: string) {
selectedUsers.value = selectedUsers.value.includes(userId)
? selectedUsers.value.filter(item => item !== userId)
: [...selectedUsers.value, userId]
}
async function publishGray() {
if (!grayReleaseId.value) return
await api.publishPackage(grayReleaseId.value, {
grayPublish: true,
hookName: 'user-platform-gray-hook',
groupCodes: filters.groupCode ? [filters.groupCode] : [],
quickSelectionCodes: filters.quickSelectionCode ? [filters.quickSelectionCode] : [],
userIds: selectedUsers.value,
})
grayReleaseId.value = ''
}
onMounted(() => { void loadAll() })
</script>

查看文件

@ -0,0 +1,72 @@
<template>
<section class="auth-layout">
<div class="panel">
<p class="section-tag">运营平台</p>
<h1>登录或注册</h1>
<p class="muted">登录后进入 IMPushApp 管理三大模块</p>
<div class="tab-row">
<button :class="mode === 'login' ? 'primary' : 'secondary'" @click="mode = 'login'">登录</button>
<button :class="mode === 'register' ? 'primary' : 'secondary'" @click="mode = 'register'">注册</button>
</div>
<form class="form-grid" @submit.prevent="submit">
<label v-if="mode === 'register'">
企业名称
<input v-model="form.accountName" placeholder="星云运营中心" required />
</label>
<label v-if="mode === 'register'">
联系人
<input v-model="form.contactName" placeholder="林青" required />
</label>
<label>
邮箱
<input v-model="form.email" type="email" placeholder="ops@nebula.example" required />
</label>
<label v-if="mode === 'register'">
手机号
<input v-model="form.phone" placeholder="13800138000" required />
</label>
<label class="full">
密码
<input v-model="form.password" type="password" placeholder="请输入密码" required />
</label>
<button class="primary full">{{ mode === 'login' ? '登录并进入工作台' : '注册并登录' }}</button>
</form>
<p v-if="message" class="success-text">{{ message }}</p>
<p class="muted">内置测试账号`ops@nebula.example / 123456`</p>
</div>
</section>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { api } from '../api/client'
const router = useRouter()
const mode = ref<'login' | 'register'>('login')
const message = ref('')
const form = reactive({
accountName: '',
contactName: '',
email: '',
phone: '',
password: '',
})
async function submit() {
message.value = ''
try {
if (mode.value === 'register') {
await api.registerAccount(form)
}
const result = await api.login({ email: form.email, password: form.password })
localStorage.setItem('ops-session', JSON.stringify(result))
await router.push('/console/apps')
} catch (error) {
message.value = error instanceof Error ? error.message : '操作失败'
}
}
</script>

查看文件

@ -0,0 +1,11 @@
<template>
<section class="panel">
<p class="section-tag">业务模块</p>
<h2>{{ title }}</h2>
<p class="muted">{{ description }}</p>
</section>
</template>
<script setup lang="ts">
defineProps<{ title: string; description: string }>()
</script>

查看文件

@ -0,0 +1,23 @@
<template>
<div class="shell">
<aside class="sidebar">
<div>
<p class="eyebrow">Vue3 · 运营平台</p>
<h1>运营控制台</h1>
<p class="muted">登录后统一管理 IMPush App/插件发布</p>
</div>
<nav class="nav">
<RouterLink to="/console/im">IM</RouterLink>
<RouterLink to="/console/push">Push</RouterLink>
<RouterLink to="/console/apps">App 管理</RouterLink>
</nav>
</aside>
<main class="content">
<RouterView />
</main>
</div>
</template>
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>

14
frontend/package.json 普通文件
查看文件

@ -0,0 +1,14 @@
{
"name": "android-libs-frontend-workspace",
"private": true,
"packageManager": "yarn@1.22.22",
"workspaces": [
"ops-platform",
"admin-platform"
],
"scripts": {
"dev:ops": "yarn workspace ops-platform dev",
"dev:admin": "yarn workspace admin-platform dev",
"build": "yarn workspaces run build"
}
}

738
frontend/yarn.lock 普通文件
查看文件

@ -0,0 +1,738 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/helper-string-parser@^7.27.1":
version "7.27.1"
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-validator-identifier@^7.28.5":
version "7.28.5"
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
"@babel/parser@^7.29.2":
version "7.29.2"
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz#58bd50b9a7951d134988a1ae177a35ef9a703ba1"
integrity sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==
dependencies:
"@babel/types" "^7.29.0"
"@babel/types@^7.29.0":
version "7.29.0"
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7"
integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@esbuild/aix-ppc64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c"
integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==
"@esbuild/android-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752"
integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==
"@esbuild/android-arm@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a"
integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==
"@esbuild/android-x64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16"
integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==
"@esbuild/darwin-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd"
integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==
"@esbuild/darwin-x64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e"
integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==
"@esbuild/freebsd-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe"
integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==
"@esbuild/freebsd-x64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3"
integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==
"@esbuild/linux-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977"
integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==
"@esbuild/linux-arm@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9"
integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==
"@esbuild/linux-ia32@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0"
integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==
"@esbuild/linux-loong64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0"
integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==
"@esbuild/linux-mips64el@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd"
integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==
"@esbuild/linux-ppc64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869"
integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==
"@esbuild/linux-riscv64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6"
integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==
"@esbuild/linux-s390x@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663"
integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==
"@esbuild/linux-x64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306"
integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==
"@esbuild/netbsd-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4"
integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==
"@esbuild/netbsd-x64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076"
integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==
"@esbuild/openbsd-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd"
integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==
"@esbuild/openbsd-x64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679"
integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==
"@esbuild/openharmony-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d"
integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==
"@esbuild/sunos-x64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6"
integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==
"@esbuild/win32-arm64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323"
integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==
"@esbuild/win32-ia32@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267"
integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==
"@esbuild/win32-x64@0.25.12":
version "0.25.12"
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5"
integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==
"@jridgewell/sourcemap-codec@^1.5.5":
version "1.5.5"
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@rollup/rollup-android-arm-eabi@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz#7e158ddfc16f78da99c0d5ccbae6cae403ef3284"
integrity sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==
"@rollup/rollup-android-arm64@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz#49f4ae0e22b6f9ffbcd3818b9a0758fa2d10b1cd"
integrity sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==
"@rollup/rollup-darwin-arm64@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz#bb200269069acf5c1c4d79ad142524f77e8b8236"
integrity sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==
"@rollup/rollup-darwin-x64@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz#1bf7a92b27ebdd5e0d1d48503c7811160773be1a"
integrity sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==
"@rollup/rollup-freebsd-arm64@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz#5ccf537b99c5175008444702193ad0b1c36f7f16"
integrity sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==
"@rollup/rollup-freebsd-x64@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz#1196ecd7bf4e128624ef83cd1f9d785114474a77"
integrity sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==
"@rollup/rollup-linux-arm-gnueabihf@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz#cc147633a4af229fee83a737bf2334fbac3dc28e"
integrity sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==
"@rollup/rollup-linux-arm-musleabihf@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz#3559f9f060153ea54594a42c3b87a297bedcc26e"
integrity sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==
"@rollup/rollup-linux-arm64-gnu@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz#e91f887b154123485cfc4b59befe2080fcd8f2df"
integrity sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==
"@rollup/rollup-linux-arm64-musl@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz#660752f040df9ba44a24765df698928917c0bf21"
integrity sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==
"@rollup/rollup-linux-loong64-gnu@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz#cb0e939a5fa479ccef264f3f45b31971695f869c"
integrity sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==
"@rollup/rollup-linux-loong64-musl@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz#42f86fbc82cd1a81be2d346476dd3231cf5ee442"
integrity sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==
"@rollup/rollup-linux-ppc64-gnu@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz#39776a647a789dc95ea049277c5ef8f098df77f9"
integrity sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==
"@rollup/rollup-linux-ppc64-musl@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz#466f20029a8e8b3bb2954c7ddebc9586420cac2c"
integrity sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==
"@rollup/rollup-linux-riscv64-gnu@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz#cff9877c78f12e7aa6246f6902ad913e99edb2b7"
integrity sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==
"@rollup/rollup-linux-riscv64-musl@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz#9a762fb99b5a82a921017f56491b7e892b9fb17d"
integrity sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==
"@rollup/rollup-linux-s390x-gnu@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz#9d25ad8ac7dab681935baf78ac5ea92d14629cdf"
integrity sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==
"@rollup/rollup-linux-x64-gnu@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz#5e5139e11819fa38a052368da79422cb4afcf466"
integrity sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==
"@rollup/rollup-linux-x64-musl@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz#b6211d46e11b1f945f5504cc794fce839331ed08"
integrity sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==
"@rollup/rollup-openbsd-x64@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz#e6e09eebaa7012bb9c7331b437a9e992bd94ca35"
integrity sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==
"@rollup/rollup-openharmony-arm64@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz#f7d99ae857032498e57a5e7259fb7100fd24a87e"
integrity sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==
"@rollup/rollup-win32-arm64-msvc@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz#41e392f5d9f3bf1253fdaf2f6d6f6b1bfc452856"
integrity sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==
"@rollup/rollup-win32-ia32-msvc@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz#f41b0490be0e5d3cf459b4dc076a192b532adea9"
integrity sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==
"@rollup/rollup-win32-x64-gnu@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz#0fcf9f1fcb750f0317b13aac3b3231687e6397a5"
integrity sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==
"@rollup/rollup-win32-x64-msvc@4.60.0":
version "4.60.0"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz#3afdb30405f6d4248df5e72e1ca86c5eab55fab8"
integrity sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==
"@types/estree@1.0.8":
version "1.0.8"
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@vitejs/plugin-vue@^5.2.3":
version "5.2.4"
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8"
integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
"@volar/language-core@2.4.15":
version "2.4.15"
resolved "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz#759d04cb4eab9920560b8bcfa4515d5b08a1b7ce"
integrity sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==
dependencies:
"@volar/source-map" "2.4.15"
"@volar/source-map@2.4.15":
version "2.4.15"
resolved "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz#18aba09994c0268e59a418f9d738e4a85302781d"
integrity sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==
"@volar/typescript@2.4.15":
version "2.4.15"
resolved "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz#1445d23f8e4f9ad821b6bfa58cf4a2b980dc5f97"
integrity sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==
dependencies:
"@volar/language-core" "2.4.15"
path-browserify "^1.0.1"
vscode-uri "^3.0.8"
"@vue/compiler-core@3.5.31":
version "3.5.31"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.31.tgz#db20d99eb3e8e9ce3c008b8cc79bdb7dab3dfe61"
integrity sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==
dependencies:
"@babel/parser" "^7.29.2"
"@vue/shared" "3.5.31"
entities "^7.0.1"
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-dom@3.5.31", "@vue/compiler-dom@^3.5.0":
version "3.5.31"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz#7d4b5688a8daef1513ee18f566ea129bded36ff7"
integrity sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==
dependencies:
"@vue/compiler-core" "3.5.31"
"@vue/shared" "3.5.31"
"@vue/compiler-sfc@3.5.31":
version "3.5.31"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz#ab3670bc81f0bf60ccd0766b16042ad5b334d821"
integrity sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==
dependencies:
"@babel/parser" "^7.29.2"
"@vue/compiler-core" "3.5.31"
"@vue/compiler-dom" "3.5.31"
"@vue/compiler-ssr" "3.5.31"
"@vue/shared" "3.5.31"
estree-walker "^2.0.2"
magic-string "^0.30.21"
postcss "^8.5.8"
source-map-js "^1.2.1"
"@vue/compiler-ssr@3.5.31":
version "3.5.31"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz#bc68bb14cedab04ff5230460badfca983de5391b"
integrity sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==
dependencies:
"@vue/compiler-dom" "3.5.31"
"@vue/shared" "3.5.31"
"@vue/compiler-vue2@^2.7.16":
version "2.7.16"
resolved "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz#2ba837cbd3f1b33c2bc865fbe1a3b53fb611e249"
integrity sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==
dependencies:
de-indent "^1.0.2"
he "^1.2.0"
"@vue/devtools-api@^6.6.4":
version "6.6.4"
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
"@vue/devtools-api@^7.7.7":
version "7.7.9"
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz#999dbea50da6b00cf59a1336f11fdc2b43d9e063"
integrity sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==
dependencies:
"@vue/devtools-kit" "^7.7.9"
"@vue/devtools-kit@^7.7.9":
version "7.7.9"
resolved "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz#bc218a815616e8987df7ab3e10fc1fb3b8706c58"
integrity sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==
dependencies:
"@vue/devtools-shared" "^7.7.9"
birpc "^2.3.0"
hookable "^5.5.3"
mitt "^3.0.1"
perfect-debounce "^1.0.0"
speakingurl "^14.0.1"
superjson "^2.2.2"
"@vue/devtools-shared@^7.7.9":
version "7.7.9"
resolved "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz#fa4c096b744927081a7dda5fcf05f34b1ae6ca14"
integrity sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==
dependencies:
rfdc "^1.4.1"
"@vue/language-core@2.2.12":
version "2.2.12"
resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz#d01f7e865f593f968cb65c12a13d8337e65641f0"
integrity sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==
dependencies:
"@volar/language-core" "2.4.15"
"@vue/compiler-dom" "^3.5.0"
"@vue/compiler-vue2" "^2.7.16"
"@vue/shared" "^3.5.0"
alien-signals "^1.0.3"
minimatch "^9.0.3"
muggle-string "^0.4.1"
path-browserify "^1.0.1"
"@vue/reactivity@3.5.31":
version "3.5.31"
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.31.tgz#c8deaab09bd26185d3153d3e4447e2c6590a6608"
integrity sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==
dependencies:
"@vue/shared" "3.5.31"
"@vue/runtime-core@3.5.31":
version "3.5.31"
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.31.tgz#bbe07e3bda17caf3ca2ba289bdd2d22badf3dce4"
integrity sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==
dependencies:
"@vue/reactivity" "3.5.31"
"@vue/shared" "3.5.31"
"@vue/runtime-dom@3.5.31":
version "3.5.31"
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz#3fe39a7bbbbf3ef2cdd6c51f8a1ea63d13959ec6"
integrity sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==
dependencies:
"@vue/reactivity" "3.5.31"
"@vue/runtime-core" "3.5.31"
"@vue/shared" "3.5.31"
csstype "^3.2.3"
"@vue/server-renderer@3.5.31":
version "3.5.31"
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.31.tgz#d5dbc14dedc37315d197d0a846ef817e02257778"
integrity sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==
dependencies:
"@vue/compiler-ssr" "3.5.31"
"@vue/shared" "3.5.31"
"@vue/shared@3.5.31", "@vue/shared@^3.5.0":
version "3.5.31"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.31.tgz#83276e1d450fea7d20dd15c3bbafbea5aada122d"
integrity sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==
alien-signals@^1.0.3:
version "1.0.13"
resolved "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz#8d6db73462f742ee6b89671fbd8c37d0b1727a7e"
integrity sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
birpc@^2.3.0:
version "2.9.0"
resolved "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz#b59550897e4cd96a223e2a6c1475b572236ed145"
integrity sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==
brace-expansion@^2.0.2:
version "2.0.3"
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.3.tgz#0493338bdd58e319b1039c67cf7ee439892c01d9"
integrity sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==
dependencies:
balanced-match "^1.0.0"
copy-anything@^4:
version "4.0.5"
resolved "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea"
integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==
dependencies:
is-what "^5.2.0"
csstype@^3.2.3:
version "3.2.3"
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
entities@^7.0.1:
version "7.0.1"
resolved "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b"
integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==
esbuild@^0.25.0:
version "0.25.12"
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5"
integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==
optionalDependencies:
"@esbuild/aix-ppc64" "0.25.12"
"@esbuild/android-arm" "0.25.12"
"@esbuild/android-arm64" "0.25.12"
"@esbuild/android-x64" "0.25.12"
"@esbuild/darwin-arm64" "0.25.12"
"@esbuild/darwin-x64" "0.25.12"
"@esbuild/freebsd-arm64" "0.25.12"
"@esbuild/freebsd-x64" "0.25.12"
"@esbuild/linux-arm" "0.25.12"
"@esbuild/linux-arm64" "0.25.12"
"@esbuild/linux-ia32" "0.25.12"
"@esbuild/linux-loong64" "0.25.12"
"@esbuild/linux-mips64el" "0.25.12"
"@esbuild/linux-ppc64" "0.25.12"
"@esbuild/linux-riscv64" "0.25.12"
"@esbuild/linux-s390x" "0.25.12"
"@esbuild/linux-x64" "0.25.12"
"@esbuild/netbsd-arm64" "0.25.12"
"@esbuild/netbsd-x64" "0.25.12"
"@esbuild/openbsd-arm64" "0.25.12"
"@esbuild/openbsd-x64" "0.25.12"
"@esbuild/openharmony-arm64" "0.25.12"
"@esbuild/sunos-x64" "0.25.12"
"@esbuild/win32-arm64" "0.25.12"
"@esbuild/win32-ia32" "0.25.12"
"@esbuild/win32-x64" "0.25.12"
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
fdir@^6.4.4, fdir@^6.5.0:
version "6.5.0"
resolved "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
he@^1.2.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hookable@^5.5.3:
version "5.5.3"
resolved "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==
is-what@^5.2.0:
version "5.5.0"
resolved "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4"
integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==
magic-string@^0.30.21:
version "0.30.21"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
minimatch@^9.0.3:
version "9.0.9"
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
dependencies:
brace-expansion "^2.0.2"
mitt@^3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
muggle-string@^0.4.1:
version "0.4.1"
resolved "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
nanoid@^3.3.11:
version "3.3.11"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
perfect-debounce@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^4.0.2, picomatch@^4.0.3:
version "4.0.4"
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
pinia@^3.0.1:
version "3.0.4"
resolved "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz#75dde12784a61e34c1fa6abcd13c1a1061c360c0"
integrity sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==
dependencies:
"@vue/devtools-api" "^7.7.7"
postcss@^8.5.3, postcss@^8.5.8:
version "8.5.8"
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399"
integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
source-map-js "^1.2.1"
rfdc@^1.4.1:
version "1.4.1"
resolved "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rollup@^4.34.9:
version "4.60.0"
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.60.0.tgz#d7d68c8cda873e96e08b2443505609b7e7be9eb8"
integrity sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==
dependencies:
"@types/estree" "1.0.8"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.60.0"
"@rollup/rollup-android-arm64" "4.60.0"
"@rollup/rollup-darwin-arm64" "4.60.0"
"@rollup/rollup-darwin-x64" "4.60.0"
"@rollup/rollup-freebsd-arm64" "4.60.0"
"@rollup/rollup-freebsd-x64" "4.60.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.60.0"
"@rollup/rollup-linux-arm-musleabihf" "4.60.0"
"@rollup/rollup-linux-arm64-gnu" "4.60.0"
"@rollup/rollup-linux-arm64-musl" "4.60.0"
"@rollup/rollup-linux-loong64-gnu" "4.60.0"
"@rollup/rollup-linux-loong64-musl" "4.60.0"
"@rollup/rollup-linux-ppc64-gnu" "4.60.0"
"@rollup/rollup-linux-ppc64-musl" "4.60.0"
"@rollup/rollup-linux-riscv64-gnu" "4.60.0"
"@rollup/rollup-linux-riscv64-musl" "4.60.0"
"@rollup/rollup-linux-s390x-gnu" "4.60.0"
"@rollup/rollup-linux-x64-gnu" "4.60.0"
"@rollup/rollup-linux-x64-musl" "4.60.0"
"@rollup/rollup-openbsd-x64" "4.60.0"
"@rollup/rollup-openharmony-arm64" "4.60.0"
"@rollup/rollup-win32-arm64-msvc" "4.60.0"
"@rollup/rollup-win32-ia32-msvc" "4.60.0"
"@rollup/rollup-win32-x64-gnu" "4.60.0"
"@rollup/rollup-win32-x64-msvc" "4.60.0"
fsevents "~2.3.2"
source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
speakingurl@^14.0.1:
version "14.0.1"
resolved "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==
superjson@^2.2.2:
version "2.2.6"
resolved "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz#a223a3a988172a5f9656e2063fe5f733af40d099"
integrity sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==
dependencies:
copy-anything "^4"
tinyglobby@^0.2.13:
version "0.2.15"
resolved "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.3"
typescript@^5.8.2:
version "5.9.3"
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
vite@^6.2.2:
version "6.4.1"
resolved "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz#afbe14518cdd6887e240a4b0221ab6d0ce733f96"
integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
dependencies:
esbuild "^0.25.0"
fdir "^6.4.4"
picomatch "^4.0.2"
postcss "^8.5.3"
rollup "^4.34.9"
tinyglobby "^0.2.13"
optionalDependencies:
fsevents "~2.3.3"
vscode-uri@^3.0.8:
version "3.1.0"
resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
vue-router@^4.5.0:
version "4.6.4"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
integrity sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==
dependencies:
"@vue/devtools-api" "^6.6.4"
vue-tsc@^2.2.8:
version "2.2.12"
resolved "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz#5f719b08ef7390a763c1a20169ca5c9d09d55688"
integrity sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==
dependencies:
"@volar/typescript" "2.4.15"
"@vue/language-core" "2.2.12"
vue@^3.5.13:
version "3.5.31"
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.31.tgz#ff20b2ca7893b4f9ae576a2064dbd3e2f5850118"
integrity sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==
dependencies:
"@vue/compiler-dom" "3.5.31"
"@vue/compiler-sfc" "3.5.31"
"@vue/runtime-dom" "3.5.31"
"@vue/server-renderer" "3.5.31"
"@vue/shared" "3.5.31"

查看文件

@ -17,6 +17,11 @@
<properties>
<java.version>21</java.version>
<spring-boot.version>3.4.4</spring-boot.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencyManagement>
@ -30,4 +35,27 @@
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<release>${maven.compiler.release}</release>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

查看文件

@ -15,6 +15,8 @@
<description>Version management microservice for operator/admin platforms</description>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.release>${java.version}</maven.compiler.release>
</properties>
@ -62,8 +64,11 @@
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<release>${java.version}</release>
</configuration>
</plugin>

查看文件

@ -5,12 +5,14 @@ import com.xuqm.versionmanagement.persistence.entity.AccountEntity;
import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
import com.xuqm.versionmanagement.persistence.entity.HookUserEntity;
import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
import com.xuqm.versionmanagement.persistence.entity.QuickSelectionEntity;
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
import com.xuqm.versionmanagement.persistence.repository.AccountRepository;
import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
import com.xuqm.versionmanagement.persistence.repository.HookGroupRepository;
import com.xuqm.versionmanagement.persistence.repository.HookUserRepository;
import com.xuqm.versionmanagement.persistence.repository.PluginRepository;
import com.xuqm.versionmanagement.persistence.repository.QuickSelectionRepository;
import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
import java.time.LocalDateTime;
@ -26,6 +28,7 @@ public class DataInitializer {
CommandLineRunner seedVersionManagementData(
AccountRepository accountRepository,
ApplicationRepository applicationRepository,
PluginRepository pluginRepository,
ReleaseRepository releaseRepository,
HookUserRepository hookUserRepository,
HookGroupRepository hookGroupRepository,
@ -41,6 +44,7 @@ public class DataInitializer {
main.setAccountName("星云运营中心");
main.setContactName("林青");
main.setEmail("ops@nebula.example");
main.setPassword("123456");
main.setPhone("13800138000");
main.setType(PlatformData.AccountType.MAIN);
main.setStatus(PlatformData.AccountStatus.ACTIVE);
@ -52,6 +56,7 @@ public class DataInitializer {
sub.setAccountName("星云发布子账号");
sub.setContactName("苏宁");
sub.setEmail("release@nebula.example");
sub.setPassword("123456");
sub.setPhone("13900139000");
sub.setType(PlatformData.AccountType.SUB);
sub.setStatus(PlatformData.AccountStatus.ACTIVE);
@ -65,25 +70,42 @@ public class DataInitializer {
app.setName("宿主 App");
app.setPackageName("com.xuqm.sample");
app.setPluginPackageName("com.xuqm.plugin.ui");
app.setDescription("Android 宿主应用,支持登录、检查更新、插件加载");
app.setPluginManagementEnabled(true);
app.setBusinessModules(List.of("IM", "PUSH", "VERSION"));
app.setCreatedAt(LocalDateTime.of(2026, 3, 27, 9, 0));
applicationRepository.save(app);
PluginEntity plugin = new PluginEntity();
plugin.setId("PLG-001");
plugin.setAppId(app.getId());
plugin.setName("UI 插件");
plugin.setPackageName("com.xuqm.plugin.ui");
plugin.setEntryActivity("com.xuqm.plugin.ui.PluginUiActivity");
plugin.setDescription("演示插件,支持宿主启动和独立安装");
plugin.setEnabled(true);
plugin.setCreatedAt(LocalDateTime.of(2026, 3, 27, 9, 5));
pluginRepository.save(plugin);
releaseRepository.saveAll(List.of(
release("REL-APP-001", app.getId(), PlatformData.PackageType.APP, "com.xuqm.sample", 2, "0.2.0",
release("REL-APP-001", app.getId(), null, PlatformData.PackageType.APP, "com.xuqm.sample", 2, "0.2.0",
"发现新版本", "1. 新增统一下载管理\n2. 新增插件版本管理\n3. 优化宿主更新流程",
"http://192.168.116.9:10223/app.apk", null, false, "sample-app-release-v0.2.0.apk",
"http://192.168.116.9:10223/app.apk", null, null, null, false, "sample-app-release-v0.2.0.apk",
PlatformData.ReleaseStatus.PUBLISHED, "FULL", null, List.of(), List.of(), List.of(),
LocalDateTime.of(2026, 3, 27, 9, 30), LocalDateTime.of(2026, 3, 27, 9, 45)),
release("REL-PLUGIN-001", app.getId(), PlatformData.PackageType.PLUGIN, "com.xuqm.plugin.ui", 2, "0.2.0",
release("REL-PLUGIN-001", app.getId(), plugin.getId(), PlatformData.PackageType.PLUGIN, "com.xuqm.plugin.ui", 2, "0.2.0",
"插件 UI 更新", "1. 修复插件页面展示问题\n2. 优化宿主拉起体验",
"http://192.168.116.9:10223/plugin-ui-release.apk", "com.xuqm.plugin.ui.PluginUiActivity", false, "plugin-ui-release-v0.2.0.apk",
"http://192.168.116.9:10223/plugin-ui-release.apk", "com.xuqm.plugin.ui.PluginUiActivity", 1, "0.1.0", false, "plugin-ui-release-v0.2.0.apk",
PlatformData.ReleaseStatus.PUBLISHED, "FULL", null, List.of(), List.of(), List.of(),
LocalDateTime.of(2026, 3, 27, 9, 35), LocalDateTime.of(2026, 3, 27, 9, 50)),
release("REL-APP-002", app.getId(), PlatformData.PackageType.APP, "com.xuqm.sample", 3, "0.3.0-beta",
release("REL-PLUGIN-002", app.getId(), plugin.getId(), PlatformData.PackageType.PLUGIN, "com.xuqm.plugin.ui", 5, "0.5.0",
"插件高版本", "1. 新增高版本插件能力\n2. 要求宿主至少升级到 0.2.0",
"http://192.168.116.9:10223/plugin-ui-v0.5.0.apk", "com.xuqm.plugin.ui.PluginUiActivity", 2, "0.2.0", false, "plugin-ui-release-v0.5.0.apk",
PlatformData.ReleaseStatus.PUBLISHED, "FULL", null, List.of(), List.of(), List.of(),
LocalDateTime.of(2026, 3, 27, 10, 5), LocalDateTime.of(2026, 3, 27, 10, 18)),
release("REL-APP-002", app.getId(), null, PlatformData.PackageType.APP, "com.xuqm.sample", 3, "0.3.0-beta",
"0.3.0 灰度测试", "1. 新增版本平台灰度能力\n2. 支持用户分组圈选",
"http://192.168.116.9:10223/app-beta.apk", null, false, "sample-app-release-v0.3.0-beta.apk",
"http://192.168.116.9:10223/app-beta.apk", null, null, null, false, "sample-app-release-v0.3.0-beta.apk",
PlatformData.ReleaseStatus.GRAYSCALE, "GRAY", "user-platform-gray-hook", List.of("beta-core"), List.of("vip-seed"), List.of("USER-1001"),
LocalDateTime.of(2026, 3, 27, 10, 0), LocalDateTime.of(2026, 3, 27, 10, 15))
));
@ -112,6 +134,7 @@ public class DataInitializer {
private ReleaseEntity release(
String id,
String appId,
String pluginId,
PlatformData.PackageType packageType,
String packageName,
int versionCode,
@ -120,6 +143,8 @@ public class DataInitializer {
String changelog,
String downloadUrl,
String entryActivity,
Integer minHostVersionCode,
String minHostVersionName,
boolean forceUpdate,
String uploadedFileName,
PlatformData.ReleaseStatus status,
@ -134,6 +159,7 @@ public class DataInitializer {
ReleaseEntity entity = new ReleaseEntity();
entity.setId(id);
entity.setAppId(appId);
entity.setPluginId(pluginId);
entity.setPackageType(packageType);
entity.setPackageName(packageName);
entity.setVersionCode(versionCode);
@ -142,6 +168,8 @@ public class DataInitializer {
entity.setChangelog(changelog);
entity.setDownloadUrl(downloadUrl);
entity.setEntryActivity(entryActivity);
entity.setMinHostVersionCode(minHostVersionCode);
entity.setMinHostVersionName(minHostVersionName);
entity.setForceUpdate(forceUpdate);
entity.setUploadedFileName(uploadedFileName);
entity.setStatus(status);

查看文件

@ -43,15 +43,27 @@ public class CompatibilityUpdateController {
@GetMapping("/api/v1/updates/plugin/latest")
public ApiResponse<Map<String, Object>> latestPlugin(
@RequestParam String packageName,
@RequestParam(required = false) String userId
@RequestParam(required = false) String userId,
@RequestParam(required = false) Integer hostVersionCode
) {
PlatformData.ReleaseRecord release = versionManagementService.getLatestPluginRelease(packageName, userId);
PlatformData.ReleaseRecord release = versionManagementService.getLatestPluginRelease(packageName, userId, hostVersionCode);
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("packageName", release.getPackageName());
payload.put("versionCode", release.getVersionCode());
payload.put("versionName", release.getVersionName());
payload.put("downloadUrl", release.getDownloadUrl());
payload.put("entryActivity", release.getEntryActivity());
payload.put("minHostVersionCode", release.getMinHostVersionCode());
payload.put("minHostVersionName", release.getMinHostVersionName());
return ApiResponse.success(payload);
}
@GetMapping("/api/v1/updates/plugins/catalog")
public ApiResponse<java.util.List<VersionManagementService.PluginCatalogItem>> pluginCatalog(
@RequestParam String appPackageName,
@RequestParam(required = false) String userId,
@RequestParam long hostVersionCode
) {
return ApiResponse.success(versionManagementService.getPluginCatalog(appPackageName, userId, hostVersionCode));
}
}

查看文件

@ -5,7 +5,6 @@ import com.xuqm.versionmanagement.model.PlatformData;
import com.xuqm.versionmanagement.service.UserHookService;
import com.xuqm.versionmanagement.service.VersionManagementService;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@ -19,7 +18,7 @@ import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("/api/v1/ops/version")
@RequestMapping("/api/v1/ops")
public class OpsVersionController {
private final VersionManagementService versionManagementService;
@ -30,12 +29,46 @@ public class OpsVersionController {
this.userHookService = userHookService;
}
@GetMapping("/applications")
@GetMapping("/apps")
public ApiResponse<List<VersionManagementService.ApplicationDetail>> listApplications() {
return ApiResponse.success(versionManagementService.listApplications());
}
@PutMapping("/applications/{appId}/plugin-management")
@PostMapping("/apps")
public ApiResponse<PlatformData.ApplicationConfig> createApplication(@RequestBody @Validated AppRequest request) {
PlatformData.ApplicationConfig application = versionManagementService.createApplication(
new VersionManagementService.CreateApplicationCommand(
request.name(),
request.packageName(),
request.pluginPackageName(),
request.description(),
request.pluginManagementEnabled(),
request.businessModules()
)
);
return ApiResponse.success(application, "应用创建成功");
}
@PutMapping("/apps/{appId}")
public ApiResponse<PlatformData.ApplicationConfig> updateApplication(
@PathVariable String appId,
@RequestBody @Validated AppRequest request
) {
PlatformData.ApplicationConfig application = versionManagementService.updateApplication(
appId,
new VersionManagementService.UpdateApplicationCommand(
request.name(),
request.packageName(),
request.pluginPackageName(),
request.description(),
request.pluginManagementEnabled(),
request.businessModules()
)
);
return ApiResponse.success(application, "应用信息已更新");
}
@PutMapping("/apps/{appId}/plugin-management")
public ApiResponse<PlatformData.ApplicationConfig> togglePluginManagement(
@PathVariable String appId,
@RequestBody TogglePluginManagementRequest request
@ -44,7 +77,12 @@ public class OpsVersionController {
return ApiResponse.success(config, "插件化能力已更新");
}
@PostMapping("/applications/{appId}/releases/upload")
@GetMapping("/apps/{appId}/packages")
public ApiResponse<List<PlatformData.ReleaseRecord>> listAppPackages(@PathVariable String appId) {
return ApiResponse.success(versionManagementService.listAppPackages(appId));
}
@PostMapping("/apps/{appId}/packages")
public ApiResponse<PlatformData.ReleaseRecord> uploadRelease(
@PathVariable String appId,
@RequestBody @Validated UploadReleaseRequest request
@ -52,13 +90,16 @@ public class OpsVersionController {
PlatformData.ReleaseRecord release = versionManagementService.uploadRelease(
appId,
new VersionManagementService.UploadReleaseCommand(
request.packageType(),
PlatformData.PackageType.APP,
request.packageName(),
request.versionCode(),
request.versionName(),
request.title(),
request.changelog(),
request.downloadUrl(),
request.entryActivity(),
null,
null,
request.forceUpdate(),
request.uploadedFileName()
)
@ -66,14 +107,79 @@ public class OpsVersionController {
return ApiResponse.success(release, "版本包已上传");
}
@PostMapping("/applications/{appId}/releases/{releaseId}/publish")
public ApiResponse<PlatformData.ReleaseRecord> publishRelease(
@GetMapping("/apps/{appId}/plugins")
public ApiResponse<List<PlatformData.PluginConfig>> listPlugins(@PathVariable String appId) {
return ApiResponse.success(versionManagementService.listPlugins(appId));
}
@PostMapping("/apps/{appId}/plugins")
public ApiResponse<PlatformData.PluginConfig> createPlugin(
@PathVariable String appId,
@RequestBody @Validated PluginRequest request
) {
PlatformData.PluginConfig plugin = versionManagementService.createPlugin(
appId,
new VersionManagementService.CreatePluginCommand(
request.name(),
request.packageName(),
request.entryActivity(),
request.description()
)
);
return ApiResponse.success(plugin, "插件创建成功");
}
@PutMapping("/plugins/{pluginId}")
public ApiResponse<PlatformData.PluginConfig> updatePlugin(
@PathVariable String pluginId,
@RequestBody @Validated PluginUpdateRequest request
) {
PlatformData.PluginConfig plugin = versionManagementService.updatePlugin(
pluginId,
new VersionManagementService.UpdatePluginCommand(
request.name(),
request.packageName(),
request.entryActivity(),
request.description(),
request.enabled()
)
);
return ApiResponse.success(plugin, "插件信息已更新");
}
@GetMapping("/plugins/{pluginId}/packages")
public ApiResponse<List<PlatformData.ReleaseRecord>> listPluginPackages(@PathVariable String pluginId) {
return ApiResponse.success(versionManagementService.listPluginPackages(pluginId));
}
@PostMapping("/plugins/{pluginId}/packages")
public ApiResponse<PlatformData.ReleaseRecord> uploadPluginRelease(
@PathVariable String pluginId,
@RequestBody @Validated UploadPluginReleaseRequest request
) {
PlatformData.ReleaseRecord release = versionManagementService.uploadPluginRelease(
pluginId,
new VersionManagementService.UploadPluginReleaseCommand(
request.versionCode(),
request.versionName(),
request.title(),
request.changelog(),
request.downloadUrl(),
request.entryActivity(),
request.minHostVersionCode(),
request.minHostVersionName(),
request.uploadedFileName()
)
);
return ApiResponse.success(release, "插件安装包已上传");
}
@PostMapping("/packages/{releaseId}/publish")
public ApiResponse<PlatformData.ReleaseRecord> publishRelease(
@PathVariable String releaseId,
@RequestBody PublishReleaseRequest request
) {
PlatformData.ReleaseRecord release = versionManagementService.publishRelease(
appId,
releaseId,
new VersionManagementService.PublishReleaseCommand(
request.grayPublish(),
@ -105,11 +211,24 @@ public class OpsVersionController {
return ApiResponse.success(userHookService.getAudienceBundle(null, null, null).quickSelections());
}
public record AppRequest(
@NotBlank(message = "不能为空") String name,
@NotBlank(message = "不能为空") String packageName,
String pluginPackageName,
String description,
boolean pluginManagementEnabled,
List<String> businessModules
) {
public List<String> businessModules() {
return businessModules == null || businessModules.isEmpty() ? List.of("IM", "PUSH", "VERSION") : businessModules;
}
}
public record TogglePluginManagementRequest(boolean enabled) {
}
public record UploadReleaseRequest(
@NotNull(message = "不能为空") PlatformData.PackageType packageType,
@NotBlank(message = "不能为空") String packageName,
int versionCode,
@NotBlank(message = "不能为空") String versionName,
@NotBlank(message = "不能为空") String title,
@ -121,6 +240,36 @@ public class OpsVersionController {
) {
}
public record PluginRequest(
@NotBlank(message = "不能为空") String name,
@NotBlank(message = "不能为空") String packageName,
String entryActivity,
String description
) {
}
public record PluginUpdateRequest(
@NotBlank(message = "不能为空") String name,
@NotBlank(message = "不能为空") String packageName,
String entryActivity,
String description,
boolean enabled
) {
}
public record UploadPluginReleaseRequest(
int versionCode,
@NotBlank(message = "不能为空") String versionName,
@NotBlank(message = "不能为空") String title,
String changelog,
@NotBlank(message = "不能为空") String downloadUrl,
String entryActivity,
Integer minHostVersionCode,
String minHostVersionName,
@NotBlank(message = "不能为空") String uploadedFileName
) {
}
public record PublishReleaseRequest(
boolean grayPublish,
String hookName,

查看文件

@ -31,10 +31,16 @@ public class PublicAccountController {
request.accountName(),
request.contactName(),
request.email(),
request.phone()
request.phone(),
request.password()
)
);
return ApiResponse.success(account, "运营平台注册成功,等待管理平台审核");
return ApiResponse.success(account, "运营平台注册成功");
}
@PostMapping("/api/v1/open/accounts/login")
public ApiResponse<AccountService.LoginAccountView> login(@RequestBody @Validated LoginRequest request) {
return ApiResponse.success(accountService.login(request.email(), request.password()), "登录成功");
}
@PostMapping("/api/v1/ops/accounts/{accountId}/sub-accounts")
@ -59,7 +65,14 @@ public class PublicAccountController {
@NotBlank(message = "不能为空") String accountName,
@NotBlank(message = "不能为空") String contactName,
@Email(message = "格式不正确") String email,
@NotBlank(message = "不能为空") String phone
@NotBlank(message = "不能为空") String phone,
@NotBlank(message = "不能为空") String password
) {
}
public record LoginRequest(
@Email(message = "格式不正确") String email,
@NotBlank(message = "不能为空") String password
) {
}

查看文件

@ -8,6 +8,7 @@ public class PlatformData {
private List<Account> accounts = new ArrayList<>();
private List<ApplicationConfig> applications = new ArrayList<>();
private List<PluginConfig> plugins = new ArrayList<>();
private List<ReleaseRecord> releases = new ArrayList<>();
private List<HookUser> hookUsers = new ArrayList<>();
private List<HookGroup> hookGroups = new ArrayList<>();
@ -29,6 +30,14 @@ public class PlatformData {
this.applications = applications;
}
public List<PluginConfig> getPlugins() {
return plugins;
}
public void setPlugins(List<PluginConfig> plugins) {
this.plugins = plugins;
}
public List<ReleaseRecord> getReleases() {
return releases;
}
@ -181,6 +190,7 @@ public class PlatformData {
private String name;
private String packageName;
private String pluginPackageName;
private String description;
private boolean pluginManagementEnabled;
private List<String> businessModules = new ArrayList<>();
private LocalDateTime createdAt;
@ -217,6 +227,14 @@ public class PlatformData {
this.pluginPackageName = pluginPackageName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isPluginManagementEnabled() {
return pluginManagementEnabled;
}
@ -242,9 +260,85 @@ public class PlatformData {
}
}
public static class PluginConfig {
private String id;
private String appId;
private String name;
private String packageName;
private String entryActivity;
private String description;
private boolean enabled;
private LocalDateTime createdAt;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getEntryActivity() {
return entryActivity;
}
public void setEntryActivity(String entryActivity) {
this.entryActivity = entryActivity;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}
public static class ReleaseRecord {
private String id;
private String appId;
private String pluginId;
private PackageType packageType;
private String packageName;
private int versionCode;
@ -253,6 +347,8 @@ public class PlatformData {
private String changelog;
private String downloadUrl;
private String entryActivity;
private Integer minHostVersionCode;
private String minHostVersionName;
private boolean forceUpdate;
private String uploadedFileName;
private ReleaseStatus status;
@ -277,6 +373,14 @@ public class PlatformData {
this.appId = appId;
}
public String getPluginId() {
return pluginId;
}
public void setPluginId(String pluginId) {
this.pluginId = pluginId;
}
public PackageType getPackageType() {
return packageType;
}
@ -341,6 +445,22 @@ public class PlatformData {
this.entryActivity = entryActivity;
}
public Integer getMinHostVersionCode() {
return minHostVersionCode;
}
public void setMinHostVersionCode(Integer minHostVersionCode) {
this.minHostVersionCode = minHostVersionCode;
}
public String getMinHostVersionName() {
return minHostVersionName;
}
public void setMinHostVersionName(String minHostVersionName) {
this.minHostVersionName = minHostVersionName;
}
public boolean isForceUpdate() {
return forceUpdate;
}

查看文件

@ -27,6 +27,9 @@ public class AccountEntity {
@Column(nullable = false, length = 128)
private String email;
@Column(nullable = false, length = 128)
private String password;
@Column(nullable = false, length = 32)
private String phone;
@ -88,6 +91,14 @@ public class AccountEntity {
this.phone = phone;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public PlatformData.AccountType getType() {
return type;
}

查看文件

@ -24,6 +24,9 @@ public class ApplicationEntity {
@Column(length = 128)
private String pluginPackageName;
@Column(length = 512)
private String description;
@Column(nullable = false)
private boolean pluginManagementEnabled;
@ -66,6 +69,14 @@ public class ApplicationEntity {
this.pluginPackageName = pluginPackageName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isPluginManagementEnabled() {
return pluginManagementEnabled;
}

查看文件

@ -0,0 +1,100 @@
package com.xuqm.versionmanagement.persistence.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
@Entity
@Table(name = "vm_plugin")
public class PluginEntity {
@Id
private String id;
@Column(nullable = false, length = 64)
private String appId;
@Column(nullable = false, length = 128)
private String name;
@Column(nullable = false, length = 128)
private String packageName;
@Column(length = 256)
private String entryActivity;
@Column(length = 512)
private String description;
@Column(nullable = false)
private boolean enabled;
@Column(nullable = false)
private LocalDateTime createdAt;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getEntryActivity() {
return entryActivity;
}
public void setEntryActivity(String entryActivity) {
this.entryActivity = entryActivity;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

查看文件

@ -21,6 +21,9 @@ public class ReleaseEntity {
@Column(nullable = false, length = 64)
private String appId;
@Column(length = 64)
private String pluginId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 16)
private PlatformData.PackageType packageType;
@ -46,6 +49,11 @@ public class ReleaseEntity {
@Column(length = 256)
private String entryActivity;
private Integer minHostVersionCode;
@Column(length = 64)
private String minHostVersionName;
@Column(nullable = false)
private boolean forceUpdate;
@ -95,6 +103,14 @@ public class ReleaseEntity {
this.appId = appId;
}
public String getPluginId() {
return pluginId;
}
public void setPluginId(String pluginId) {
this.pluginId = pluginId;
}
public PlatformData.PackageType getPackageType() {
return packageType;
}
@ -159,6 +175,22 @@ public class ReleaseEntity {
this.entryActivity = entryActivity;
}
public Integer getMinHostVersionCode() {
return minHostVersionCode;
}
public void setMinHostVersionCode(Integer minHostVersionCode) {
this.minHostVersionCode = minHostVersionCode;
}
public String getMinHostVersionName() {
return minHostVersionName;
}
public void setMinHostVersionName(String minHostVersionName) {
this.minHostVersionName = minHostVersionName;
}
public boolean isForceUpdate() {
return forceUpdate;
}

查看文件

@ -10,4 +10,6 @@ public interface AccountRepository extends JpaRepository<AccountEntity, String>
List<AccountEntity> findByTypeOrderByCreatedAtAsc(PlatformData.AccountType type);
List<AccountEntity> findByParentAccountIdOrderByCreatedAtAsc(String parentAccountId);
java.util.Optional<AccountEntity> findByEmailAndPassword(String email, String password);
}

查看文件

@ -0,0 +1,10 @@
package com.xuqm.versionmanagement.persistence.repository;
import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PluginRepository extends JpaRepository<PluginEntity, String> {
List<PluginEntity> findByAppIdOrderByCreatedAtAsc(String appId);
}

查看文件

@ -10,6 +10,8 @@ public interface ReleaseRepository extends JpaRepository<ReleaseEntity, String>
List<ReleaseEntity> findByAppIdOrderByUploadedAtDesc(String appId);
List<ReleaseEntity> findByPluginIdOrderByUploadedAtDesc(String pluginId);
List<ReleaseEntity> findByPackageNameAndPackageType(String packageName, PlatformData.PackageType packageType);
Optional<ReleaseEntity> findByIdAndAppId(String id, String appId);

查看文件

@ -27,14 +27,22 @@ public class AccountService {
entity.setAccountName(command.accountName());
entity.setContactName(command.contactName());
entity.setEmail(command.email());
entity.setPassword(command.password());
entity.setPhone(command.phone());
entity.setType(PlatformData.AccountType.MAIN);
entity.setStatus(PlatformData.AccountStatus.PENDING);
entity.setStatus(PlatformData.AccountStatus.ACTIVE);
entity.setPermissions(List.of("version:read", "version:write", "release:publish", "subaccount:grant"));
entity.setCreatedAt(LocalDateTime.now());
return platformMapper.toAccount(accountRepository.save(entity));
}
public LoginAccountView login(String email, String password) {
AccountEntity entity = accountRepository.findByEmailAndPassword(email, password)
.orElseThrow(() -> new IllegalArgumentException("账号或密码错误"));
PlatformData.Account account = platformMapper.toAccount(entity);
return new LoginAccountView(account, entity.getType() == PlatformData.AccountType.SUB ? entity.getParentAccountId() : entity.getId());
}
@Transactional
public PlatformData.Account createSubAccount(String parentAccountId, CreateSubAccountCommand command) {
findAccount(parentAccountId);
@ -90,7 +98,7 @@ public class AccountService {
return prefix + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
public record RegisterAccountCommand(String accountName, String contactName, String email, String phone) {
public record RegisterAccountCommand(String accountName, String contactName, String email, String phone, String password) {
}
public record CreateSubAccountCommand(
@ -104,4 +112,7 @@ public class AccountService {
public record AccountView(PlatformData.Account mainAccount, List<PlatformData.Account> subAccounts) {
}
public record LoginAccountView(PlatformData.Account account, String tenantAccountId) {
}
}

查看文件

@ -5,6 +5,7 @@ import com.xuqm.versionmanagement.persistence.entity.AccountEntity;
import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
import com.xuqm.versionmanagement.persistence.entity.HookUserEntity;
import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
import com.xuqm.versionmanagement.persistence.entity.QuickSelectionEntity;
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
import java.util.List;
@ -34,16 +35,31 @@ public class PlatformMapper {
config.setName(entity.getName());
config.setPackageName(entity.getPackageName());
config.setPluginPackageName(entity.getPluginPackageName());
config.setDescription(entity.getDescription());
config.setPluginManagementEnabled(entity.isPluginManagementEnabled());
config.setBusinessModules(entity.getBusinessModules());
config.setCreatedAt(entity.getCreatedAt());
return config;
}
public PlatformData.PluginConfig toPlugin(PluginEntity entity) {
PlatformData.PluginConfig plugin = new PlatformData.PluginConfig();
plugin.setId(entity.getId());
plugin.setAppId(entity.getAppId());
plugin.setName(entity.getName());
plugin.setPackageName(entity.getPackageName());
plugin.setEntryActivity(entity.getEntryActivity());
plugin.setDescription(entity.getDescription());
plugin.setEnabled(entity.isEnabled());
plugin.setCreatedAt(entity.getCreatedAt());
return plugin;
}
public PlatformData.ReleaseRecord toRelease(ReleaseEntity entity) {
PlatformData.ReleaseRecord release = new PlatformData.ReleaseRecord();
release.setId(entity.getId());
release.setAppId(entity.getAppId());
release.setPluginId(entity.getPluginId());
release.setPackageType(entity.getPackageType());
release.setPackageName(entity.getPackageName());
release.setVersionCode(entity.getVersionCode());
@ -52,6 +68,8 @@ public class PlatformMapper {
release.setChangelog(entity.getChangelog());
release.setDownloadUrl(entity.getDownloadUrl());
release.setEntryActivity(entity.getEntryActivity());
release.setMinHostVersionCode(entity.getMinHostVersionCode());
release.setMinHostVersionName(entity.getMinHostVersionName());
release.setForceUpdate(entity.isForceUpdate());
release.setUploadedFileName(entity.getUploadedFileName());
release.setStatus(entity.getStatus());
@ -98,18 +116,27 @@ public class PlatformMapper {
return selection;
}
public ReleaseEntity toReleaseEntity(String appId, PlatformData.ApplicationConfig app, VersionManagementService.UploadReleaseCommand command, String id) {
public ReleaseEntity toReleaseEntity(
String appId,
String pluginId,
PlatformData.ApplicationConfig app,
VersionManagementService.UploadReleaseCommand command,
String id
) {
ReleaseEntity entity = new ReleaseEntity();
entity.setId(id);
entity.setAppId(appId);
entity.setPluginId(pluginId);
entity.setPackageType(command.packageType());
entity.setPackageName(command.packageType() == PlatformData.PackageType.PLUGIN ? app.getPluginPackageName() : app.getPackageName());
entity.setPackageName(command.packageName());
entity.setVersionCode(command.versionCode());
entity.setVersionName(command.versionName());
entity.setTitle(command.title());
entity.setChangelog(command.changelog());
entity.setDownloadUrl(command.downloadUrl());
entity.setEntryActivity(command.entryActivity());
entity.setMinHostVersionCode(command.minHostVersionCode());
entity.setMinHostVersionName(command.minHostVersionName());
entity.setForceUpdate(command.forceUpdate());
entity.setUploadedFileName(command.uploadedFileName());
entity.setStatus(PlatformData.ReleaseStatus.DRAFT);

查看文件

@ -2,8 +2,10 @@ package com.xuqm.versionmanagement.service;
import com.xuqm.versionmanagement.model.PlatformData;
import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
import com.xuqm.versionmanagement.persistence.repository.PluginRepository;
import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
import java.time.LocalDateTime;
import java.util.Comparator;
@ -16,17 +18,20 @@ import org.springframework.transaction.annotation.Transactional;
public class VersionManagementService {
private final ApplicationRepository applicationRepository;
private final PluginRepository pluginRepository;
private final ReleaseRepository releaseRepository;
private final UserHookService userHookService;
private final PlatformMapper platformMapper;
public VersionManagementService(
ApplicationRepository applicationRepository,
PluginRepository pluginRepository,
ReleaseRepository releaseRepository,
UserHookService userHookService,
PlatformMapper platformMapper
) {
this.applicationRepository = applicationRepository;
this.pluginRepository = pluginRepository;
this.releaseRepository = releaseRepository;
this.userHookService = userHookService;
this.platformMapper = platformMapper;
@ -37,13 +42,43 @@ public class VersionManagementService {
.map(platformMapper::toApplication)
.map(app -> new ApplicationDetail(
app,
pluginRepository.findByAppIdOrderByCreatedAtAsc(app.getId()).stream()
.map(platformMapper::toPlugin)
.toList(),
releaseRepository.findByAppIdOrderByUploadedAtDesc(app.getId()).stream()
.filter(release -> release.getPluginId() == null)
.map(platformMapper::toRelease)
.toList()
))
.toList();
}
@Transactional
public PlatformData.ApplicationConfig createApplication(CreateApplicationCommand command) {
ApplicationEntity entity = new ApplicationEntity();
entity.setId(nextId("APP"));
entity.setName(command.name());
entity.setPackageName(command.packageName());
entity.setPluginPackageName(command.pluginPackageName());
entity.setDescription(command.description());
entity.setPluginManagementEnabled(command.pluginManagementEnabled());
entity.setBusinessModules(command.businessModules());
entity.setCreatedAt(LocalDateTime.now());
return platformMapper.toApplication(applicationRepository.save(entity));
}
@Transactional
public PlatformData.ApplicationConfig updateApplication(String appId, UpdateApplicationCommand command) {
ApplicationEntity entity = findApplicationEntity(appId);
entity.setName(command.name());
entity.setPackageName(command.packageName());
entity.setPluginPackageName(command.pluginPackageName());
entity.setDescription(command.description());
entity.setPluginManagementEnabled(command.pluginManagementEnabled());
entity.setBusinessModules(command.businessModules());
return platformMapper.toApplication(applicationRepository.save(entity));
}
@Transactional
public PlatformData.ApplicationConfig togglePluginManagement(String appId, boolean enabled) {
ApplicationEntity entity = findApplicationEntity(appId);
@ -51,18 +86,93 @@ public class VersionManagementService {
return platformMapper.toApplication(applicationRepository.save(entity));
}
public List<PlatformData.ReleaseRecord> listAppPackages(String appId) {
findApplicationEntity(appId);
return releaseRepository.findByAppIdOrderByUploadedAtDesc(appId).stream()
.filter(item -> item.getPluginId() == null)
.map(platformMapper::toRelease)
.toList();
}
@Transactional
public PlatformData.ReleaseRecord uploadRelease(String appId, UploadReleaseCommand command) {
PlatformData.ApplicationConfig app = platformMapper.toApplication(findApplicationEntity(appId));
ReleaseEntity entity = platformMapper.toReleaseEntity(appId, app, command, nextId("REL"));
ReleaseEntity entity = platformMapper.toReleaseEntity(appId, null, app, command, nextId("REL"));
entity.setUploadedAt(LocalDateTime.now());
return platformMapper.toRelease(releaseRepository.save(entity));
}
public List<PlatformData.PluginConfig> listPlugins(String appId) {
findApplicationEntity(appId);
return pluginRepository.findByAppIdOrderByCreatedAtAsc(appId).stream()
.map(platformMapper::toPlugin)
.toList();
}
@Transactional
public PlatformData.PluginConfig createPlugin(String appId, CreatePluginCommand command) {
findApplicationEntity(appId);
PluginEntity entity = new PluginEntity();
entity.setId(nextId("PLG"));
entity.setAppId(appId);
entity.setName(command.name());
entity.setPackageName(command.packageName());
entity.setEntryActivity(command.entryActivity());
entity.setDescription(command.description());
entity.setEnabled(true);
entity.setCreatedAt(LocalDateTime.now());
return platformMapper.toPlugin(pluginRepository.save(entity));
}
@Transactional
public PlatformData.PluginConfig updatePlugin(String pluginId, UpdatePluginCommand command) {
PluginEntity entity = findPluginEntity(pluginId);
entity.setName(command.name());
entity.setPackageName(command.packageName());
entity.setEntryActivity(command.entryActivity());
entity.setDescription(command.description());
entity.setEnabled(command.enabled());
return platformMapper.toPlugin(pluginRepository.save(entity));
}
public List<PlatformData.ReleaseRecord> listPluginPackages(String pluginId) {
findPluginEntity(pluginId);
return releaseRepository.findByPluginIdOrderByUploadedAtDesc(pluginId).stream()
.map(platformMapper::toRelease)
.toList();
}
@Transactional
public PlatformData.ReleaseRecord uploadPluginRelease(String pluginId, UploadPluginReleaseCommand command) {
PluginEntity plugin = findPluginEntity(pluginId);
PlatformData.ApplicationConfig app = platformMapper.toApplication(findApplicationEntity(plugin.getAppId()));
ReleaseEntity entity = platformMapper.toReleaseEntity(
plugin.getAppId(),
pluginId,
app,
new UploadReleaseCommand(
PlatformData.PackageType.PLUGIN,
plugin.getPackageName(),
command.versionCode(),
command.versionName(),
command.title(),
command.changelog(),
command.downloadUrl(),
command.entryActivity() == null || command.entryActivity().isBlank() ? plugin.getEntryActivity() : command.entryActivity(),
command.minHostVersionCode(),
command.minHostVersionName(),
false,
command.uploadedFileName()
),
nextId("REL")
);
entity.setUploadedAt(LocalDateTime.now());
return platformMapper.toRelease(releaseRepository.save(entity));
}
@Transactional
public PlatformData.ReleaseRecord publishRelease(String appId, String releaseId, PublishReleaseCommand command) {
findApplicationEntity(appId);
ReleaseEntity release = findReleaseEntity(appId, releaseId);
public PlatformData.ReleaseRecord publishRelease(String releaseId, PublishReleaseCommand command) {
ReleaseEntity release = findReleaseEntity(releaseId);
if (command.grayPublish()) {
List<PlatformData.HookUser> matchedUsers = userHookService.findUsersByIds(command.userIds());
if (!command.userIds().isEmpty() && matchedUsers.size() != command.userIds().size()) {
@ -90,8 +200,41 @@ public class VersionManagementService {
return selectLatestRelease(packageName, PlatformData.PackageType.APP, userId);
}
public PlatformData.ReleaseRecord getLatestPluginRelease(String packageName, String userId) {
return selectLatestRelease(packageName, PlatformData.PackageType.PLUGIN, userId);
public PlatformData.ReleaseRecord getLatestPluginRelease(String packageName, String userId, Integer hostVersionCode) {
return selectLatestPluginRelease(packageName, userId, hostVersionCode, true);
}
public List<PluginCatalogItem> getPluginCatalog(String appPackageName, String userId, long hostVersionCode) {
ApplicationEntity app = applicationRepository.findAll().stream()
.filter(item -> appPackageName.equals(item.getPackageName()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("应用不存在"));
PlatformData.HookUser user = userId == null ? null : userHookService.findUsersByIds(List.of(userId)).stream().findFirst().orElse(null);
return pluginRepository.findByAppIdOrderByCreatedAtAsc(app.getId()).stream()
.map(platformMapper::toPlugin)
.map(plugin -> {
List<PlatformData.ReleaseRecord> visibleReleases = releaseRepository.findByPluginIdOrderByUploadedAtDesc(plugin.getId()).stream()
.map(platformMapper::toRelease)
.filter(release -> release.getStatus() == PlatformData.ReleaseStatus.PUBLISHED
|| (release.getStatus() == PlatformData.ReleaseStatus.GRAYSCALE && userHookService.matchesGrayRule(user, release.getGrayRule())))
.toList();
PlatformData.ReleaseRecord latest = visibleReleases.stream()
.max(Comparator.comparingInt(PlatformData.ReleaseRecord::getVersionCode))
.orElse(null);
PlatformData.ReleaseRecord compatible = visibleReleases.stream()
.filter(release -> isHostCompatible(release, hostVersionCode))
.max(Comparator.comparingInt(PlatformData.ReleaseRecord::getVersionCode))
.orElse(null);
boolean blocked = latest != null && !isHostCompatible(latest, hostVersionCode);
String message = blocked
? "当前宿主版本过低,不能升级到插件最新版本"
: compatible == null ? "暂无可安装插件版本" : "可升级或进入插件";
return new PluginCatalogItem(plugin, latest, compatible, blocked, message);
})
.toList();
}
private PlatformData.ReleaseRecord selectLatestRelease(String packageName, PlatformData.PackageType packageType, String userId) {
@ -106,13 +249,35 @@ public class VersionManagementService {
.orElseThrow(() -> new IllegalArgumentException("版本配置不存在"));
}
private PlatformData.ReleaseRecord selectLatestPluginRelease(String packageName, String userId, Integer hostVersionCode, boolean compatibleOnly) {
PlatformData.HookUser user = userId == null ? null : userHookService.findUsersByIds(List.of(userId)).stream().findFirst().orElse(null);
return releaseRepository.findByPackageNameAndPackageType(packageName, PlatformData.PackageType.PLUGIN).stream()
.map(platformMapper::toRelease)
.filter(release -> release.getStatus() == PlatformData.ReleaseStatus.PUBLISHED
|| (release.getStatus() == PlatformData.ReleaseStatus.GRAYSCALE && userHookService.matchesGrayRule(user, release.getGrayRule())))
.filter(release -> !compatibleOnly || isHostCompatible(release, hostVersionCode == null ? Long.MAX_VALUE : hostVersionCode.longValue()))
.max(Comparator.comparingInt(PlatformData.ReleaseRecord::getVersionCode)
.thenComparing(PlatformData.ReleaseRecord::getPublishedAt, Comparator.nullsLast(Comparator.naturalOrder())))
.orElseThrow(() -> new IllegalArgumentException("插件版本配置不存在"));
}
private boolean isHostCompatible(PlatformData.ReleaseRecord release, long hostVersionCode) {
Integer minHostVersionCode = release.getMinHostVersionCode();
return minHostVersionCode == null || hostVersionCode >= minHostVersionCode;
}
private ApplicationEntity findApplicationEntity(String appId) {
return applicationRepository.findById(appId)
.orElseThrow(() -> new IllegalArgumentException("应用不存在"));
}
private ReleaseEntity findReleaseEntity(String appId, String releaseId) {
return releaseRepository.findByIdAndAppId(releaseId, appId)
private PluginEntity findPluginEntity(String pluginId) {
return pluginRepository.findById(pluginId)
.orElseThrow(() -> new IllegalArgumentException("插件不存在"));
}
private ReleaseEntity findReleaseEntity(String releaseId) {
return releaseRepository.findById(releaseId)
.orElseThrow(() -> new IllegalArgumentException("版本不存在"));
}
@ -120,22 +285,88 @@ public class VersionManagementService {
return prefix + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
public record ApplicationDetail(PlatformData.ApplicationConfig application, List<PlatformData.ReleaseRecord> releases) {
public record ApplicationDetail(
PlatformData.ApplicationConfig application,
List<PlatformData.PluginConfig> plugins,
List<PlatformData.ReleaseRecord> releases
) {
}
public record PluginCatalogItem(
PlatformData.PluginConfig plugin,
PlatformData.ReleaseRecord latestRelease,
PlatformData.ReleaseRecord compatibleRelease,
boolean updateBlockedByHost,
String message
) {
}
public record CreateApplicationCommand(
String name,
String packageName,
String pluginPackageName,
String description,
boolean pluginManagementEnabled,
List<String> businessModules
) {
}
public record UpdateApplicationCommand(
String name,
String packageName,
String pluginPackageName,
String description,
boolean pluginManagementEnabled,
List<String> businessModules
) {
}
public record CreatePluginCommand(
String name,
String packageName,
String entryActivity,
String description
) {
}
public record UpdatePluginCommand(
String name,
String packageName,
String entryActivity,
String description,
boolean enabled
) {
}
public record UploadReleaseCommand(
PlatformData.PackageType packageType,
String packageName,
int versionCode,
String versionName,
String title,
String changelog,
String downloadUrl,
String entryActivity,
Integer minHostVersionCode,
String minHostVersionName,
boolean forceUpdate,
String uploadedFileName
) {
}
public record UploadPluginReleaseCommand(
int versionCode,
String versionName,
String title,
String changelog,
String downloadUrl,
String entryActivity,
Integer minHostVersionCode,
String minHostVersionName,
String uploadedFileName
) {
}
public record PublishReleaseCommand(
boolean grayPublish,
String hookName,

查看文件

@ -5,11 +5,20 @@ spring:
application:
name: version-management-service
datasource:
url: jdbc:mysql://xuqinmin.com:3306/androidLibsServer?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: androidLibsServer
password: iXc8rHydtzRpYFHJ
url: ${DB_URL:jdbc:mysql://39.107.53.187:3306/android-libs?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8}
username: ${DB_USERNAME:android-libs}
password: ${DB_PASSWORD:fiiL8mxhxrDCHc86}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
minimum-idle: 10
maximum-pool-size: 20
connection-timeout: 30000
validation-timeout: 5000
idle-timeout: 300000
max-lifetime: 900000
connection-test-query: SELECT 1
jpa:
database-platform: org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: update
open-in-view: false
@ -18,10 +27,17 @@ spring:
format_sql: true
data:
redis:
host: redisdev.xuqinmin.com
port: 6379
database: 0
password: xuqinmin1022
host: ${REDIS_HOST:redisdev.xuqinmin.com}
port: ${REDIS_PORT:6379}
database: ${REDIS_DATABASE:0}
password: ${REDIS_PASSWORD:xuqinmin1022}
timeout: 10s
lettuce:
pool:
min-idle: 0
max-idle: 8
max-active: 8
max-wait: -1ms
cache:
type: redis
redis:

查看文件

@ -1,68 +0,0 @@
# version-service
用于给 `sample-app``plugin-ui` 提供统一的版本管理接口。
## 启动
```bash
cd __server__/version-service
npm start
```
默认监听:
```text
http://0.0.0.0:3000
```
## 接口
### 健康检查
```bash
curl http://127.0.0.1:3000/health
```
### 获取 App 最新版本
```bash
curl "http://127.0.0.1:3000/api/v1/updates/app/latest?packageName=com.xuqm.sample"
```
### 获取插件最新版本
```bash
curl "http://127.0.0.1:3000/api/v1/updates/plugin/latest?packageName=com.xuqm.plugin.ui"
```
### 更新 App 配置
```bash
curl --location --request PUT "http://127.0.0.1:3000/api/v1/admin/updates/app" \
--header "Content-Type: application/json" \
--data '{
"packageName": "com.xuqm.sample",
"versionCode": 2,
"versionName": "0.2.0",
"title": "发现新版本",
"changelog": "更新内容",
"downloadUrl": "http://192.168.116.9:10223/app.apk",
"forceUpdate": false
}'
```
### 更新插件配置
```bash
curl --location --request PUT "http://127.0.0.1:3000/api/v1/admin/updates/plugin" \
--header "Content-Type: application/json" \
--data '{
"packageName": "com.xuqm.plugin.ui",
"versionCode": 2,
"versionName": "0.2.0",
"downloadUrl": "http://192.168.116.9:10223/plugin-ui-release.apk",
"entryActivity": "com.xuqm.plugin.ui.PluginUiActivity"
}'
```
版本数据存放在 [`data/version-config.json`](./data/version-config.json)。

查看文件

@ -1,22 +0,0 @@
{
"apps": {
"com.xuqm.sample": {
"packageName": "com.xuqm.sample",
"versionCode": 2,
"versionName": "0.2.0",
"title": "发现新版本",
"changelog": "1. 新增统一下载管理\n2. 新增插件版本管理\n3. 优化宿主更新流程",
"downloadUrl": "http://192.168.116.9:10223/app.apk",
"forceUpdate": false
}
},
"plugins": {
"com.xuqm.plugin.ui": {
"packageName": "com.xuqm.plugin.ui",
"versionCode": 2,
"versionName": "0.2.0",
"downloadUrl": "http://192.168.116.9:10223/plugin-ui-release.apk",
"entryActivity": "com.xuqm.plugin.ui.PluginUiActivity"
}
}
}

查看文件

@ -1,12 +0,0 @@
{
"name": "@xuqm/version-service",
"version": "0.1.0",
"private": true,
"description": "Version service for Android host app and plugin update management",
"scripts": {
"start": "node src/index.js"
},
"engines": {
"node": ">=18"
}
}

查看文件

@ -1,141 +0,0 @@
const http = require("http");
const fs = require("fs/promises");
const path = require("path");
const { URL } = require("url");
const HOST = process.env.HOST || "0.0.0.0";
const PORT = Number(process.env.PORT || 3000);
const DATA_FILE = path.join(__dirname, "..", "data", "version-config.json");
async function readConfig() {
const raw = await fs.readFile(DATA_FILE, "utf8");
return JSON.parse(raw);
}
async function writeConfig(config) {
await fs.writeFile(DATA_FILE, JSON.stringify(config, null, 2), "utf8");
}
function sendJson(res, statusCode, payload) {
res.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,PUT,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
});
res.end(JSON.stringify(payload));
}
function ok(data, message = "success") {
return { code: 200, status: "0", data, message };
}
function fail(statusCode, message) {
return {
statusCode,
payload: { code: statusCode, status: String(statusCode), data: null, message }
};
}
async function parseBody(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString("utf8");
return raw ? JSON.parse(raw) : {};
}
async function handleGetLatestApp(url, res) {
const packageName = url.searchParams.get("packageName");
if (!packageName) {
const result = fail(400, "packageName is required");
return sendJson(res, result.statusCode, result.payload);
}
const config = await readConfig();
const app = config.apps[packageName];
if (!app) {
const result = fail(404, `app config not found for ${packageName}`);
return sendJson(res, result.statusCode, result.payload);
}
return sendJson(res, 200, ok(app));
}
async function handleGetLatestPlugin(url, res) {
const packageName = url.searchParams.get("packageName");
if (!packageName) {
const result = fail(400, "packageName is required");
return sendJson(res, result.statusCode, result.payload);
}
const config = await readConfig();
const plugin = config.plugins[packageName];
if (!plugin) {
const result = fail(404, `plugin config not found for ${packageName}`);
return sendJson(res, result.statusCode, result.payload);
}
return sendJson(res, 200, ok(plugin));
}
async function handleUpdateConfig(kind, req, res) {
const body = await parseBody(req);
const packageName = body.packageName;
if (!packageName) {
const result = fail(400, "packageName is required");
return sendJson(res, result.statusCode, result.payload);
}
const config = await readConfig();
const target = kind === "app" ? config.apps : config.plugins;
target[packageName] = body;
await writeConfig(config);
return sendJson(res, 200, ok(body, `${kind} config updated`));
}
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
if (req.method === "OPTIONS") {
res.writeHead(204, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,PUT,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
});
return res.end();
}
if (req.method === "GET" && url.pathname === "/health") {
return sendJson(res, 200, ok({ status: "UP" }));
}
if (req.method === "GET" && url.pathname === "/api/v1/updates/app/latest") {
return handleGetLatestApp(url, res);
}
if (req.method === "GET" && url.pathname === "/api/v1/updates/plugin/latest") {
return handleGetLatestPlugin(url, res);
}
if (req.method === "PUT" && url.pathname === "/api/v1/admin/updates/app") {
return handleUpdateConfig("app", req, res);
}
if (req.method === "PUT" && url.pathname === "/api/v1/admin/updates/plugin") {
return handleUpdateConfig("plugin", req, res);
}
const result = fail(404, "route not found");
return sendJson(res, result.statusCode, result.payload);
} catch (error) {
return sendJson(res, 500, {
code: 500,
status: "500",
data: null,
message: error.message || "internal server error"
});
}
});
server.listen(PORT, HOST, () => {
console.log(`version-service is running at http://${HOST}:${PORT}`);
});