“mitao”
2026-03-27 5e8b3b757b3d882484677be0039950d46e0a54f3
fix:APP版本管理修改
8个文件已修改
243 ■■■■■ 已修改文件
tcm-training-admin-backend/sql/init.sql 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/update_app_version_settings_schema.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/upgrade_rbac.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/dto/AppVersionCreateRequest.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/system/app-version-settings/index.tsx 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/system/app-version-settings/style.less 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/init.sql
@@ -1049,10 +1049,10 @@
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `version_no` VARCHAR(32) NOT NULL COMMENT '展示版本号',
  `version_code` INT NOT NULL COMMENT '版本字段',
  `android_file_url` VARCHAR(512) NOT NULL COMMENT '安卓文件地址',
  `android_file_name` VARCHAR(255) NOT NULL COMMENT '安卓文件名称',
  `ios_file_url` VARCHAR(512) NOT NULL COMMENT 'iOS文件地址',
  `ios_file_name` VARCHAR(255) NOT NULL COMMENT 'iOS文件名称',
  `android_file_url` VARCHAR(512) DEFAULT NULL COMMENT '安卓文件地址',
  `android_file_name` VARCHAR(255) DEFAULT NULL COMMENT '安卓文件名称',
  `ios_file_url` VARCHAR(512) DEFAULT NULL COMMENT 'iOS文件地址',
  `ios_file_name` VARCHAR(255) DEFAULT NULL COMMENT 'iOS文件名称',
  `force_update` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否强制更新:1是 0否',
  `update_notice` TEXT NOT NULL COMMENT '更新说明',
  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
tcm-training-admin-backend/sql/update_app_version_settings_schema.sql
@@ -2,10 +2,10 @@
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `version_no` VARCHAR(32) NOT NULL COMMENT '展示版本号',
  `version_code` INT NOT NULL COMMENT '版本字段',
  `android_file_url` VARCHAR(512) NOT NULL COMMENT '安卓文件地址',
  `android_file_name` VARCHAR(255) NOT NULL COMMENT '安卓文件名称',
  `ios_file_url` VARCHAR(512) NOT NULL COMMENT 'iOS文件地址',
  `ios_file_name` VARCHAR(255) NOT NULL COMMENT 'iOS文件名称',
  `android_file_url` VARCHAR(512) DEFAULT NULL COMMENT '安卓文件地址',
  `android_file_name` VARCHAR(255) DEFAULT NULL COMMENT '安卓文件名称',
  `ios_file_url` VARCHAR(512) DEFAULT NULL COMMENT 'iOS文件地址',
  `ios_file_name` VARCHAR(255) DEFAULT NULL COMMENT 'iOS文件名称',
  `force_update` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否强制更新:1是 0否',
  `update_notice` TEXT NOT NULL COMMENT '更新说明',
  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
@@ -24,6 +24,12 @@
(1, '1.0.0', 1, 'https://example.com/app/android/1.0.0.apk', 'app-1.0.0.apk', 'https://example.com/app/ios/1.0.0.ipa', 'app-1.0.0.ipa', 0, '反馈内容反馈内容反馈内容反馈内容反馈内容', 0, '系统初始化', '系统初始化'),
(2, '1.0.1', 2, 'https://example.com/app/android/1.0.1.apk', 'app-1.0.1.apk', 'https://example.com/app/ios/1.0.1.ipa', 'app-1.0.1.ipa', 1, '反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容', 0, '系统初始化', '系统初始化');
ALTER TABLE `tcm_app_version_config`
  MODIFY COLUMN `android_file_url` VARCHAR(512) DEFAULT NULL COMMENT '安卓文件地址',
  MODIFY COLUMN `android_file_name` VARCHAR(255) DEFAULT NULL COMMENT '安卓文件名称',
  MODIFY COLUMN `ios_file_url` VARCHAR(512) DEFAULT NULL COMMENT 'iOS文件地址',
  MODIFY COLUMN `ios_file_name` VARCHAR(255) DEFAULT NULL COMMENT 'iOS文件名称';
