From 0afa0f8c1a1232f82c4a929700372e62c5f75e36 Mon Sep 17 00:00:00 2001
From: ltx <1006158920@qq.com>
Date: Thu, 5 Nov 2026 16:05:45 +0800
Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E7=89=88=E6=9C=AC=E6=8E=A7?=
=?UTF-8?q?=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../AppUpdate/AppUpdateController.cs | 75 +++++--------------
1 file changed, 17 insertions(+), 58 deletions(-)
diff --git a/YD_Event.Application/AppUpdate/AppUpdateController.cs b/YD_Event.Application/AppUpdate/AppUpdateController.cs
index ba1dbbc..0619932 100644
--- a/YD_Event.Application/AppUpdate/AppUpdateController.cs
+++ b/YD_Event.Application/AppUpdate/AppUpdateController.cs
@@ -1,4 +1,3 @@
-using System.Diagnostics;
using System.Security.Cryptography;
using YD_Event.Application.Auth;
using YD_Event.Application.Common;
@@ -47,7 +46,7 @@ public class AppUpdateController : IDynamicApiController
};
}
- /// 只允许下载当前环境已发布的版本,文件由现有对象存储分发。
+ /// 只允许下载当前环境已发布的版本,沿用现有文件服务流式返回。
[HttpGet("download/{id:long}")]
[AllowAnonymous]
public async Task Download(long id)
@@ -55,7 +54,7 @@ public class AppUpdateController : IDynamicApiController
var release = await _releases.GetFirstAsync(x => x.Id == id && x.Channel == _channel &&
x.PackageName == PackageName && x.ReleaseStatus == 1);
if (release == null || string.IsNullOrWhiteSpace(release.FileUrl)) return new NotFoundResult();
- return new RedirectResult(release.FileUrl);
+ return _files.Download(release.FileUrl);
}
[HttpGet("admin/releases")]
@@ -71,7 +70,9 @@ public class AppUpdateController : IDynamicApiController
[RequestSizeLimit(MaxApkSize + 1024 * 1024)]
[RequestFormLimits(MultipartBodyLengthLimit = MaxApkSize + 1024 * 1024)]
public async Task Upload([FromForm] IFormFile file,
- [FromForm] string notesZh, [FromForm] string? notesEn = null,
+ [FromForm] string versionName, [FromForm] int versionCode,
+ [FromForm] string certificateSha256, [FromForm] string notesZh,
+ [FromForm] string? notesEn = null,
[FromForm] bool isMandatory = false)
{
EnsureAdmin();
@@ -79,6 +80,16 @@ public class AppUpdateController : IDynamicApiController
!string.Equals(Path.GetExtension(file.FileName), ".apk", StringComparison.OrdinalIgnoreCase))
throw Oops.Bah("仅允许上传 1KB 至 200MB 的 APK");
if (string.IsNullOrWhiteSpace(notesZh)) throw Oops.Bah("请填写中文更新说明");
+ if (versionCode <= 0 || string.IsNullOrWhiteSpace(versionName) || versionName.Length > 64)
+ throw Oops.Bah("请填写有效的版本名和正整数版本号");
+ // 本阶段由超级管理员提供 build-info.json 中的元数据;后续再恢复 APK 内部解析与签名比对。
+ // 文件哈希仍在服务端计算,平板安装前仍会核对真实 APK 的包名、版本和签名。
+ var fingerprint = certificateSha256?.Trim().Replace(":", "").ToUpperInvariant();
+ if (fingerprint == null || !Regex.IsMatch(fingerprint, "^[0-9A-F]{64}$"))
+ throw Oops.Bah("请填写 64 位的公开签名证书 SHA-256 指纹");
+ var existing = await _releases.GetFirstAsync(x => x.Channel == _channel &&
+ x.PackageName == PackageName && x.VersionCode == versionCode);
+ if (existing != null) throw Oops.Bah("该版本号已存在,请使用新的递增版本号");
var temp = Path.Combine(Path.GetTempPath(), $"yd-app-release-{Guid.NewGuid():N}.apk");
try
{
@@ -91,25 +102,16 @@ public class AppUpdateController : IDynamicApiController
if (await input.ReadAsync(magic) != 4 || magic[0] != 0x50 || magic[1] != 0x4B || magic[2] != 0x03 || magic[3] != 0x04)
throw Oops.Bah("文件不是有效的 APK/ZIP");
}
- var metadata = await InspectApk(temp);
- if (metadata.PackageName != PackageName || metadata.VersionCode <= 0)
- throw Oops.Bah("APK 包名或版本号不正确");
- var expected = Environment.GetEnvironmentVariable("APP_RELEASE_CERT_SHA256")?.Trim().ToUpperInvariant();
- if (expected == null || !Regex.IsMatch(expected, "^[0-9A-F]{64}$"))
- throw Oops.Bah("服务器未配置正式签名指纹,禁止发布");
- if (metadata.CertificateSha256 != expected) throw Oops.Bah("APK 签名与正式签名不一致");
- var existing = await _releases.GetFirstAsync(x => x.Channel == _channel && x.PackageName == PackageName && x.VersionCode == metadata.VersionCode);
- if (existing != null) throw Oops.Bah("该版本号已存在,请使用新的递增版本号");
await using var hashStream = System.IO.File.OpenRead(temp);
var sha = Convert.ToHexString(await SHA256.HashDataAsync(hashStream));
await using var uploadStream = System.IO.File.OpenRead(temp);
var release = new JudgeAppRelease
{
Id = _ids.NextId(), Channel = _channel, PackageName = PackageName,
- VersionCode = metadata.VersionCode, VersionName = metadata.VersionName,
+ VersionCode = versionCode, VersionName = versionName.Trim(),
NotesZh = notesZh?.Trim() ?? string.Empty, NotesEn = notesEn?.Trim() ?? string.Empty,
IsMandatory = isMandatory, FileSize = uploadStream.Length,
- Sha256 = sha, CertificateSha256 = metadata.CertificateSha256,
+ Sha256 = sha, CertificateSha256 = fingerprint,
FileUrl = await _files.UploadFileStreamAsync($"app-releases/{_channel}/{Guid.NewGuid():N}.apk", uploadStream),
ReleaseStatus = 0, CreatedUserId = _auth.UserId, CreatedTime = DateTime.UtcNow
};
@@ -125,9 +127,6 @@ public class AppUpdateController : IDynamicApiController
EnsureAdmin();
var release = await FindOwn(id);
if (release.ReleaseStatus != 0) throw Oops.Bah("只有草稿可以发布");
- if (_channel == "production" &&
- (!Uri.TryCreate(release.FileUrl, UriKind.Absolute, out var fileUri) || fileUri.Scheme != Uri.UriSchemeHttps))
- throw Oops.Bah("正式环境的 APK 下载地址必须为 HTTPS");
var latest = await _releases.AsQueryable()
.Where(x => x.Channel == _channel && x.PackageName == PackageName && x.ReleaseStatus != 0)
.OrderByDescending(x => x.VersionCode).FirstAsync();
@@ -164,44 +163,4 @@ public class AppUpdateController : IDynamicApiController
return release ?? throw Oops.Bah("版本不存在");
}
- private static async Task<(string PackageName, int VersionCode, string VersionName, string CertificateSha256)> InspectApk(string path)
- {
- var directory = Environment.GetEnvironmentVariable("ANDROID_BUILD_TOOLS_DIR");
- if (string.IsNullOrWhiteSpace(directory)) throw Oops.Bah("服务器未配置 Android build-tools,禁止盲目发布");
- var aapt = Path.Combine(directory, OperatingSystem.IsWindows() ? "aapt.exe" : "aapt");
- var signer = Path.Combine(directory, OperatingSystem.IsWindows() ? "apksigner.bat" : "apksigner");
- var badging = await RunTool(aapt, "dump", "badging", path);
- var match = Regex.Match(badging, "package: name='([^']+)' versionCode='([0-9]+)' versionName='([^']*)'");
- if (!match.Success) throw Oops.Bah("无法读取 APK 包名及版本");
- var result = await RunTool(signer, "verify", "--print-certs", path);
- var digest = Regex.Match(result, "certificate SHA-256 digest: ([0-9a-fA-F]{64})");
- if (!digest.Success) throw Oops.Bah("无法验证 APK 签名");
- return (match.Groups[1].Value, int.Parse(match.Groups[2].Value), match.Groups[3].Value, digest.Groups[1].Value.ToUpperInvariant());
- }
-
- private static async Task RunTool(string executable, params string[] arguments)
- {
- if (!System.IO.File.Exists(executable)) throw Oops.Bah("Android build-tools 不完整");
- ProcessStartInfo start;
- if (OperatingSystem.IsWindows() && executable.EndsWith(".bat", StringComparison.OrdinalIgnoreCase))
- {
- start = new ProcessStartInfo("cmd.exe") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
- start.ArgumentList.Add("/d");
- start.ArgumentList.Add("/c");
- start.ArgumentList.Add("\"" + executable + "\" " + string.Join(" ", arguments.Select(x => "\"" + x.Replace("\"", "") + "\"")));
- }
- else
- {
- start = new ProcessStartInfo(executable) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
- foreach (var argument in arguments) start.ArgumentList.Add(argument);
- }
- using var process = Process.Start(start) ?? throw Oops.Bah("无法启动 APK 检查工具");
- var output = process.StandardOutput.ReadToEndAsync();
- var error = process.StandardError.ReadToEndAsync();
- using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
- try { await process.WaitForExitAsync(timeout.Token); }
- catch (OperationCanceledException) { process.Kill(entireProcessTree: true); throw Oops.Bah("APK 检查超时"); }
- if (process.ExitCode != 0) throw Oops.Bah("APK 检查失败:" + (await error).Trim());
- return await output;
- }
}