init
这个提交包含在:
父节点
6e44428e8a
当前提交
0314acc18e
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
.m2/
|
.m2/
|
||||||
|
.yarn/
|
||||||
.gradle/
|
.gradle/
|
||||||
.kotlin/
|
.kotlin/
|
||||||
build/
|
build/
|
||||||
@ -8,6 +9,7 @@ target/
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
coverage/
|
coverage/
|
||||||
|
.pnp.*
|
||||||
*.iml
|
*.iml
|
||||||
*.log
|
*.log
|
||||||
AndroidLibs/.gradle-home/
|
AndroidLibs/.gradle-home/
|
||||||
|
|||||||
@ -4,8 +4,9 @@
|
|||||||
|
|
||||||
## 模块结构
|
## 模块结构
|
||||||
|
|
||||||
- `commonsdk-core`: SDK 核心,承载网络、共享缓存、插件管理、App 更新、设备信息与时间工具。
|
- `commonsdk-core`: SDK 核心,承载网络、下载、文件、权限、缓存、日志、全局弹窗、设备与时间等基础能力。
|
||||||
- `commonsdk-compose`: Compose 扩展组件。
|
- `commonsdk-compose`: Compose 扩展组件,提供下拉刷新、折叠面板、滑动操作、WebView、图片等业务可复用组件。
|
||||||
|
- `commonsdk-update`: 更新 SDK,独立承载 APK 更新、插件更新、版本比较与安装编排。
|
||||||
- `lib-szyx`: 项目专属 SDK,承载真实登录接口、签名、业务 Header 与会话管理。
|
- `lib-szyx`: 项目专属 SDK,承载真实登录接口、签名、业务 Header 与会话管理。
|
||||||
- `sample-app`: 示例宿主应用。
|
- `sample-app`: 示例宿主应用。
|
||||||
- `plugins/plugin-ui`: UI 演示插件,可独立运行,也可被宿主拉起。
|
- `plugins/plugin-ui`: UI 演示插件,可独立运行,也可被宿主拉起。
|
||||||
@ -48,6 +49,10 @@ nexus.password=your-password
|
|||||||
- `commonsdk-core` 提供:
|
- `commonsdk-core` 提供:
|
||||||
- `HttpManager / RetrofitManager`
|
- `HttpManager / RetrofitManager`
|
||||||
- `SharedCacheManager / SharedCacheProvider`
|
- `SharedCacheManager / SharedCacheProvider`
|
||||||
|
- `DownloadManager / FileHelper`
|
||||||
|
- `PermissionManager / GlobalCache / Logger / DialogCenter`
|
||||||
|
- `commonsdk-update` 提供:
|
||||||
|
- `UpdateSDK`
|
||||||
- `PluginPackageManager`
|
- `PluginPackageManager`
|
||||||
- `AppUpdater`
|
- `AppUpdater`
|
||||||
- `lib-szyx` 提供:
|
- `lib-szyx` 提供:
|
||||||
|
|||||||
@ -32,8 +32,11 @@ dependencies {
|
|||||||
api(project(":commonsdk-core"))
|
api(project(":commonsdk-core"))
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(libs.androidx.activity.ktx)
|
||||||
|
implementation(libs.androidx.webkit)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.bundles.compose)
|
implementation(libs.bundles.compose)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
debugImplementation(libs.bundles.compose.debug)
|
debugImplementation(libs.bundles.compose.debug)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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 {
|
dependencies {
|
||||||
api(libs.androidx.core.ktx)
|
api(libs.androidx.core.ktx)
|
||||||
|
api(libs.androidx.activity.ktx)
|
||||||
api(libs.bundles.network)
|
api(libs.bundles.network)
|
||||||
api(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
|||||||
@ -1,17 +1,24 @@
|
|||||||
package com.xuqm.sdk
|
package com.xuqm.sdk
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
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.HttpConfig
|
||||||
import com.xuqm.sdk.network.HttpManager
|
import com.xuqm.sdk.network.HttpManager
|
||||||
import com.xuqm.sdk.plugin.PluginPackageManager
|
import com.xuqm.sdk.ui.DialogCenter
|
||||||
import com.xuqm.sdk.update.AppUpdater
|
import com.xuqm.sdk.ui.ToastCenter
|
||||||
import com.xuqm.sdk.update.DownloadManager
|
import com.xuqm.sdk.update.DownloadManager
|
||||||
import com.xuqm.sdk.utils.DeviceUtils
|
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 {
|
object CoreSDK {
|
||||||
private var appContext: Context? = null
|
private var appContext: Context? = null
|
||||||
private var config: SDKConfig = SDKConfig()
|
private var config: SDKConfig = SDKConfig()
|
||||||
|
private var topActivityRef: WeakReference<Activity>? = null
|
||||||
|
|
||||||
data class SDKConfig(
|
data class SDKConfig(
|
||||||
val debugMode: Boolean = false,
|
val debugMode: Boolean = false,
|
||||||
@ -20,20 +27,66 @@ object CoreSDK {
|
|||||||
|
|
||||||
fun init(context: Context, config: SDKConfig = SDKConfig()) {
|
fun init(context: Context, config: SDKConfig = SDKConfig()) {
|
||||||
if (appContext != null) return
|
if (appContext != null) return
|
||||||
appContext = context.applicationContext
|
val applicationContext = context.applicationContext
|
||||||
|
appContext = applicationContext
|
||||||
this.config = config
|
this.config = config
|
||||||
HttpManager.init(HttpConfig(debugMode = config.debugMode))
|
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 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 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 deviceId(): String = DeviceUtils.getDeviceId(context())
|
||||||
|
|
||||||
fun deviceInfo() = DeviceUtils.getDeviceInfo(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 val serviceCache = ConcurrentHashMap<String, Any>()
|
||||||
private var globalConfig: HttpConfig = HttpConfig()
|
private var globalConfig: HttpConfig = HttpConfig()
|
||||||
|
|
||||||
fun init(config: HttpConfig = HttpConfig()) {
|
internal fun init(config: HttpConfig = HttpConfig()) {
|
||||||
globalConfig = config
|
globalConfig = config
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ class RetrofitManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object HttpManager {
|
object HttpManager {
|
||||||
fun init(config: HttpConfig = HttpConfig()) {
|
internal fun init(config: HttpConfig = HttpConfig()) {
|
||||||
RetrofitManager.getInstance().init(config)
|
RetrofitManager.getInstance().init(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,4 +83,3 @@ object HttpManager {
|
|||||||
return RetrofitManager.getInstance().getService(baseUrl, serviceClass, config)
|
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 val handler = Handler(Looper.getMainLooper())
|
||||||
private var appContext: Context? = null
|
private var appContext: Context? = null
|
||||||
|
|
||||||
fun init(context: Context) {
|
internal fun init(context: Context) {
|
||||||
appContext = context.applicationContext
|
appContext = context.applicationContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
package com.xuqm.sdk.update
|
package com.xuqm.sdk.update
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.core.content.FileProvider
|
import com.xuqm.sdk.utils.FileHelper
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -159,20 +156,7 @@ class DownloadManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun installApk(file: File): Boolean {
|
fun installApk(file: File): Boolean {
|
||||||
return runCatching {
|
return FileHelper.installApk(file)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolvePath(storagePath: StoragePath, customPath: String?): String {
|
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 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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import com.xuqm.sdk.cache.CacheKeys
|
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.DownloadDecision
|
||||||
import com.xuqm.sdk.update.DownloadManager
|
import com.xuqm.sdk.update.DownloadManager
|
||||||
import com.xuqm.sdk.update.DownloadRequest
|
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.VersionCheckStrategy
|
||||||
import com.xuqm.sdk.update.VersionComparator
|
import com.xuqm.sdk.update.VersionComparator
|
||||||
import com.xuqm.sdk.update.VersionInfo
|
import com.xuqm.sdk.update.VersionInfo
|
||||||
|
import com.xuqm.sdk.utils.FileHelper
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
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)
|
private val downloadManager = DownloadManager.getInstance(context)
|
||||||
|
|
||||||
fun cacheCurrentUser(
|
fun cacheCurrentUser(
|
||||||
@ -57,11 +55,15 @@ class PluginPackageManager private constructor(private val context: Context) {
|
|||||||
put("timestamp", System.currentTimeMillis())
|
put("timestamp", System.currentTimeMillis())
|
||||||
extraData.forEach { (key, value) -> put(key, value) }
|
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? {
|
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 {
|
fun isPluginInstalled(packageName: String): Boolean {
|
||||||
@ -92,11 +94,6 @@ class PluginPackageManager private constructor(private val context: Context) {
|
|||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun compareVersion(packageName: String, remoteVersionCode: Long): Int {
|
|
||||||
val local = getLocalPluginInfo(packageName) ?: return 1
|
|
||||||
return remoteVersionCode.compareTo(local.versionCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun shouldDownloadPlugin(
|
fun shouldDownloadPlugin(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
remoteVersionCode: Long? = null,
|
remoteVersionCode: Long? = null,
|
||||||
@ -207,22 +204,7 @@ class PluginPackageManager private constructor(private val context: Context) {
|
|||||||
return runCatching { context.startActivity(launchIntent) }.isSuccess
|
return runCatching { context.startActivity(launchIntent) }.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installPlugin(apkFile: File): Boolean {
|
fun installPlugin(apkFile: File): Boolean = FileHelper.installApk(apkFile)
|
||||||
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 loadPlugin(apkFile: File): Boolean = installPlugin(apkFile)
|
fun loadPlugin(apkFile: File): Boolean = installPlugin(apkFile)
|
||||||
|
|
||||||
@ -3,6 +3,7 @@ package com.xuqm.sdk.update
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import com.xuqm.sdk.utils.FileHelper
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
@ -119,7 +120,7 @@ class AppUpdater private constructor(private val context: Context) {
|
|||||||
|
|
||||||
fun clear(taskId: String): Boolean = downloadManager.clear(taskId)
|
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 {
|
fun installFromTask(taskId: String): Boolean {
|
||||||
val file = downloadManager.getDownloadedFile(taskId) ?: return false
|
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"
|
coreKtx = "1.18.0"
|
||||||
lifecycle = "2.10.0"
|
lifecycle = "2.10.0"
|
||||||
activityCompose = "1.13.0"
|
activityCompose = "1.13.0"
|
||||||
|
activityKtx = "1.13.0"
|
||||||
composeBom = "2026.03.00"
|
composeBom = "2026.03.00"
|
||||||
coroutines = "1.10.2"
|
coroutines = "1.10.2"
|
||||||
datastore = "1.1.7"
|
datastore = "1.1.7"
|
||||||
@ -14,6 +15,8 @@ retrofit = "3.0.0"
|
|||||||
okhttp = "5.3.2"
|
okhttp = "5.3.2"
|
||||||
gson = "2.13.2"
|
gson = "2.13.2"
|
||||||
jserialization = "1.9.0"
|
jserialization = "1.9.0"
|
||||||
|
webkit = "1.14.0"
|
||||||
|
coil = "2.7.0"
|
||||||
junit4 = "4.13.2"
|
junit4 = "4.13.2"
|
||||||
androidxJunit = "1.3.0"
|
androidxJunit = "1.3.0"
|
||||||
espresso = "3.7.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-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-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-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-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
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-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
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" }
|
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 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", 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" }
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
|
|||||||
@ -31,7 +31,6 @@ class SzyxLoginActivity : ComponentActivity() {
|
|||||||
if (!SzyxSDK.isInitialized()) {
|
if (!SzyxSDK.isInitialized()) {
|
||||||
SzyxSDK.init(this)
|
SzyxSDK.init(this)
|
||||||
}
|
}
|
||||||
ToastCenter.init(this)
|
|
||||||
setContent { MaterialTheme { LoginScreen { finish() } } }
|
setContent { MaterialTheme { LoginScreen { finish() } } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ android {
|
|||||||
manifestPlaceholders["authProviderAuthority"] = "com.xuqm.sample.auth"
|
manifestPlaceholders["authProviderAuthority"] = "com.xuqm.sample.auth"
|
||||||
manifestPlaceholders["sharedCacheAuthority"] = "com.xuqm.sample.sdk.cache.provider"
|
manifestPlaceholders["sharedCacheAuthority"] = "com.xuqm.sample.sdk.cache.provider"
|
||||||
manifestPlaceholders["coreFileProviderAuthority"] = "com.xuqm.sample.sdk.fileprovider"
|
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 {
|
buildTypes {
|
||||||
@ -48,6 +48,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":commonsdk-core"))
|
implementation(project(":commonsdk-core"))
|
||||||
implementation(project(":commonsdk-compose"))
|
implementation(project(":commonsdk-compose"))
|
||||||
|
implementation(project(":commonsdk-update"))
|
||||||
implementation(project(":lib-szyx"))
|
implementation(project(":lib-szyx"))
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.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.UpdateInfo
|
||||||
import com.xuqm.sdk.update.VersionCheckResult
|
import com.xuqm.sdk.update.VersionCheckResult
|
||||||
import com.xuqm.sdk.update.VersionCheckStrategy
|
import com.xuqm.sdk.update.VersionCheckStrategy
|
||||||
|
import com.xuqm.sdk.updatekit.UpdateSDK
|
||||||
import com.xuqm.sdk.utils.DateTimeUtils
|
import com.xuqm.sdk.utils.DateTimeUtils
|
||||||
import com.xuqm.sample.update.UpdateRepository
|
import com.xuqm.sample.update.UpdateRepository
|
||||||
import com.xuqm.szyx.SzyxSDK
|
import com.xuqm.szyx.SzyxSDK
|
||||||
@ -89,7 +90,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (intent.action == Intent.ACTION_PACKAGE_ADDED && reloadPluginAfterInstall) {
|
if (intent.action == Intent.ACTION_PACKAGE_ADDED && reloadPluginAfterInstall) {
|
||||||
reloadPluginAfterInstall = false
|
reloadPluginAfterInstall = false
|
||||||
val pluginUpdateInfo = currentPluginUpdateInfo
|
val pluginUpdateInfo = currentPluginUpdateInfo
|
||||||
CoreSDK.pluginPackageManager().reloadPlugin(
|
UpdateSDK.pluginPackageManager().reloadPlugin(
|
||||||
packageName = PLUGIN_PACKAGE_NAME,
|
packageName = PLUGIN_PACKAGE_NAME,
|
||||||
entryActivity = pluginUpdateInfo?.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
|
entryActivity = pluginUpdateInfo?.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
|
||||||
extras = pluginUpdateInfo?.extras ?: mapOf("hostPackageName" to this@MainActivity.packageName),
|
extras = pluginUpdateInfo?.extras ?: mapOf("hostPackageName" to this@MainActivity.packageName),
|
||||||
@ -117,7 +118,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
debugMode = true,
|
debugMode = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ToastCenter.init(this)
|
|
||||||
refreshState()
|
refreshState()
|
||||||
ensureLoginOnLaunch()
|
ensureLoginOnLaunch()
|
||||||
registerPackageChangeReceiver()
|
registerPackageChangeReceiver()
|
||||||
@ -170,7 +170,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private fun refreshState() {
|
private fun refreshState() {
|
||||||
refreshSession()
|
refreshSession()
|
||||||
pluginInstalledState.value = CoreSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
|
pluginInstalledState.value = UpdateSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openLogin() {
|
private fun openLogin() {
|
||||||
@ -199,7 +199,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val pluginManager = CoreSDK.pluginPackageManager()
|
val pluginManager = UpdateSDK.pluginPackageManager()
|
||||||
pluginManager.cacheCurrentUser(
|
pluginManager.cacheCurrentUser(
|
||||||
userId = session.loginModel.userId,
|
userId = session.loginModel.userId,
|
||||||
sessionId = session.loginModel.sessionId,
|
sessionId = session.loginModel.sessionId,
|
||||||
@ -224,7 +224,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
entryActivity = remoteUpdate.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
|
entryActivity = remoteUpdate.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
|
||||||
extras = mapOf("hostPackageName" to packageName),
|
extras = mapOf("hostPackageName" to packageName),
|
||||||
)
|
)
|
||||||
val checkResult = CoreSDK.pluginPackageManager().checkPluginUpdate(
|
val checkResult = UpdateSDK.pluginPackageManager().checkPluginUpdate(
|
||||||
packageName = pluginUpdateInfo.packageName,
|
packageName = pluginUpdateInfo.packageName,
|
||||||
remoteVersionCode = pluginUpdateInfo.versionCode,
|
remoteVersionCode = pluginUpdateInfo.versionCode,
|
||||||
remoteVersionName = pluginUpdateInfo.versionName,
|
remoteVersionName = pluginUpdateInfo.versionName,
|
||||||
@ -235,7 +235,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
return@onSuccess
|
return@onSuccess
|
||||||
}
|
}
|
||||||
currentPluginUpdateInfo = pluginUpdateInfo
|
currentPluginUpdateInfo = pluginUpdateInfo
|
||||||
val taskId = CoreSDK.pluginPackageManager().downloadPlugin(
|
val taskId = UpdateSDK.pluginPackageManager().downloadPlugin(
|
||||||
updateInfo = pluginUpdateInfo,
|
updateInfo = pluginUpdateInfo,
|
||||||
fileName = "plugin-ui-release.apk",
|
fileName = "plugin-ui-release.apk",
|
||||||
storagePath = StoragePath.EXTERNAL_FILES,
|
storagePath = StoragePath.EXTERNAL_FILES,
|
||||||
@ -259,7 +259,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
updateRepository.fetchLatestAppUpdate(packageName)
|
updateRepository.fetchLatestAppUpdate(packageName)
|
||||||
.onSuccess { updateInfo ->
|
.onSuccess { updateInfo ->
|
||||||
when (
|
when (
|
||||||
val checkResult = CoreSDK.appUpdater().checkUpdate(
|
val checkResult = UpdateSDK.appUpdater().checkUpdate(
|
||||||
updateInfo = updateInfo,
|
updateInfo = updateInfo,
|
||||||
strategy = VersionCheckStrategy.VERSION_CODE_OR_NAME,
|
strategy = VersionCheckStrategy.VERSION_CODE_OR_NAME,
|
||||||
)
|
)
|
||||||
@ -281,7 +281,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
private fun confirmDownloadAppUpdate() {
|
private fun confirmDownloadAppUpdate() {
|
||||||
val updateInfo = pendingAppUpdateState.value ?: return
|
val updateInfo = pendingAppUpdateState.value ?: return
|
||||||
pendingAppUpdateState.value = null
|
pendingAppUpdateState.value = null
|
||||||
val taskId = CoreSDK.appUpdater().downloadUpdate(
|
val taskId = UpdateSDK.appUpdater().downloadUpdate(
|
||||||
updateInfo = updateInfo,
|
updateInfo = updateInfo,
|
||||||
fileName = "sample-app-update.apk",
|
fileName = "sample-app-update.apk",
|
||||||
storagePath = StoragePath.EXTERNAL_FILES,
|
storagePath = StoragePath.EXTERNAL_FILES,
|
||||||
@ -308,7 +308,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
is DownloadState.Success -> {
|
is DownloadState.Success -> {
|
||||||
ToastCenter.show("插件下载完成,准备重新加载")
|
ToastCenter.show("插件下载完成,准备重新加载")
|
||||||
reloadPluginAfterInstall = true
|
reloadPluginAfterInstall = true
|
||||||
if (!CoreSDK.pluginPackageManager().loadPlugin(state.file)) {
|
if (!UpdateSDK.pluginPackageManager().loadPlugin(state.file)) {
|
||||||
reloadPluginAfterInstall = false
|
reloadPluginAfterInstall = false
|
||||||
ToastCenter.show("插件加载拉起失败")
|
ToastCenter.show("插件加载拉起失败")
|
||||||
}
|
}
|
||||||
@ -343,7 +343,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
when (state) {
|
when (state) {
|
||||||
is DownloadState.Success -> {
|
is DownloadState.Success -> {
|
||||||
ToastCenter.show("安装包下载完成,准备安装")
|
ToastCenter.show("安装包下载完成,准备安装")
|
||||||
if (!CoreSDK.appUpdater().installApk(state.file)) {
|
if (!UpdateSDK.appUpdater().installApk(state.file)) {
|
||||||
ToastCenter.show("应用安装拉起失败")
|
ToastCenter.show("应用安装拉起失败")
|
||||||
}
|
}
|
||||||
CoreSDK.downloadManager().clear(taskId)
|
CoreSDK.downloadManager().clear(taskId)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ rootProject.name = "AndroidLibs"
|
|||||||
|
|
||||||
include(":commonsdk-core")
|
include(":commonsdk-core")
|
||||||
include(":commonsdk-compose")
|
include(":commonsdk-compose")
|
||||||
|
include(":commonsdk-update")
|
||||||
include(":lib-szyx")
|
include(":lib-szyx")
|
||||||
include(":sample-app")
|
include(":sample-app")
|
||||||
include(":plugins:plugin-ui")
|
include(":plugins:plugin-ui")
|
||||||
|
|||||||
35
doc/01-project-overview.md
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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 密码已直接写入配置文件,仅适合当前内部开发阶段
|
||||||
|
- 后续应迁移到环境变量、配置中心或密钥管理服务
|
||||||
55
doc/06-version-management.md
普通文件
55
doc/06-version-management.md
普通文件
@ -0,0 +1,55 @@
|
|||||||
|
# 06 版本管理与灰度发布
|
||||||
|
|
||||||
|
## 业务范围
|
||||||
|
|
||||||
|
当前版本管理覆盖:
|
||||||
|
|
||||||
|
- App 版本管理
|
||||||
|
- 插件版本管理
|
||||||
|
- 插件化开关
|
||||||
|
- 版本包上传登记
|
||||||
|
- 全量发布
|
||||||
|
- 灰度发布
|
||||||
|
|
||||||
|
## 灰度发布规则
|
||||||
|
|
||||||
|
灰度发布基于用户平台钩子能力,当前支持三种圈选方式:
|
||||||
|
|
||||||
|
- 分组选择
|
||||||
|
- 快速选择
|
||||||
|
- 单选用户
|
||||||
|
|
||||||
|
系统返回的用户字段包含:
|
||||||
|
|
||||||
|
- 用户 ID
|
||||||
|
- 昵称
|
||||||
|
- 手机号
|
||||||
|
- 邮箱
|
||||||
|
- 地区
|
||||||
|
- 分组信息
|
||||||
|
|
||||||
|
其中 ID、昵称、手机号、邮箱在前台展示时进行脱敏。
|
||||||
|
|
||||||
|
## Android 拉取版本逻辑
|
||||||
|
|
||||||
|
### 全量版本
|
||||||
|
|
||||||
|
所有用户都可拉取到最新已发布版本。
|
||||||
|
|
||||||
|
### 灰度版本
|
||||||
|
|
||||||
|
只有命中灰度规则的用户才能拉取到灰度版本。
|
||||||
|
|
||||||
|
支持命中条件:
|
||||||
|
|
||||||
|
- 命中指定用户 ID
|
||||||
|
- 命中指定用户分组
|
||||||
|
- 命中指定快速选择集合
|
||||||
|
|
||||||
|
## 后续升级项
|
||||||
|
|
||||||
|
- 支持灰度比例发布
|
||||||
|
- 支持按渠道、设备、地区、版本范围灰度
|
||||||
|
- 支持灰度回滚
|
||||||
|
- 支持发布时间窗口
|
||||||
|
- 支持上传真实 APK/AAB/插件包并持久化元数据
|
||||||
42
doc/07-development-workflow.md
普通文件
42
doc/07-development-workflow.md
普通文件
@ -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
普通文件
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
普通文件
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
|
# frontend
|
||||||
|
|
||||||
当前目录新增两个 Vue 3 前端项目:
|
当前目录新增两个 Vue 3 前端项目,并统一使用 Yarn workspace 管理:
|
||||||
|
|
||||||
- `ops-platform`:运营平台,提供开放注册、版本管理、插件化开关、全量/灰度发布。
|
- `ops-platform`:运营平台,提供开放注册、版本管理、插件化开关、全量/灰度发布。
|
||||||
- `admin-platform`:管理平台,负责审核运营账户、禁用账户、管理子账户权限。
|
- `admin-platform`:管理平台,负责审核运营账户、禁用账户、管理子账户权限。
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
## 启动方式
|
## 启动方式
|
||||||
|
|
||||||
先启动后端:
|
先启动后端:
|
||||||
@ -17,15 +24,20 @@ mvn -pl version-management-service spring-boot:run
|
|||||||
再分别启动前端:
|
再分别启动前端:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend/ops-platform
|
cd frontend
|
||||||
npm install
|
yarn dev:ops
|
||||||
npm run dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend/admin-platform
|
cd frontend
|
||||||
npm install
|
yarn dev:admin
|
||||||
npm run dev
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
yarn build
|
||||||
```
|
```
|
||||||
|
|
||||||
前端默认请求 `http://127.0.0.1:8080`,如需调整可通过 `VITE_API_BASE_URL` 覆盖。
|
前端默认请求 `http://127.0.0.1:8080`,如需调整可通过 `VITE_API_BASE_URL` 覆盖。
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
"name": "admin-platform",
|
"name": "admin-platform",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "yarn@1.22.22",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
"name": "ops-platform",
|
"name": "ops-platform",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "yarn@1.22.22",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -1,22 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shell">
|
<RouterView />
|
||||||
<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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink, RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -18,15 +18,36 @@ export interface Account {
|
|||||||
createdAt: string
|
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 {
|
export interface ReleaseRecord {
|
||||||
id: string
|
id: string
|
||||||
|
appId: string
|
||||||
|
pluginId?: string | null
|
||||||
packageType: 'APP' | 'PLUGIN'
|
packageType: 'APP' | 'PLUGIN'
|
||||||
|
packageName: string
|
||||||
versionCode: number
|
versionCode: number
|
||||||
versionName: string
|
versionName: string
|
||||||
title: string
|
title: string
|
||||||
changelog: string
|
changelog: string
|
||||||
downloadUrl: string
|
downloadUrl: string
|
||||||
uploadedFileName: string
|
uploadedFileName: string
|
||||||
|
entryActivity?: string | null
|
||||||
|
minHostVersionCode?: number | null
|
||||||
|
minHostVersionName?: string | null
|
||||||
status: 'DRAFT' | 'PUBLISHED' | 'GRAYSCALE'
|
status: 'DRAFT' | 'PUBLISHED' | 'GRAYSCALE'
|
||||||
publishStrategy: string
|
publishStrategy: string
|
||||||
grayRule?: {
|
grayRule?: {
|
||||||
@ -42,10 +63,12 @@ export interface ApplicationDetail {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
packageName: string
|
packageName: string
|
||||||
pluginPackageName: string
|
pluginPackageName?: string | null
|
||||||
|
description?: string | null
|
||||||
pluginManagementEnabled: boolean
|
pluginManagementEnabled: boolean
|
||||||
businessModules: string[]
|
businessModules: string[]
|
||||||
}
|
}
|
||||||
|
plugins: PluginConfig[]
|
||||||
releases: ReleaseRecord[]
|
releases: ReleaseRecord[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,29 +115,80 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const api = {
|
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', {
|
return request<Account>('/api/v1/open/accounts/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
listApplications() {
|
login(payload: { email: string; password: string }) {
|
||||||
return request<ApplicationDetail[]>('/api/v1/ops/version/applications')
|
return request<LoginResult>('/api/v1/open/accounts/login', {
|
||||||
},
|
|
||||||
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`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
publishRelease(appId: string, releaseId: string, payload: Record<string, unknown>) {
|
listApplications() {
|
||||||
return request<ReleaseRecord>(`/api/v1/ops/version/applications/${appId}/releases/${releaseId}/publish`, {
|
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',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,12 +1,23 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import RegisterView from '../views/RegisterView.vue'
|
import LoginView from '../views/LoginView.vue'
|
||||||
import VersionManagementView from '../views/VersionManagementView.vue'
|
import OpsShellView from '../views/OpsShellView.vue'
|
||||||
|
import ModulePlaceholderView from '../views/ModulePlaceholderView.vue'
|
||||||
|
import AppManagementView from '../views/AppManagementView.vue'
|
||||||
|
|
||||||
export default createRouter({
|
export default createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', redirect: '/register' },
|
{ path: '/', redirect: '/login' },
|
||||||
{ path: '/register', component: RegisterView },
|
{ path: '/login', component: LoginView },
|
||||||
{ path: '/versions', component: VersionManagementView },
|
{
|
||||||
|
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;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.stack {
|
.stack {
|
||||||
grid-template-columns: 1.25fr 1fr;
|
grid-template-columns: 1.25fr 1fr;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
@ -167,6 +174,38 @@ button {
|
|||||||
flex-wrap: wrap;
|
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,
|
.chips span,
|
||||||
.chip-button {
|
.chip-button {
|
||||||
padding: 6px 10px;
|
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">登录后进入 IM、Push、App 管理三大模块。</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">登录后统一管理 IM、Push 和 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
普通文件
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
普通文件
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>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<spring-boot.version>3.4.4</spring-boot.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>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@ -30,4 +35,27 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</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>
|
</project>
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
<description>Version management microservice for operator/admin platforms</description>
|
<description>Version management microservice for operator/admin platforms</description>
|
||||||
|
|
||||||
<properties>
|
<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>
|
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@ -62,8 +64,11 @@
|
|||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<configuration>
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
<release>${java.version}</release>
|
<release>${java.version}</release>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</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.ApplicationEntity;
|
||||||
import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
|
import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
|
||||||
import com.xuqm.versionmanagement.persistence.entity.HookUserEntity;
|
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.QuickSelectionEntity;
|
||||||
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
|
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
|
||||||
import com.xuqm.versionmanagement.persistence.repository.AccountRepository;
|
import com.xuqm.versionmanagement.persistence.repository.AccountRepository;
|
||||||
import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
|
import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
|
||||||
import com.xuqm.versionmanagement.persistence.repository.HookGroupRepository;
|
import com.xuqm.versionmanagement.persistence.repository.HookGroupRepository;
|
||||||
import com.xuqm.versionmanagement.persistence.repository.HookUserRepository;
|
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.QuickSelectionRepository;
|
||||||
import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
|
import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -26,6 +28,7 @@ public class DataInitializer {
|
|||||||
CommandLineRunner seedVersionManagementData(
|
CommandLineRunner seedVersionManagementData(
|
||||||
AccountRepository accountRepository,
|
AccountRepository accountRepository,
|
||||||
ApplicationRepository applicationRepository,
|
ApplicationRepository applicationRepository,
|
||||||
|
PluginRepository pluginRepository,
|
||||||
ReleaseRepository releaseRepository,
|
ReleaseRepository releaseRepository,
|
||||||
HookUserRepository hookUserRepository,
|
HookUserRepository hookUserRepository,
|
||||||
HookGroupRepository hookGroupRepository,
|
HookGroupRepository hookGroupRepository,
|
||||||
@ -41,6 +44,7 @@ public class DataInitializer {
|
|||||||
main.setAccountName("星云运营中心");
|
main.setAccountName("星云运营中心");
|
||||||
main.setContactName("林青");
|
main.setContactName("林青");
|
||||||
main.setEmail("ops@nebula.example");
|
main.setEmail("ops@nebula.example");
|
||||||
|
main.setPassword("123456");
|
||||||
main.setPhone("13800138000");
|
main.setPhone("13800138000");
|
||||||
main.setType(PlatformData.AccountType.MAIN);
|
main.setType(PlatformData.AccountType.MAIN);
|
||||||
main.setStatus(PlatformData.AccountStatus.ACTIVE);
|
main.setStatus(PlatformData.AccountStatus.ACTIVE);
|
||||||
@ -52,6 +56,7 @@ public class DataInitializer {
|
|||||||
sub.setAccountName("星云发布子账号");
|
sub.setAccountName("星云发布子账号");
|
||||||
sub.setContactName("苏宁");
|
sub.setContactName("苏宁");
|
||||||
sub.setEmail("release@nebula.example");
|
sub.setEmail("release@nebula.example");
|
||||||
|
sub.setPassword("123456");
|
||||||
sub.setPhone("13900139000");
|
sub.setPhone("13900139000");
|
||||||
sub.setType(PlatformData.AccountType.SUB);
|
sub.setType(PlatformData.AccountType.SUB);
|
||||||
sub.setStatus(PlatformData.AccountStatus.ACTIVE);
|
sub.setStatus(PlatformData.AccountStatus.ACTIVE);
|
||||||
@ -65,25 +70,42 @@ public class DataInitializer {
|
|||||||
app.setName("宿主 App");
|
app.setName("宿主 App");
|
||||||
app.setPackageName("com.xuqm.sample");
|
app.setPackageName("com.xuqm.sample");
|
||||||
app.setPluginPackageName("com.xuqm.plugin.ui");
|
app.setPluginPackageName("com.xuqm.plugin.ui");
|
||||||
|
app.setDescription("Android 宿主应用,支持登录、检查更新、插件加载");
|
||||||
app.setPluginManagementEnabled(true);
|
app.setPluginManagementEnabled(true);
|
||||||
app.setBusinessModules(List.of("IM", "PUSH", "VERSION"));
|
app.setBusinessModules(List.of("IM", "PUSH", "VERSION"));
|
||||||
app.setCreatedAt(LocalDateTime.of(2026, 3, 27, 9, 0));
|
app.setCreatedAt(LocalDateTime.of(2026, 3, 27, 9, 0));
|
||||||
applicationRepository.save(app);
|
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(
|
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. 优化宿主更新流程",
|
"发现新版本", "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(),
|
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)),
|
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. 优化宿主拉起体验",
|
"插件 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(),
|
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)),
|
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. 支持用户分组圈选",
|
"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"),
|
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))
|
LocalDateTime.of(2026, 3, 27, 10, 0), LocalDateTime.of(2026, 3, 27, 10, 15))
|
||||||
));
|
));
|
||||||
@ -112,6 +134,7 @@ public class DataInitializer {
|
|||||||
private ReleaseEntity release(
|
private ReleaseEntity release(
|
||||||
String id,
|
String id,
|
||||||
String appId,
|
String appId,
|
||||||
|
String pluginId,
|
||||||
PlatformData.PackageType packageType,
|
PlatformData.PackageType packageType,
|
||||||
String packageName,
|
String packageName,
|
||||||
int versionCode,
|
int versionCode,
|
||||||
@ -120,6 +143,8 @@ public class DataInitializer {
|
|||||||
String changelog,
|
String changelog,
|
||||||
String downloadUrl,
|
String downloadUrl,
|
||||||
String entryActivity,
|
String entryActivity,
|
||||||
|
Integer minHostVersionCode,
|
||||||
|
String minHostVersionName,
|
||||||
boolean forceUpdate,
|
boolean forceUpdate,
|
||||||
String uploadedFileName,
|
String uploadedFileName,
|
||||||
PlatformData.ReleaseStatus status,
|
PlatformData.ReleaseStatus status,
|
||||||
@ -134,6 +159,7 @@ public class DataInitializer {
|
|||||||
ReleaseEntity entity = new ReleaseEntity();
|
ReleaseEntity entity = new ReleaseEntity();
|
||||||
entity.setId(id);
|
entity.setId(id);
|
||||||
entity.setAppId(appId);
|
entity.setAppId(appId);
|
||||||
|
entity.setPluginId(pluginId);
|
||||||
entity.setPackageType(packageType);
|
entity.setPackageType(packageType);
|
||||||
entity.setPackageName(packageName);
|
entity.setPackageName(packageName);
|
||||||
entity.setVersionCode(versionCode);
|
entity.setVersionCode(versionCode);
|
||||||
@ -142,6 +168,8 @@ public class DataInitializer {
|
|||||||
entity.setChangelog(changelog);
|
entity.setChangelog(changelog);
|
||||||
entity.setDownloadUrl(downloadUrl);
|
entity.setDownloadUrl(downloadUrl);
|
||||||
entity.setEntryActivity(entryActivity);
|
entity.setEntryActivity(entryActivity);
|
||||||
|
entity.setMinHostVersionCode(minHostVersionCode);
|
||||||
|
entity.setMinHostVersionName(minHostVersionName);
|
||||||
entity.setForceUpdate(forceUpdate);
|
entity.setForceUpdate(forceUpdate);
|
||||||
entity.setUploadedFileName(uploadedFileName);
|
entity.setUploadedFileName(uploadedFileName);
|
||||||
entity.setStatus(status);
|
entity.setStatus(status);
|
||||||
|
|||||||
@ -43,15 +43,27 @@ public class CompatibilityUpdateController {
|
|||||||
@GetMapping("/api/v1/updates/plugin/latest")
|
@GetMapping("/api/v1/updates/plugin/latest")
|
||||||
public ApiResponse<Map<String, Object>> latestPlugin(
|
public ApiResponse<Map<String, Object>> latestPlugin(
|
||||||
@RequestParam String packageName,
|
@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<>();
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
payload.put("packageName", release.getPackageName());
|
payload.put("packageName", release.getPackageName());
|
||||||
payload.put("versionCode", release.getVersionCode());
|
payload.put("versionCode", release.getVersionCode());
|
||||||
payload.put("versionName", release.getVersionName());
|
payload.put("versionName", release.getVersionName());
|
||||||
payload.put("downloadUrl", release.getDownloadUrl());
|
payload.put("downloadUrl", release.getDownloadUrl());
|
||||||
payload.put("entryActivity", release.getEntryActivity());
|
payload.put("entryActivity", release.getEntryActivity());
|
||||||
|
payload.put("minHostVersionCode", release.getMinHostVersionCode());
|
||||||
|
payload.put("minHostVersionName", release.getMinHostVersionName());
|
||||||
return ApiResponse.success(payload);
|
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.UserHookService;
|
||||||
import com.xuqm.versionmanagement.service.VersionManagementService;
|
import com.xuqm.versionmanagement.service.VersionManagementService;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@ -19,7 +18,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
@Validated
|
@Validated
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/ops/version")
|
@RequestMapping("/api/v1/ops")
|
||||||
public class OpsVersionController {
|
public class OpsVersionController {
|
||||||
|
|
||||||
private final VersionManagementService versionManagementService;
|
private final VersionManagementService versionManagementService;
|
||||||
@ -30,12 +29,46 @@ public class OpsVersionController {
|
|||||||
this.userHookService = userHookService;
|
this.userHookService = userHookService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/applications")
|
@GetMapping("/apps")
|
||||||
public ApiResponse<List<VersionManagementService.ApplicationDetail>> listApplications() {
|
public ApiResponse<List<VersionManagementService.ApplicationDetail>> listApplications() {
|
||||||
return ApiResponse.success(versionManagementService.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(
|
public ApiResponse<PlatformData.ApplicationConfig> togglePluginManagement(
|
||||||
@PathVariable String appId,
|
@PathVariable String appId,
|
||||||
@RequestBody TogglePluginManagementRequest request
|
@RequestBody TogglePluginManagementRequest request
|
||||||
@ -44,7 +77,12 @@ public class OpsVersionController {
|
|||||||
return ApiResponse.success(config, "插件化能力已更新");
|
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(
|
public ApiResponse<PlatformData.ReleaseRecord> uploadRelease(
|
||||||
@PathVariable String appId,
|
@PathVariable String appId,
|
||||||
@RequestBody @Validated UploadReleaseRequest request
|
@RequestBody @Validated UploadReleaseRequest request
|
||||||
@ -52,13 +90,16 @@ public class OpsVersionController {
|
|||||||
PlatformData.ReleaseRecord release = versionManagementService.uploadRelease(
|
PlatformData.ReleaseRecord release = versionManagementService.uploadRelease(
|
||||||
appId,
|
appId,
|
||||||
new VersionManagementService.UploadReleaseCommand(
|
new VersionManagementService.UploadReleaseCommand(
|
||||||
request.packageType(),
|
PlatformData.PackageType.APP,
|
||||||
|
request.packageName(),
|
||||||
request.versionCode(),
|
request.versionCode(),
|
||||||
request.versionName(),
|
request.versionName(),
|
||||||
request.title(),
|
request.title(),
|
||||||
request.changelog(),
|
request.changelog(),
|
||||||
request.downloadUrl(),
|
request.downloadUrl(),
|
||||||
request.entryActivity(),
|
request.entryActivity(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
request.forceUpdate(),
|
request.forceUpdate(),
|
||||||
request.uploadedFileName()
|
request.uploadedFileName()
|
||||||
)
|
)
|
||||||
@ -66,14 +107,79 @@ public class OpsVersionController {
|
|||||||
return ApiResponse.success(release, "版本包已上传");
|
return ApiResponse.success(release, "版本包已上传");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/applications/{appId}/releases/{releaseId}/publish")
|
@GetMapping("/apps/{appId}/plugins")
|
||||||
public ApiResponse<PlatformData.ReleaseRecord> publishRelease(
|
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,
|
@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,
|
@PathVariable String releaseId,
|
||||||
@RequestBody PublishReleaseRequest request
|
@RequestBody PublishReleaseRequest request
|
||||||
) {
|
) {
|
||||||
PlatformData.ReleaseRecord release = versionManagementService.publishRelease(
|
PlatformData.ReleaseRecord release = versionManagementService.publishRelease(
|
||||||
appId,
|
|
||||||
releaseId,
|
releaseId,
|
||||||
new VersionManagementService.PublishReleaseCommand(
|
new VersionManagementService.PublishReleaseCommand(
|
||||||
request.grayPublish(),
|
request.grayPublish(),
|
||||||
@ -105,11 +211,24 @@ public class OpsVersionController {
|
|||||||
return ApiResponse.success(userHookService.getAudienceBundle(null, null, null).quickSelections());
|
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 TogglePluginManagementRequest(boolean enabled) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record UploadReleaseRequest(
|
public record UploadReleaseRequest(
|
||||||
@NotNull(message = "不能为空") PlatformData.PackageType packageType,
|
@NotBlank(message = "不能为空") String packageName,
|
||||||
int versionCode,
|
int versionCode,
|
||||||
@NotBlank(message = "不能为空") String versionName,
|
@NotBlank(message = "不能为空") String versionName,
|
||||||
@NotBlank(message = "不能为空") String title,
|
@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(
|
public record PublishReleaseRequest(
|
||||||
boolean grayPublish,
|
boolean grayPublish,
|
||||||
String hookName,
|
String hookName,
|
||||||
|
|||||||
@ -31,10 +31,16 @@ public class PublicAccountController {
|
|||||||
request.accountName(),
|
request.accountName(),
|
||||||
request.contactName(),
|
request.contactName(),
|
||||||
request.email(),
|
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")
|
@PostMapping("/api/v1/ops/accounts/{accountId}/sub-accounts")
|
||||||
@ -59,7 +65,14 @@ public class PublicAccountController {
|
|||||||
@NotBlank(message = "不能为空") String accountName,
|
@NotBlank(message = "不能为空") String accountName,
|
||||||
@NotBlank(message = "不能为空") String contactName,
|
@NotBlank(message = "不能为空") String contactName,
|
||||||
@Email(message = "格式不正确") String email,
|
@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<Account> accounts = new ArrayList<>();
|
||||||
private List<ApplicationConfig> applications = new ArrayList<>();
|
private List<ApplicationConfig> applications = new ArrayList<>();
|
||||||
|
private List<PluginConfig> plugins = new ArrayList<>();
|
||||||
private List<ReleaseRecord> releases = new ArrayList<>();
|
private List<ReleaseRecord> releases = new ArrayList<>();
|
||||||
private List<HookUser> hookUsers = new ArrayList<>();
|
private List<HookUser> hookUsers = new ArrayList<>();
|
||||||
private List<HookGroup> hookGroups = new ArrayList<>();
|
private List<HookGroup> hookGroups = new ArrayList<>();
|
||||||
@ -29,6 +30,14 @@ public class PlatformData {
|
|||||||
this.applications = applications;
|
this.applications = applications;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<PluginConfig> getPlugins() {
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlugins(List<PluginConfig> plugins) {
|
||||||
|
this.plugins = plugins;
|
||||||
|
}
|
||||||
|
|
||||||
public List<ReleaseRecord> getReleases() {
|
public List<ReleaseRecord> getReleases() {
|
||||||
return releases;
|
return releases;
|
||||||
}
|
}
|
||||||
@ -181,6 +190,7 @@ public class PlatformData {
|
|||||||
private String name;
|
private String name;
|
||||||
private String packageName;
|
private String packageName;
|
||||||
private String pluginPackageName;
|
private String pluginPackageName;
|
||||||
|
private String description;
|
||||||
private boolean pluginManagementEnabled;
|
private boolean pluginManagementEnabled;
|
||||||
private List<String> businessModules = new ArrayList<>();
|
private List<String> businessModules = new ArrayList<>();
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
@ -217,6 +227,14 @@ public class PlatformData {
|
|||||||
this.pluginPackageName = pluginPackageName;
|
this.pluginPackageName = pluginPackageName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPluginManagementEnabled() {
|
public boolean isPluginManagementEnabled() {
|
||||||
return pluginManagementEnabled;
|
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 {
|
public static class ReleaseRecord {
|
||||||
private String id;
|
private String id;
|
||||||
private String appId;
|
private String appId;
|
||||||
|
private String pluginId;
|
||||||
private PackageType packageType;
|
private PackageType packageType;
|
||||||
private String packageName;
|
private String packageName;
|
||||||
private int versionCode;
|
private int versionCode;
|
||||||
@ -253,6 +347,8 @@ public class PlatformData {
|
|||||||
private String changelog;
|
private String changelog;
|
||||||
private String downloadUrl;
|
private String downloadUrl;
|
||||||
private String entryActivity;
|
private String entryActivity;
|
||||||
|
private Integer minHostVersionCode;
|
||||||
|
private String minHostVersionName;
|
||||||
private boolean forceUpdate;
|
private boolean forceUpdate;
|
||||||
private String uploadedFileName;
|
private String uploadedFileName;
|
||||||
private ReleaseStatus status;
|
private ReleaseStatus status;
|
||||||
@ -277,6 +373,14 @@ public class PlatformData {
|
|||||||
this.appId = appId;
|
this.appId = appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPluginId() {
|
||||||
|
return pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPluginId(String pluginId) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
public PackageType getPackageType() {
|
public PackageType getPackageType() {
|
||||||
return packageType;
|
return packageType;
|
||||||
}
|
}
|
||||||
@ -341,6 +445,22 @@ public class PlatformData {
|
|||||||
this.entryActivity = entryActivity;
|
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() {
|
public boolean isForceUpdate() {
|
||||||
return forceUpdate;
|
return forceUpdate;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,9 @@ public class AccountEntity {
|
|||||||
@Column(nullable = false, length = 128)
|
@Column(nullable = false, length = 128)
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 128)
|
||||||
|
private String password;
|
||||||
|
|
||||||
@Column(nullable = false, length = 32)
|
@Column(nullable = false, length = 32)
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
@ -88,6 +91,14 @@ public class AccountEntity {
|
|||||||
this.phone = phone;
|
this.phone = phone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
public PlatformData.AccountType getType() {
|
public PlatformData.AccountType getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,9 @@ public class ApplicationEntity {
|
|||||||
@Column(length = 128)
|
@Column(length = 128)
|
||||||
private String pluginPackageName;
|
private String pluginPackageName;
|
||||||
|
|
||||||
|
@Column(length = 512)
|
||||||
|
private String description;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private boolean pluginManagementEnabled;
|
private boolean pluginManagementEnabled;
|
||||||
|
|
||||||
@ -66,6 +69,14 @@ public class ApplicationEntity {
|
|||||||
this.pluginPackageName = pluginPackageName;
|
this.pluginPackageName = pluginPackageName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPluginManagementEnabled() {
|
public boolean isPluginManagementEnabled() {
|
||||||
return pluginManagementEnabled;
|
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)
|
@Column(nullable = false, length = 64)
|
||||||
private String appId;
|
private String appId;
|
||||||
|
|
||||||
|
@Column(length = 64)
|
||||||
|
private String pluginId;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false, length = 16)
|
@Column(nullable = false, length = 16)
|
||||||
private PlatformData.PackageType packageType;
|
private PlatformData.PackageType packageType;
|
||||||
@ -46,6 +49,11 @@ public class ReleaseEntity {
|
|||||||
@Column(length = 256)
|
@Column(length = 256)
|
||||||
private String entryActivity;
|
private String entryActivity;
|
||||||
|
|
||||||
|
private Integer minHostVersionCode;
|
||||||
|
|
||||||
|
@Column(length = 64)
|
||||||
|
private String minHostVersionName;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private boolean forceUpdate;
|
private boolean forceUpdate;
|
||||||
|
|
||||||
@ -95,6 +103,14 @@ public class ReleaseEntity {
|
|||||||
this.appId = appId;
|
this.appId = appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPluginId() {
|
||||||
|
return pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPluginId(String pluginId) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
public PlatformData.PackageType getPackageType() {
|
public PlatformData.PackageType getPackageType() {
|
||||||
return packageType;
|
return packageType;
|
||||||
}
|
}
|
||||||
@ -159,6 +175,22 @@ public class ReleaseEntity {
|
|||||||
this.entryActivity = entryActivity;
|
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() {
|
public boolean isForceUpdate() {
|
||||||
return forceUpdate;
|
return forceUpdate;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,4 +10,6 @@ public interface AccountRepository extends JpaRepository<AccountEntity, String>
|
|||||||
List<AccountEntity> findByTypeOrderByCreatedAtAsc(PlatformData.AccountType type);
|
List<AccountEntity> findByTypeOrderByCreatedAtAsc(PlatformData.AccountType type);
|
||||||
|
|
||||||
List<AccountEntity> findByParentAccountIdOrderByCreatedAtAsc(String parentAccountId);
|
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> findByAppIdOrderByUploadedAtDesc(String appId);
|
||||||
|
|
||||||
|
List<ReleaseEntity> findByPluginIdOrderByUploadedAtDesc(String pluginId);
|
||||||
|
|
||||||
List<ReleaseEntity> findByPackageNameAndPackageType(String packageName, PlatformData.PackageType packageType);
|
List<ReleaseEntity> findByPackageNameAndPackageType(String packageName, PlatformData.PackageType packageType);
|
||||||
|
|
||||||
Optional<ReleaseEntity> findByIdAndAppId(String id, String appId);
|
Optional<ReleaseEntity> findByIdAndAppId(String id, String appId);
|
||||||
|
|||||||
@ -27,14 +27,22 @@ public class AccountService {
|
|||||||
entity.setAccountName(command.accountName());
|
entity.setAccountName(command.accountName());
|
||||||
entity.setContactName(command.contactName());
|
entity.setContactName(command.contactName());
|
||||||
entity.setEmail(command.email());
|
entity.setEmail(command.email());
|
||||||
|
entity.setPassword(command.password());
|
||||||
entity.setPhone(command.phone());
|
entity.setPhone(command.phone());
|
||||||
entity.setType(PlatformData.AccountType.MAIN);
|
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.setPermissions(List.of("version:read", "version:write", "release:publish", "subaccount:grant"));
|
||||||
entity.setCreatedAt(LocalDateTime.now());
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
return platformMapper.toAccount(accountRepository.save(entity));
|
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
|
@Transactional
|
||||||
public PlatformData.Account createSubAccount(String parentAccountId, CreateSubAccountCommand command) {
|
public PlatformData.Account createSubAccount(String parentAccountId, CreateSubAccountCommand command) {
|
||||||
findAccount(parentAccountId);
|
findAccount(parentAccountId);
|
||||||
@ -90,7 +98,7 @@ public class AccountService {
|
|||||||
return prefix + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
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(
|
public record CreateSubAccountCommand(
|
||||||
@ -104,4 +112,7 @@ public class AccountService {
|
|||||||
|
|
||||||
public record AccountView(PlatformData.Account mainAccount, List<PlatformData.Account> subAccounts) {
|
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.ApplicationEntity;
|
||||||
import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
|
import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
|
||||||
import com.xuqm.versionmanagement.persistence.entity.HookUserEntity;
|
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.QuickSelectionEntity;
|
||||||
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
|
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -34,16 +35,31 @@ public class PlatformMapper {
|
|||||||
config.setName(entity.getName());
|
config.setName(entity.getName());
|
||||||
config.setPackageName(entity.getPackageName());
|
config.setPackageName(entity.getPackageName());
|
||||||
config.setPluginPackageName(entity.getPluginPackageName());
|
config.setPluginPackageName(entity.getPluginPackageName());
|
||||||
|
config.setDescription(entity.getDescription());
|
||||||
config.setPluginManagementEnabled(entity.isPluginManagementEnabled());
|
config.setPluginManagementEnabled(entity.isPluginManagementEnabled());
|
||||||
config.setBusinessModules(entity.getBusinessModules());
|
config.setBusinessModules(entity.getBusinessModules());
|
||||||
config.setCreatedAt(entity.getCreatedAt());
|
config.setCreatedAt(entity.getCreatedAt());
|
||||||
return config;
|
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) {
|
public PlatformData.ReleaseRecord toRelease(ReleaseEntity entity) {
|
||||||
PlatformData.ReleaseRecord release = new PlatformData.ReleaseRecord();
|
PlatformData.ReleaseRecord release = new PlatformData.ReleaseRecord();
|
||||||
release.setId(entity.getId());
|
release.setId(entity.getId());
|
||||||
release.setAppId(entity.getAppId());
|
release.setAppId(entity.getAppId());
|
||||||
|
release.setPluginId(entity.getPluginId());
|
||||||
release.setPackageType(entity.getPackageType());
|
release.setPackageType(entity.getPackageType());
|
||||||
release.setPackageName(entity.getPackageName());
|
release.setPackageName(entity.getPackageName());
|
||||||
release.setVersionCode(entity.getVersionCode());
|
release.setVersionCode(entity.getVersionCode());
|
||||||
@ -52,6 +68,8 @@ public class PlatformMapper {
|
|||||||
release.setChangelog(entity.getChangelog());
|
release.setChangelog(entity.getChangelog());
|
||||||
release.setDownloadUrl(entity.getDownloadUrl());
|
release.setDownloadUrl(entity.getDownloadUrl());
|
||||||
release.setEntryActivity(entity.getEntryActivity());
|
release.setEntryActivity(entity.getEntryActivity());
|
||||||
|
release.setMinHostVersionCode(entity.getMinHostVersionCode());
|
||||||
|
release.setMinHostVersionName(entity.getMinHostVersionName());
|
||||||
release.setForceUpdate(entity.isForceUpdate());
|
release.setForceUpdate(entity.isForceUpdate());
|
||||||
release.setUploadedFileName(entity.getUploadedFileName());
|
release.setUploadedFileName(entity.getUploadedFileName());
|
||||||
release.setStatus(entity.getStatus());
|
release.setStatus(entity.getStatus());
|
||||||
@ -98,18 +116,27 @@ public class PlatformMapper {
|
|||||||
return selection;
|
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();
|
ReleaseEntity entity = new ReleaseEntity();
|
||||||
entity.setId(id);
|
entity.setId(id);
|
||||||
entity.setAppId(appId);
|
entity.setAppId(appId);
|
||||||
|
entity.setPluginId(pluginId);
|
||||||
entity.setPackageType(command.packageType());
|
entity.setPackageType(command.packageType());
|
||||||
entity.setPackageName(command.packageType() == PlatformData.PackageType.PLUGIN ? app.getPluginPackageName() : app.getPackageName());
|
entity.setPackageName(command.packageName());
|
||||||
entity.setVersionCode(command.versionCode());
|
entity.setVersionCode(command.versionCode());
|
||||||
entity.setVersionName(command.versionName());
|
entity.setVersionName(command.versionName());
|
||||||
entity.setTitle(command.title());
|
entity.setTitle(command.title());
|
||||||
entity.setChangelog(command.changelog());
|
entity.setChangelog(command.changelog());
|
||||||
entity.setDownloadUrl(command.downloadUrl());
|
entity.setDownloadUrl(command.downloadUrl());
|
||||||
entity.setEntryActivity(command.entryActivity());
|
entity.setEntryActivity(command.entryActivity());
|
||||||
|
entity.setMinHostVersionCode(command.minHostVersionCode());
|
||||||
|
entity.setMinHostVersionName(command.minHostVersionName());
|
||||||
entity.setForceUpdate(command.forceUpdate());
|
entity.setForceUpdate(command.forceUpdate());
|
||||||
entity.setUploadedFileName(command.uploadedFileName());
|
entity.setUploadedFileName(command.uploadedFileName());
|
||||||
entity.setStatus(PlatformData.ReleaseStatus.DRAFT);
|
entity.setStatus(PlatformData.ReleaseStatus.DRAFT);
|
||||||
|
|||||||
@ -2,8 +2,10 @@ package com.xuqm.versionmanagement.service;
|
|||||||
|
|
||||||
import com.xuqm.versionmanagement.model.PlatformData;
|
import com.xuqm.versionmanagement.model.PlatformData;
|
||||||
import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
|
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.entity.ReleaseEntity;
|
||||||
import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
|
import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
|
||||||
|
import com.xuqm.versionmanagement.persistence.repository.PluginRepository;
|
||||||
import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
|
import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@ -16,17 +18,20 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
public class VersionManagementService {
|
public class VersionManagementService {
|
||||||
|
|
||||||
private final ApplicationRepository applicationRepository;
|
private final ApplicationRepository applicationRepository;
|
||||||
|
private final PluginRepository pluginRepository;
|
||||||
private final ReleaseRepository releaseRepository;
|
private final ReleaseRepository releaseRepository;
|
||||||
private final UserHookService userHookService;
|
private final UserHookService userHookService;
|
||||||
private final PlatformMapper platformMapper;
|
private final PlatformMapper platformMapper;
|
||||||
|
|
||||||
public VersionManagementService(
|
public VersionManagementService(
|
||||||
ApplicationRepository applicationRepository,
|
ApplicationRepository applicationRepository,
|
||||||
|
PluginRepository pluginRepository,
|
||||||
ReleaseRepository releaseRepository,
|
ReleaseRepository releaseRepository,
|
||||||
UserHookService userHookService,
|
UserHookService userHookService,
|
||||||
PlatformMapper platformMapper
|
PlatformMapper platformMapper
|
||||||
) {
|
) {
|
||||||
this.applicationRepository = applicationRepository;
|
this.applicationRepository = applicationRepository;
|
||||||
|
this.pluginRepository = pluginRepository;
|
||||||
this.releaseRepository = releaseRepository;
|
this.releaseRepository = releaseRepository;
|
||||||
this.userHookService = userHookService;
|
this.userHookService = userHookService;
|
||||||
this.platformMapper = platformMapper;
|
this.platformMapper = platformMapper;
|
||||||
@ -37,13 +42,43 @@ public class VersionManagementService {
|
|||||||
.map(platformMapper::toApplication)
|
.map(platformMapper::toApplication)
|
||||||
.map(app -> new ApplicationDetail(
|
.map(app -> new ApplicationDetail(
|
||||||
app,
|
app,
|
||||||
|
pluginRepository.findByAppIdOrderByCreatedAtAsc(app.getId()).stream()
|
||||||
|
.map(platformMapper::toPlugin)
|
||||||
|
.toList(),
|
||||||
releaseRepository.findByAppIdOrderByUploadedAtDesc(app.getId()).stream()
|
releaseRepository.findByAppIdOrderByUploadedAtDesc(app.getId()).stream()
|
||||||
|
.filter(release -> release.getPluginId() == null)
|
||||||
.map(platformMapper::toRelease)
|
.map(platformMapper::toRelease)
|
||||||
.toList()
|
.toList()
|
||||||
))
|
))
|
||||||
.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
|
@Transactional
|
||||||
public PlatformData.ApplicationConfig togglePluginManagement(String appId, boolean enabled) {
|
public PlatformData.ApplicationConfig togglePluginManagement(String appId, boolean enabled) {
|
||||||
ApplicationEntity entity = findApplicationEntity(appId);
|
ApplicationEntity entity = findApplicationEntity(appId);
|
||||||
@ -51,18 +86,93 @@ public class VersionManagementService {
|
|||||||
return platformMapper.toApplication(applicationRepository.save(entity));
|
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
|
@Transactional
|
||||||
public PlatformData.ReleaseRecord uploadRelease(String appId, UploadReleaseCommand command) {
|
public PlatformData.ReleaseRecord uploadRelease(String appId, UploadReleaseCommand command) {
|
||||||
PlatformData.ApplicationConfig app = platformMapper.toApplication(findApplicationEntity(appId));
|
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());
|
entity.setUploadedAt(LocalDateTime.now());
|
||||||
return platformMapper.toRelease(releaseRepository.save(entity));
|
return platformMapper.toRelease(releaseRepository.save(entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public PlatformData.ReleaseRecord publishRelease(String appId, String releaseId, PublishReleaseCommand command) {
|
public PlatformData.ReleaseRecord publishRelease(String releaseId, PublishReleaseCommand command) {
|
||||||
findApplicationEntity(appId);
|
ReleaseEntity release = findReleaseEntity(releaseId);
|
||||||
ReleaseEntity release = findReleaseEntity(appId, releaseId);
|
|
||||||
if (command.grayPublish()) {
|
if (command.grayPublish()) {
|
||||||
List<PlatformData.HookUser> matchedUsers = userHookService.findUsersByIds(command.userIds());
|
List<PlatformData.HookUser> matchedUsers = userHookService.findUsersByIds(command.userIds());
|
||||||
if (!command.userIds().isEmpty() && matchedUsers.size() != command.userIds().size()) {
|
if (!command.userIds().isEmpty() && matchedUsers.size() != command.userIds().size()) {
|
||||||
@ -90,8 +200,41 @@ public class VersionManagementService {
|
|||||||
return selectLatestRelease(packageName, PlatformData.PackageType.APP, userId);
|
return selectLatestRelease(packageName, PlatformData.PackageType.APP, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlatformData.ReleaseRecord getLatestPluginRelease(String packageName, String userId) {
|
public PlatformData.ReleaseRecord getLatestPluginRelease(String packageName, String userId, Integer hostVersionCode) {
|
||||||
return selectLatestRelease(packageName, PlatformData.PackageType.PLUGIN, userId);
|
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) {
|
private PlatformData.ReleaseRecord selectLatestRelease(String packageName, PlatformData.PackageType packageType, String userId) {
|
||||||
@ -106,13 +249,35 @@ public class VersionManagementService {
|
|||||||
.orElseThrow(() -> new IllegalArgumentException("版本配置不存在"));
|
.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) {
|
private ApplicationEntity findApplicationEntity(String appId) {
|
||||||
return applicationRepository.findById(appId)
|
return applicationRepository.findById(appId)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("应用不存在"));
|
.orElseThrow(() -> new IllegalArgumentException("应用不存在"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReleaseEntity findReleaseEntity(String appId, String releaseId) {
|
private PluginEntity findPluginEntity(String pluginId) {
|
||||||
return releaseRepository.findByIdAndAppId(releaseId, appId)
|
return pluginRepository.findById(pluginId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("插件不存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReleaseEntity findReleaseEntity(String releaseId) {
|
||||||
|
return releaseRepository.findById(releaseId)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("版本不存在"));
|
.orElseThrow(() -> new IllegalArgumentException("版本不存在"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,22 +285,88 @@ public class VersionManagementService {
|
|||||||
return prefix + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
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(
|
public record UploadReleaseCommand(
|
||||||
PlatformData.PackageType packageType,
|
PlatformData.PackageType packageType,
|
||||||
|
String packageName,
|
||||||
int versionCode,
|
int versionCode,
|
||||||
String versionName,
|
String versionName,
|
||||||
String title,
|
String title,
|
||||||
String changelog,
|
String changelog,
|
||||||
String downloadUrl,
|
String downloadUrl,
|
||||||
String entryActivity,
|
String entryActivity,
|
||||||
|
Integer minHostVersionCode,
|
||||||
|
String minHostVersionName,
|
||||||
boolean forceUpdate,
|
boolean forceUpdate,
|
||||||
String uploadedFileName
|
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(
|
public record PublishReleaseCommand(
|
||||||
boolean grayPublish,
|
boolean grayPublish,
|
||||||
String hookName,
|
String hookName,
|
||||||
|
|||||||
@ -5,11 +5,20 @@ spring:
|
|||||||
application:
|
application:
|
||||||
name: version-management-service
|
name: version-management-service
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://xuqinmin.com:3306/androidLibsServer?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
url: ${DB_URL:jdbc:mysql://39.107.53.187:3306/android-libs?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8}
|
||||||
username: androidLibsServer
|
username: ${DB_USERNAME:android-libs}
|
||||||
password: iXc8rHydtzRpYFHJ
|
password: ${DB_PASSWORD:fiiL8mxhxrDCHc86}
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
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:
|
jpa:
|
||||||
|
database-platform: org.hibernate.dialect.MySQLDialect
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: update
|
ddl-auto: update
|
||||||
open-in-view: false
|
open-in-view: false
|
||||||
@ -18,10 +27,17 @@ spring:
|
|||||||
format_sql: true
|
format_sql: true
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: redisdev.xuqinmin.com
|
host: ${REDIS_HOST:redisdev.xuqinmin.com}
|
||||||
port: 6379
|
port: ${REDIS_PORT:6379}
|
||||||
database: 0
|
database: ${REDIS_DATABASE:0}
|
||||||
password: xuqinmin1022
|
password: ${REDIS_PASSWORD:xuqinmin1022}
|
||||||
|
timeout: 10s
|
||||||
|
lettuce:
|
||||||
|
pool:
|
||||||
|
min-idle: 0
|
||||||
|
max-idle: 8
|
||||||
|
max-active: 8
|
||||||
|
max-wait: -1ms
|
||||||
cache:
|
cache:
|
||||||
type: redis
|
type: redis
|
||||||
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}`);
|
|
||||||
});
|
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户