INSERT IGNORE INTO `sys_menu`
(`id`, `parent_id`, `name`, `menu_code`, `type`, `path`, `component`, `icon`, `sort`, `status`, `visible`, `permission_key`, `remark`, `deleted`)
VALUES
tcm-training-admin-backend/sql/upgrade_rbac.sql
@@ -775,10 +775,10 @@
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `version_no` VARCHAR(32) NOT NULL COMMENT '展示版本号',
  `version_code` INT NOT NULL COMMENT '版本字段',
  `android_file_url` VARCHAR(512) NOT NULL COMMENT '安卓文件地址',
  `android_file_name` VARCHAR(255) NOT NULL COMMENT '安卓文件名称',
  `ios_file_url` VARCHAR(512) NOT NULL COMMENT 'iOS文件地址',
  `ios_file_name` VARCHAR(255) NOT NULL COMMENT 'iOS文件名称',
  `android_file_url` VARCHAR(512) DEFAULT NULL COMMENT '安卓文件地址',
  `android_file_name` VARCHAR(255) DEFAULT NULL COMMENT '安卓文件名称',
  `ios_file_url` VARCHAR(512) DEFAULT NULL COMMENT 'iOS文件地址',
  `ios_file_name` VARCHAR(255) DEFAULT NULL COMMENT 'iOS文件名称',
  `force_update` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否强制更新:1是 0否',
  `update_notice` TEXT NOT NULL COMMENT '更新说明',
  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
@@ -797,6 +797,12 @@
(1, '1.0.0', 1, 'https://example.com/app/android/1.0.0.apk', 'app-1.0.0.apk', 'https://example.com/app/ios/1.0.0.ipa', 'app-1.0.0.ipa', 0, '反馈内容反馈内容反馈内容反馈内容反馈内容', 0, '系统初始化', '系统初始化'),
(2, '1.0.1', 2, 'https://example.com/app/android/1.0.1.apk', 'app-1.0.1.apk', 'https://example.com/app/ios/1.0.1.ipa', 'app-1.0.1.ipa', 1, '反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容', 0, '系统初始化', '系统初始化');
ALTER TABLE `tcm_app_version_config`
  MODIFY COLUMN `android_file_url` VARCHAR(512) DEFAULT NULL COMMENT '安卓文件地址',
  MODIFY COLUMN `android_file_name` VARCHAR(255) DEFAULT NULL COMMENT '安卓文件名称',
  MODIFY COLUMN `ios_file_url` VARCHAR(512) DEFAULT NULL COMMENT 'iOS文件地址',
  MODIFY COLUMN `ios_file_name` VARCHAR(255) DEFAULT NULL COMMENT 'iOS文件名称';
