feat: unify service port and add GitHub packages workflow
Build Packages / Test (push) Has been cancelled
Build Packages / Build macOS package (push) Has been cancelled
Build Packages / Build Linux package (push) Has been cancelled
Build Packages / Build Windows package (push) Has been cancelled
Build Packages / Publish GitHub Release (push) Has been cancelled
Build Packages / Test (push) Has been cancelled
Build Packages / Build macOS package (push) Has been cancelled
Build Packages / Build Linux package (push) Has been cancelled
Build Packages / Build Windows package (push) Has been cancelled
Build Packages / Publish GitHub Release (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
name: Build Packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
- name: Run frontend tests
|
||||
run: npm test -- --run
|
||||
- name: Build frontend assets
|
||||
run: npm run build
|
||||
- name: Run Go tests
|
||||
run: go test -buildvcs=false ./...
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.name }} package
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Windows
|
||||
goos: windows
|
||||
extension: .exe
|
||||
archive: zip
|
||||
- name: Linux
|
||||
goos: linux
|
||||
extension: ''
|
||||
archive: tar.gz
|
||||
- name: macOS
|
||||
goos: darwin
|
||||
extension: ''
|
||||
archive: tar.gz
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- name: Build embedded frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
- name: Build binary and package
|
||||
shell: bash
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
set -euo pipefail
|
||||
package_name="home-vue-go-${GOOS}-amd64"
|
||||
binary_name="${package_name}${{ matrix.extension }}"
|
||||
go build -trimpath -buildvcs=false -ldflags="-s -w" -o "${binary_name}" .
|
||||
mkdir package
|
||||
cp "${binary_name}" README.md LICENSE package/
|
||||
if [ "${{ matrix.archive }}" = "zip" ]; then
|
||||
(cd package && zip -q -r "../${package_name}.zip" .)
|
||||
else
|
||||
tar -czf "${package_name}.tar.gz" -C package .
|
||||
fi
|
||||
- name: Upload package artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: package-${{ matrix.goos }}
|
||||
path: home-vue-go-${{ matrix.goos }}-amd64.${{ matrix.archive }}
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download package artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: package-*
|
||||
path: release
|
||||
merge-multiple: true
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: release/*
|
||||
@@ -1,8 +1,6 @@
|
||||
# 构建说明
|
||||
|
||||
本项目支持打包为**单一可执行文件**,包含前后端,启动后同时提供:
|
||||
- **1551端口**:后端API服务
|
||||
- **1552端口**:前端界面服务(自动代理API请求到1551)
|
||||
本项目支持打包为**单一可执行文件**,包含前后端。启动后通过一个端口同时提供前端、API和上传文件,默认端口为 **1552**。
|
||||
|
||||
所有前端文件已嵌入到二进制文件中,无需额外文件。
|
||||
|
||||
@@ -123,18 +121,13 @@ cd dist
|
||||
|
||||
## 访问地址
|
||||
|
||||
启动后,服务器会同时提供两个服务:
|
||||
启动后,服务器通过统一端点提供完整服务:
|
||||
|
||||
- **后端API**: http://localhost:1551
|
||||
- API接口:http://localhost:1551/api
|
||||
- 管理接口:http://localhost:1551/api/admin
|
||||
|
||||
- **前端界面**: http://localhost:1552
|
||||
- **访问端点**: http://localhost:1552
|
||||
- 主页:http://localhost:1552
|
||||
- 管理界面:http://localhost:1552/admin
|
||||
- 登录页面:http://localhost:1552/login
|
||||
|
||||
**注意**:前端会自动将 `/api` 请求代理到 `http://localhost:1551`,无需额外配置。
|
||||
- API接口:http://localhost:1552/api
|
||||
|
||||
## 1Panel 配置
|
||||
|
||||
@@ -142,9 +135,7 @@ cd dist
|
||||
|
||||
1. **上传文件**:将 `home-vue-go`(Linux版本)上传到服务器
|
||||
|
||||
2. **配置端口**:
|
||||
- 后端API端口:`1551`
|
||||
- 前端服务端口:`1552`
|
||||
2. **配置端口**:只需放行统一服务端口 `1552`
|
||||
|
||||
3. **运行命令**:
|
||||
```bash
|
||||
@@ -155,8 +146,7 @@ cd dist
|
||||
|
||||
## 配置说明
|
||||
|
||||
- **后端API端口**:默认 `1551`,可通过环境变量 `API_PORT` 修改
|
||||
- **前端服务端口**:默认 `1552`,可通过环境变量 `FRONTEND_PORT` 修改
|
||||
- **统一服务端口**:默认 `1552`,可通过环境变量 `PORT` 修改
|
||||
- **数据目录**:运行时会自动在二进制文件同目录下创建 `data` 目录
|
||||
- **默认管理员账号**:`admin` / `admin123`(首次启动时显示)
|
||||
|
||||
@@ -164,15 +154,13 @@ cd dist
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
set API_PORT=8080
|
||||
set FRONTEND_PORT=8081
|
||||
set PORT=8080
|
||||
home-vue-go.exe
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
export API_PORT=8080
|
||||
export FRONTEND_PORT=8081
|
||||
export PORT=8080
|
||||
./home-vue-go
|
||||
```
|
||||
|
||||
@@ -181,7 +169,7 @@ export FRONTEND_PORT=8081
|
||||
1. **SQLite驱动**:使用纯 Go SQLite 驱动,构建无需 CGO 或 GCC
|
||||
2. **前端构建**:构建时必须先运行 `npm run build` 生成dist目录,Go编译时会嵌入这些文件
|
||||
3. **Go版本**:需要Go 1.23或更高版本(支持embed功能)
|
||||
4. **端口占用**:确保1551和1552端口未被占用
|
||||
4. **端口占用**:确保统一服务端口(默认1552)未被占用
|
||||
5. **单一文件**:构建完成后,只需一个可执行文件即可运行,无需其他依赖
|
||||
6. **Make工具**:Windows用户需要安装make工具(Git for Windows自带,或使用Chocolatey安装)
|
||||
7. **跨平台编译**:Windows、Linux 和 macOS amd64 目标均可直接交叉编译
|
||||
@@ -222,24 +210,13 @@ choco install make
|
||||
│ 包含: 后端代码 + 前端文件(嵌入) │
|
||||
├─────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────┐ │
|
||||
│ │ 后端API服务 │ │ 前端服务 │ │
|
||||
│ │ 端口: 1551 │ │ 端口:1552│ │
|
||||
│ └──────┬───────┘ └────┬─────┘ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ └───────┬───────┘ │
|
||||
│ │ │
|
||||
│ API代理 │
|
||||
│ (前端/api/* → 后端1551) │
|
||||
│ │
|
||||
│ 统一 HTTP 服务 :1552 │
|
||||
│ /api /uploads /assets SPA │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **后端服务(1551)**:提供所有API接口
|
||||
- **前端服务(1552)**:
|
||||
- 从嵌入的文件系统提供前端静态文件(HTML、CSS、JS)
|
||||
- 自动代理 `/api/*` 请求到后端1551端口
|
||||
- 支持SPA路由
|
||||
- **统一服务(1552)**:直接提供 API、上传文件、嵌入的前端静态文件和 SPA 路由
|
||||
|
||||
## 完整构建流程示例
|
||||
|
||||
@@ -331,7 +308,7 @@ make build-linux
|
||||
|
||||
**1Panel配置:**
|
||||
- 运行命令:`./home-vue-go`(或完整路径)
|
||||
- 端口映射:1551(后端API)、1552(前端界面)
|
||||
- 端口映射:1552(统一服务端口)
|
||||
- 工作目录:可执行文件所在目录
|
||||
|
||||
**注意**:SQLite 使用纯 Go 驱动,因此从 Windows 交叉构建 Linux 版本不需要额外的 C 工具链。
|
||||
@@ -365,26 +342,23 @@ make clean
|
||||
|
||||
开发时,可以分别运行前后端:
|
||||
|
||||
**终端1 - 后端:**
|
||||
**终端1 - 统一服务:**
|
||||
```bash
|
||||
make run
|
||||
# 或
|
||||
go run main.go
|
||||
# 后端运行在 http://localhost:1551
|
||||
# 前端和 API 均运行在 http://localhost:1552
|
||||
```
|
||||
|
||||
**终端2 - 前端:**
|
||||
```bash
|
||||
npm run dev
|
||||
# 前端运行在 http://localhost:1552,自动代理API到1551
|
||||
# 热更新页面运行在 http://localhost:5173,API代理到统一服务端口
|
||||
```
|
||||
|
||||
## 部署优势
|
||||
|
||||
✅ **单一可执行文件**:前后端一体化,所有文件嵌入在二进制中
|
||||
✅ **无需依赖**:不需要Node.js、npm或其他运行时
|
||||
✅ **端口分离**:API和前端服务分离,便于管理和扩展
|
||||
✅ **自动代理**:前端自动代理API请求,无需额外配置
|
||||
✅ **1Panel友好**:只需配置两个端口,运行一个命令即可
|
||||
✅ **单端口服务**:前端、API和上传文件共用一个端口
|
||||
✅ **1Panel友好**:只需配置一个端口,运行一个命令即可
|
||||
✅ **部署简单**:上传一个文件,配置端口,即可运行
|
||||
✅ **跨平台构建**:使用make统一构建流程,支持多平台
|
||||
|
||||
@@ -42,35 +42,35 @@ frontend:
|
||||
backend-windows: frontend
|
||||
@echo [backend] Building Windows amd64 executable...
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=windows" && set "GOARCH=amd64" && $(GO) build -trimpath -ldflags="-s -w" -o $(DIST_DIR)\$(BINARY_NAME).exe .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=windows" && set "GOARCH=amd64" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DIST_DIR)\$(BINARY_NAME).exe .
|
||||
else
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build -trimpath -ldflags="-s -w" -o $(DIST_DIR)/$(BINARY_NAME).exe .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DIST_DIR)/$(BINARY_NAME).exe .
|
||||
endif
|
||||
|
||||
backend-linux: frontend
|
||||
@echo [backend] Building Linux amd64 executable...
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=linux" && set "GOARCH=amd64" && $(GO) build -trimpath -ldflags="-s -w" -o $(DIST_DIR)\$(BINARY_NAME) .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=linux" && set "GOARCH=amd64" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DIST_DIR)\$(BINARY_NAME) .
|
||||
else
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -trimpath -ldflags="-s -w" -o $(DIST_DIR)/$(BINARY_NAME) .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DIST_DIR)/$(BINARY_NAME) .
|
||||
@chmod +x $(DIST_DIR)/$(BINARY_NAME)
|
||||
endif
|
||||
|
||||
backend-darwin: frontend
|
||||
@echo [backend] Building macOS amd64 executable...
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=darwin" && set "GOARCH=amd64" && $(GO) build -trimpath -ldflags="-s -w" -o $(DIST_DIR)\$(BINARY_NAME) .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=darwin" && set "GOARCH=amd64" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DIST_DIR)\$(BINARY_NAME) .
|
||||
else
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build -trimpath -ldflags="-s -w" -o $(DIST_DIR)/$(BINARY_NAME) .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DIST_DIR)/$(BINARY_NAME) .
|
||||
@chmod +x $(DIST_DIR)/$(BINARY_NAME)
|
||||
endif
|
||||
|
||||
backend: frontend
|
||||
@echo [backend] Building for the current platform...
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && $(GO) build -trimpath -ldflags="-s -w" -o $(DIST_DIR)\$(BINARY_NAME).exe .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DIST_DIR)\$(BINARY_NAME).exe .
|
||||
else
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 $(GO) build -trimpath -ldflags="-s -w" -o $(DIST_DIR)/$(BINARY_NAME) .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DIST_DIR)/$(BINARY_NAME) .
|
||||
@chmod +x $(DIST_DIR)/$(BINARY_NAME)
|
||||
endif
|
||||
|
||||
@@ -78,12 +78,12 @@ endif
|
||||
build-windows: clean frontend
|
||||
@echo [build] Building Windows amd64 package...
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=windows" && set "GOARCH=amd64" && $(GO) build -trimpath -ldflags="-s -w" -o $(WINDOWS_TEMP) .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=windows" && set "GOARCH=amd64" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(WINDOWS_TEMP) .
|
||||
@if exist $(DIST_DIR) rmdir /s /q $(DIST_DIR)
|
||||
@mkdir $(DIST_DIR)
|
||||
@move /y $(WINDOWS_TEMP) $(DIST_DIR)\$(BINARY_NAME).exe >nul
|
||||
else
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build -trimpath -ldflags="-s -w" -o $(WINDOWS_TEMP) .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(WINDOWS_TEMP) .
|
||||
@rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)
|
||||
@mv $(WINDOWS_TEMP) $(DIST_DIR)/$(BINARY_NAME).exe
|
||||
endif
|
||||
@@ -92,12 +92,12 @@ endif
|
||||
build-linux: clean frontend
|
||||
@echo [build] Building Linux amd64 package...
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=linux" && set "GOARCH=amd64" && $(GO) build -trimpath -ldflags="-s -w" -o $(LINUX_TEMP) .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=linux" && set "GOARCH=amd64" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(LINUX_TEMP) .
|
||||
@if exist $(DIST_DIR) rmdir /s /q $(DIST_DIR)
|
||||
@mkdir $(DIST_DIR)
|
||||
@move /y $(LINUX_TEMP) $(DIST_DIR)\$(BINARY_NAME) >nul
|
||||
else
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -trimpath -ldflags="-s -w" -o $(LINUX_TEMP) .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(LINUX_TEMP) .
|
||||
@rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)
|
||||
@mv $(LINUX_TEMP) $(DIST_DIR)/$(BINARY_NAME)
|
||||
@chmod +x $(DIST_DIR)/$(BINARY_NAME)
|
||||
@@ -107,12 +107,12 @@ endif
|
||||
build-darwin: clean frontend
|
||||
@echo [build] Building macOS amd64 package...
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=darwin" && set "GOARCH=amd64" && $(GO) build -trimpath -ldflags="-s -w" -o $(DARWIN_TEMP) .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=darwin" && set "GOARCH=amd64" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DARWIN_TEMP) .
|
||||
@if exist $(DIST_DIR) rmdir /s /q $(DIST_DIR)
|
||||
@mkdir $(DIST_DIR)
|
||||
@move /y $(DARWIN_TEMP) $(DIST_DIR)\$(BINARY_NAME) >nul
|
||||
else
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build -trimpath -ldflags="-s -w" -o $(DARWIN_TEMP) .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(DARWIN_TEMP) .
|
||||
@rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)
|
||||
@mv $(DARWIN_TEMP) $(DIST_DIR)/$(BINARY_NAME)
|
||||
@chmod +x $(DIST_DIR)/$(BINARY_NAME)
|
||||
@@ -122,22 +122,22 @@ endif
|
||||
build: clean frontend
|
||||
@echo [build] Building Windows amd64 package...
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=windows" && set "GOARCH=amd64" && $(GO) build -trimpath -ldflags="-s -w" -o $(ALL_WINDOWS_TEMP) .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=windows" && set "GOARCH=amd64" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(ALL_WINDOWS_TEMP) .
|
||||
@echo [build] Building Linux amd64 package...
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=linux" && set "GOARCH=amd64" && $(GO) build -trimpath -ldflags="-s -w" -o $(ALL_LINUX_TEMP) .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=linux" && set "GOARCH=amd64" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(ALL_LINUX_TEMP) .
|
||||
@echo [build] Building macOS amd64 package...
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=darwin" && set "GOARCH=amd64" && $(GO) build -trimpath -ldflags="-s -w" -o $(ALL_DARWIN_TEMP) .
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOOS=darwin" && set "GOARCH=amd64" && $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(ALL_DARWIN_TEMP) .
|
||||
@if exist $(DIST_DIR) rmdir /s /q $(DIST_DIR)
|
||||
@mkdir $(DIST_DIR)
|
||||
@move /y $(ALL_WINDOWS_TEMP) $(DIST_DIR)\$(BINARY_NAME)-windows-amd64.exe >nul
|
||||
@move /y $(ALL_LINUX_TEMP) $(DIST_DIR)\$(BINARY_NAME)-linux-amd64 >nul
|
||||
@move /y $(ALL_DARWIN_TEMP) $(DIST_DIR)\$(BINARY_NAME)-darwin-amd64 >nul
|
||||
else
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build -trimpath -ldflags="-s -w" -o $(ALL_WINDOWS_TEMP) .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(ALL_WINDOWS_TEMP) .
|
||||
@echo [build] Building Linux amd64 package...
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -trimpath -ldflags="-s -w" -o $(ALL_LINUX_TEMP) .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(ALL_LINUX_TEMP) .
|
||||
@echo [build] Building macOS amd64 package...
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build -trimpath -ldflags="-s -w" -o $(ALL_DARWIN_TEMP) .
|
||||
@GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build -trimpath -buildvcs=false -ldflags="-s -w" -o $(ALL_DARWIN_TEMP) .
|
||||
@rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)
|
||||
@mv $(ALL_WINDOWS_TEMP) $(DIST_DIR)/$(BINARY_NAME)-windows-amd64.exe
|
||||
@mv $(ALL_LINUX_TEMP) $(DIST_DIR)/$(BINARY_NAME)-linux-amd64
|
||||
@@ -150,7 +150,7 @@ endif
|
||||
|
||||
dist: build
|
||||
|
||||
run:
|
||||
run: frontend
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && $(GO) run .
|
||||
else
|
||||
|
||||
@@ -82,12 +82,12 @@ cd ../..
|
||||
|
||||
**开发模式:**
|
||||
|
||||
需要打开两个终端窗口:
|
||||
生产运行只需要启动一个服务。需要前端热更新时,可额外启动 Vite 开发服务器:
|
||||
|
||||
**终端1 - 启动Go后端:**
|
||||
**终端1 - 启动统一服务:**
|
||||
```bash
|
||||
# 在项目根目录运行
|
||||
go run main.go
|
||||
make run
|
||||
```
|
||||
|
||||
**终端2 - 启动前端开发服务器:**
|
||||
@@ -97,11 +97,11 @@ npm run dev
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- `go run main.go` 会编译并运行Go程序
|
||||
- 后端默认运行在 `http://localhost:1551`
|
||||
- 前端默认运行在 `http://localhost:1552`
|
||||
- `make run` 会先构建前端,再启动同时提供前端、API和上传文件的统一服务
|
||||
- 统一服务默认运行在 `http://localhost:1552`
|
||||
- Vite 开发服务器运行在 `http://localhost:5173`,并将 API 请求代理到统一服务
|
||||
- 首次运行会自动创建 `data/` 目录和数据库
|
||||
- 可以通过环境变量 `PORT` 修改后端端口(默认1551)
|
||||
- 可以通过环境变量 `PORT` 修改统一服务端口(默认1552)
|
||||
|
||||
**Windows用户注意:** 如果遇到中文乱码,在PowerShell中运行:
|
||||
```powershell
|
||||
@@ -113,6 +113,7 @@ chcp 65001
|
||||
- 前端:http://localhost:1552
|
||||
- 管理界面:http://localhost:1552/admin
|
||||
- 登录页面:http://localhost:1552/login
|
||||
- API:http://localhost:1552/api
|
||||
|
||||
**默认管理员账号:**
|
||||
- 用户名:`admin`
|
||||
@@ -146,8 +147,8 @@ npm run build
|
||||
```
|
||||
|
||||
构建完成后:
|
||||
- Go二进制文件:`./home-vue-go` (Linux) 或 `./home-vue-go.exe` (Windows)
|
||||
- 前端构建文件:`./dist`
|
||||
- `make build` 的发布包位于 `./dist/`
|
||||
- 前端资源已经嵌入每个平台的二进制文件,部署时只需要对应的可执行文件
|
||||
|
||||
**说明:**
|
||||
- `go build` 编译Go程序为二进制文件
|
||||
@@ -159,7 +160,6 @@ npm run build
|
||||
|
||||
1. **上传文件到服务器:**
|
||||
- 上传 `home-vue-go` 二进制文件
|
||||
- 上传 `dist` 目录(前端构建文件)
|
||||
|
||||
2. **运行二进制文件:**
|
||||
```bash
|
||||
@@ -173,7 +173,7 @@ npm run build
|
||||
|
||||
4. **环境变量(可选):**
|
||||
```bash
|
||||
export PORT=1551 # 服务端口,默认1551
|
||||
export PORT=1552 # 统一服务端口,默认1552
|
||||
export JWT_SECRET=your-secret-key # JWT密钥,建议修改
|
||||
```
|
||||
|
||||
@@ -239,7 +239,7 @@ go mod download
|
||||
cd internal/ent && go generate ./... && cd ../..
|
||||
|
||||
# 运行后端(开发模式)
|
||||
go run main.go
|
||||
make run
|
||||
|
||||
# 构建后端(当前平台)
|
||||
CGO_ENABLED=0 go build -o home-vue-go .
|
||||
@@ -269,6 +269,21 @@ npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### GitHub Actions 构建与安装包
|
||||
|
||||
仓库内置 `.github/workflows/build.yml`,在推送到 `main` 或创建 Pull Request 时运行测试和跨平台构建。每次工作流运行都会产生三个可下载的安装包:
|
||||
|
||||
- Windows amd64:ZIP
|
||||
- Linux amd64:TAR.GZ
|
||||
- macOS amd64:TAR.GZ
|
||||
|
||||
在 GitHub Actions 的运行详情页下载 `package-windows`、`package-linux` 或 `package-darwin`。发布版本时推送一个 `v` 开头的标签,工作流会自动创建 GitHub Release 并附加这三个安装包:
|
||||
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push github v1.0.0
|
||||
```
|
||||
|
||||
### 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -2,11 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -24,263 +23,120 @@ import (
|
||||
var distFS embed.FS
|
||||
|
||||
func main() {
|
||||
// 设置亚洲/上海时区
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
log.Fatal("无法加载时区:", err)
|
||||
}
|
||||
time.Local = loc
|
||||
|
||||
// 获取可执行文件所在目录
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatal("无法获取可执行文件路径:", err)
|
||||
}
|
||||
exeDir := filepath.Dir(exePath)
|
||||
|
||||
// 创建data目录
|
||||
dataDir := filepath.Join(exeDir, "data")
|
||||
dataDir := filepath.Join(filepath.Dir(exePath), "data")
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
log.Fatal("无法创建data目录:", err)
|
||||
}
|
||||
|
||||
// 初始化配置
|
||||
cfg := config.New(dataDir)
|
||||
|
||||
// 初始化数据库
|
||||
db, err := database.Init(cfg.DatabasePath, cfg)
|
||||
if err != nil {
|
||||
log.Fatal("数据库初始化失败:", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置Gin模式
|
||||
if os.Getenv("GIN_MODE") == "" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// 创建Gin路由(不使用Default以避免重复日志)
|
||||
r := gin.New()
|
||||
|
||||
// 添加恢复中间件
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
// 静态文件服务 - 从文件系统提供前端构建文件
|
||||
// 优先使用可执行文件同目录下的dist目录
|
||||
distPath := filepath.Join(exeDir, "dist")
|
||||
if _, err := os.Stat(distPath); err == nil {
|
||||
// 使用可执行文件同目录下的dist
|
||||
r.Static("/static", filepath.Join(distPath, "static"))
|
||||
r.StaticFile("/favicon.ico", filepath.Join(distPath, "favicon.ico"))
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
// API和上传路径不处理
|
||||
if path == "/api" || strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/uploads/") {
|
||||
c.Status(404)
|
||||
return
|
||||
}
|
||||
// 其他路径返回index.html(SPA路由支持)
|
||||
c.File(filepath.Join(distPath, "index.html"))
|
||||
})
|
||||
log.Printf("前端文件目录: %s", distPath)
|
||||
} else {
|
||||
// 回退到当前工作目录的dist(开发模式)
|
||||
if _, err := os.Stat("./dist"); err == nil {
|
||||
r.Static("/static", "./dist/static")
|
||||
r.StaticFile("/favicon.ico", "./dist/favicon.ico")
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
if c.Request.URL.Path != "/api" && !strings.HasPrefix(c.Request.URL.Path, "/api/") && !strings.HasPrefix(c.Request.URL.Path, "/uploads/") {
|
||||
c.File("./dist/index.html")
|
||||
}
|
||||
})
|
||||
log.Println("使用当前目录的dist文件夹(开发模式)")
|
||||
} else {
|
||||
log.Println("警告: 未找到前端文件目录,前端功能不可用")
|
||||
log.Println("提示: 请将前端构建文件放在可执行文件同目录的dist文件夹中")
|
||||
}
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "1552"
|
||||
}
|
||||
|
||||
// 配置CORS
|
||||
r.Use(corsMiddleware())
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery(), corsMiddleware(port))
|
||||
api.SetupRoutes(router, db, cfg)
|
||||
|
||||
// 初始化API路由
|
||||
api.SetupRoutes(r, db, cfg)
|
||||
|
||||
apiPort := os.Getenv("API_PORT")
|
||||
if apiPort == "" {
|
||||
apiPort = "1551"
|
||||
}
|
||||
|
||||
// 创建前端服务器(1552端口)- 从嵌入的文件系统提供前端文件
|
||||
frontendRouter := gin.New()
|
||||
frontendRouter.Use(gin.Recovery())
|
||||
frontendRouter.Use(corsMiddleware())
|
||||
|
||||
// 从嵌入的文件系统加载前端文件
|
||||
distRoot, err := fs.Sub(distFS, "dist")
|
||||
if err == nil {
|
||||
// 使用嵌入的文件系统
|
||||
frontendRouter.StaticFS("/static", http.FS(distRoot))
|
||||
|
||||
// 提供favicon
|
||||
frontendRouter.GET("/favicon.ico", func(c *gin.Context) {
|
||||
data, err := distRoot.Open("favicon.ico")
|
||||
if err != nil {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer data.Close()
|
||||
content, err := io.ReadAll(data)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "image/x-icon", content)
|
||||
})
|
||||
|
||||
// API代理:将/api请求转发到配置的API端口
|
||||
frontendRouter.Any("/api/*path", proxyAPIRequest(apiPort))
|
||||
|
||||
// /uploads代理
|
||||
frontendRouter.Static("/uploads", cfg.UploadDir)
|
||||
|
||||
// SPA路由支持
|
||||
frontendRouter.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/uploads/") {
|
||||
c.Status(404)
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试打开文件
|
||||
filePath := strings.TrimPrefix(path, "/")
|
||||
if filePath == "" {
|
||||
filePath = "index.html"
|
||||
}
|
||||
file, err := distRoot.Open(filePath)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
stat, err := file.Stat()
|
||||
if err == nil && !stat.IsDir() {
|
||||
content, err := io.ReadAll(file)
|
||||
if err == nil {
|
||||
contentType := "text/html"
|
||||
if strings.HasSuffix(filePath, ".css") {
|
||||
contentType = "text/css"
|
||||
} else if strings.HasSuffix(filePath, ".js") {
|
||||
contentType = "application/javascript"
|
||||
} else if strings.HasSuffix(filePath, ".json") {
|
||||
contentType = "application/json"
|
||||
} else if strings.HasSuffix(filePath, ".ico") {
|
||||
contentType = "image/x-icon"
|
||||
} else if strings.HasSuffix(filePath, ".png") {
|
||||
contentType = "image/png"
|
||||
} else if strings.HasSuffix(filePath, ".jpg") || strings.HasSuffix(filePath, ".jpeg") {
|
||||
contentType = "image/jpeg"
|
||||
} else if strings.HasSuffix(filePath, ".svg") {
|
||||
contentType = "image/svg+xml"
|
||||
}
|
||||
c.Data(http.StatusOK, contentType, content)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回index.html(SPA路由)
|
||||
indexFile, err := distRoot.Open("index.html")
|
||||
if err == nil {
|
||||
defer indexFile.Close()
|
||||
content, err := io.ReadAll(indexFile)
|
||||
if err == nil {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", content)
|
||||
} else {
|
||||
c.Status(http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
c.Status(http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
|
||||
log.Println("使用嵌入的前端文件(单一可执行文件模式)")
|
||||
} else {
|
||||
// 回退到文件系统(开发模式)
|
||||
log.Println("警告: 无法加载嵌入的前端文件,尝试从文件系统加载")
|
||||
distPath := filepath.Join(exeDir, "dist")
|
||||
if _, err := os.Stat(distPath); err == nil {
|
||||
frontendRouter.Static("/static", filepath.Join(distPath, "static"))
|
||||
frontendRouter.StaticFile("/favicon.ico", filepath.Join(distPath, "favicon.ico"))
|
||||
|
||||
frontendRouter.Any("/api/*path", proxyAPIRequest(apiPort))
|
||||
|
||||
frontendRouter.Static("/uploads", cfg.UploadDir)
|
||||
frontendRouter.NoRoute(func(c *gin.Context) {
|
||||
if !strings.HasPrefix(c.Request.URL.Path, "/api/") && !strings.HasPrefix(c.Request.URL.Path, "/uploads/") {
|
||||
c.File(filepath.Join(distPath, "index.html"))
|
||||
}
|
||||
})
|
||||
log.Printf("使用文件系统前端文件: %s", distPath)
|
||||
} else if _, err := os.Stat("./dist"); err == nil {
|
||||
frontendRouter.Static("/static", "./dist/static")
|
||||
frontendRouter.StaticFile("/favicon.ico", "./dist/favicon.ico")
|
||||
frontendRouter.Any("/api/*path", proxyAPIRequest(apiPort))
|
||||
frontendRouter.Static("/uploads", cfg.UploadDir)
|
||||
frontendRouter.NoRoute(func(c *gin.Context) {
|
||||
if !strings.HasPrefix(c.Request.URL.Path, "/api/") && !strings.HasPrefix(c.Request.URL.Path, "/uploads/") {
|
||||
c.File("./dist/index.html")
|
||||
}
|
||||
})
|
||||
log.Println("使用当前目录的dist文件夹(开发模式)")
|
||||
} else {
|
||||
log.Println("警告: 未找到前端文件,前端功能不可用")
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal("无法加载嵌入的前端文件:", err)
|
||||
}
|
||||
router.NoRoute(serveFrontend(distRoot))
|
||||
log.Println("使用嵌入的前端文件(单一可执行文件模式)")
|
||||
|
||||
// 启动两个服务器
|
||||
frontendPort := os.Getenv("FRONTEND_PORT")
|
||||
if frontendPort == "" {
|
||||
frontendPort = "1552"
|
||||
}
|
||||
|
||||
// 创建API服务器
|
||||
apiServer := &http.Server{
|
||||
Addr: ":" + apiPort,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
// 创建前端服务器
|
||||
frontendServer := &http.Server{
|
||||
Addr: ":" + frontendPort,
|
||||
Handler: frontendRouter,
|
||||
}
|
||||
|
||||
// 只在首次启动时显示默认账号信息
|
||||
firstRunFile := filepath.Join(dataDir, ".first_run")
|
||||
if _, err := os.Stat(firstRunFile); os.IsNotExist(err) {
|
||||
os.WriteFile(firstRunFile, []byte(""), 0644)
|
||||
if err := os.WriteFile(firstRunFile, []byte(""), 0644); err != nil {
|
||||
log.Printf("记录首次启动状态失败: %v", err)
|
||||
}
|
||||
log.Printf("默认管理员账号: admin, 密码: admin123")
|
||||
log.Printf("提示: 首次启动后,请及时修改默认密码以确保安全")
|
||||
}
|
||||
|
||||
log.Printf("========================================")
|
||||
log.Printf("服务器启动成功!")
|
||||
log.Printf("后端API: http://localhost:%s", apiPort)
|
||||
log.Printf("前端界面: http://localhost:%s", frontendPort)
|
||||
log.Printf("访问端点: http://localhost:%s", port)
|
||||
log.Printf("========================================")
|
||||
|
||||
// 在goroutine中启动前端服务器
|
||||
go func() {
|
||||
if err := frontendServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("前端服务器启动失败: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 在主goroutine中启动API服务器
|
||||
if err := apiServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("API服务器启动失败: %v", err)
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: router,
|
||||
}
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("服务器启动失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
func serveFrontend(root fs.FS) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestPath := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||
if requestPath == "api" || strings.HasPrefix(requestPath, "api/") ||
|
||||
requestPath == "uploads" || strings.HasPrefix(requestPath, "uploads/") {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
filePath := requestPath
|
||||
if filePath == "" {
|
||||
filePath = "index.html"
|
||||
}
|
||||
if serveEmbeddedFile(c, root, filePath) {
|
||||
return
|
||||
}
|
||||
if !serveEmbeddedFile(c, root, "index.html") {
|
||||
c.Status(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveEmbeddedFile(c *gin.Context, root fs.FS, filePath string) bool {
|
||||
file, err := root.Open(filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
stat, err := file.Stat()
|
||||
if err != nil || stat.IsDir() {
|
||||
return false
|
||||
}
|
||||
content, err := fs.ReadFile(root, filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
contentType := mime.TypeByExtension(filepath.Ext(filePath))
|
||||
if contentType == "" {
|
||||
contentType = http.DetectContentType(content)
|
||||
}
|
||||
c.Data(http.StatusOK, contentType, content)
|
||||
return true
|
||||
}
|
||||
|
||||
func corsMiddleware(port string) gin.HandlerFunc {
|
||||
allowedOrigins := make(map[string]struct{})
|
||||
for _, origin := range strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ",") {
|
||||
if value := strings.TrimSpace(origin); value != "" {
|
||||
@@ -288,8 +144,8 @@ func corsMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
if len(allowedOrigins) == 0 {
|
||||
allowedOrigins["http://localhost:1552"] = struct{}{}
|
||||
allowedOrigins["http://127.0.0.1:1552"] = struct{}{}
|
||||
allowedOrigins["http://localhost:"+port] = struct{}{}
|
||||
allowedOrigins["http://127.0.0.1:"+port] = struct{}{}
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
@@ -301,41 +157,10 @@ func corsMiddleware() gin.HandlerFunc {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func proxyAPIRequest(apiPort string) gin.HandlerFunc {
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
return func(c *gin.Context) {
|
||||
target := &url.URL{Scheme: "http", Host: "localhost:" + apiPort, Path: c.Request.URL.Path, RawQuery: c.Request.URL.RawQuery}
|
||||
req, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, target.String(), c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建代理请求失败"})
|
||||
return
|
||||
}
|
||||
for key, values := range c.Request.Header {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "代理请求失败"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Writer.Header().Add(key, value)
|
||||
}
|
||||
}
|
||||
c.Status(resp.StatusCode)
|
||||
_, _ = io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
}
|
||||
|
||||
+44
-23
@@ -1,38 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestProxyAPIRequestUsesConfiguredPort(t *testing.T) {
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"path":"` + r.URL.RequestURI() + `"}`))
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
backendURL, err := url.Parse(backend.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, port, err := net.SplitHostPort(backendURL.Host)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
func TestAPIAndFrontendShareRouter(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/api/*path", proxyAPIRequest(port))
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/config?custom=1", nil))
|
||||
router.GET("/api/ping", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
router.NoRoute(serveFrontend(fstest.MapFS{
|
||||
"index.html": {Data: []byte("<html>app</html>")},
|
||||
"assets/app.js": {Data: []byte("console.log('app')")},
|
||||
}))
|
||||
|
||||
if recorder.Code != http.StatusOK || recorder.Body.String() != `{"path":"/api/config?custom=1"}` {
|
||||
t.Fatalf("unexpected proxy response: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
tests := []struct {
|
||||
path string
|
||||
statusCode int
|
||||
body string
|
||||
}{
|
||||
{path: "/api/ping", statusCode: http.StatusOK, body: `{"status":"ok"}`},
|
||||
{path: "/admin", statusCode: http.StatusOK, body: "<html>app</html>"},
|
||||
{path: "/assets/app.js", statusCode: http.StatusOK, body: "console.log('app')"},
|
||||
{path: "/api/missing", statusCode: http.StatusNotFound, body: "404 page not found"},
|
||||
{path: "/uploads/missing.png", statusCode: http.StatusNotFound, body: "404 page not found"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.path, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, test.path, nil))
|
||||
if recorder.Code != test.statusCode || recorder.Body.String() != test.body {
|
||||
t.Fatalf("unexpected response: status=%d body=%q", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddlewareUsesServicePort(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(corsMiddleware("8080"))
|
||||
router.GET("/api/ping", func(c *gin.Context) { c.Status(http.StatusNoContent) })
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
||||
request.Header.Set("Origin", "http://localhost:8080")
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if origin := recorder.Header().Get("Access-Control-Allow-Origin"); origin != "http://localhost:8080" {
|
||||
t.Fatalf("unexpected allowed origin: %q", origin)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
const apiPort = process.env.API_PORT || '1551';
|
||||
const servicePort = process.env.PORT || '1552';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
@@ -17,14 +17,14 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 1552,
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: `http://localhost:${apiPort}`,
|
||||
target: `http://localhost:${servicePort}`,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/uploads': {
|
||||
target: `http://localhost:${apiPort}`,
|
||||
target: `http://localhost:${servicePort}`,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user