优化
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package com.baiyue.file.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.file.service.PdaReleaseService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class PdaReleaseController {
|
||||
private final PdaReleaseService releases;
|
||||
|
||||
public PdaReleaseController(PdaReleaseService releases) { this.releases = releases; }
|
||||
|
||||
@GetMapping("/api/platform/v1/files/pda-releases/{app}")
|
||||
@PreAuthorize("hasAuthority('platform:pda-release:list')")
|
||||
public ApiResponse<List<PdaReleaseService.Release>> list(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String app) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(releases.list(app));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/api/platform/v1/files/pda-releases/{app}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('platform:pda-release:manage')")
|
||||
public ApiResponse<PdaReleaseService.Release> upload(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String app, @RequestParam String packageType, @RequestParam String versionName,
|
||||
@RequestParam int versionCode, @RequestParam int minAppVersionCode,
|
||||
@RequestParam(defaultValue = "false") boolean mandatory, @RequestParam(defaultValue = "") String releaseNotes,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
return ApiResponse.success(releases.upload(app, packageType, versionName, versionCode,
|
||||
minAppVersionCode, mandatory, releaseNotes, file, requirePlatform(user)));
|
||||
}
|
||||
|
||||
@PostMapping("/api/platform/v1/files/pda-releases/{id}/publish")
|
||||
@PreAuthorize("hasAuthority('platform:pda-release:manage')")
|
||||
public ApiResponse<PdaReleaseService.Release> publish(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String id) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(releases.publish(id));
|
||||
}
|
||||
|
||||
@PostMapping("/api/platform/v1/files/pda-releases/{id}/withdraw")
|
||||
@PreAuthorize("hasAuthority('platform:pda-release:manage')")
|
||||
public ApiResponse<PdaReleaseService.Release> withdraw(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String id) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(releases.withdraw(id));
|
||||
}
|
||||
|
||||
@GetMapping("/api/pda-updates/{app}/latest")
|
||||
public Map<String, Object> latest(@PathVariable String app, @RequestParam String appid,
|
||||
@RequestParam String platform, @RequestParam int appVersionCode, @RequestParam String wgtVersion) {
|
||||
return releases.latest(app, appid, platform, appVersionCode, wgtVersion);
|
||||
}
|
||||
|
||||
@GetMapping("/api/pda-updates/{app}/{id}/package")
|
||||
public ResponseEntity<Resource> download(@PathVariable String app, @PathVariable String id) {
|
||||
PdaReleaseService.PackageFile file = releases.packageFile(app, id);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(file.sizeBytes())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
||||
.filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.body(file.resource());
|
||||
}
|
||||
|
||||
private static long requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || !"PLATFORM".equalsIgnoreCase(user.appCode())
|
||||
|| user.tenantId() == null || user.tenantId() != 0L
|
||||
|| user.userId() == null || user.userId() <= 0) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "需要平台端身份");
|
||||
}
|
||||
return user.userId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.baiyue.file.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.PathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
@Component
|
||||
public class PdaPackageStorage {
|
||||
private static final long MAX_BYTES = 200L * 1024 * 1024;
|
||||
private final Path root;
|
||||
|
||||
public PdaPackageStorage(@Value("${baiyue.file.storage-root}") String storageRoot) {
|
||||
root = Path.of(storageRoot).toAbsolutePath().normalize().resolve("pda-packages");
|
||||
}
|
||||
|
||||
public record Stored(String key, long sizeBytes, String sha256) { }
|
||||
|
||||
public Stored store(String app, String id, String type, MultipartFile file) {
|
||||
if (file == null || file.isEmpty() || file.getSize() > MAX_BYTES) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "更新包为空或超过 200MB");
|
||||
}
|
||||
String name = file.getOriginalFilename();
|
||||
if (name == null || !name.toLowerCase().endsWith("." + type)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "文件扩展名与包类型不符");
|
||||
}
|
||||
String key = app + "/" + id + "." + type;
|
||||
Path target = path(key);
|
||||
try {
|
||||
Files.createDirectories(target.getParent());
|
||||
file.transferTo(target);
|
||||
try (ZipFile zip = new ZipFile(target.toFile())) {
|
||||
if (zip.size() == 0) throw new IOException("empty archive");
|
||||
}
|
||||
return new Stored(key, Files.size(target), sha256(target));
|
||||
} catch (IOException | IllegalStateException exception) {
|
||||
delete(key);
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "更新包不是有效的 APK/WGT 文件");
|
||||
}
|
||||
}
|
||||
|
||||
public Resource load(String key) {
|
||||
Path file = path(key);
|
||||
if (!Files.isRegularFile(file)) throw new BizException(ResultCode.NOT_FOUND, "更新包文件不存在");
|
||||
return new PathResource(file);
|
||||
}
|
||||
|
||||
public void delete(String key) {
|
||||
try { Files.deleteIfExists(path(key)); } catch (IOException ignored) { }
|
||||
}
|
||||
|
||||
private Path path(String key) {
|
||||
if (key == null || !key.matches("(domestic|overseas)/[a-f0-9-]{36}\\.(apk|wgt)")) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "更新包路径无效");
|
||||
}
|
||||
Path path = root.resolve(key).normalize();
|
||||
if (!path.startsWith(root)) throw new BizException(ResultCode.FORBIDDEN, "更新包路径越界");
|
||||
return path;
|
||||
}
|
||||
|
||||
private static String sha256(Path file) throws IOException {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
try (InputStream input = Files.newInputStream(file)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
for (int count; (count = input.read(buffer)) >= 0;) digest.update(buffer, 0, count);
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package com.baiyue.file.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class PdaReleaseService {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final PdaPackageStorage storage;
|
||||
private final String publicBaseUrl;
|
||||
private final String domesticAppId;
|
||||
private final String overseasAppId;
|
||||
|
||||
public PdaReleaseService(JdbcTemplate jdbc, PdaPackageStorage storage,
|
||||
@Value("${baiyue.file.pda.public-base-url:}") String publicBaseUrl,
|
||||
@Value("${baiyue.file.pda.domestic-appid:__UNI__9BFFAF2}") String domesticAppId,
|
||||
@Value("${baiyue.file.pda.overseas-appid:__UNI__936D079}") String overseasAppId) {
|
||||
this.jdbc = jdbc;
|
||||
this.storage = storage;
|
||||
this.publicBaseUrl = publicBaseUrl.replaceAll("/+$", "");
|
||||
this.domesticAppId = domesticAppId;
|
||||
this.overseasAppId = overseasAppId;
|
||||
if (!this.publicBaseUrl.isEmpty() && !this.publicBaseUrl.startsWith("https://")) {
|
||||
throw new IllegalArgumentException("PDA_UPDATE_PUBLIC_BASE_URL must use HTTPS");
|
||||
}
|
||||
}
|
||||
|
||||
public record Release(String id, String app, String appid, String packageType, String versionName,
|
||||
int versionCode, int minAppVersionCode, boolean mandatory, String releaseNotes,
|
||||
String status, boolean current, String sha256, long sizeBytes, String downloadUrl,
|
||||
LocalDateTime createdAt, LocalDateTime publishedAt) { }
|
||||
public record PackageFile(Resource resource, long sizeBytes, String fileName) { }
|
||||
|
||||
private record Row(String id, String app, String appid, String type, String name, int code,
|
||||
int minCode, boolean mandatory, String notes, String status,
|
||||
String key, String sha256, long bytes, LocalDateTime createdAt,
|
||||
LocalDateTime publishedAt) { }
|
||||
private record Channel(String appid, String apkId, String wgtId) { }
|
||||
|
||||
public Release upload(String app, String type, String versionName, int versionCode,
|
||||
int minAppVersionCode, boolean mandatory, String notes,
|
||||
MultipartFile file, long operatorId) {
|
||||
String appid = appid(app);
|
||||
type = type == null ? "" : type.toLowerCase();
|
||||
if (!type.equals("apk") && !type.equals("wgt")) throw bad("只支持 APK 或 WGT");
|
||||
if (versionName == null || versionName.length() > 32
|
||||
|| !versionName.matches("\\d+(\\.\\d+)+")) throw bad("版本号格式无效");
|
||||
if (versionCode < 1 || minAppVersionCode < 1
|
||||
|| (type.equals("apk") && versionCode != minAppVersionCode)) {
|
||||
throw bad("APK 版本号或 WGT 最低基座版本无效");
|
||||
}
|
||||
if (notes != null && notes.length() > 1000) throw bad("更新说明过长");
|
||||
String id = UUID.randomUUID().toString();
|
||||
PdaPackageStorage.Stored stored = storage.store(app, id, type, file);
|
||||
try {
|
||||
jdbc.update("""
|
||||
INSERT INTO file_pda_release
|
||||
(id,app_code,app_id,package_type,version_name,version_code,min_app_version_code,
|
||||
mandatory,release_notes,status,object_key,sha256,size_bytes,created_by)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,'DRAFT',?,?,?,?)
|
||||
""", id, app, appid, type, versionName, versionCode, minAppVersionCode,
|
||||
mandatory, notes == null ? "" : notes, stored.key(), stored.sha256(),
|
||||
stored.sizeBytes(), operatorId);
|
||||
} catch (RuntimeException exception) {
|
||||
storage.delete(stored.key());
|
||||
throw exception;
|
||||
}
|
||||
return view(get(id));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Release publish(String id) {
|
||||
Row row = get(id);
|
||||
if (!row.status().equals("DRAFT")) throw conflict("只有草稿可以发布");
|
||||
if (publicBaseUrl.isEmpty()) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "更新服务公开地址未配置");
|
||||
}
|
||||
Channel channel = channel(row.app(), true);
|
||||
if (!Objects.equals(channel.appid(), row.appid())) throw conflict("应用 AppID 已改变,请重新上传");
|
||||
Row current = current(row.type().equals("apk") ? channel.apkId() : channel.wgtId());
|
||||
if (current != null && (row.type().equals("apk")
|
||||
? row.code() <= current.code()
|
||||
: compareVersion(row.name(), current.name()) <= 0)) {
|
||||
throw conflict("新版本必须高于当前发布版本");
|
||||
}
|
||||
if (row.type().equals("wgt")) {
|
||||
Row apk = current(channel.apkId());
|
||||
if (apk == null || row.minCode() > apk.code()) throw conflict("请先发布兼容的 APK 基座");
|
||||
}
|
||||
storage.load(row.key());
|
||||
if (jdbc.update("UPDATE file_pda_release SET status='PUBLISHED',published_at=CURRENT_TIMESTAMP(3) WHERE id=? AND status='DRAFT'", id) != 1) {
|
||||
throw conflict("版本状态已改变");
|
||||
}
|
||||
String column = row.type().equals("apk") ? "current_apk_id" : "current_wgt_id";
|
||||
jdbc.update("UPDATE file_pda_release_channel SET " + column + "=? WHERE app_code=?", id, row.app());
|
||||
return view(get(id));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Release withdraw(String id) {
|
||||
Row row = get(id);
|
||||
Channel channel = channel(row.app(), true);
|
||||
if (!row.status().equals("PUBLISHED")) throw conflict("只有已发布版本可以停发");
|
||||
String column = row.type().equals("apk") ? "current_apk_id" : "current_wgt_id";
|
||||
String currentId = row.type().equals("apk") ? channel.apkId() : channel.wgtId();
|
||||
if (id.equals(currentId)) {
|
||||
jdbc.update("UPDATE file_pda_release_channel SET " + column + "=NULL WHERE app_code=?", row.app());
|
||||
}
|
||||
jdbc.update("UPDATE file_pda_release SET status='WITHDRAWN' WHERE id=?", id);
|
||||
return view(get(id));
|
||||
}
|
||||
|
||||
public List<Release> list(String app) {
|
||||
appid(app);
|
||||
Channel channel = channel(app, false);
|
||||
List<Row> rows = jdbc.query("SELECT * FROM file_pda_release WHERE app_code=? ORDER BY created_at DESC LIMIT 100",
|
||||
(rs, index) -> map(rs), app);
|
||||
return rows.stream().map(row -> view(row, channel)).toList();
|
||||
}
|
||||
|
||||
public Map<String, Object> latest(String app, String requestedAppid, String platform,
|
||||
int appVersionCode, String wgtVersion) {
|
||||
if (!appid(app).equals(requestedAppid) || !"android".equalsIgnoreCase(platform)
|
||||
|| appVersionCode < 1 || wgtVersion == null || !wgtVersion.matches("\\d+(\\.\\d+)+")) {
|
||||
throw bad("更新检查参数无效");
|
||||
}
|
||||
Channel channel = channel(app, false);
|
||||
Row apk = current(channel.apkId());
|
||||
if (apk != null && apk.code() > appVersionCode) return manifest(apk);
|
||||
Row wgt = current(channel.wgtId());
|
||||
if (wgt != null && wgt.minCode() <= appVersionCode
|
||||
&& compareVersion(wgt.name(), wgtVersion) > 0) return manifest(wgt);
|
||||
return Map.of("update", false);
|
||||
}
|
||||
|
||||
public PackageFile packageFile(String app, String id) {
|
||||
Row row = get(id);
|
||||
if (!row.app().equals(app) || !row.status().equals("PUBLISHED")) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "更新包不存在");
|
||||
}
|
||||
return new PackageFile(storage.load(row.key()), row.bytes(),
|
||||
row.app() + "-pda-" + row.name() + "." + row.type());
|
||||
}
|
||||
|
||||
private Map<String, Object> manifest(Row row) {
|
||||
if (publicBaseUrl.isEmpty()) throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "更新服务公开地址未配置");
|
||||
return Map.of("update", true, "appid", row.appid(), "packageType", row.type(),
|
||||
"versionName", row.name(), "versionCode", row.code(), "minAppVersionCode", row.minCode(),
|
||||
"mandatory", row.mandatory(), "releaseNotes", row.notes(), "downloadUrl", url(row));
|
||||
}
|
||||
|
||||
private Release view(Row row) {
|
||||
return view(row, channel(row.app(), false));
|
||||
}
|
||||
|
||||
private Release view(Row row, Channel channel) {
|
||||
boolean current = row.id().equals(row.type().equals("apk") ? channel.apkId() : channel.wgtId());
|
||||
return new Release(row.id(), row.app(), row.appid(), row.type(), row.name(), row.code(),
|
||||
row.minCode(), row.mandatory(), row.notes(), row.status(), current, row.sha256(), row.bytes(),
|
||||
row.status().equals("PUBLISHED") && !publicBaseUrl.isEmpty() ? url(row) : null,
|
||||
row.createdAt(), row.publishedAt());
|
||||
}
|
||||
|
||||
private String url(Row row) {
|
||||
return publicBaseUrl + "/api/pda-updates/" + row.app() + "/" + row.id() + "/package";
|
||||
}
|
||||
|
||||
private Channel channel(String app, boolean locked) {
|
||||
String sql = "SELECT app_id,current_apk_id,current_wgt_id FROM file_pda_release_channel WHERE app_code=?"
|
||||
+ (locked ? " FOR UPDATE" : "");
|
||||
List<Channel> rows = jdbc.query(sql, (rs, index) ->
|
||||
new Channel(rs.getString(1), rs.getString(2), rs.getString(3)), app);
|
||||
if (rows.isEmpty()) throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "PDA 发布通道未初始化");
|
||||
return rows.get(0);
|
||||
}
|
||||
|
||||
private Row current(String id) { return id == null ? null : get(id); }
|
||||
|
||||
private Row get(String id) {
|
||||
List<Row> rows = jdbc.query("SELECT * FROM file_pda_release WHERE id=?", (rs, index) -> map(rs), id);
|
||||
if (rows.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "PDA 版本不存在");
|
||||
return rows.get(0);
|
||||
}
|
||||
|
||||
private String appid(String app) {
|
||||
return switch (app) {
|
||||
case "domestic" -> domesticAppId;
|
||||
case "overseas" -> overseasAppId;
|
||||
default -> throw bad("PDA 应用类型无效");
|
||||
};
|
||||
}
|
||||
|
||||
private static Row map(ResultSet rs) throws SQLException {
|
||||
return new Row(rs.getString("id"), rs.getString("app_code"), rs.getString("app_id"),
|
||||
rs.getString("package_type"), rs.getString("version_name"), rs.getInt("version_code"),
|
||||
rs.getInt("min_app_version_code"), rs.getBoolean("mandatory"), rs.getString("release_notes"),
|
||||
rs.getString("status"), rs.getString("object_key"), rs.getString("sha256"), rs.getLong("size_bytes"),
|
||||
rs.getTimestamp("created_at").toLocalDateTime(),
|
||||
rs.getTimestamp("published_at") == null ? null : rs.getTimestamp("published_at").toLocalDateTime());
|
||||
}
|
||||
|
||||
private static int compareVersion(String left, String right) {
|
||||
String[] a = left.split("\\.");
|
||||
String[] b = right.split("\\.");
|
||||
for (int i = 0; i < Math.max(a.length, b.length); i++) {
|
||||
int x = i < a.length ? Integer.parseInt(a[i]) : 0;
|
||||
int y = i < b.length ? Integer.parseInt(b[i]) : 0;
|
||||
if (x != y) return Integer.compare(x, y);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static BizException bad(String message) { return new BizException(ResultCode.BAD_REQUEST, message); }
|
||||
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
|
||||
}
|
||||
@@ -40,8 +40,8 @@ spring:
|
||||
read-timeout: 15000
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 50MB
|
||||
max-request-size: 51MB
|
||||
max-file-size: 200MB
|
||||
max-request-size: 201MB
|
||||
rocketmq:
|
||||
name-server: ${ROCKETMQ_NAME_SERVER:127.0.0.1:9876}
|
||||
producer:
|
||||
@@ -60,6 +60,10 @@ baiyue:
|
||||
url: ${FINANCE_SERVICE_URL:}
|
||||
file:
|
||||
storage-root: ${FILE_STORAGE_ROOT:${user.home}/baiyue-firstleg/uploads}
|
||||
pda:
|
||||
public-base-url: ${PDA_UPDATE_PUBLIC_BASE_URL:}
|
||||
domestic-appid: ${PDA_DOMESTIC_APPID:__UNI__9BFFAF2}
|
||||
overseas-appid: ${PDA_OVERSEAS_APPID:__UNI__936D079}
|
||||
price-book:
|
||||
max-size-bytes: ${FILE_PRICE_BOOK_MAX_SIZE_BYTES:10485760}
|
||||
export:
|
||||
|
||||
@@ -28,6 +28,40 @@ CREATE TABLE IF NOT EXISTS file_object (
|
||||
KEY idx_file_tenant_created (tenant_id, created_at)
|
||||
) ENGINE=InnoDB COMMENT='文件元数据表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS file_pda_release_channel (
|
||||
app_code VARCHAR(16) NOT NULL,
|
||||
app_id VARCHAR(64) NOT NULL,
|
||||
current_apk_id CHAR(36) NULL,
|
||||
current_wgt_id CHAR(36) NULL,
|
||||
PRIMARY KEY (app_code)
|
||||
) ENGINE=InnoDB COMMENT='国内/海外PDA当前发布指针';
|
||||
|
||||
INSERT INTO file_pda_release_channel (app_code, app_id) VALUES
|
||||
('domestic', '__UNI__9BFFAF2'), ('overseas', '__UNI__936D079')
|
||||
ON DUPLICATE KEY UPDATE app_id=VALUES(app_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS file_pda_release (
|
||||
id CHAR(36) NOT NULL,
|
||||
app_code VARCHAR(16) NOT NULL,
|
||||
app_id VARCHAR(64) NOT NULL,
|
||||
package_type VARCHAR(8) NOT NULL,
|
||||
version_name VARCHAR(32) NOT NULL,
|
||||
version_code INT UNSIGNED NOT NULL,
|
||||
min_app_version_code INT UNSIGNED NOT NULL,
|
||||
mandatory TINYINT(1) NOT NULL DEFAULT 0,
|
||||
release_notes VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'DRAFT',
|
||||
object_key VARCHAR(128) NOT NULL,
|
||||
sha256 CHAR(64) NOT NULL,
|
||||
size_bytes BIGINT UNSIGNED NOT NULL,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
published_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_pda_release_version (app_code, package_type, version_name),
|
||||
KEY idx_pda_release_created (app_code, created_at)
|
||||
) ENGINE=InnoDB COMMENT='PDA安装包与热更新发布历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS file_download_token (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '下载令牌ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
|
||||
Reference in New Issue
Block a user