CREATE TABLE IF NOT EXISTS `tcm_message_push_config` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `notice_type_code` VARCHAR(64) NOT NULL COMMENT '通知类型编码',
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/dto/AppVersionCreateRequest.java
@@ -17,19 +17,15 @@
    @Size(max = 32, message = "版本号长度不能超过32个字符")
    private String versionNo;
    @NotBlank(message = "请上传安卓文件")
    @Size(max = 512, message = "安卓文件地址长度不能超过512个字符")
    private String androidFileUrl;
    @NotBlank(message = "请填写安卓文件名称")
    @Size(max = 255, message = "安卓文件名称长度不能超过255个字符")
    private String androidFileName;
    @NotBlank(message = "请上传iOS文件")
    @Size(max = 512, message = "iOS文件地址长度不能超过512个字符")
    private String iosFileUrl;
    @NotBlank(message = "请填写iOS文件名称")
    @Size(max = 255, message = "iOS文件名称长度不能超过255个字符")
    private String iosFileName;
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java
@@ -45,13 +45,18 @@
    @Transactional
    public AppVersionDetailVO create(AppVersionCreateRequest request) {
        AppVersionConfig latest = latest();
        String androidFileUrl = normalizeOptionalText(request.getAndroidFileUrl());
        String androidFileName = normalizeOptionalText(request.getAndroidFileName());
        String iosFileUrl = normalizeOptionalText(request.getIosFileUrl());
        String iosFileName = normalizeOptionalText(request.getIosFileName());
        validatePackageFields(androidFileUrl, androidFileName, iosFileUrl, iosFileName);
        AppVersionConfig item = new AppVersionConfig();
        item.setVersionNo(request.getVersionNo().trim());
        item.setVersionCode(latest == null ? 1 : defaultNumber(latest.getVersionCode()) + 1);
        item.setAndroidFileUrl(request.getAndroidFileUrl().trim());
        item.setAndroidFileName(request.getAndroidFileName().trim());
        item.setIosFileUrl(request.getIosFileUrl().trim());
        item.setIosFileName(request.getIosFileName().trim());
        item.setAndroidFileUrl(androidFileUrl);
        item.setAndroidFileName(androidFileName);
        item.setIosFileUrl(iosFileUrl);
        item.setIosFileName(iosFileName);
        item.setForceUpdate(booleanToInteger(request.getForceUpdate()));
        item.setUpdateNotice(request.getUpdateNotice().trim());
        item.setDeleted(0);
@@ -74,15 +79,19 @@
            return vo;
        }
        boolean hasUpdate = defaultNumber(latest.getVersionCode()) > currentVersionCode;
        String downloadUrl = AppVersionConfig.PLATFORM_ANDROID.equals(normalizedPlatform)
                ? normalizeOptionalText(latest.getAndroidFileUrl())
                : normalizeOptionalText(latest.getIosFileUrl());
        boolean hasUpdate = downloadUrl != null && defaultNumber(latest.getVersionCode()) > currentVersionCode;
        vo.setHasUpdate(hasUpdate);
        if (!hasUpdate) {
            return vo;
        }
        vo.setLatestVersionNo(latest.getVersionNo());
        vo.setLatestVersionCode(defaultNumber(latest.getVersionCode()));
        vo.setForceUpdate(integerToBoolean(latest.getForceUpdate()));
        vo.setUpdateNotice(latest.getUpdateNotice());
        vo.setDownloadUrl(AppVersionConfig.PLATFORM_ANDROID.equals(normalizedPlatform)
                ? latest.getAndroidFileUrl()
                : latest.getIosFileUrl());
        vo.setDownloadUrl(downloadUrl);
        return vo;
    }
@@ -151,4 +160,31 @@
    private Boolean integerToBoolean(Integer value) {
        return value != null && value == 1;
    }
    private void validatePackageFields(String androidFileUrl, String androidFileName, String iosFileUrl, String iosFileName) {
        boolean hasAndroidFileUrl = androidFileUrl != null;
        boolean hasAndroidFileName = androidFileName != null;
        boolean hasIosFileUrl = iosFileUrl != null;
        boolean hasIosFileName = iosFileName != null;
        boolean hasAndroidPackage = hasAndroidFileUrl || hasAndroidFileName;
        boolean hasIosPackage = hasIosFileUrl || hasIosFileName;
        if (hasAndroidPackage && (!hasAndroidFileUrl || !hasAndroidFileName)) {
            throw new IllegalArgumentException("安卓文件地址和名称必须同时填写");
        }
        if (hasIosPackage && (!hasIosFileUrl || !hasIosFileName)) {
            throw new IllegalArgumentException("iOS文件地址和名称必须同时填写");
        }
        if (!hasAndroidPackage && !hasIosPackage) {
            throw new IllegalArgumentException("请至少上传一个安装包");
        }
    }
    private String normalizeOptionalText(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java
@@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
@@ -100,6 +101,6 @@
        AppVersionCheckVO result = appVersionSettingsAppService.checkUpdate("IOS", 5);
        assertFalse(result.getHasUpdate());
        assertEquals("https://cdn.test/app-ios.ipa", result.getDownloadUrl());
        assertNull(result.getDownloadUrl());
    }
}
tcm-training-admin/src/pages/system/app-version-settings/index.tsx
@@ -6,7 +6,7 @@
} from './service';
import { UploadOutlined } from '@ant-design/icons';
import { history, useAccess } from '@umijs/max';
import { Button, Form, Input, Modal, Pagination, Spin, Switch, Table, message } from 'antd';
import { Button, Form, Input, Modal, Pagination, Spin, Switch, Table, Upload, message } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import React, { useEffect, useState } from 'react';
import './style.less';
@@ -66,6 +66,7 @@
  const [modalMode, setModalMode] = useState<ModalMode>('create');
  const [uploadingAndroid, setUploadingAndroid] = useState(false);
  const [uploadingIos, setUploadingIos] = useState(false);
  const [packageValidationMessage, setPackageValidationMessage] = useState('');
  const canView = access['system:app-version:view'];
  const canAdd = access['system:app-version:add'];
