feat(app): 支持多平台包名配置和应用信息编辑功能
- 后端增加至少填写一个平台包名的验证逻辑 - 前端调整应用数据模型,将包名字段改为可选类型 - 添加应用详情页的编辑功能和表单验证 - 优化应用列表页包名显示逻辑,支持多平台包名展示 - 重构应用配置指引页面,按平台分类展示商店配置指南 - 在版本管理页面增加包名配置检查和相应提示 - 新增应用信息编辑弹窗组件和相关业务逻辑
这个提交包含在:
父节点
3e2db6441e
当前提交
77553cd105
@ -69,8 +69,8 @@ public class AppController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<ApiResponse<AppEntity>> create(@RequestBody CreateAppRequest req,
|
public ResponseEntity<ApiResponse<AppEntity>> create(@RequestBody CreateAppRequest req,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
requireNonBlank(req.packageName(), "packageName");
|
|
||||||
requireNonBlank(req.name(), "name");
|
requireNonBlank(req.name(), "name");
|
||||||
|
requireAtLeastOnePackageName(req);
|
||||||
return ResponseEntity.ok(ApiResponse.success(appService.create(tenantId, req)));
|
return ResponseEntity.ok(ApiResponse.success(appService.create(tenantId, req)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,8 +78,8 @@ public class AppController {
|
|||||||
public ResponseEntity<ApiResponse<AppEntity>> update(@PathVariable String appKey,
|
public ResponseEntity<ApiResponse<AppEntity>> update(@PathVariable String appKey,
|
||||||
@RequestBody CreateAppRequest req,
|
@RequestBody CreateAppRequest req,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
requireNonBlank(req.packageName(), "packageName");
|
|
||||||
requireNonBlank(req.name(), "name");
|
requireNonBlank(req.name(), "name");
|
||||||
|
requireAtLeastOnePackageName(req);
|
||||||
return ResponseEntity.ok(ApiResponse.success(appService.update(appKey, tenantId, req)));
|
return ResponseEntity.ok(ApiResponse.success(appService.update(appKey, tenantId, req)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +89,15 @@ public class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void requireAtLeastOnePackageName(CreateAppRequest req) {
|
||||||
|
boolean hasAny = (req.packageName() != null && !req.packageName().isBlank())
|
||||||
|
|| (req.iosBundleId() != null && !req.iosBundleId().isBlank())
|
||||||
|
|| (req.harmonyBundleName() != null && !req.harmonyBundleName().isBlank());
|
||||||
|
if (!hasAny) {
|
||||||
|
throw new BusinessException(400, "至少填写一个平台的包名(packageName / iosBundleId / harmonyBundleName)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{appKey}")
|
@DeleteMapping("/{appKey}")
|
||||||
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable String appKey,
|
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable String appKey,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
|
|||||||
@ -0,0 +1,124 @@
|
|||||||
|
package com.xuqm.update.config;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||||
|
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||||
|
import org.springframework.core.PriorityOrdered;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update-service 数据库迁移,在 Hibernate 初始化之前执行。
|
||||||
|
*
|
||||||
|
* 使用 BeanFactoryPostProcessor + PriorityOrdered 确保在所有 bean 创建之前运行,
|
||||||
|
* 避免旧的 GrayMode 枚举值(IM_PUSH_USERS/CUSTOMER_SYNC/CUSTOMER_CALLBACK)
|
||||||
|
* 导致 Hibernate 反序列化失败。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SchemaMigrationRunner implements BeanFactoryPostProcessor, PriorityOrdered {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SchemaMigrationRunner.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return PriorityOrdered.HIGHEST_PRECEDENCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
|
||||||
|
try {
|
||||||
|
DataSource dataSource = beanFactory.getBean(DataSource.class);
|
||||||
|
migrate_v20260610_gray_mode_simplify(dataSource);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Schema migration skipped (DataSource not ready): {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将旧灰度模式迁移到新的 PERCENT / MEMBERS 模式。
|
||||||
|
*
|
||||||
|
* 旧值:IM_PUSH_USERS / CUSTOMER_SYNC / CUSTOMER_CALLBACK → MEMBERS
|
||||||
|
* 清理:grayCallbackUrl 旧数据(已废弃,改为配置级 publishCallbackUrl)
|
||||||
|
*/
|
||||||
|
private void migrate_v20260610_gray_mode_simplify(DataSource dataSource) {
|
||||||
|
String migrationId = "v20260610_gray_mode_simplify";
|
||||||
|
try (Connection conn = dataSource.getConnection()) {
|
||||||
|
// 确保迁移记录表存在
|
||||||
|
try (Statement stmt = conn.createStatement()) {
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS _schema_migrations (
|
||||||
|
id VARCHAR(128) NOT NULL PRIMARY KEY,
|
||||||
|
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
description VARCHAR(255)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已执行
|
||||||
|
try (var ps = conn.prepareStatement(
|
||||||
|
"SELECT COUNT(*) FROM _schema_migrations WHERE id = ?")) {
|
||||||
|
ps.setString(1, migrationId);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next() && rs.getInt(1) > 0) {
|
||||||
|
return; // 已执行,跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Applying migration: {}", migrationId);
|
||||||
|
|
||||||
|
// 迁移 update_app_version 表
|
||||||
|
int appVersions = 0;
|
||||||
|
try (var ps = conn.prepareStatement(
|
||||||
|
"UPDATE update_app_version SET gray_mode = 'MEMBERS' " +
|
||||||
|
"WHERE gray_mode IN ('IM_PUSH_USERS', 'CUSTOMER_SYNC', 'CUSTOMER_CALLBACK')")) {
|
||||||
|
appVersions = ps.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移 update_rn_bundle 表
|
||||||
|
int rnBundles = 0;
|
||||||
|
try (var ps = conn.prepareStatement(
|
||||||
|
"UPDATE update_rn_bundle SET gray_mode = 'MEMBERS' " +
|
||||||
|
"WHERE gray_mode IN ('IM_PUSH_USERS', 'CUSTOMER_SYNC', 'CUSTOMER_CALLBACK')")) {
|
||||||
|
rnBundles = ps.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理旧的 grayCallbackUrl
|
||||||
|
try (var ps = conn.prepareStatement(
|
||||||
|
"UPDATE update_app_version SET gray_callback_url = NULL " +
|
||||||
|
"WHERE gray_callback_url IS NOT NULL AND gray_callback_url != ''")) {
|
||||||
|
int cleaned = ps.executeUpdate();
|
||||||
|
if (cleaned > 0) {
|
||||||
|
log.info("Cleared {} old grayCallbackUrl from update_app_version", cleaned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (var ps = conn.prepareStatement(
|
||||||
|
"UPDATE update_rn_bundle SET gray_callback_url = NULL " +
|
||||||
|
"WHERE gray_callback_url IS NOT NULL AND gray_callback_url != ''")) {
|
||||||
|
int cleaned = ps.executeUpdate();
|
||||||
|
if (cleaned > 0) {
|
||||||
|
log.info("Cleared {} old grayCallbackUrl from update_rn_bundle", cleaned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录迁移
|
||||||
|
try (var ps = conn.prepareStatement(
|
||||||
|
"INSERT IGNORE INTO _schema_migrations (id, description) VALUES (?, ?)")) {
|
||||||
|
ps.setString(1, migrationId);
|
||||||
|
ps.setString(2, "Simplify GrayMode to PERCENT/MEMBERS, clear grayCallbackUrl");
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Migration {} done: {} app versions, {} RN bundles converted to MEMBERS",
|
||||||
|
migrationId, appVersions, rnBundles);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Migration {} failed: {}", migrationId, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户