Compare commits

..

13 Commits

Author SHA1 Message Date
wangjia 056ae914ce fix(client): Windows main.cpp 用 ASCII 十六进制转义写标题,修复 C4819
Deploy / build-linux-web (push) Successful in 52s
Deploy / build-windows (push) Successful in 1m38s
Deploy / build-macos (push) Successful in 1m3s
Deploy / release-deploy (push) Successful in 1m25s
v1.0.14 Windows 构建失败:main.cpp 含中文(u8 标题+中文注释),MSVC 在
GBK(936) 代码页报 C4819「字符无法表示」,而 runner 开了 /WX 警告即错误。
改为标题用 UTF-8 字节的 \x 十六进制转义、注释改英文,源码纯 ASCII,
运行时 MultiByteToWideChar(CP_UTF8) 解码,窗口标题仍是岩美酒库。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:32:48 +08:00
wangjia 022443476c docs(changelog): 1.0.14(Windows 打印修复 + 关于我们真实信息/配置化)
Deploy / build-linux-web (push) Successful in 50s
Deploy / build-windows (push) Failing after 1m29s
Deploy / build-macos (push) Successful in 1m1s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:48:27 +08:00
wangjia 93f4f87e44 fix(client): 打印字体改 TrueType,修复 Windows 打印卡死
打印字体实为 OpenType/CFF,dart_pdf 只支持 TrueType(glyf),渲染中文
抛 'does not support Unicode';标签打印时异常抛在原生 onLayout 回调内
导致打印插件死等->卡死。用 fonttools 将字体转为 TrueType/glyf 替换。
加回归测试防止退回 CFF 字体。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 01:08:45 +08:00
wangjia 20e7d07be3 feat(client): 关于我们内容改用配置文件 + 窗口标题改岩美酒库
- 新增 assets/config/app_info.json(服务商/网站/邮箱真实值),启动时
  AppInfo.load() 读取,不硬编码、不走环境变量
- 关于我们/续费/反馈处的服务商、网站、邮箱统一读配置
- Windows 窗口标题 jiu_client -> 岩美酒库(u8 + UTF-8→UTF-16 防乱码)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 01:08:45 +08:00
wangjia 1834740676 fix(ci): Inno Setup 去掉中文语言段,用内置英文向导
Deploy / build-windows (push) Successful in 1m37s
Deploy / build-linux-web (push) Successful in 46s
Deploy / build-macos (push) Successful in 1m0s
Deploy / release-deploy (push) Successful in 1m21s
ChineseSimplified.isl 默认不随 Inno Setup 安装,引用它导致编译报
找不到文件。去掉 [Languages] 段用内置默认(英文)向导;app 名称与
快捷方式仍为中文。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:34:44 +08:00
wangjia 90a194fc00 fix(ci): ISCC 调用关闭 MSYS 参数路径转换,修复 more than one script filename
Deploy / build-linux-web (push) Successful in 50s
Deploy / build-windows (push) Failing after 1m22s
Deploy / build-macos (push) Successful in 59s
Deploy / release-deploy (push) Has been skipped
Git Bash(MSYS) 把 /D 开头的参数当 POSIX 路径自动转换,导致 ISCC 的
/DAppVer /DSrcDir /DOutDir 丢失 /D 前缀、被当成多个脚本文件名而报错。
调用 ISCC 时设 MSYS_NO_PATHCONV=1 + MSYS2_ARG_CONV_EXCL='*' 关闭转换。
install-innosetup.ps1 顺带加 /LOG 失败时打印安装日志。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:05:09 +08:00
wangjia fba8b0a917 fix(ci): 健壮化 Inno Setup 安装 + 部署仅保留最新安装包
Deploy / build-linux-web (push) Successful in 51s
Deploy / build-macos (push) Successful in 57s
Deploy / build-windows (push) Failing after 1m22s
Deploy / release-deploy (push) Has been skipped
- 新增 install-innosetup.ps1:校验下载大小、检查安装程序退出码、双
  Program Files 目录定位 ISCC,修复原内联命令静默成功但没真正装上的问题
  (SYSTEM 账户下 Invoke-WebRequest 无 -UseBasicParsing + 未检查退出码)
