Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa02fdbfd8 | |||
| 211fafc7e1 | |||
| 5278bea940 | |||
| 0bc3f4b702 | |||
| cfeb8f0bba |
@@ -1,30 +0,0 @@
|
|||||||
# 一次性:把 Android release keystore 从 CI secret 导出为 artifact,便于本地/Bitwarden 备份。
|
|
||||||
# 用完务必删除:① 本 workflow 文件;② 跑出来的这次 run 的 artifact(含密钥)。
|
|
||||||
# 触发:Forgejo 网页 → Actions → Export Keystore → Run workflow(手动)。
|
|
||||||
name: Export Keystore
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
export:
|
|
||||||
runs-on: mac
|
|
||||||
steps:
|
|
||||||
- name: Restore keystore from secret
|
|
||||||
env:
|
|
||||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
mkdir -p out
|
|
||||||
# 原始 base64(方便直接粘进 Bitwarden Secure Note)
|
|
||||||
printf '%s' "$ANDROID_KEYSTORE_BASE64" > out/keystore.b64.txt
|
|
||||||
# 解码回二进制 keystore
|
|
||||||
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > out/jiu-release.jks
|
|
||||||
echo "base64 长度: $(wc -c < out/keystore.b64.txt)"
|
|
||||||
echo "jks 字节数: $(wc -c < out/jiu-release.jks)"
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: keystore-backup
|
|
||||||
path: out/
|
|
||||||
@@ -5,6 +5,21 @@ 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.21] - 2026-06-07
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- 商品详情页添加照片时,手机端(Android/iOS)支持直接拍照或从相册选择
|
||||||
|
|
||||||
|
## [1.0.20] - 2026-06-07
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复 Android 客户端登录页提示「服务不可达」、无法连接服务器的问题(正式版安装包此前缺少联网权限)
|
||||||
|
|
||||||
|
## [1.0.19] - 2026-06-07
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
- 优化 Android 构建与部署流程(CI/CD 内部调整),功能与界面无变化
|
||||||
|
|
||||||
## [1.0.18] - 2026-06-06
|
## [1.0.18] - 2026-06-06
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- release 包走此主清单,必须显式声明联网权限;debug/profile 清单由 Flutter 模板自动添加,
|
||||||
|
release 不带会导致所有网络请求失败(登录页显示「服务不可达」)。 -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<application
|
<application
|
||||||
android:label="岩美酒库"
|
android:label="岩美酒库"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>用于拍摄商品照片</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>用于从相册选择商品照片</string>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import '../../core/utils/dialog_util.dart';
|
import '../../core/utils/dialog_util.dart';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -91,7 +92,56 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移动端(Android/iOS)支持拍照,用 image_picker;桌面/Web 用 file_picker 选文件。
|
||||||
|
// 用 defaultTargetPlatform 而非 dart:io 的 Platform,避免在参与 Web 编译的文件里引入 dart:io。
|
||||||
|
bool get _isMobile =>
|
||||||
|
!kIsWeb &&
|
||||||
|
(defaultTargetPlatform == TargetPlatform.android ||
|
||||||
|
defaultTargetPlatform == TargetPlatform.iOS);
|
||||||
|
|
||||||
Future<void> _pickAndUpload() async {
|
Future<void> _pickAndUpload() async {
|
||||||
|
if (_isMobile) {
|
||||||
|
await _pickFromCameraOrGallery();
|
||||||
|
} else {
|
||||||
|
await _pickFromFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 移动端:底部弹出「拍照 / 从相册选择」
|
||||||
|
Future<void> _pickFromCameraOrGallery() async {
|
||||||
|
final source = await showModalBottomSheet<ImageSource>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.camera_alt_outlined),
|
||||||
|
title: const Text('拍照'),
|
||||||
|
onTap: () => Navigator.pop(ctx, ImageSource.camera),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.photo_library_outlined),
|
||||||
|
title: const Text('从相册选择'),
|
||||||
|
onTap: () => Navigator.pop(ctx, ImageSource.gallery),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (source == null) return;
|
||||||
|
|
||||||
|
final XFile? xfile = await ImagePicker().pickImage(
|
||||||
|
source: source,
|
||||||
|
maxWidth: 2000,
|
||||||
|
imageQuality: 85,
|
||||||
|
);
|
||||||
|
if (xfile == null) return;
|
||||||
|
await _uploadImage(filePath: xfile.path, fileName: xfile.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 桌面 / Web:文件选择器
|
||||||
|
Future<void> _pickFromFiles() async {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.image,
|
type: FileType.image,
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
@@ -102,13 +152,17 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
final String? filePath = kIsWeb ? null : file.path;
|
final String? filePath = kIsWeb ? null : file.path;
|
||||||
final fileBytes = file.bytes;
|
final fileBytes = file.bytes;
|
||||||
if (filePath == null && fileBytes == null) return;
|
if (filePath == null && fileBytes == null) return;
|
||||||
|
await _uploadImage(
|
||||||
|
filePath: filePath, bytes: fileBytes, fileName: file.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _uploadImage(
|
||||||
|
{String? filePath, Uint8List? bytes, required String fileName}) async {
|
||||||
setState(() => _uploading = true);
|
setState(() => _uploading = true);
|
||||||
try {
|
try {
|
||||||
final img = await ref
|
final img = await ref.read(productRepositoryProvider).uploadImage(
|
||||||
.read(productRepositoryProvider)
|
_product!.id, filePath,
|
||||||
.uploadImage(_product!.id, filePath,
|
bytes: bytes, fileName: fileName);
|
||||||
bytes: fileBytes, fileName: file.name);
|
|
||||||
_updateImages([..._product!.images, img]);
|
_updateImages([..._product!.images, img]);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import file_picker
|
import file_picker
|
||||||
|
import file_selector_macos
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import printing
|
import printing
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
@@ -13,6 +14,7 @@ import url_launcher_macos
|
|||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
|||||||
@@ -161,6 +161,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.3.7"
|
version: "8.3.7"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.5"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+5"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -253,6 +285,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.0"
|
version: "4.3.0"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+19"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+6"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2+1"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ dependencies:
|
|||||||
printing: ^5.13.0
|
printing: ^5.13.0
|
||||||
pdf: ^3.10.8
|
pdf: ^3.10.8
|
||||||
web: ^1.1.0
|
web: ^1.1.0
|
||||||
|
image_picker: ^1.2.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <printing/printing_plugin.h>
|
#include <printing/printing_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
PrintingPluginRegisterWithRegistrar(
|
PrintingPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
printing
|
printing
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|||||||
Executable
+116
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# render-env.sh — 从 Bitwarden 渲染 jiu 的 production.env(方案一)
|
||||||
|
#
|
||||||
|
# 配置基底 = production.env.template(非密钥项的权威来源,随仓库维护)。
|
||||||
|
# 密钥 = 从 Bitwarden 条目 DB_PASSWORD 的同名字段按需取,Mac 上不常驻明文。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# sh deploy/render-env.sh --check # 只校验:4 个密钥都能从金库取到、长度非空
|
||||||
|
# sh deploy/render-env.sh --print # 渲染到 stdout 预览(密钥打码,可安全粘贴)
|
||||||
|
# sh deploy/render-env.sh --out FILE # 渲染到指定文件(含明文,慎用,自负保管)
|
||||||
|
# sh deploy/render-env.sh --deploy # 渲染→scp 到 EC2→重启 jiu→健康检查→删本地临时文件
|
||||||
|
#
|
||||||
|
# 前提:rbw 已 unlock。
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---- 可配置项 ----
|
||||||
|
RBW="${RBW_BIN:-/opt/homebrew/bin/rbw}"
|
||||||
|
BW_ITEM="${JIU_BW_ITEM:-DB_PASSWORD}" # 存放 jiu 密钥字段的 Bitwarden 条目
|
||||||
|
SECRET_KEYS="DATABASE_DSN JWT_SECRET LICENSE_HMAC_SECRET DB_PASSWORD"
|
||||||
|
|
||||||
|
EC2_HOST="${EC2_HOST:-18.136.60.128}"
|
||||||
|
EC2_USER="${EC2_USER:-ec2-user}"
|
||||||
|
EC2_KEY="${EC2_KEY:-$HOME/.ssh/wangjia.pem}"
|
||||||
|
REMOTE_ENV="/opt/jiu/config/production.env"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
TEMPLATE="$SCRIPT_DIR/production.env.template"
|
||||||
|
|
||||||
|
# ---- 工具函数 ----
|
||||||
|
is_secret() {
|
||||||
|
case " $SECRET_KEYS " in *" $1 "*) return 0;; *) return 1;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
require_unlocked() {
|
||||||
|
if ! "$RBW" unlocked >/dev/null 2>&1; then
|
||||||
|
echo "render-env: 金库未解锁,请先运行: rbw unlock" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 渲染:逐行读模板,密钥行用金库值替换。$1=mask 时密钥打码。
|
||||||
|
render() {
|
||||||
|
local mask="${1:-}"
|
||||||
|
local line key val
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in
|
||||||
|
''|\#*) printf '%s\n' "$line"; continue ;;
|
||||||
|
esac
|
||||||
|
key="${line%%=*}"
|
||||||
|
if is_secret "$key"; then
|
||||||
|
val="$("$RBW" get --field "$key" "$BW_ITEM")"
|
||||||
|
if [ -z "$val" ]; then
|
||||||
|
echo "render-env: 金库条目 '$BW_ITEM' 的字段 '$key' 为空" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$mask" = "mask" ]; then
|
||||||
|
printf '%s=<%d 字符·已打码>\n' "$key" "${#val}"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$key" "$val"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' "$line"
|
||||||
|
fi
|
||||||
|
done < "$TEMPLATE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 主流程 ----
|
||||||
|
[ -f "$TEMPLATE" ] || { echo "render-env: 找不到模板 $TEMPLATE" >&2; exit 1; }
|
||||||
|
MODE="${1:---print}"
|
||||||
|
require_unlocked
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
--check)
|
||||||
|
for k in $SECRET_KEYS; do
|
||||||
|
v="$("$RBW" get --field "$k" "$BW_ITEM")"
|
||||||
|
printf ' %-22s %s\n' "$k" "$([ -n "$v" ] && echo "OK (${#v} 字符)" || echo "缺失 ❌")"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
|
||||||
|
--print)
|
||||||
|
render mask
|
||||||
|
;;
|
||||||
|
|
||||||
|
--out)
|
||||||
|
OUT="${2:?用法: render-env.sh --out <文件路径>}"
|
||||||
|
umask 077
|
||||||
|
render > "$OUT"
|
||||||
|
echo "render-env: 已写入 $OUT(含明文,注意保管)"
|
||||||
|
;;
|
||||||
|
|
||||||
|
--deploy)
|
||||||
|
TMP="$(mktemp -t jiu-prod-env)"
|
||||||
|
trap 'rm -f "$TMP"' EXIT
|
||||||
|
umask 077
|
||||||
|
render > "$TMP"
|
||||||
|
echo "render-env: 已本地渲染(临时文件,退出即删)"
|
||||||
|
echo "render-env: 上传到 $EC2_USER@$EC2_HOST:$REMOTE_ENV"
|
||||||
|
scp -i "$EC2_KEY" "$TMP" "$EC2_USER@$EC2_HOST:/tmp/production.env.new"
|
||||||
|
ssh -i "$EC2_KEY" "$EC2_USER@$EC2_HOST" bash <<REMOTE
|
||||||
|
set -e
|
||||||
|
mkdir -p /opt/jiu/config
|
||||||
|
mv /tmp/production.env.new "$REMOTE_ENV"
|
||||||
|
chmod 600 "$REMOTE_ENV"
|
||||||
|
sudo systemctl restart jiu
|
||||||
|
sleep 3
|
||||||
|
curl -sf http://localhost:8080/health >/dev/null && echo " 健康检查通过 ✅" || { echo " 健康检查失败 ❌"; exit 1; }
|
||||||
|
REMOTE
|
||||||
|
echo "render-env: 部署完成,EC2 上的 production.env 已更新并重启"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "未知参数: $MODE" >&2
|
||||||
|
sed -n '2,16p' "$0" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -20,20 +20,23 @@ echo "==> compile-android: version=${VER} build=${BUILD}"
|
|||||||
# Sync Flutter pubspec version (BSD sed on macOS)
|
# Sync Flutter pubspec version (BSD sed on macOS)
|
||||||
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml
|
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml
|
||||||
|
|
||||||
# --- release 签名:从 CI secrets 还原 keystore 与 key.properties ---
|
# --- release 签名(强制):从 CI secrets 还原 keystore 与 key.properties ---
|
||||||
if [ -n "${ANDROID_KEYSTORE_BASE64:-}" ]; then
|
# 必须 release 签名。缺任一签名 secret 直接失败,绝不回退 debug:
|
||||||
echo "==> compile-android: configuring release signing"
|
# debug 包用公开调试密钥签名,不能正式分发,也无法与正式版互相覆盖升级。
|
||||||
KEYSTORE_PATH="$(pwd)/client/android/jiu-release.jks"
|
: "${ANDROID_KEYSTORE_BASE64:?缺少 ANDROID_KEYSTORE_BASE64:未配置 Android release 签名,构建中止(不回退 debug)}"
|
||||||
echo "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${KEYSTORE_PATH}"
|
: "${ANDROID_KEYSTORE_PASSWORD:?缺少 ANDROID_KEYSTORE_PASSWORD:未配置 Android release 签名,构建中止}"
|
||||||
cat > client/android/key.properties <<EOF
|
: "${ANDROID_KEY_ALIAS:?缺少 ANDROID_KEY_ALIAS:未配置 Android release 签名,构建中止}"
|
||||||
|
: "${ANDROID_KEY_PASSWORD:?缺少 ANDROID_KEY_PASSWORD:未配置 Android release 签名,构建中止}"
|
||||||
|
|
||||||
|
echo "==> compile-android: configuring release signing"
|
||||||
|
KEYSTORE_PATH="$(pwd)/client/android/jiu-release.jks"
|
||||||
|
echo "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${KEYSTORE_PATH}"
|
||||||
|
cat > client/android/key.properties <<EOF
|
||||||
storeFile=${KEYSTORE_PATH}
|
storeFile=${KEYSTORE_PATH}
|
||||||
storePassword=${ANDROID_KEYSTORE_PASSWORD}
|
storePassword=${ANDROID_KEYSTORE_PASSWORD}
|
||||||
keyAlias=${ANDROID_KEY_ALIAS}
|
keyAlias=${ANDROID_KEY_ALIAS}
|
||||||
keyPassword=${ANDROID_KEY_PASSWORD}
|
keyPassword=${ANDROID_KEY_PASSWORD}
|
||||||
EOF
|
EOF
|
||||||
else
|
|
||||||
echo "==> compile-android: WARNING — no ANDROID_KEYSTORE_BASE64, falling back to debug signing"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build APK (universal, all ABIs — simplest for sideload download)
|
# Build APK (universal, all ABIs — simplest for sideload download)
|
||||||
cd client
|
cd client
|
||||||
|
|||||||
Reference in New Issue
Block a user