@@ -100,6 +101,7 @@
  const openCreateModal = () => {
    setModalMode('create');
    setModalLoading(false);
    setPackageValidationMessage('');
    form.resetFields();
    form.setFieldsValue(defaultValues);
    setModalOpen(true);
@@ -107,6 +109,7 @@
  const openDetailModal = async (record: AppVersionItem) => {
    setModalMode('detail');
    setPackageValidationMessage('');
    form.resetFields();
    setModalOpen(true);
    setModalLoading(true);
@@ -136,6 +139,7 @@
    }
    setModalOpen(false);
    setModalLoading(false);
    setPackageValidationMessage('');
    form.resetFields();
  };
@@ -154,12 +158,42 @@
        [urlField]: url,
        [nameField]: res.data?.fileName || file.name,
      });
      setPackageValidationMessage('');
      message.success(platform === 'ANDROID' ? '安卓安装包上传成功' : 'iOS安装包上传成功');
      return res;
    } catch (error) {
      message.error((error as Error)?.message || '安装包上传失败');
      const uploadError =
        error instanceof Error ? error : new Error(platform === 'ANDROID' ? '安卓安装包上传失败' : 'iOS安装包上传失败');
      message.error(uploadError.message || '安装包上传失败');
      throw uploadError;
    } finally {
      setUploading(false);
    }
  };
  const trimOptionalValue = (value?: string) => {
    const trimmed = value?.trim();
    return trimmed ? trimmed : '';
  };
  const validatePackages = (values: AppVersionFormValues) => {
    const normalizedAndroidFileUrl = trimOptionalValue(values.androidFileUrl);
    const normalizedAndroidFileName = trimOptionalValue(values.androidFileName);
    const normalizedIosFileUrl = trimOptionalValue(values.iosFileUrl);
    const normalizedIosFileName = trimOptionalValue(values.iosFileName);
    const hasAndroidPackage = !!normalizedAndroidFileUrl || !!normalizedAndroidFileName;
    const hasIosPackage = !!normalizedIosFileUrl || !!normalizedIosFileName;
    if (hasAndroidPackage && (!normalizedAndroidFileUrl || !normalizedAndroidFileName)) {
      return '安卓文件地址和名称必须同时填写';
    }
    if (hasIosPackage && (!normalizedIosFileUrl || !normalizedIosFileName)) {
      return 'iOS文件地址和名称必须同时填写';
    }
    if (!hasAndroidPackage && !hasIosPackage) {
      return '请至少上传一个安装包';
    }
    return '';
  };
  const handleSubmit = async () => {
@@ -167,20 +201,32 @@
      handleCloseModal();
      return;
    }
    const values = await form.validateFields();
    let values: AppVersionFormValues;
    try {
      values = await form.validateFields();
    } catch {
      return;
    }
    const validationMessage = validatePackages(values);
    if (validationMessage) {
      setPackageValidationMessage(validationMessage);
      return;
    }
    setPackageValidationMessage('');
    setSubmitting(true);
    try {
      await createAppVersion({
        versionNo: values.versionNo?.trim(),
        androidFileUrl: values.androidFileUrl?.trim(),
        androidFileName: values.androidFileName?.trim(),
        iosFileUrl: values.iosFileUrl?.trim(),
        iosFileName: values.iosFileName?.trim(),
        versionNo: trimOptionalValue(values.versionNo),
        androidFileUrl: trimOptionalValue(values.androidFileUrl),
        androidFileName: trimOptionalValue(values.androidFileName),
        iosFileUrl: trimOptionalValue(values.iosFileUrl),
        iosFileName: trimOptionalValue(values.iosFileName),
        forceUpdate: !!values.forceUpdate,
        updateNotice: values.updateNotice?.trim(),
        updateNotice: trimOptionalValue(values.updateNotice),
      });
      message.success('新增成功');
      setModalOpen(false);
      setPackageValidationMessage('');
      form.resetFields();
      await loadList(1, pagination.pageSize);
    } catch (error) {
@@ -290,6 +336,11 @@
            disabled={isDetailMode}
            labelCol={{ flex: '78px' }}
            wrapperCol={{ flex: '1' }}
            onValuesChange={() => {
              if (packageValidationMessage) {
                setPackageValidationMessage('');
              }
            }}
          >
            <Form.Item
              label="版本号"
@@ -307,64 +358,65 @@
              <div className="app-version-settings-form__upload-row">
                <div className="app-version-settings-form__upload-col">
                  <div className="app-version-settings-form__upload-label">安卓文件:</div>
                  <Form.Item name="androidFileUrl" rules={[{ required: true, message: '请上传安卓文件' }]} hidden>
                  <Form.Item name="androidFileUrl" hidden>
                    <Input />
                  </Form.Item>
                  <Form.Item name="androidFileName" rules={[{ required: true, message: '请填写安卓文件名称' }]} hidden>
                  <Form.Item name="androidFileName" hidden>
                    <Input />
                  </Form.Item>
                  <label className="app-version-settings-form__upload-trigger">
                    <input
                      type="file"
                      accept=".apk"
                      hidden
                      disabled={isDetailMode}
                      onChange={async (event) => {
                        const file = event.target.files?.[0];
                        event.target.value = '';
                        if (!file) {
                          return;
                        }
                        await handlePackageUpload(file, 'ANDROID');
                      }}
                    />
                  <Upload
                    accept=".apk"
                    showUploadList={false}
                    disabled={isDetailMode}
                    className="app-version-settings-form__upload-trigger"
                    customRequest={async (options) => {
                      try {
                        const res = await handlePackageUpload(options.file as File, 'ANDROID');
                        options.onSuccess?.(res, options.file);
                      } catch (error) {
                        options.onError?.(error as Error);
                      }
                    }}
                  >
                    <Button icon={<UploadOutlined />} loading={uploadingAndroid} disabled={isDetailMode}>
                      上传
                    </Button>
                  </label>
                  </Upload>
                  <div className="app-version-settings-form__file-name">{androidFileName || '未上传文件'}</div>
                </div>
                <div className="app-version-settings-form__upload-col">
                  <div className="app-version-settings-form__upload-label">ios文件:</div>
                  <Form.Item name="iosFileUrl" rules={[{ required: true, message: '请上传iOS文件' }]} hidden>
                  <Form.Item name="iosFileUrl" hidden>
                    <Input />
                  </Form.Item>
                  <Form.Item name="iosFileName" rules={[{ required: true, message: '请填写iOS文件名称' }]} hidden>
                  <Form.Item name="iosFileName" hidden>
                    <Input />
                  </Form.Item>
                  <label className="app-version-settings-form__upload-trigger">
                    <input
                      type="file"
                      accept=".ipa"
                      hidden
                      disabled={isDetailMode}
                      onChange={async (event) => {
                        const file = event.target.files?.[0];
                        event.target.value = '';
                        if (!file) {
                          return;
                        }
                        await handlePackageUpload(file, 'IOS');
                      }}
                    />
                  <Upload
                    accept=".ipa"
                    showUploadList={false}
                    disabled={isDetailMode}
                    className="app-version-settings-form__upload-trigger"
                    customRequest={async (options) => {
                      try {
                        const res = await handlePackageUpload(options.file as File, 'IOS');
                        options.onSuccess?.(res, options.file);
                      } catch (error) {
                        options.onError?.(error as Error);
                      }
                    }}
                  >
                    <Button icon={<UploadOutlined />} loading={uploadingIos} disabled={isDetailMode}>
                      上传
                    </Button>
                  </label>
                  </Upload>
                  <div className="app-version-settings-form__file-name">{iosFileName || '未上传文件'}</div>
                </div>
              </div>
              {!isDetailMode && packageValidationMessage ? (
                <div className="app-version-settings-form__upload-error">{packageValidationMessage}</div>
              ) : null}
            </div>
            <Form.Item label="强制更新" name="forceUpdate" valuePropName="checked">
tcm-training-admin/src/pages/system/app-version-settings/style.less
@@ -124,6 +124,12 @@
  word-break: break-all;
}
.app-version-settings-form__upload-error {
  color: #ff4d4f;
  font-size: 14px;
  line-height: 22px;
}
.app-version-settings-form__textarea-item {
  margin-top: 4px;
}