From 53f56ce086c8a48eb7c657ac7564dcca3bb5588c Mon Sep 17 00:00:00 2001
From: wangjia <809946525@qq.com>
Date: Sun, 24 May 2026 22:41:08 +0800
Subject: [PATCH] =?UTF-8?q?feat(deploy):=20SemVer=20=E7=89=88=E6=9C=AC?=
=?UTF-8?q?=E7=AE=A1=E7=90=86=20+=20=E8=87=AA=E5=8A=A8=E5=8C=96=E5=8F=91?=
=?UTF-8?q?=E7=89=88=E6=B5=81=E6=B0=B4=E7=BA=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
.gitea/workflows/deploy.yml | 99 ++++------------------
.gitea/workflows/manual.yml | 28 ++++++
CHANGELOG.md | 20 +++++
backend/config/version.yaml | 8 +-
backend/internal/handler/public.go | 29 +++----
scripts/ci/compile.sh | 38 +++++++++
scripts/ci/deploy.sh | 131 +++++++++++++++++++++++++++++
scripts/ci/release.sh | 111 ++++++++++++++++++++++++
scripts/ci/test.sh | 20 +++++
web/download.html | 23 ++++-
10 files changed, 398 insertions(+), 109 deletions(-)
create mode 100644 .gitea/workflows/manual.yml
create mode 100644 CHANGELOG.md
create mode 100755 scripts/ci/compile.sh
create mode 100755 scripts/ci/deploy.sh
create mode 100755 scripts/ci/release.sh
create mode 100755 scripts/ci/test.sh
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index 7cecdba..80efcf2 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -2,7 +2,8 @@ name: Deploy
on:
push:
- branches: [main]
+ tags:
+ - 'v[0-9]*.[0-9]*.[0-9]*'
jobs:
deploy:
@@ -11,97 +12,29 @@ 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
-
- - name: Cleanup SSH key
- if: always()
- run: rm -f ~/.ssh/ec2_deploy.pem
+ run: sh scripts/ci/deploy.sh "${{ gitea.ref_name }}"
diff --git a/.gitea/workflows/manual.yml b/.gitea/workflows/manual.yml
new file mode 100644
index 0000000..91076b2
--- /dev/null
+++ b/.gitea/workflows/manual.yml
@@ -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 }}"
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..32c0099
--- /dev/null
+++ b/CHANGELOG.md
@@ -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 批量导入商品与库存
diff --git a/backend/config/version.yaml b/backend/config/version.yaml
index a356ef6..68eae8d 100644
--- a/backend/config/version.yaml
+++ b/backend/config/version.yaml
@@ -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"
diff --git a/backend/internal/handler/public.go b/backend/internal/handler/public.go
index 2c903cd..bf70267 100644
--- a/backend/internal/handler/public.go
+++ b/backend/internal/handler/public.go
@@ -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,
})
}
diff --git a/scripts/ci/compile.sh b/scripts/ci/compile.sh
new file mode 100755
index 0000000..57afc5a
--- /dev/null
+++ b/scripts/ci/compile.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+# compile.sh — 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/
diff --git a/scripts/ci/deploy.sh b/scripts/ci/deploy.sh
new file mode 100755
index 0000000..3a7f5fa
--- /dev/null
+++ b/scripts/ci/deploy.sh
@@ -0,0 +1,131 @@
+#!/usr/bin/env bash
+# deploy.sh — 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"
diff --git a/scripts/ci/release.sh b/scripts/ci/release.sh
new file mode 100755
index 0000000..9440ec3
--- /dev/null
+++ b/scripts/ci/release.sh
@@ -0,0 +1,111 @@
+#!/usr/bin/env bash
+# release.sh — 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"
diff --git a/scripts/ci/test.sh b/scripts/ci/test.sh
new file mode 100755
index 0000000..8637c66
--- /dev/null
+++ b/scripts/ci/test.sh
@@ -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
+cd ..
+
+echo "==> test: done"
diff --git a/web/download.html b/web/download.html
index ff7ad58..7c2d996 100644
--- a/web/download.html
+++ b/web/download.html
@@ -954,15 +954,15 @@ footer {
-
+
-
多仓库联动盘点与权限增强
+
加载中...
新功能
@@ -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();
});