- provision/compile-windows.sh 在两个 Program Files 目录查找 ISCC
- deploy.sh 发布安装包到 /opt/jiu/downloads 前先清旧包,仅保留最新版本

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 08:42:48 +08:00
wangjia c001f0e8e6 feat(client): Windows 提供 Inno Setup 安装程序 + 下载改同源 HTTPS
Deploy / build-windows (push) Failing after 22s
Deploy / build-linux-web (push) Failing after 14m4s
Deploy / build-macos (push) Has been skipped
Deploy / release-deploy (push) Has been skipped
- Windows 客户端打成 jiu-windows-x64-setup.exe(Inno Setup,中文向导、
  桌面/开始菜单快捷方式、卸载项),替代解压版 zip
- provision-windows.sh 自动静默安装 Inno Setup(缺则装)
- 下载 URL 从内网 HTTP Forgejo 改为 https://jiu.51yanmei.com/downloads/,
  修复 HTTPS 页面下发 http 内网地址被浏览器拦截(无法安全下载),win/mac 均生效
- deploy.sh 将安装包发布到 EC2 /opt/jiu/downloads/,nginx 新增 /downloads/ 路由

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:46:41 +08:00
wangjia f039bd0c61 fix(ci): deploy 适配容器化 nginx(pangolin-edge)
Deploy / build-windows (push) Failing after 25s
Deploy / build-linux-web (push) Successful in 1m14s
Deploy / build-macos (push) Successful in 1m12s
Deploy / release-deploy (push) Has been skipped
nginx 已从主机迁入 pangolin-edge 容器(host 网络模式),其
/etc/nginx/conf.d 由主机 ~/pangolin/edge/conf.d bind-mount。
deploy 改为:jiu.conf 写入该挂载目录 + docker exec pangolin-edge
nginx -t/-s reload,不再 cp 到主机 /etc/nginx 或 systemctl 操作
已废弃的主机 nginx。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:20:50 +08:00
wangjia f4beb90769 fix(ci): nginx reload 改用 systemctl reload-or-restart,避免空 PID 文件报错
Deploy / build-linux-web (push) Successful in 1m1s
Deploy / build-windows (push) Successful in 1m42s
Deploy / build-macos (push) Successful in 1m2s
Deploy / release-deploy (push) Failing after 1m17s
nginx -s reload 依赖 /run/nginx.pid,该文件为空时报 invalid PID number。
改用 systemctl reload-or-restart(经 systemd MAINPID 发 HUP,运行中则 reload、
未运行则启动),并 enable nginx 保证重启后自启。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:33:24 +08:00
wangjia 6f0b273950 fix(ci): Windows 构建清除 cp 复制的插件软链,避免 PathExists(errno 183)
Deploy / build-windows (push) Successful in 1m31s
Deploy / build-linux-web (push) Successful in 55s
Deploy / build-macos (push) Successful in 58s
Deploy / release-deploy (push) Failing after 1m20s
cp -r 把 windows/flutter/ephemeral/.plugin_symlinks 复制成真实目录,
flutter createPluginSymlinks 只清软链、清不掉真目录,重建时撞车。
复制后删除 ephemeral/.dart_tool/build,由 flutter 干净重建。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:06:36 +08:00
wangjia b43601b4fd ci: 运行环境自愈预置 + 发版任务串行 + 删 ci.yml/暂停备份
Deploy / build-windows (push) Failing after 25s
Deploy / build-macos (push) Has been cancelled
Deploy / release-deploy (push) Has been cancelled
Deploy / build-linux-web (push) Has been cancelled
- 新增 scripts/ci/provision-{mac,windows}.sh:幂等预置,按 Flutter 版本
  stamp 短路,pipeline 首跑自动 precache + 预热缓存,运行时不再下载 engine
- scripts/ci/_env.sh:抽出共用镜像 env,compile 脚本统一 source
- deploy.yml:build-macos needs build-linux-web,同一 mac runner 串行
  执行,避免排队超时;各 build job 加 Provision 前置步骤
