feat(app): 支持多平台包名配置和应用信息编辑功能

- 后端增加至少填写一个平台包名的验证逻辑
- 前端调整应用数据模型,将包名字段改为可选类型
- 添加应用详情页的编辑功能和表单验证
- 优化应用列表页包名显示逻辑,支持多平台包名展示
- 重构应用配置指引页面,按平台分类展示商店配置指南
- 在版本管理页面增加包名配置检查和相应提示
- 新增应用信息编辑弹窗组件和相关业务逻辑
这个提交包含在:
XuqmGroup 2026-06-11 13:04:28 +08:00
父节点 3e2db6441e
当前提交 77553cd105
共有 2 个文件被更改,包括 135 次插入2 次删除

查看文件

@ -69,8 +69,8 @@ public class AppController {
@PostMapping
public ResponseEntity<ApiResponse<AppEntity>> create(@RequestBody CreateAppRequest req,
@AuthenticationPrincipal String tenantId) {
requireNonBlank(req.packageName(), "packageName");
requireNonBlank(req.name(), "name");
requireAtLeastOnePackageName(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,
@RequestBody CreateAppRequest req,
@AuthenticationPrincipal String tenantId) {
requireNonBlank(req.packageName(), "packageName");
requireNonBlank(req.name(), "name");
requireAtLeastOnePackageName(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}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable String appKey,
@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);
}
}
}