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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
tags:
|
||||||
|
- 'v[0-9]*.[0-9]*.[0-9]*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
@@ -11,97 +12,36 @@ jobs:
|
|||||||
GOPROXY: https://goproxy.cn,direct
|
GOPROXY: https://goproxy.cn,direct
|
||||||
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
||||||
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
|
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Go tests
|
- name: Compile
|
||||||
working-directory: backend
|
run: sh scripts/ci/compile.sh "${{ gitea.ref_name }}"
|
||||||
run: go test ./...
|
|
||||||
|
|
||||||
- name: Flutter tests
|
- name: Test
|
||||||
working-directory: client
|
run: sh scripts/ci/test.sh
|
||||||
run: flutter test
|
|
||||||
|
|
||||||
- name: Build backend (linux/amd64)
|
- name: Release
|
||||||
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
|
|
||||||
env:
|
env:
|
||||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||||
run: |
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
tar -czf web.tar.gz -C client/build web
|
run: sh scripts/ci/release.sh "${{ gitea.ref_name }}"
|
||||||
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}")
|
|
||||||
|
|
||||||
RESP=$(curl -s -X POST "${FORGEJO_URL}/api/v1/repos/${{ gitea.repository }}/releases" \
|
- name: Deploy
|
||||||
-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
|
|
||||||
env:
|
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_HOST: ${{ secrets.EC2_HOST }}
|
||||||
EC2_USER: ${{ secrets.EC2_USER }}
|
EC2_USER: ${{ secrets.EC2_USER }}
|
||||||
run: |
|
run: sh scripts/ci/deploy.sh "${{ gitea.ref_name }}"
|
||||||
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
|
|
||||||
|
|
||||||
- name: Cleanup SSH key
|
- name: Notify
|
||||||
if: always()
|
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"
|
version: "1.0.0"
|
||||||
build_number: 2
|
build_number: 1
|
||||||
force_update: false
|
force_update: false
|
||||||
release_notes: "修复了离线模式问题,优化状态栏显示"
|
release_notes: "初始版本发布,完成核心库存管理功能。"
|
||||||
download_urls:
|
download_urls:
|
||||||
macos: ""
|
macos: ""
|
||||||
windows: ""
|
windows: ""
|
||||||
ios: ""
|
ios: ""
|
||||||
android: ""
|
android: ""
|
||||||
web: ""
|
web: "https://jiu.51yanmei.com/app"
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -80,23 +78,18 @@ func (h *PublicHandler) GetProduct(c *gin.Context) {
|
|||||||
|
|
||||||
// GetRelease GET /api/v1/public/release (no auth)
|
// GetRelease GET /api/v1/public/release (no auth)
|
||||||
func (h *PublicHandler) GetRelease(c *gin.Context) {
|
func (h *PublicHandler) GetRelease(c *gin.Context) {
|
||||||
version := os.Getenv("APP_VERSION")
|
cfg, err := loadVersionConfig()
|
||||||
if version == "" {
|
if err != nil {
|
||||||
version = "1.0.0"
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"version": version,
|
"version": cfg.Version,
|
||||||
"build_date": buildDate,
|
"release_notes": cfg.ReleaseNotes,
|
||||||
"download_urls": gin.H{
|
"download_urls": cfg.DownloadURLs,
|
||||||
"macos": os.Getenv("DOWNLOAD_URL_MACOS"),
|
|
||||||
"windows": os.Getenv("DOWNLOAD_URL_WINDOWS"),
|
|
||||||
"web": "https://jiu.51yanmei.com/app",
|
|
||||||
},
|
|
||||||
"changelog": []gin.H{},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
|||||||
slivers: [
|
slivers: [
|
||||||
// 1. Gallery
|
// 1. Gallery
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _HeroGallery(imageUrls: imageUrls),
|
child: _HeroGallery(imageUrls: imageUrls, shopName: shop?['name'] as String? ?? ''),
|
||||||
),
|
),
|
||||||
// 2. Verified ribbon
|
// 2. Verified ribbon
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
@@ -227,7 +227,8 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
|||||||
|
|
||||||
class _HeroGallery extends StatefulWidget {
|
class _HeroGallery extends StatefulWidget {
|
||||||
final List<String> imageUrls;
|
final List<String> imageUrls;
|
||||||
const _HeroGallery({required this.imageUrls});
|
final String shopName;
|
||||||
|
const _HeroGallery({required this.imageUrls, this.shopName = ''});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_HeroGallery> createState() => _HeroGalleryState();
|
State<_HeroGallery> createState() => _HeroGalleryState();
|
||||||
@@ -280,7 +281,7 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
|||||||
),
|
),
|
||||||
// Image or placeholder
|
// Image or placeholder
|
||||||
if (urls.isEmpty)
|
if (urls.isEmpty)
|
||||||
_EmptyGalleryContent()
|
_EmptyGalleryContent(shopName: widget.shopName)
|
||||||
else
|
else
|
||||||
PageView.builder(
|
PageView.builder(
|
||||||
controller: _ctrl,
|
controller: _ctrl,
|
||||||
@@ -291,7 +292,7 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
|||||||
child: Image.network(
|
child: Image.network(
|
||||||
urls[i],
|
urls[i],
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (_, __, ___) => _EmptyGalleryContent(),
|
errorBuilder: (_, __, ___) => _EmptyGalleryContent(shopName: widget.shopName),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -409,8 +410,12 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _EmptyGalleryContent extends StatelessWidget {
|
class _EmptyGalleryContent extends StatelessWidget {
|
||||||
|
final String shopName;
|
||||||
|
const _EmptyGalleryContent({this.shopName = ''});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final initial = shopName.isNotEmpty ? shopName.characters.first : '店';
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -421,9 +426,9 @@ class _EmptyGalleryContent extends StatelessWidget {
|
|||||||
color: const Color(0xFF0F3057),
|
color: const Color(0xFF0F3057),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: Center(
|
||||||
child: Text('岩美',
|
child: Text(initial,
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 18, fontWeight: FontWeight.w700)),
|
style: const TextStyle(color: Colors.white70, fontSize: 28, fontWeight: FontWeight.w700)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
@@ -19,6 +19,13 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="岩美酒库管理系统 — 酒店饮品库存与采购管理平台">
|
<meta name="description" content="岩美酒库管理系统 — 酒店饮品库存与采购管理平台">
|
||||||
|
<meta name="color-scheme" content="light">
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
|||||||
@@ -39,6 +39,20 @@ server {
|
|||||||
try_files $uri $uri/ /app/index.html;
|
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)
|
# 扫码页(/scan/<id> → scan.html,JS 从 URL 读取 id)
|
||||||
location /scan/ {
|
location /scan/ {
|
||||||
root /opt/jiu/marketing;
|
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 {
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
/* ---------- Brand: Primary (Slate Blue) ---------- */
|
/* ---------- Brand: Primary (Slate Blue) ---------- */
|
||||||
/* Trustworthy enterprise blue with a slight slate cast. */
|
/* Trustworthy enterprise blue with a slight slate cast. */
|
||||||
--brand-50: #EEF4FB;
|
--brand-50: #EEF4FB;
|
||||||
|
|||||||
+19
-4
@@ -954,15 +954,15 @@ footer {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="changelog">
|
<div class="changelog">
|
||||||
<!-- v1.6.2 -->
|
<!-- 当前版本(由 JS 动态更新) -->
|
||||||
<div class="changelog-entry current">
|
<div class="changelog-entry current">
|
||||||
<div class="changelog-meta">
|
<div class="changelog-meta">
|
||||||
<div class="changelog-version">v1.6.2</div>
|
<div class="changelog-version" id="version-badge">...</div>
|
||||||
<div class="changelog-date">2026-05-15</div>
|
<div class="changelog-date" id="release-date">-</div>
|
||||||
<span class="changelog-tag">当前版本</span>
|
<span class="changelog-tag">当前版本</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="changelog-body">
|
<div class="changelog-body">
|
||||||
<h3>多仓库联动盘点与权限增强</h3>
|
<h3 id="release-notes">加载中...</h3>
|
||||||
<div class="changelog-cat">
|
<div class="changelog-cat">
|
||||||
<div class="changelog-cat-label"><span class="dot feature"></span>新功能</div>
|
<div class="changelog-cat-label"><span class="dot feature"></span>新功能</div>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -1193,8 +1193,23 @@ function renderNav() {
|
|||||||
if (window.lucide) lucide.createIcons();
|
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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
renderNav();
|
renderNav();
|
||||||
|
loadReleaseInfo();
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="color-scheme" content="light" />
|
||||||
<title>岩美酒库管理系统 — 为酒行与酒店设计的库存管理平台</title>
|
<title>岩美酒库管理系统 — 为酒行与酒店设计的库存管理平台</title>
|
||||||
<link rel="stylesheet" href="assets/colors_and_type.css" />
|
<link rel="stylesheet" href="assets/colors_and_type.css" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
|||||||
Reference in New Issue
Block a user