Compare commits

...

7 Commits

Author SHA1 Message Date
wangjia 626efa9c34 fix(ci): flutter analyze 忽略 warning/info,只在 error 时失败
Deploy / deploy (push) Successful in 1m16s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:20:02 +08:00
wangjia b69c23c88a feat(deploy): 部署完成后发送 Telegram 通知
Deploy / deploy (push) Failing after 48s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:09:09 +08:00
wangjia 53f56ce086 feat(deploy): SemVer 版本管理 + 自动化发版流水线
- deploy.yml: tag 触发(v*.*.*),四阶段调脚本(compile/test/release/deploy)
- manual.yml: workflow_dispatch 支持手动回滚,checkout 指定 tag
- scripts/ci/compile.sh: 构建 Go 二进制 + Flutter Web,打包 dist/
- scripts/ci/test.sh: go test + flutter analyze
- scripts/ci/release.sh: 解析 CHANGELOG.md → 更新 version.yaml → 创建 Forgejo Release
- scripts/ci/deploy.sh: 从 Release 下载产物(自动/手动均可)→ 部署到 EC2
- CHANGELOG.md: Keep a Changelog 格式,初始 v1.0.0 条目
- backend: GetRelease 改读 version.yaml,移除 ENV 变量依赖
- backend/config/version.yaml: 重置为 v1.0.0
- web/download.html: 动态拉取 /api/v1/public/release 展示版本号和更新内容

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:41:08 +08:00
wangjia d48f9f4bd0 fix(client): 商品公开页暂无图片时显示门店名首字,替换硬编码岩美
Deploy / deploy (push) Successful in 1m20s
_EmptyGalleryContent 接收 shopName 参数,无图片时展示
门店名第一个字符的文字头像,不再显示品牌名。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:59:17 +08:00
wangjia b591bc522d fix(web): 禁用深色模式自动适配,修复渲染叠层问题
Deploy / deploy (push) Successful in 2m9s
Flutter Web canvas 在系统深色模式下背景透明,导致底层 HTML
内容透出形成叠层。营销站点未声明 color-scheme,浏览器自动调色
也会破坏白色背景设计。

- client/web/index.html: 添加 color-scheme=light meta 及 body 白色背景
- web/index.html: 添加 color-scheme=light meta
- web/assets/colors_and_type.css: :root 声明 color-scheme: light

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:40:03 +08:00
wangjia b15e422953 fix(nginx): /product/ 路径转发给 Flutter Web,修复扫码跳转营销页问题
Deploy / deploy (push) Successful in 1m16s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:34:28 +08:00
wangjia 634162e0d9 fix(nginx): index.html 禁用缓存,确保发版后立即生效
Deploy / deploy (push) Successful in 1m17s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:28:48 +08:00
16 changed files with 463 additions and 114 deletions
+21 -81
View File
@@ -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 }}"
+28
View File
@@ -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 }}"
+20
View File
@@ -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 批量导入商品与库存
+4 -4
View File
@@ -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"
+11 -18
View File
@@ -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),
+7
View File
@@ -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">
+14
View File
@@ -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.htmlJS 从 URL 读取 id
location /scan/ {
root /opt/jiu/marketing;
+38
View File
@@ -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/
+131
View File
@@ -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"
+24
View File
@@ -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})"
+111
View File
@@ -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"
+20
View File
@@ -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"
+2
View File
@@ -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
View File
@@ -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>
+1
View File
@@ -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">