Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 056ae914ce | |||
| 022443476c | |||
| 93f4f87e44 | |||
| 20e7d07be3 | |||
| 1834740676 | |||
| 90a194fc00 | |||
| fba8b0a917 | |||
| c001f0e8e6 | |||
| f039bd0c61 |
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.15] - 2026-06-05
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复 Windows 桌面端打印卡死:打印字体由 OpenType/CFF 改为 TrueType(glyf),dart_pdf 渲染中文不再抛错(此前点「打印」后卡死)
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
- 「关于我们」内容改为真实信息(服务商:岩美技术有限公司;网站:jiu.yanmei.com;邮箱:yammy2023@163.com),且统一从配置文件读取,不再硬编码
|
||||||
|
- Windows 桌面端窗口标题由 jiu_client 改为「岩美酒库」
|
||||||
|
|
||||||
|
## [1.0.13] - 2026-06-04
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- Windows 客户端改为提供安装程序(Inno Setup 打包的 `jiu-windows-x64-setup.exe`,含安装向导、桌面/开始菜单快捷方式、卸载项),不再是解压版 zip
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复客户端「无法安全下载」:下载地址从内网 HTTP Forgejo(`http://192.168.3.200:3000`,公网不可达且被浏览器拦截)改为同源 HTTPS `https://jiu.51yanmei.com/downloads/...`,Windows、macOS 均生效
|
||||||
|
- 部署时将最新安装包发布到 web 服务(`/opt/jiu/downloads/`,nginx `/downloads/` 提供),仅保留最新版本(清旧放新)
|
||||||
|
|
||||||
|
## [1.0.9] - 2026-06-04
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 适配 nginx 容器化部署:nginx 已迁入 `pangolin-edge` 容器(host 网络),deploy 改为将 jiu.conf 写入 pangolin 挂载目录并 `docker exec pangolin-edge nginx -s reload`,不再操作已废弃的主机 nginx
|
||||||
|
|
||||||
## [1.0.8] - 2026-06-03
|
## [1.0.8] - 2026-06-03
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"provider": "岩美技术有限公司",
|
||||||
|
"website": "https://jiu.yanmei.com",
|
||||||
|
"email": "yammy2023@163.com"
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// 应用「关于我们」等品牌信息,从配置文件 assets/config/app_info.json 读取,
|
||||||
|
/// 不硬编码、不走环境变量。在 main() 启动时调用 [load] 加载一次。
|
||||||
|
class AppInfo {
|
||||||
|
const AppInfo._();
|
||||||
|
|
||||||
|
static const _assetPath = 'assets/config/app_info.json';
|
||||||
|
|
||||||
|
static String provider = '';
|
||||||
|
static String website = '';
|
||||||
|
static String email = '';
|
||||||
|
|
||||||
|
/// 加载配置文件。失败时保留为空(不影响应用启动)。
|
||||||
|
static Future<void> load() async {
|
||||||
|
try {
|
||||||
|
final raw = await rootBundle.loadString(_assetPath);
|
||||||
|
final map = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
|
provider = (map['provider'] as String?) ?? '';
|
||||||
|
website = (map['website'] as String?) ?? '';
|
||||||
|
email = (map['email'] as String?) ?? '';
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('AppInfo.load failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||||
import 'core/auth/auth_state.dart';
|
import 'core/auth/auth_state.dart';
|
||||||
|
import 'core/config/app_info.dart';
|
||||||
import 'core/errors/error_reporter.dart';
|
import 'core/errors/error_reporter.dart';
|
||||||
import 'core/router/app_router.dart';
|
import 'core/router/app_router.dart';
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
@@ -24,7 +25,11 @@ void main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
runZonedGuarded(
|
runZonedGuarded(
|
||||||
() => runApp(const ProviderScope(child: JiuApp())),
|
() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await AppInfo.load(); // 从配置文件加载「关于我们」品牌信息
|
||||||
|
runApp(const ProviderScope(child: JiuApp()));
|
||||||
|
},
|
||||||
(error, stack) {
|
(error, stack) {
|
||||||
debugPrint('═══ Zone Error ═══════════════════════════════');
|
debugPrint('═══ Zone Error ═══════════════════════════════');
|
||||||
debugPrint(error.toString());
|
debugPrint(error.toString());
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../../core/auth/auth_state.dart';
|
import '../../core/auth/auth_state.dart';
|
||||||
import '../../core/config/app_config.dart';
|
import '../../core/config/app_config.dart';
|
||||||
|
import '../../core/config/app_info.dart';
|
||||||
import '../../core/theme/app_theme.dart';
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../models/number_rule.dart';
|
import '../../models/number_rule.dart';
|
||||||
import '../../models/user.dart';
|
import '../../models/user.dart';
|
||||||
@@ -828,9 +829,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
_AboutSection(
|
_AboutSection(
|
||||||
title: '关于我们',
|
title: '关于我们',
|
||||||
children: [
|
children: [
|
||||||
const _AboutRow(label: '开发商', value: '酒库科技有限公司'),
|
_AboutRow(label: '服务商', value: AppInfo.provider),
|
||||||
const _AboutRow(label: '官方网站', value: 'https://jiu.example.com'),
|
_AboutRow(label: '官方网站', value: AppInfo.website),
|
||||||
const _AboutRow(label: '联系邮箱', value: 'support@jiu.example.com'),
|
_AboutRow(label: '联系邮箱', value: AppInfo.email),
|
||||||
const _AboutRow(label: '技术支持', value: '周一至周五 9:00 - 18:00'),
|
const _AboutRow(label: '技术支持', value: '周一至周五 9:00 - 18:00'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
@@ -838,8 +839,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final uri = Uri.parse('mailto:support@jiu.example.com'
|
final uri = Uri.parse(
|
||||||
'?subject=酒库管理系统咨询');
|
'mailto:${AppInfo.email}?subject=酒库管理系统咨询');
|
||||||
if (await canLaunchUrl(uri)) launchUrl(uri);
|
if (await canLaunchUrl(uri)) launchUrl(uri);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.email_outlined, size: 16),
|
icon: const Icon(Icons.email_outlined, size: 16),
|
||||||
@@ -887,17 +888,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('续费 / 升级授权'),
|
title: const Text('续费 / 升级授权'),
|
||||||
content: const Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('请联系我们获取续费报价:'),
|
const Text('请联系我们获取续费报价:'),
|
||||||
SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SelectableText('📧 support@jiu.example.com',
|
SelectableText('📧 ${AppInfo.email}',
|
||||||
style: TextStyle(fontSize: 13)),
|
style: const TextStyle(fontSize: 13)),
|
||||||
SizedBox(height: 6),
|
|
||||||
SelectableText('📞 400-000-0000',
|
|
||||||
style: TextStyle(fontSize: 13)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -908,7 +906,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Clipboard.setData(
|
await Clipboard.setData(
|
||||||
const ClipboardData(text: 'support@jiu.example.com'));
|
ClipboardData(text: AppInfo.email));
|
||||||
if (ctx.mounted) {
|
if (ctx.mounted) {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -952,7 +950,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
final subject = Uri.encodeComponent(isBug ? 'Bug反馈' : '功能建议');
|
final subject = Uri.encodeComponent(isBug ? 'Bug反馈' : '功能建议');
|
||||||
final body = Uri.encodeComponent(ctrl.text);
|
final body = Uri.encodeComponent(ctrl.text);
|
||||||
final uri = Uri.parse(
|
final uri = Uri.parse(
|
||||||
'mailto:support@jiu.example.com?subject=$subject&body=$body');
|
'mailto:${AppInfo.email}?subject=$subject&body=$body');
|
||||||
if (await canLaunchUrl(uri)) launchUrl(uri);
|
if (await canLaunchUrl(uri)) launchUrl(uri);
|
||||||
if (ctx.mounted) Navigator.pop(ctx);
|
if (ctx.mounted) Navigator.pop(ctx);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ flutter:
|
|||||||
|
|
||||||
assets:
|
assets:
|
||||||
- assets/fonts/NotoSansSC-Regular.ttf
|
- assets/fonts/NotoSansSC-Regular.ttf
|
||||||
|
- assets/config/app_info.json
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
|
|
||||||
|
// 回归测试:打印用的中文字体必须是 TrueType(glyf) 轮廓。
|
||||||
|
// 若误用 OpenType/CFF 字体(如 Noto 的 .otf),dart_pdf 渲染中文会抛
|
||||||
|
// "This font does not support Unicode characters",标签打印会在原生 onLayout
|
||||||
|
// 回调里抛异常导致 Windows 打印卡死。此测试确保打印 PDF 能正常生成中文。
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('打印字体能渲染中文(防止退回 CFF 字体导致打印卡死)', () async {
|
||||||
|
final data = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf');
|
||||||
|
final font = pw.Font.ttf(data);
|
||||||
|
|
||||||
|
final doc = pw.Document();
|
||||||
|
doc.addPage(pw.Page(
|
||||||
|
build: (_) => pw.Text('入库单 茅台飞天 53度 500ml 扫码溯源',
|
||||||
|
style: pw.TextStyle(font: font, fontSize: 12)),
|
||||||
|
));
|
||||||
|
|
||||||
|
// 若字体是 CFF,会在此抛 Latin1/Unicode 异常。
|
||||||
|
final bytes = await doc.save();
|
||||||
|
expect(bytes.isNotEmpty, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
#include <flutter/flutter_view_controller.h>
|
#include <flutter/flutter_view_controller.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "flutter_window.h"
|
#include "flutter_window.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
@@ -27,7 +29,19 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|||||||
FlutterWindow window(project);
|
FlutterWindow window(project);
|
||||||
Win32Window::Point origin(10, 10);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(1280, 720);
|
Win32Window::Size size(1280, 720);
|
||||||
if (!window.Create(L"jiu_client", origin, size)) {
|
// Window title as UTF-8 bytes (hex-escaped to keep this source pure ASCII and
|
||||||
|
// avoid MSVC C4819 under GBK code page 936). Decoded to UTF-16 for Win32.
|
||||||
|
// Bytes below are the UTF-8 encoding of the Chinese app title.
|
||||||
|
const std::string title_utf8 =
|
||||||
|
"\xE5\xB2\xA9\xE7\xBE\x8E\xE9\x85\x92\xE5\xBA\x93";
|
||||||
|
int title_len = ::MultiByteToWideChar(CP_UTF8, 0, title_utf8.c_str(), -1,
|
||||||
|
nullptr, 0);
|
||||||
|
std::wstring window_title(title_len > 0 ? title_len - 1 : 0, L'\0');
|
||||||
|
if (title_len > 0) {
|
||||||
|
::MultiByteToWideChar(CP_UTF8, 0, title_utf8.c_str(), -1, &window_title[0],
|
||||||
|
title_len);
|
||||||
|
}
|
||||||
|
if (!window.Create(window_title, origin, size)) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ server {
|
|||||||
try_files /scan.html =404;
|
try_files /scan.html =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 桌面客户端安装包下载(Windows .exe / macOS .zip)
|
||||||
|
location /downloads/ {
|
||||||
|
alias /opt/jiu/downloads/;
|
||||||
|
add_header Content-Disposition "attachment";
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|
||||||
# 营销站点兜底(根路径及所有其他路径)
|
# 营销站点兜底(根路径及所有其他路径)
|
||||||
location / {
|
location / {
|
||||||
root /opt/jiu/marketing;
|
root /opt/jiu/marketing;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
; Inno Setup script for 岩美酒库 Windows 客户端
|
||||||
|
; Compiled in CI:
|
||||||
|
; ISCC /DAppVer=<ver> "/DSrcDir=<ReleaseDir>" "/DOutDir=<dist>" jiu-installer.iss
|
||||||
|
; Produces: <OutDir>\jiu-windows-x64-setup.exe
|
||||||
|
|
||||||
|
#ifndef AppVer
|
||||||
|
#define AppVer "0.0.0"
|
||||||
|
#endif
|
||||||
|
#ifndef SrcDir
|
||||||
|
#define SrcDir "."
|
||||||
|
#endif
|
||||||
|
#ifndef OutDir
|
||||||
|
#define OutDir "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define MyAppName "岩美酒库"
|
||||||
|
#define MyAppExe "jiu_client.exe"
|
||||||
|
#define MyAppPublisher "岩美"
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
AppId={{A3F5C1E2-9B47-4D8A-B6E0-2C1D4F8A9E33}
|
||||||
|
AppName={#MyAppName}
|
||||||
|
AppVersion={#AppVer}
|
||||||
|
AppPublisher={#MyAppPublisher}
|
||||||
|
DefaultDirName={autopf}\Jiu
|
||||||
|
DefaultGroupName={#MyAppName}
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
OutputDir={#OutDir}
|
||||||
|
OutputBaseFilename=jiu-windows-x64-setup
|
||||||
|
Compression=lzma2
|
||||||
|
SolidCompression=yes
|
||||||
|
WizardStyle=modern
|
||||||
|
ArchitecturesAllowed=x64
|
||||||
|
ArchitecturesInstallIn64BitMode=x64
|
||||||
|
UninstallDisplayName={#MyAppName}
|
||||||
|
UninstallDisplayIcon={app}\{#MyAppExe}
|
||||||
|
|
||||||
|
; 说明:未声明 [Languages],使用 Inno Setup 内置英文向导(简体中文语言文件
|
||||||
|
; ChineseSimplified.isl 默认不随 Inno Setup 安装,依赖它会导致编译找不到文件)。
|
||||||
|
; app 名称与快捷方式仍为中文(下方 UTF-8 字符串,与向导语言无关)。
|
||||||
|
|
||||||
|
[Tasks]
|
||||||
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: "{#SrcDir}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExe}"
|
||||||
|
Name: "{group}\卸载 {#MyAppName}"; Filename: "{uninstallexe}"
|
||||||
|
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExe}"; Tasks: desktopicon
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{app}\{#MyAppExe}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent
|
||||||
@@ -44,13 +44,37 @@ flutter build windows --release \
|
|||||||
"--dart-define=APP_VERSION=${TAG}"
|
"--dart-define=APP_VERSION=${TAG}"
|
||||||
popd > /dev/null
|
popd > /dev/null
|
||||||
|
|
||||||
# Package the Release folder into dist/ using PowerShell Compress-Archive.
|
# Package the Release folder into a Windows installer with Inno Setup (ISCC).
|
||||||
# Source is an absolute Windows path; destination is relative to the workspace.
|
# Inno Setup is installed by provision-windows.sh; check both Program Files dirs.
|
||||||
|
ISCC=""
|
||||||
|
for c in "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" "/c/Program Files/Inno Setup 6/ISCC.exe"; do
|
||||||
|
if [ -f "$c" ]; then ISCC="$c"; break; fi
|
||||||
|
done
|
||||||
|
if [ -z "$ISCC" ]; then
|
||||||
|
echo "ERROR: Inno Setup (ISCC) not found. Run provision-windows.sh first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run ISCC entirely under the clean build root: 32-bit ISCC would hit WOW64
|
||||||
|
# redirection if it read the .iss / wrote output under the System32 workspace.
|
||||||
|
cp deploy/windows/jiu-installer.iss "${BUILD_ROOT}/jiu-installer.iss"
|
||||||
|
mkdir -p "${BUILD_ROOT}/dist"
|
||||||
|
echo "==> building installer with Inno Setup (version ${VER})"
|
||||||
|
# MSYS_NO_PATHCONV / MSYS2_ARG_CONV_EXCL disable Git Bash's automatic conversion
|
||||||
|
# of /-leading args into Windows paths, which otherwise mangles ISCC's /D defines
|
||||||
|
# (turning them into extra "script filenames").
|
||||||
|
MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "$ISCC" \
|
||||||
|
"/DAppVer=${VER}" \
|
||||||
|
"/DSrcDir=C:\\jiu-build\\client\\build\\windows\\x64\\runner\\Release" \
|
||||||
|
"/DOutDir=C:\\jiu-build\\dist" \
|
||||||
|
"C:\\jiu-build\\jiu-installer.iss"
|
||||||
|
|
||||||
|
# Copy the installer back to the workspace dist/ via PowerShell (64-bit, so it
|
||||||
|
# can write the System32 workspace path without WOW64 redirection).
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
REL_WIN='C:\jiu-build\client\build\windows\x64\runner\Release'
|
echo "==> copying installer -> dist/jiu-windows-x64-setup.exe"
|
||||||
echo "==> packaging ${REL_WIN} -> dist/jiu-windows-x64.zip"
|
|
||||||
powershell -NoProfile -Command \
|
powershell -NoProfile -Command \
|
||||||
"Compress-Archive -Path '${REL_WIN}\*' -DestinationPath 'dist\jiu-windows-x64.zip' -Force"
|
"Copy-Item 'C:\\jiu-build\\dist\\jiu-windows-x64-setup.exe' 'dist\\jiu-windows-x64-setup.exe' -Force"
|
||||||
|
|
||||||
echo "==> compile-windows: done — dist/ contents:"
|
echo "==> compile-windows: done — dist/ contents:"
|
||||||
ls -lh dist/
|
ls -lh dist/
|
||||||
|
|||||||
+18
-7
@@ -90,6 +90,11 @@ rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no
|
|||||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \
|
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \
|
||||||
/tmp/jiu-marketing-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/"
|
/tmp/jiu-marketing-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/"
|
||||||
|
|
||||||
|
# Upload desktop client installers (served from /downloads/ by nginx).
|
||||||
|
# Guarded: may be absent in a partial manual deploy.
|
||||||
|
[ -f dist/jiu-windows-x64-setup.exe ] && ${SCP} dist/jiu-windows-x64-setup.exe "${EC2_USER}@${EC2_HOST}:/tmp/jiu-windows-x64-setup.exe" || true
|
||||||
|
[ -f dist/jiu-macos-x64.zip ] && ${SCP} dist/jiu-macos-x64.zip "${EC2_USER}@${EC2_HOST}:/tmp/jiu-macos-x64.zip" || true
|
||||||
|
|
||||||
echo "==> deploy: running remote deploy"
|
echo "==> deploy: running remote deploy"
|
||||||
|
|
||||||
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
|
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
|
||||||
@@ -126,13 +131,19 @@ mkdir -p /opt/jiu/marketing
|
|||||||
rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/
|
rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/
|
||||||
rm -rf /tmp/jiu-marketing-new
|
rm -rf /tmp/jiu-marketing-new
|
||||||
|
|
||||||
# Update nginx config and reload.
|
# Publish desktop client installers to the nginx-served downloads dir.
|
||||||
# reload-or-restart: reload if nginx is running, start it if it's not — avoids
|
# Keep only the latest version: clear old installers, then drop in the new ones.
|
||||||
# `nginx -s reload` failing with "invalid PID number" when nginx isn't running
|
mkdir -p /opt/jiu/downloads
|
||||||
# (e.g. after an EC2 reboot where nginx didn't auto-start).
|
rm -f /opt/jiu/downloads/jiu-windows-* /opt/jiu/downloads/jiu-macos-* 2>/dev/null || true
|
||||||
sudo cp /tmp/nginx-jiu.conf /etc/nginx/conf.d/jiu.conf
|
[ -f /tmp/jiu-windows-x64-setup.exe ] && mv -f /tmp/jiu-windows-x64-setup.exe /opt/jiu/downloads/ || true
|
||||||
sudo systemctl enable nginx >/dev/null 2>&1 || true # auto-start on future reboots
|
[ -f /tmp/jiu-macos-x64.zip ] && mv -f /tmp/jiu-macos-x64.zip /opt/jiu/downloads/ || true
|
||||||
sudo nginx -t && sudo systemctl reload-or-restart nginx
|
|
||||||
|
# Update jiu nginx config in the pangolin-edge reverse proxy.
|
||||||
|
# nginx now runs inside the `pangolin-edge` container (host networking), not on
|
||||||
|
# the host. Its /etc/nginx/conf.d is bind-mounted from the host dir below, so we
|
||||||
|
# write the config there and reload nginx *inside the container*.
|
||||||
|
cp /tmp/nginx-jiu.conf /home/ec2-user/pangolin/edge/conf.d/jiu.conf
|
||||||
|
docker exec pangolin-edge nginx -t && docker exec pangolin-edge nginx -s reload
|
||||||
|
|
||||||
echo "Deploy complete!"
|
echo "Deploy complete!"
|
||||||
ENDSSH
|
ENDSSH
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# install-innosetup.ps1 — robust silent install of Inno Setup 6 for the Windows runner.
|
||||||
|
# Idempotent: exits 0 immediately if ISCC.exe is already present.
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$candidates = @(
|
||||||
|
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
|
||||||
|
"$env:ProgramFiles\Inno Setup 6\ISCC.exe"
|
||||||
|
)
|
||||||
|
foreach ($c in $candidates) {
|
||||||
|
if (Test-Path $c) { Write-Output "ISCC already present: $c"; exit 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = 'https://jrsoftware.org/download.php/is.exe'
|
||||||
|
$out = Join-Path $env:TEMP 'innosetup-installer.exe'
|
||||||
|
|
||||||
|
Write-Output "Downloading Inno Setup: $url"
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
# -UseBasicParsing avoids the IE-engine dependency that fails under the SYSTEM account.
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing
|
||||||
|
|
||||||
|
$size = (Get-Item $out).Length
|
||||||
|
Write-Output "Downloaded $size bytes"
|
||||||
|
if ($size -lt 1000000) { throw "Downloaded file too small ($size bytes) - not a valid installer" }
|
||||||
|
|
||||||
|
Write-Output "Installing silently..."
|
||||||
|
$log = Join-Path $env:TEMP 'innosetup-install.log'
|
||||||
|
$p = Start-Process -FilePath $out -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/SP-','/NOICONS',"/LOG=$log" -Wait -PassThru
|
||||||
|
Write-Output "Installer exit code: $($p.ExitCode)"
|
||||||
|
if ($p.ExitCode -ne 0) {
|
||||||
|
Write-Output "----- install log tail -----"
|
||||||
|
if (Test-Path $log) { Get-Content $log -Tail 40 | ForEach-Object { Write-Output $_ } }
|
||||||
|
throw "Inno Setup installer failed with exit code $($p.ExitCode)"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($c in $candidates) {
|
||||||
|
if (Test-Path $c) { Write-Output "ISCC installed: $c"; exit 0 }
|
||||||
|
}
|
||||||
|
throw "ISCC not found after install (looked in: $($candidates -join ', '))"
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
# provision-windows.sh — idempotent provisioning for the Windows host runner.
|
# provision-windows.sh — idempotent provisioning for the Windows host runner.
|
||||||
#
|
#
|
||||||
# Pre-warms what compile-windows.sh would download at runtime: Flutter Windows
|
# Pre-warms what compile-windows.sh would download at runtime: Flutter Windows
|
||||||
# engine artifacts and pub packages. Short-circuits via a stamp file keyed to
|
# engine artifacts, pub packages, and the Inno Setup compiler (ISCC) used to
|
||||||
# the installed Flutter version, so repeat runs return immediately.
|
# build the installer. The Flutter part short-circuits via a stamp keyed to the
|
||||||
|
# installed Flutter version; Inno Setup is ensured on every run (cheap when
|
||||||
|
# already present) so it self-heals independently of the Flutter stamp.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(dirname "$0")"
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
@@ -15,7 +17,23 @@ STAMP="${STAMP_DIR}/provisioned-windows"
|
|||||||
CUR="${STAMP_DIR}/.flutter-version-win.cur"
|
CUR="${STAMP_DIR}/.flutter-version-win.cur"
|
||||||
mkdir -p "$STAMP_DIR"
|
mkdir -p "$STAMP_DIR"
|
||||||
|
|
||||||
# --- detection ---
|
# --- ensure Inno Setup (ISCC) for building the installer (always checked) ---
|
||||||
|
# ISCC installs to Program Files (x86) (32-bit app); check both to be safe.
|
||||||
|
ISCC_X86="/c/Program Files (x86)/Inno Setup 6/ISCC.exe"
|
||||||
|
ISCC_X64="/c/Program Files/Inno Setup 6/ISCC.exe"
|
||||||
|
if [ ! -f "$ISCC_X86" ] && [ ! -f "$ISCC_X64" ]; then
|
||||||
|
echo "==> installing Inno Setup 6 (silent)"
|
||||||
|
# Robust installer (checks download size + installer exit code) — inline
|
||||||
|
# bash->powershell quoting was the original failure; a .ps1 avoids it.
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "${SCRIPT_DIR}/install-innosetup.ps1"
|
||||||
|
if [ ! -f "$ISCC_X86" ] && [ ! -f "$ISCC_X64" ]; then
|
||||||
|
echo "ERROR: Inno Setup install failed — ISCC not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> Inno Setup installed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- detection (Flutter) ---
|
||||||
if ! command -v flutter >/dev/null 2>&1; then
|
if ! command -v flutter >/dev/null 2>&1; then
|
||||||
echo "ERROR: flutter not found on PATH. Install Flutter on this runner first." >&2
|
echo "ERROR: flutter not found on PATH. Install Flutter on this runner first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -44,9 +44,13 @@ notes = sys.argv[2]
|
|||||||
tag = sys.argv[3]
|
tag = sys.argv[3]
|
||||||
furl = sys.argv[4].rstrip('/')
|
furl = sys.argv[4].rstrip('/')
|
||||||
repo = sys.argv[5]
|
repo = sys.argv[5]
|
||||||
base = f"{furl}/{repo}/releases/download/{tag}"
|
# Public download URLs are served by jiu.51yanmei.com (same-origin HTTPS via the
|
||||||
|
# pangolin nginx /downloads/ location), NOT the LAN-only HTTP Forgejo instance —
|
||||||
|
# otherwise the HTTPS download page hands the browser an http://192.168.x URL and
|
||||||
|
# Chrome blocks it as insecure / it's unreachable from the public internet.
|
||||||
|
base = "https://jiu.51yanmei.com/downloads"
|
||||||
mac_url = f"{base}/jiu-macos-x64.zip"
|
mac_url = f"{base}/jiu-macos-x64.zip"
|
||||||
win_url = f"{base}/jiu-windows-x64.zip"
|
win_url = f"{base}/jiu-windows-x64-setup.exe"
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
with open('backend/config/version.yaml') as f:
|
with open('backend/config/version.yaml') as f:
|
||||||
@@ -128,6 +132,6 @@ upload_asset dist/web.tar.gz
|
|||||||
upload_asset dist/marketing.tar.gz
|
upload_asset dist/marketing.tar.gz
|
||||||
upload_asset dist/configs.tar.gz
|
upload_asset dist/configs.tar.gz
|
||||||
upload_asset dist/jiu-macos-x64.zip
|
upload_asset dist/jiu-macos-x64.zip
|
||||||
upload_asset dist/jiu-windows-x64.zip
|
upload_asset dist/jiu-windows-x64-setup.exe
|
||||||
|
|
||||||
echo "==> release: done — Release ${TAG} created"
|
echo "==> release: done — Release ${TAG} created"
|
||||||
|
|||||||
Reference in New Issue
Block a user