| | |
| | | `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, |
| | |
| | | `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, |
| | |
| | | (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 |
| | |
| | | `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, |
| | |
| | | (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 '通知类型编码', |
| | |
| | | @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; |
| | | |
| | |
| | | @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); |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | AppVersionCheckVO result = appVersionSettingsAppService.checkUpdate("IOS", 5); |
| | | |
| | | assertFalse(result.getHasUpdate()); |
| | | assertEquals("https://cdn.test/app-ios.ipa", result.getDownloadUrl()); |
| | | assertNull(result.getDownloadUrl()); |
| | | } |
| | | } |
| | |
| | | } 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'; |
| | |
| | | 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']; |
| | |
| | | const openCreateModal = () => { |
| | | setModalMode('create'); |
| | | setModalLoading(false); |
| | | setPackageValidationMessage(''); |
| | | form.resetFields(); |
| | | form.setFieldsValue(defaultValues); |
| | | setModalOpen(true); |
| | |
| | | |
| | | const openDetailModal = async (record: AppVersionItem) => { |
| | | setModalMode('detail'); |
| | | setPackageValidationMessage(''); |
| | | form.resetFields(); |
| | | setModalOpen(true); |
| | | setModalLoading(true); |
| | |
| | | } |
| | | setModalOpen(false); |
| | | setModalLoading(false); |
| | | setPackageValidationMessage(''); |
| | | form.resetFields(); |
| | | }; |
| | | |
| | |
| | | [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 () => { |
| | |
| | | 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) { |
| | |
| | | disabled={isDetailMode} |
| | | labelCol={{ flex: '78px' }} |
| | | wrapperCol={{ flex: '1' }} |
| | | onValuesChange={() => { |
| | | if (packageValidationMessage) { |
| | | setPackageValidationMessage(''); |
| | | } |
| | | }} |
| | | > |
| | | <Form.Item |
| | | label="版本号" |
| | |
| | | <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"> |
| | |
| | | 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; |
| | | } |