Compare commits
7 Commits
v20260524.2016
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 626efa9c34 | |||
| b69c23c88a | |||
| 53f56ce086 | |||
| d48f9f4bd0 | |||
| b591bc522d | |||
| b15e422953 | |||
| 634162e0d9 |
+21
-81
@@ -2,7 +2,8 @@ name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags:
|
||||
- 'v[0-9]*.[0-9]*.[0-9]*'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -11,97 +12,36 @@ jobs:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
||||
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Go tests
|
||||
working-directory: backend
|
||||
run: go test ./...
|
||||
- name: Compile
|
||||
run: sh scripts/ci/compile.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Flutter tests
|
||||
working-directory: client
|
||||
run: flutter test
|
||||
- name: Test
|
||||
run: sh scripts/ci/test.sh
|
||||
|
||||
- name: Build backend (linux/amd64)
|
||||
working-directory: backend
|
||||
run: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o jiu-server .
|
||||
|
||||
- name: Build Flutter Web
|
||||
working-directory: client
|
||||
run: flutter build web --release --base-href=/app/ --dart-define=BASE_URL=https://jiu.51yanmei.com --dart-define=PUBLIC_URL=https://jiu.51yanmei.com
|
||||
|
||||
- name: Package & Create Forgejo Release
|
||||
- name: Release
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
run: |
|
||||
tar -czf web.tar.gz -C client/build web
|
||||
tar -czf configs.tar.gz \
|
||||
deploy/nginx-jiu.conf \
|
||||
deploy/jiu.service \
|
||||
deploy/production.env.template \
|
||||
deploy/setup-ec2.sh \
|
||||
deploy/docker-compose.yml \
|
||||
deploy/docker-compose.jiu.yml
|
||||
VERSION="v$(date +%Y%m%d.%H%M)"
|
||||
COMMIT="${{ gitea.sha }}"
|
||||
COMMIT_MSG=$(git log -1 --pretty="%s" HEAD)
|
||||
RECENT_LOGS=$(git log -5 --pretty="- %s (%h)" HEAD)
|
||||
BODY=$(printf '## 构建信息\n- **版本**: %s\n- **Commit**: %s\n- **时间**: %s\n\n## 最近提交\n%s' \
|
||||
"${VERSION}" "${COMMIT}" "$(date '+%Y-%m-%d %H:%M:%S %Z')" "${RECENT_LOGS}")
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
run: sh scripts/ci/release.sh "${{ gitea.ref_name }}"
|
||||
|
||||
RESP=$(curl -s -X POST "${FORGEJO_URL}/api/v1/repos/${{ gitea.repository }}/releases" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${VERSION}\",\"name\":\"Release ${VERSION}\",\"body\":$(echo "$BODY" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))'),\"draft\":false,\"prerelease\":false}")
|
||||
echo "API response: ${RESP}"
|
||||
RELEASE_ID=$(echo "${RESP}" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||
curl -sf -X POST "${FORGEJO_URL}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" -F "attachment=@backend/jiu-server"
|
||||
curl -sf -X POST "${FORGEJO_URL}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" -F "attachment=@web.tar.gz"
|
||||
curl -sf -X POST "${FORGEJO_URL}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" -F "attachment=@configs.tar.gz"
|
||||
echo "Release ${VERSION} created"
|
||||
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s' "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/ec2_deploy.pem
|
||||
chmod 600 ~/.ssh/ec2_deploy.pem
|
||||
ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy to EC2
|
||||
- name: Deploy
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
|
||||
EC2_HOST: ${{ secrets.EC2_HOST }}
|
||||
EC2_USER: ${{ secrets.EC2_USER }}
|
||||
run: |
|
||||
scp -O -i ~/.ssh/ec2_deploy.pem backend/jiu-server ${EC2_USER}@${EC2_HOST}:/tmp/jiu-server
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem" \
|
||||
client/build/web/ ${EC2_USER}@${EC2_HOST}:/tmp/jiu-web-new/
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem" \
|
||||
web/ ${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/
|
||||
scp -O -i ~/.ssh/ec2_deploy.pem deploy/nginx-jiu.conf \
|
||||
${EC2_USER}@${EC2_HOST}:/tmp/nginx-jiu.conf
|
||||
ssh -i ~/.ssh/ec2_deploy.pem ${EC2_USER}@${EC2_HOST} << 'ENDSSH'
|
||||
sudo systemctl stop jiu
|
||||
cp /tmp/jiu-server /opt/jiu/backend/jiu-server
|
||||
chmod +x /opt/jiu/backend/jiu-server
|
||||
sudo systemctl start jiu
|
||||
for i in $(seq 1 30); do
|
||||
curl -sf http://localhost:8080/health && break
|
||||
sleep 2
|
||||
done
|
||||
rm -rf /opt/jiu/web-old
|
||||
mv /opt/jiu/web /opt/jiu/web-old 2>/dev/null || true
|
||||
mv /tmp/jiu-web-new /opt/jiu/web
|
||||
mkdir -p /opt/jiu/marketing
|
||||
rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/
|
||||
rm -rf /tmp/jiu-marketing-new
|
||||
sudo cp /tmp/nginx-jiu.conf /etc/nginx/conf.d/jiu.conf
|
||||
sudo nginx -t && sudo nginx -s reload
|
||||
ENDSSH
|
||||
run: sh scripts/ci/deploy.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Cleanup SSH key
|
||||
- name: Notify
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/ec2_deploy.pem
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
run: sh scripts/ci/notify.sh "${{ gitea.ref_name }}" "${{ job.status }}"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Manual Deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Tag to deploy (e.g. v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: mac
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.version }}
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
|
||||
EC2_HOST: ${{ secrets.EC2_HOST }}
|
||||
EC2_USER: ${{ secrets.EC2_USER }}
|
||||
run: sh scripts/ci/deploy.sh "${{ inputs.version }}"
|
||||
@@ -0,0 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
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.0] - 2026-05-24
|
||||
|
||||
初始版本发布,完成核心库存管理功能。
|
||||
|
||||
### 新功能
|
||||
- 商品管理:多规格商品档案、图片、公开二维码扫码展示
|
||||
- 库存管理:入库审核、出库审核、库存盘点、流水记录
|
||||
- 财务管理:往来账目、对账单
|
||||
- 用户权限:管理员 / 成员 / 只读三级权限控制
|
||||
- 多租户隔离:门店数据完全独立
|
||||
- Flutter Web 管理端(支持 PC 浏览器)
|
||||
- 商品扫码公开展示页
|
||||
- Excel 批量导入商品与库存
|
||||
@@ -1,10 +1,10 @@
|
||||
version: "1.1.1"
|
||||
build_number: 2
|
||||
version: "1.0.0"
|
||||
build_number: 1
|
||||
force_update: false
|
||||
release_notes: "修复了离线模式问题,优化状态栏显示"
|
||||
release_notes: "初始版本发布,完成核心库存管理功能。"
|
||||
download_urls:
|
||||
macos: ""
|
||||
windows: ""
|
||||
ios: ""
|
||||
android: ""
|
||||
web: ""
|
||||
web: "https://jiu.51yanmei.com/app"
|
||||
|
||||
@@ -2,8 +2,6 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -80,23 +78,18 @@ func (h *PublicHandler) GetProduct(c *gin.Context) {
|
||||
|
||||
// GetRelease GET /api/v1/public/release (no auth)
|
||||
func (h *PublicHandler) GetRelease(c *gin.Context) {
|
||||
version := os.Getenv("APP_VERSION")
|
||||
if version == "" {
|
||||
version = "1.0.0"
|
||||
cfg, err := loadVersionConfig()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": "1.0.0",
|
||||
"release_notes": "",
|
||||
"download_urls": gin.H{"web": "https://jiu.51yanmei.com/app"},
|
||||
})
|
||||
return
|
||||
}
|
||||
buildDate := os.Getenv("BUILD_DATE")
|
||||
if buildDate == "" {
|
||||
buildDate = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": version,
|
||||
"build_date": buildDate,
|
||||
"download_urls": gin.H{
|
||||
"macos": os.Getenv("DOWNLOAD_URL_MACOS"),
|
||||
"windows": os.Getenv("DOWNLOAD_URL_WINDOWS"),
|
||||
"web": "https://jiu.51yanmei.com/app",
|
||||
},
|
||||
"changelog": []gin.H{},
|
||||
"version": cfg.Version,
|
||||
"release_notes": cfg.ReleaseNotes,
|
||||
"download_urls": cfg.DownloadURLs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
slivers: [
|
||||
// 1. Gallery
|
||||
SliverToBoxAdapter(
|
||||
child: _HeroGallery(imageUrls: imageUrls),
|
||||
child: _HeroGallery(imageUrls: imageUrls, shopName: shop?['name'] as String? ?? ''),
|
||||
),
|
||||
// 2. Verified ribbon
|
||||
SliverToBoxAdapter(
|
||||
@@ -227,7 +227,8 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
|
||||
class _HeroGallery extends StatefulWidget {
|
||||
final List<String> imageUrls;
|
||||
const _HeroGallery({required this.imageUrls});
|
||||
final String shopName;
|
||||
const _HeroGallery({required this.imageUrls, this.shopName = ''});
|
||||
|
||||
@override
|
||||
State<_HeroGallery> createState() => _HeroGalleryState();
|
||||
@@ -280,7 +281,7 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
||||
),
|
||||
// Image or placeholder
|
||||
if (urls.isEmpty)
|
||||
_EmptyGalleryContent()
|
||||
_EmptyGalleryContent(shopName: widget.shopName)
|
||||
else
|
||||
PageView.builder(
|
||||
controller: _ctrl,
|
||||
@@ -291,7 +292,7 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
||||
child: Image.network(
|
||||
urls[i],
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => _EmptyGalleryContent(),
|
||||
errorBuilder: (_, __, ___) => _EmptyGalleryContent(shopName: widget.shopName),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -409,8 +410,12 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
||||
}
|
||||
|
||||
class _EmptyGalleryContent extends StatelessWidget {
|
||||
final String shopName;
|
||||
const _EmptyGalleryContent({this.shopName = ''});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final initial = shopName.isNotEmpty ? shopName.characters.first : '店';
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -421,9 +426,9 @@ class _EmptyGalleryContent extends StatelessWidget {
|
||||
color: const Color(0xFF0F3057),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text('岩美',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 18, fontWeight: FontWeight.w700)),
|
||||
child: Center(
|
||||
child: Text(initial,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 28, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="岩美酒库管理系统 — 酒店饮品库存与采购管理平台">
|
||||
<meta name="color-scheme" content="light">
|
||||
<style>
|
||||
html, body {
|
||||
background-color: #ffffff;
|
||||
color-scheme: light;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
@@ -39,6 +39,20 @@ server {
|
||||
try_files $uri $uri/ /app/index.html;
|
||||
}
|
||||
|
||||
# index.html 不缓存,确保每次发版后浏览器加载最新版
|
||||
location = /app/index.html {
|
||||
alias /opt/jiu/web/index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
expires 0;
|
||||
}
|
||||
|
||||
# 公开商品详情页(扫码跳转)→ 交给 Flutter Web 路由处理
|
||||
location ~ ^/product/ {
|
||||
root /opt/jiu/web;
|
||||
try_files $uri /app/index.html;
|
||||
}
|
||||
|
||||
# 扫码页(/scan/<id> → scan.html,JS 从 URL 读取 id)
|
||||
location /scan/ {
|
||||
root /opt/jiu/marketing;
|
||||
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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}"
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
|
||||
echo "==> compile: version=$VER"
|
||||
|
||||
# Sync Flutter pubspec version
|
||||
sed -i '' "s/^version:.*/version: ${VER}+1/" client/pubspec.yaml
|
||||
|
||||
# Build Go backend (linux/amd64 for EC2)
|
||||
cd backend
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o jiu-server .
|
||||
cd ..
|
||||
|
||||
# Build Flutter Web
|
||||
cd client
|
||||
flutter build web --release \
|
||||
--base-href=/app/ \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=${TAG}"
|
||||
cd ..
|
||||
|
||||
# Package artifacts
|
||||
mkdir -p dist
|
||||
mv backend/jiu-server dist/jiu-server
|
||||
tar -czf dist/web.tar.gz -C client/build web
|
||||
|
||||
echo "==> compile: done — dist/ contents:"
|
||||
ls -lh dist/
|
||||
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.sh <tag> — deploy release to EC2
|
||||
# Works in two modes:
|
||||
# 1. Automated (after compile.sh): uses dist/ built in the same job
|
||||
# 2. Manual/rollback: downloads assets from Forgejo Release by tag
|
||||
set -euo pipefail
|
||||
|
||||
TAG="$1"
|
||||
|
||||
echo "==> deploy: tag=${TAG}"
|
||||
|
||||
# Download from Forgejo if dist/ was not built in this job
|
||||
if [ ! -f dist/jiu-server ] || [ ! -f dist/web.tar.gz ] || [ ! -f dist/configs.tar.gz ]; then
|
||||
echo "==> deploy: dist/ incomplete — downloading assets for ${TAG} from Forgejo"
|
||||
mkdir -p dist
|
||||
|
||||
curl -sf \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
"${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/tags/${TAG}" \
|
||||
-o /tmp/release-info.json
|
||||
|
||||
python3 - <<'PYEOF'
|
||||
import json, urllib.request, os, sys
|
||||
|
||||
with open('/tmp/release-info.json') as f:
|
||||
release = json.load(f)
|
||||
|
||||
token = os.environ['FORGEJO_TOKEN']
|
||||
forgejo_url = os.environ['FORGEJO_URL']
|
||||
repo = os.environ['GITEA_REPOSITORY']
|
||||
release_id = release['id']
|
||||
|
||||
for asset in release.get('assets', []):
|
||||
url = f"{forgejo_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset['id']}"
|
||||
req = urllib.request.Request(url, headers={'Authorization': f'token {token}'})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
with open(f"dist/{asset['name']}", 'wb') as out:
|
||||
out.write(resp.read())
|
||||
print(f"Downloaded: {asset['name']}")
|
||||
PYEOF
|
||||
fi
|
||||
|
||||
echo "==> deploy: dist/ contents:"
|
||||
ls -lh dist/
|
||||
|
||||
# Extract configs (nginx, version.yaml, etc.)
|
||||
rm -rf /tmp/jiu-configs
|
||||
mkdir -p /tmp/jiu-configs
|
||||
tar -xzf dist/configs.tar.gz -C /tmp/jiu-configs
|
||||
|
||||
# Extract Flutter web build
|
||||
rm -rf /tmp/jiu-web-new
|
||||
mkdir -p /tmp/jiu-web-new
|
||||
tar -xzf dist/web.tar.gz -C /tmp/jiu-web-new --strip-components=1
|
||||
|
||||
# Setup SSH
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s' "${EC2_SSH_KEY}" > ~/.ssh/ec2_deploy.pem
|
||||
chmod 600 ~/.ssh/ec2_deploy.pem
|
||||
ssh-keyscan -H "${EC2_HOST}" >> ~/.ssh/known_hosts
|
||||
|
||||
SSH="ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no"
|
||||
SCP="scp -O -i ~/.ssh/ec2_deploy.pem"
|
||||
|
||||
echo "==> deploy: uploading files to EC2"
|
||||
|
||||
# Upload backend binary
|
||||
${SCP} dist/jiu-server "${EC2_USER}@${EC2_HOST}:/tmp/jiu-server"
|
||||
|
||||
# Upload version.yaml
|
||||
${SCP} /tmp/jiu-configs/backend/config/version.yaml "${EC2_USER}@${EC2_HOST}:/tmp/version.yaml"
|
||||
|
||||
# Upload nginx config
|
||||
${SCP} /tmp/jiu-configs/deploy/nginx-jiu.conf "${EC2_USER}@${EC2_HOST}:/tmp/nginx-jiu.conf"
|
||||
|
||||
# Upload Flutter web
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \
|
||||
/tmp/jiu-web-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-web-new/"
|
||||
|
||||
# Upload marketing site (from repo checkout)
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \
|
||||
web/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/"
|
||||
|
||||
echo "==> deploy: running remote deploy"
|
||||
|
||||
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
|
||||
set -e
|
||||
|
||||
# Replace backend binary
|
||||
sudo systemctl stop jiu
|
||||
cp /tmp/jiu-server /opt/jiu/backend/jiu-server
|
||||
chmod +x /opt/jiu/backend/jiu-server
|
||||
|
||||
# Update version config
|
||||
mkdir -p /opt/jiu/backend/config
|
||||
cp /tmp/version.yaml /opt/jiu/backend/config/version.yaml
|
||||
|
||||
# Start and health check
|
||||
sudo systemctl start jiu
|
||||
echo "Waiting for health check..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
|
||||
echo "Health check passed"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
curl -sf http://localhost:8080/health > /dev/null || { echo "Health check failed!"; exit 1; }
|
||||
|
||||
# Swap Flutter web app (atomic)
|
||||
rm -rf /opt/jiu/web-old
|
||||
mv /opt/jiu/web /opt/jiu/web-old 2>/dev/null || true
|
||||
mv /tmp/jiu-web-new /opt/jiu/web
|
||||
|
||||
# Update marketing site
|
||||
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
|
||||
|
||||
echo "Deploy complete!"
|
||||
ENDSSH
|
||||
|
||||
# Cleanup SSH key
|
||||
rm -f ~/.ssh/ec2_deploy.pem
|
||||
rm -rf /tmp/jiu-configs /tmp/jiu-web-new
|
||||
|
||||
echo "==> deploy: done"
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# notify.sh <tag> <status> — send Telegram deploy notification
|
||||
set -euo pipefail
|
||||
|
||||
TAG="$1"
|
||||
STATUS="$2"
|
||||
|
||||
if [ "$STATUS" = "success" ]; then
|
||||
ICON="✅"
|
||||
LABEL="部署成功"
|
||||
else
|
||||
ICON="❌"
|
||||
LABEL="部署失败"
|
||||
fi
|
||||
|
||||
MSG="${ICON} 岩美 ${TAG} ${LABEL}
|
||||
https://jiu.51yanmei.com"
|
||||
|
||||
curl -sf -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${TELEGRAM_CHAT_ID}" \
|
||||
--data-urlencode "text=${MSG}" \
|
||||
> /dev/null
|
||||
|
||||
echo "==> notify: sent to Telegram (${STATUS})"
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
# release.sh <tag> — update version.yaml, package configs, create Forgejo Release
|
||||
set -euo pipefail
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
|
||||
echo "==> release: tag=${TAG} version=${VER}"
|
||||
|
||||
# Extract release notes from CHANGELOG.md (first summary line of the latest section)
|
||||
RELEASE_NOTES=$(python3 - <<'PYEOF'
|
||||
import re, sys
|
||||
try:
|
||||
with open('CHANGELOG.md') as f:
|
||||
content = f.read()
|
||||
parts = re.split(r'\n## ', '\n' + content)
|
||||
if len(parts) > 1:
|
||||
section = parts[1]
|
||||
lines = section.strip().split('\n')
|
||||
notes = []
|
||||
for line in lines[1:]:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('#'):
|
||||
break
|
||||
if stripped:
|
||||
notes.append(stripped)
|
||||
print('\n'.join(notes) if notes else lines[0])
|
||||
else:
|
||||
print('')
|
||||
except Exception as e:
|
||||
print('', file=sys.stderr)
|
||||
print('')
|
||||
PYEOF
|
||||
)
|
||||
|
||||
echo "==> release: release_notes=${RELEASE_NOTES}"
|
||||
|
||||
# Update backend/config/version.yaml
|
||||
python3 - "${VER}" "${RELEASE_NOTES}" <<'PYEOF'
|
||||
import sys
|
||||
ver = sys.argv[1]
|
||||
notes = sys.argv[2]
|
||||
lines = []
|
||||
with open('backend/config/version.yaml') as f:
|
||||
for line in f:
|
||||
if line.startswith('version:'):
|
||||
lines.append('version: "' + ver + '"\n')
|
||||
elif line.startswith('build_number:'):
|
||||
try:
|
||||
build = int(line.split(':', 1)[1].strip()) + 1
|
||||
except Exception:
|
||||
build = 1
|
||||
lines.append('build_number: ' + str(build) + '\n')
|
||||
elif line.startswith('release_notes:'):
|
||||
lines.append('release_notes: "' + notes.replace('\\', '\\\\').replace('"', '\\"') + '"\n')
|
||||
else:
|
||||
lines.append(line)
|
||||
with open('backend/config/version.yaml', 'w') as f:
|
||||
f.writelines(lines)
|
||||
print('version.yaml updated to', ver)
|
||||
PYEOF
|
||||
|
||||
# Package configs.tar.gz (includes updated version.yaml)
|
||||
tar -czf dist/configs.tar.gz \
|
||||
deploy/nginx-jiu.conf \
|
||||
deploy/jiu.service \
|
||||
deploy/production.env.template \
|
||||
deploy/setup-ec2.sh \
|
||||
deploy/docker-compose.yml \
|
||||
deploy/docker-compose.jiu.yml \
|
||||
backend/config/version.yaml
|
||||
|
||||
echo "==> release: creating Forgejo Release ${TAG}"
|
||||
|
||||
# Build release body from CHANGELOG excerpt
|
||||
BODY=$(python3 - "${TAG}" "${RELEASE_NOTES}" <<'PYEOF'
|
||||
import sys, json
|
||||
tag = sys.argv[1]
|
||||
notes = sys.argv[2]
|
||||
body = '## ' + tag + '\n\n' + notes
|
||||
print(json.dumps(body))
|
||||
PYEOF
|
||||
)
|
||||
|
||||
RESP=$(curl -sf -X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Release ${TAG}\",\"body\":${BODY},\"draft\":false,\"prerelease\":false}")
|
||||
|
||||
echo "==> release: API response: ${RESP}"
|
||||
|
||||
RELEASE_ID=$(python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" <<< "${RESP}")
|
||||
echo "==> release: release_id=${RELEASE_ID}"
|
||||
|
||||
# Upload assets
|
||||
curl -sf -X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-F "attachment=@dist/jiu-server"
|
||||
echo "==> release: uploaded jiu-server"
|
||||
|
||||
curl -sf -X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-F "attachment=@dist/web.tar.gz"
|
||||
echo "==> release: uploaded web.tar.gz"
|
||||
|
||||
curl -sf -X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-F "attachment=@dist/configs.tar.gz"
|
||||
echo "==> release: uploaded configs.tar.gz"
|
||||
|
||||
echo "==> release: done — Release ${TAG} created"
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# test.sh — run backend and frontend checks
|
||||
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}"
|
||||
|
||||
echo "==> test: go test"
|
||||
cd backend
|
||||
go test ./...
|
||||
cd ..
|
||||
|
||||
echo "==> test: flutter analyze"
|
||||
cd client
|
||||
flutter analyze --no-fatal-infos --no-fatal-warnings
|
||||
cd ..
|
||||
|
||||
echo "==> test: done"
|
||||
@@ -5,6 +5,8 @@
|
||||
========================================================================= */
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
/* ---------- Brand: Primary (Slate Blue) ---------- */
|
||||
/* Trustworthy enterprise blue with a slight slate cast. */
|
||||
--brand-50: #EEF4FB;
|
||||
|
||||
+19
-4
@@ -954,15 +954,15 @@ footer {
|
||||
</p>
|
||||
|
||||
<div class="changelog">
|
||||
<!-- v1.6.2 -->
|
||||
<!-- 当前版本(由 JS 动态更新) -->
|
||||
<div class="changelog-entry current">
|
||||
<div class="changelog-meta">
|
||||
<div class="changelog-version">v1.6.2</div>
|
||||
<div class="changelog-date">2026-05-15</div>
|
||||
<div class="changelog-version" id="version-badge">...</div>
|
||||
<div class="changelog-date" id="release-date">-</div>
|
||||
<span class="changelog-tag">当前版本</span>
|
||||
</div>
|
||||
<div class="changelog-body">
|
||||
<h3>多仓库联动盘点与权限增强</h3>
|
||||
<h3 id="release-notes">加载中...</h3>
|
||||
<div class="changelog-cat">
|
||||
<div class="changelog-cat-label"><span class="dot feature"></span>新功能</div>
|
||||
<ul>
|
||||
@@ -1193,8 +1193,23 @@ function renderNav() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function loadReleaseInfo() {
|
||||
fetch('https://jiu.51yanmei.com/api/v1/public/release')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var badge = document.getElementById('version-badge');
|
||||
var notes = document.getElementById('release-notes');
|
||||
var date = document.getElementById('release-date');
|
||||
if (badge && data.version) badge.textContent = 'v' + data.version;
|
||||
if (notes && data.release_notes) notes.textContent = data.release_notes;
|
||||
if (date) date.textContent = new Date().toISOString().slice(0, 10);
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
renderNav();
|
||||
loadReleaseInfo();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<title>岩美酒库管理系统 — 为酒行与酒店设计的库存管理平台</title>
|
||||
<link rel="stylesheet" href="assets/colors_and_type.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
|
||||
Reference in New Issue
Block a user