- 删除 .gitea/workflows/ci.yml
- backup.yml:暂停定时备份(保留 workflow_dispatch 手动触发)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 01:04:23 +08:00
wangjia 3aa63d6575 fix(ci): Windows 构建改用 C:\jiu-build 短路径,弃用 subst W: 映射
Deploy / build-windows (push) Successful in 1m41s
Deploy / build-linux-web (push) Failing after 14m36s
Deploy / build-macos (push) Successful in 33m24s
Deploy / release-deploy (push) Has been skipped
subst W: 因 cmd/MSYS 引号转义失败(报"找不到路径"),导致 W: 盘
未创建、cd /w/client 失败。改为将 client/ 复制到 System32 之外的
C:\jiu-build 构建,规避 WOW64 重定向,并用 PowerShell Compress-Archive 打包。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:23:09 +08:00
23 changed files with 475 additions and 91 deletions
+3 -2
View File
@@ -1,8 +1,9 @@
name: DB Backup
on:
schedule:
- cron: '0 18 * * *' # UTC 18:00 = 北京时间 02:00
# 定时备份已暂停(保留手动触发)。恢复时取消下面 schedule 的注释即可。
# schedule:
# - cron: '0 18 * * *' # UTC 18:00 = 北京时间 02:00
workflow_dispatch:
jobs:
-30
View File
@@ -1,30 +0,0 @@
name: CI
on:
push:
branches-ignore: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- name: Go tests
working-directory: backend
run: go test ./...
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Flutter tests
working-directory: client
run: flutter test
+13
View File
@@ -16,6 +16,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Provision (idempotent — auto-runs if host not initialized)
run: sh scripts/ci/provision-mac.sh
- name: Compile (Linux backend + Flutter Web)
run: sh scripts/ci/compile.sh "${{ gitea.ref_name }}"
@@ -26,6 +29,9 @@ jobs:
path: dist/
build-macos:
# Same physical runner as build-linux-web (label: mac, capacity 1). Run them
# serially via needs so the second job doesn't sit queued and time out.
needs: build-linux-web
runs-on: mac
env:
PUB_HOSTED_URL: https://pub.flutter-io.cn
@@ -34,6 +40,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Provision (idempotent — auto-runs if host not initialized)
run: sh scripts/ci/provision-mac.sh
- name: Compile (Flutter macOS)
run: sh scripts/ci/compile-macos.sh "${{ gitea.ref_name }}"
@@ -53,6 +62,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Provision (idempotent — auto-runs if host not initialized)
shell: bash
run: sh scripts/ci/provision-windows.sh
- name: Compile (Flutter Windows)
shell: bash
run: sh scripts/ci/compile-windows.sh "${{ gitea.ref_name }}"
+45
View File
@@ -5,6 +5,51 @@ 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/),
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
### 修复
- 修复 EC2 部署最后一步 nginx reload 失败(`invalid PID number`):改用 `systemctl reload-or-restart nginx`(经 systemd MAINPID 发 HUP,不依赖空的 /run/nginx.pid),并 `enable nginx` 确保重启后自启
## [1.0.7] - 2026-06-03
### 修复
- 修复 Windows 构建 `PathExistsException`errno 183):复制 client/ 到 C:\jiu-build 后,先清除 cp 复制成真实目录的插件软链(windows/flutter/ephemeral)及 .dart_tool/build,再交给 flutter 干净重建
## [1.0.6] - 2026-06-03
### 改进
- CI 运行环境自愈预置:mac/windows runner 首次构建自动 `flutter precache` 并预热 pub/go/npm 缓存(stamp 文件按 Flutter 版本短路),后续构建不再运行时下载 engine
- 发版流水线同一 mac runner 上的任务改为串行(build-macos 依赖 build-linux-web),避免排队等待超时失败
- 暂停定时数据库备份(保留手动触发)
## [1.0.5] - 2026-06-03
### 修复
- 修复 Windows CI 构建失败:放弃脆弱的 `subst W:` 路径映射,改为将 client/ 复制到 System32 之外的干净短路径(C:\jiu-build)构建,规避 WOW64 文件系统重定向
## [1.0.4] - 2026-05-30
### 新功能
+5
View File
@@ -0,0 +1,5 @@
{
"provider": "岩美技术有限公司",
"website": "https://jiu.yanmei.com",
"email": "yammy2023@163.com"
}
Binary file not shown.
+28
View File
@@ -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');
}
}
}
+6 -1
View File
@@ -4,6 +4,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'core/auth/auth_state.dart';
import 'core/config/app_info.dart';
import 'core/errors/error_reporter.dart';
import 'core/router/app_router.dart';
import 'core/theme/app_theme.dart';
@@ -24,7 +25,11 @@ void main() {
};
runZonedGuarded(
() => runApp(const ProviderScope(child: JiuApp())),
() async {
WidgetsFlutterBinding.ensureInitialized();
await AppInfo.load(); // 从配置文件加载「关于我们」品牌信息
runApp(const ProviderScope(child: JiuApp()));
},
(error, stack) {
debugPrint('═══ Zone Error ═══════════════════════════════');
debugPrint(error.toString());
@@ -11,6 +11,7 @@ import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/auth/auth_state.dart';
import '../../core/config/app_config.dart';
import '../../core/config/app_info.dart';
import '../../core/theme/app_theme.dart';
import '../../models/number_rule.dart';
import '../../models/user.dart';
@@ -828,9 +829,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
_AboutSection(
title: '关于我们',
children: [
const _AboutRow(label: '开发', value: '酒库科技有限公司'),
const _AboutRow(label: '官方网站', value: 'https://jiu.example.com'),
const _AboutRow(label: '联系邮箱', value: 'support@jiu.example.com'),
_AboutRow(label: '服务', value: AppInfo.provider),
_AboutRow(label: '官方网站', value: AppInfo.website),
_AboutRow(label: '联系邮箱', value: AppInfo.email),
const _AboutRow(label: '技术支持', value: '周一至周五 9:00 - 18:00'),
const SizedBox(height: 8),
Wrap(
@@ -838,8 +839,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
children: [
OutlinedButton.icon(
onPressed: () async {
final uri = Uri.parse('mailto:support@jiu.example.com'
'?subject=酒库管理系统咨询');
final uri = Uri.parse(
'mailto:${AppInfo.email}?subject=酒库管理系统咨询');
if (await canLaunchUrl(uri)) launchUrl(uri);
},
icon: const Icon(Icons.email_outlined, size: 16),
@@ -887,17 +888,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
context: context,
builder: (ctx) => AlertDialog(
title: const Text('续费 / 升级授权'),
content: const Column(
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('请联系我们获取续费报价:'),
SizedBox(height: 12),
SelectableText('📧 support@jiu.example.com',
style: TextStyle(fontSize: 13)),
SizedBox(height: 6),
SelectableText('📞 400-000-0000',
style: TextStyle(fontSize: 13)),
const Text('请联系我们获取续费报价:'),
const SizedBox(height: 12),
SelectableText('📧 ${AppInfo.email}',
style: const TextStyle(fontSize: 13)),
],
),
actions: [
@@ -908,7 +906,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
ElevatedButton(
onPressed: () async {
await Clipboard.setData(
const ClipboardData(text: 'support@jiu.example.com'));
ClipboardData(text: AppInfo.email));
if (ctx.mounted) {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
@@ -952,7 +950,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
final subject = Uri.encodeComponent(isBug ? 'Bug反馈' : '功能建议');
final body = Uri.encodeComponent(ctrl.text);
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 (ctx.mounted) Navigator.pop(ctx);
},
+1
View File
@@ -37,3 +37,4 @@ flutter:
assets:
- assets/fonts/NotoSansSC-Regular.ttf
- assets/config/app_info.json
+26
View File
@@ -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);
});
}
+15 -1
View File
@@ -2,6 +2,8 @@
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include <string>
#include "flutter_window.h"
#include "utils.h"
@@ -27,7 +29,19 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
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;
}
window.SetQuitOnClose(true);
+8
View File
@@ -59,6 +59,14 @@ server {
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 / {
root /opt/jiu/marketing;
+54
View File
@@ -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
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# _env.sh — shared CI mirror env. `source` this from other scripts.
# Idempotent: only sets vars if not already provided by the environment.
export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://pub.flutter-io.cn}"
export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://storage.flutter-io.cn}"
# Ensure Homebrew tools are on PATH (macOS runners); harmless elsewhere.
case ":${PATH}:" in
*":/opt/homebrew/bin:"*) ;;
*) export PATH="/opt/homebrew/bin:${PATH}" ;;
esac
+2 -3
View File
@@ -2,9 +2,8 @@
# compile-macos.sh <tag> — build Flutter macOS desktop, package into dist/
set -euo pipefail
export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://pub.flutter-io.cn}"
export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://storage.flutter-io.cn}"
# shellcheck source=scripts/ci/_env.sh
. "$(dirname "$0")/_env.sh"
TAG="$1"
VER="${TAG#v}"
+56 -29
View File
@@ -2,52 +2,79 @@
# compile-windows.sh <tag> — build Flutter Windows desktop, package into dist/
set -euo pipefail
export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://pub.flutter-io.cn}"
export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://storage.flutter-io.cn}"
# shellcheck source=scripts/ci/_env.sh
. "$(dirname "$0")/_env.sh"
TAG="$1"
VER="${TAG#v}"
echo "==> compile-windows: version=${VER}"
# Sync Flutter pubspec version
# Sync Flutter pubspec version (done in the checkout, before copying out)
sed -i "s/^version:.*/version: ${VER}+1/" client/pubspec.yaml
# Build Flutter Windows
# Use subst to create a short path (W:) to avoid WOW64 redirection issue
# when workspace is under C:\Windows\System32\config\systemprofile
WORKSPACE_WIN=$(cygpath -w "$(pwd)")
echo "==> workspace: $WORKSPACE_WIN"
cmd //c "subst W: \"${WORKSPACE_WIN}\"" || true
# The Forgejo Windows runner checks out under
# C:\Windows\System32\config\systemprofile\.cache\act\...\hostexecutor
# Building there triggers WOW64 filesystem redirection (System32 -> SysWOW64)
# which breaks CMake/Flutter native paths. So copy the Flutter project to a
# clean short path OUTSIDE System32 and build there. This replaces the previous
# `subst W:` trick, which was fragile due to cmd/MSYS quoting.
BUILD_ROOT="/c/jiu-build"
BUILD_CLIENT="${BUILD_ROOT}/client"
cd /w/client
echo "==> copying client/ to ${BUILD_CLIENT}"
rm -rf "$BUILD_ROOT"
mkdir -p "$BUILD_ROOT"
cp -r client "$BUILD_CLIENT"
# `cp -r` turns Flutter's plugin symlinks (windows/flutter/ephemeral/.plugin_symlinks/*)
# into real directories. flutter's createPluginSymlinks only cleans up *symlinks*, so it
# then fails to create a symlink over the copied real dir (errno 183 / PathExists).
# Drop all regenerable artifacts so the build below recreates them cleanly.
rm -rf "${BUILD_CLIENT}/windows/flutter/ephemeral" \
"${BUILD_CLIENT}/.dart_tool" \
"${BUILD_CLIENT}/build"
# Build (run from the clean path; pushd/popd avoids needing $() to save cwd)
pushd "$BUILD_CLIENT" > /dev/null
flutter create --platforms=windows . --project-name jiu_client
flutter build windows --release \
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
"--dart-define=APP_VERSION=${TAG}"
cd /w
popd > /dev/null
cmd //c "subst W: /d" || true
# Package the Release folder into a Windows installer with Inno Setup (ISCC).
# 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
# Package release folder into zip using Python (available on all platforms)
# 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
python3 - <<'PYEOF'
import zipfile, os
src = os.path.join('W:\\', 'client', 'build', 'windows', 'x64', 'runner', 'Release') if os.path.exists('W:\\') else os.path.join('client', 'build', 'windows', 'x64', 'runner', 'Release')
dst = os.path.join('dist', 'jiu-windows-x64.zip')
with zipfile.ZipFile(dst, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(src):
for fname in files:
fp = os.path.join(root, fname)
zf.write(fp, os.path.relpath(fp, src))
size = os.path.getsize(dst)
print(f'Packaged {dst} ({size // 1024 // 1024} MB)')
PYEOF
echo "==> copying installer -> dist/jiu-windows-x64-setup.exe"
powershell -NoProfile -Command \
"Copy-Item 'C:\\jiu-build\\dist\\jiu-windows-x64-setup.exe' 'dist\\jiu-windows-x64-setup.exe' -Force"
echo "==> compile-windows: done — dist/ contents:"
ls -lh dist/
+2 -4
View File
@@ -2,10 +2,8 @@
# compile.sh <tag> — build backend + Flutter web, package into dist/
set -euo pipefail
export PATH="/opt/homebrew/bin:$PATH"
export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://pub.flutter-io.cn}"
export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://storage.flutter-io.cn}"
# shellcheck source=scripts/ci/_env.sh
. "$(dirname "$0")/_env.sh"
TAG="$1"
VER="${TAG#v}"
+18 -3
View File
@@ -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" \
/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"
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
@@ -126,9 +131,19 @@ mkdir -p /opt/jiu/marketing
rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/
rm -rf /tmp/jiu-marketing-new
# Update nginx config and reload
sudo cp /tmp/nginx-jiu.conf /etc/nginx/conf.d/jiu.conf
sudo nginx -t && sudo nginx -s reload
# Publish desktop client installers to the nginx-served downloads dir.
# Keep only the latest version: clear old installers, then drop in the new ones.
mkdir -p /opt/jiu/downloads
rm -f /opt/jiu/downloads/jiu-windows-* /opt/jiu/downloads/jiu-macos-* 2>/dev/null || true
[ -f /tmp/jiu-windows-x64-setup.exe ] && mv -f /tmp/jiu-windows-x64-setup.exe /opt/jiu/downloads/ || true
[ -f /tmp/jiu-macos-x64.zip ] && mv -f /tmp/jiu-macos-x64.zip /opt/jiu/downloads/ || true
# 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!"
ENDSSH
+38
View File
@@ -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 ', '))"
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# provision-mac.sh — idempotent provisioning for the macOS host runner.
#
# Pre-warms everything the build jobs (compile.sh / compile-macos.sh) would
# otherwise download at runtime: Flutter macOS+web engine artifacts, pub
# packages, Go modules, npm deps. Safe to run on every pipeline run — it
# short-circuits via a stamp file keyed to the installed Flutter version, so
# once provisioned it returns in milliseconds and downloads nothing.
set -euo pipefail
SCRIPT_DIR="$(dirname "$0")"
# shellcheck source=scripts/ci/_env.sh
. "${SCRIPT_DIR}/_env.sh"
STAMP_DIR="${HOME}/.cache/jiu-ci"
STAMP="${STAMP_DIR}/provisioned-mac"
CUR="${STAMP_DIR}/.flutter-version-mac.cur"
mkdir -p "$STAMP_DIR"
# --- detection: skip if already provisioned for this Flutter version ---
if ! command -v flutter >/dev/null 2>&1; then
echo "ERROR: flutter not found on PATH. Install Flutter on this runner first." >&2
exit 1
fi
flutter --version --machine > "$CUR" 2>/dev/null || flutter --version > "$CUR" 2>&1 || true
if [ -s "$STAMP" ] && cmp -s "$STAMP" "$CUR"; then
echo "==> provision-mac: already provisioned (Flutter unchanged), skipping."
exit 0
fi
echo "==> provision-mac: not provisioned (or Flutter changed) — running."
# --- toolchain presence (do not auto-install system-level tools) ---
for tool in go node npm; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "ERROR: '$tool' not found on PATH. Install it on this runner first." >&2
exit 1
fi
done
# --- warm Flutter platform engines (the macOS SDK download seen in CI) ---
echo "==> precache Flutter engines (macos, web)"
flutter precache --macos --web --universal
# --- warm dependency caches ---
echo "==> flutter pub get (client)"
(cd "${SCRIPT_DIR}/../../client" && flutter pub get)
echo "==> go mod download (backend)"
(cd "${SCRIPT_DIR}/../../backend" && go mod download)
echo "==> npm ci (web)"
(cd "${SCRIPT_DIR}/../../web" && npm ci --prefer-offline)
# --- mark provisioned ---
cp "$CUR" "$STAMP"
echo "==> provision-mac: done."
echo "--- versions ---"
flutter --version | head -1
go version
node --version
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# provision-windows.sh — idempotent provisioning for the Windows host runner.
#
# Pre-warms what compile-windows.sh would download at runtime: Flutter Windows
# engine artifacts, pub packages, and the Inno Setup compiler (ISCC) used to
# 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
SCRIPT_DIR="$(dirname "$0")"
# shellcheck source=scripts/ci/_env.sh
. "${SCRIPT_DIR}/_env.sh"
STAMP_DIR="${HOME}/.cache/jiu-ci"
STAMP="${STAMP_DIR}/provisioned-windows"
CUR="${STAMP_DIR}/.flutter-version-win.cur"
mkdir -p "$STAMP_DIR"
# --- 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
echo "ERROR: flutter not found on PATH. Install Flutter on this runner first." >&2
exit 1
fi
flutter --version --machine > "$CUR" 2>/dev/null || flutter --version > "$CUR" 2>&1 || true
if [ -s "$STAMP" ] && cmp -s "$STAMP" "$CUR"; then
echo "==> provision-windows: already provisioned (Flutter unchanged), skipping."
exit 0
fi
echo "==> provision-windows: not provisioned (or Flutter changed) — running."
# --- warm Flutter Windows engine + pub deps ---
echo "==> precache Flutter engine (windows)"
flutter precache --windows
echo "==> flutter pub get (client)"
(cd "${SCRIPT_DIR}/../../client" && flutter pub get)
# --- mark provisioned ---
cp "$CUR" "$STAMP"
echo "==> provision-windows: done."
flutter --version | head -1
+7 -3
View File
@@ -44,9 +44,13 @@ notes = sys.argv[2]
tag = sys.argv[3]
furl = sys.argv[4].rstrip('/')
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"
win_url = f"{base}/jiu-windows-x64.zip"
win_url = f"{base}/jiu-windows-x64-setup.exe"
lines = []
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/configs.tar.gz
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"