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

This commit is contained in:
2026-08-05 15:13:29 +08:00
parent 56db1cc642
commit 6f07849a53
7 changed files with 318 additions and 367 deletions
+116
View File
@@ -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/*
+31 -57
View File
@@ -1,8 +1,6 @@
# 构建说明 # 构建说明
本项目支持打包为**单一可执行文件**,包含前后端启动后同时提供: 本项目支持打包为**单一可执行文件**,包含前后端启动后通过一个端口同时提供前端、API和上传文件,默认端口为 **1552**
- **1551端口**:后端API服务
- **1552端口**:前端界面服务(自动代理API请求到1551)
所有前端文件已嵌入到二进制文件中,无需额外文件。 所有前端文件已嵌入到二进制文件中,无需额外文件。
@@ -123,18 +121,13 @@ cd dist
## 访问地址 ## 访问地址
启动后,服务器会同时提供两个服务: 启动后,服务器通过统一端点提供完整服务:
- **后端API**: http://localhost:1551 - **访问端点**: http://localhost:1552
- API接口http://localhost:1551/api - 主页http://localhost:1552
- 管理接口http://localhost:1551/api/admin - 管理界面http://localhost:1552/admin
- 登录页面:http://localhost:1552/login
- **前端界面**: http://localhost:1552 - API接口:http://localhost:1552/api
- 主页:http://localhost:1552
- 管理界面:http://localhost:1552/admin
- 登录页面:http://localhost:1552/login
**注意**:前端会自动将 `/api` 请求代理到 `http://localhost:1551`,无需额外配置。
## 1Panel 配置 ## 1Panel 配置
@@ -142,9 +135,7 @@ cd dist
1. **上传文件**:将 `home-vue-go`Linux版本)上传到服务器 1. **上传文件**:将 `home-vue-go`Linux版本)上传到服务器
2. **配置端口** 2. **配置端口**只需放行统一服务端口 `1552`
- 后端API端口:`1551`
- 前端服务端口:`1552`
3. **运行命令** 3. **运行命令**
```bash ```bash
@@ -155,8 +146,7 @@ cd dist
## 配置说明 ## 配置说明
- **后端API端口**:默认 `1551`,可通过环境变量 `API_PORT` 修改 - **统一服务端口**:默认 `1552`,可通过环境变量 `PORT` 修改
- **前端服务端口**:默认 `1552`,可通过环境变量 `FRONTEND_PORT` 修改
- **数据目录**:运行时会自动在二进制文件同目录下创建 `data` 目录 - **数据目录**:运行时会自动在二进制文件同目录下创建 `data` 目录
- **默认管理员账号**`admin` / `admin123`(首次启动时显示) - **默认管理员账号**`admin` / `admin123`(首次启动时显示)
@@ -164,16 +154,14 @@ cd dist
**Windows:** **Windows:**
```bash ```bash
set API_PORT=8080 set PORT=8080
set FRONTEND_PORT=8081 home-vue-go.exe
home-vue-go.exe
``` ```
**Linux/macOS:** **Linux/macOS:**
```bash ```bash
export API_PORT=8080 export PORT=8080
export FRONTEND_PORT=8081 ./home-vue-go
./home-vue-go
``` ```
## 注意事项 ## 注意事项
@@ -181,7 +169,7 @@ export FRONTEND_PORT=8081
1. **SQLite驱动**:使用纯 Go SQLite 驱动,构建无需 CGO 或 GCC 1. **SQLite驱动**:使用纯 Go SQLite 驱动,构建无需 CGO 或 GCC
2. **前端构建**:构建时必须先运行 `npm run build` 生成dist目录,Go编译时会嵌入这些文件 2. **前端构建**:构建时必须先运行 `npm run build` 生成dist目录,Go编译时会嵌入这些文件
3. **Go版本**:需要Go 1.23或更高版本(支持embed功能) 3. **Go版本**:需要Go 1.23或更高版本(支持embed功能)
4. **端口占用**:确保1551和1552端口未被占用 4. **端口占用**:确保统一服务端口(默认1552未被占用
5. **单一文件**:构建完成后,只需一个可执行文件即可运行,无需其他依赖 5. **单一文件**:构建完成后,只需一个可执行文件即可运行,无需其他依赖
6. **Make工具**Windows用户需要安装make工具(Git for Windows自带,或使用Chocolatey安装) 6. **Make工具**Windows用户需要安装make工具(Git for Windows自带,或使用Chocolatey安装)
7. **跨平台编译**Windows、Linux 和 macOS amd64 目标均可直接交叉编译 7. **跨平台编译**Windows、Linux 和 macOS amd64 目标均可直接交叉编译
@@ -222,24 +210,13 @@ choco install make
│ 包含: 后端代码 + 前端文件(嵌入) │ │ 包含: 后端代码 + 前端文件(嵌入) │
├─────────────────────────────────┤ ├─────────────────────────────────┤
│ │ │ │
┌──────────────┐ ┌──────────┐
│ 后端API服务 │ │ 前端服务 │ 统一 HTTP 服务 :1552
│ 端口: 1551 │ │ 端口:1552│ /api /uploads /assets SPA
└─────────────┘ └────┬─────┘ └─────────────────────────────────┘
│ │ │ │ ```
│ │ │ │
│ └───────┬───────┘ │ - **统一服务(1552)**:直接提供 API、上传文件、嵌入的前端静态文件和 SPA 路由
│ │ │
│ API代理 │
│ (前端/api/* → 后端1551) │
└─────────────────────────────────┘
```
- **后端服务(1551)**:提供所有API接口
- **前端服务(1552**
- 从嵌入的文件系统提供前端静态文件(HTML、CSS、JS)
- 自动代理 `/api/*` 请求到后端1551端口
- 支持SPA路由
## 完整构建流程示例 ## 完整构建流程示例
@@ -331,7 +308,7 @@ make build-linux
**1Panel配置:** **1Panel配置:**
- 运行命令:`./home-vue-go`(或完整路径) - 运行命令:`./home-vue-go`(或完整路径)
- 端口映射:1551(后端API)、1552(前端界面 - 端口映射:1552(统一服务端口
- 工作目录:可执行文件所在目录 - 工作目录:可执行文件所在目录
**注意**SQLite 使用纯 Go 驱动,因此从 Windows 交叉构建 Linux 版本不需要额外的 C 工具链。 **注意**SQLite 使用纯 Go 驱动,因此从 Windows 交叉构建 Linux 版本不需要额外的 C 工具链。
@@ -365,26 +342,23 @@ make clean
开发时,可以分别运行前后端: 开发时,可以分别运行前后端:
**终端1 - 后端** **终端1 - 统一服务**
```bash ```bash
make run make run
# # 前端和 API 均运行在 http://localhost:1552
go run main.go
# 后端运行在 http://localhost:1551
``` ```
**终端2 - 前端:** **终端2 - 前端:**
```bash ```bash
npm run dev npm run dev
# 前端运行在 http://localhost:1552,自动代理API到1551 # 热更新页面运行在 http://localhost:5173API代理到统一服务端口
``` ```
## 部署优势 ## 部署优势
**单一可执行文件**:前后端一体化,所有文件嵌入在二进制中 **单一可执行文件**:前后端一体化,所有文件嵌入在二进制中
**无需依赖**:不需要Node.js、npm或其他运行时 **无需依赖**:不需要Node.js、npm或其他运行时
**端口分离**API和前端服务分离,便于管理和扩展 **端口服务**前端、API和上传文件共用一个端口
**自动代理**:前端自动代理API请求,无需额外配置 **1Panel友好**:只需配置一个端口,运行一个命令即可
**1Panel友好**:只需配置两个端口,运行一个命令即可
**部署简单**:上传一个文件,配置端口,即可运行 **部署简单**:上传一个文件,配置端口,即可运行
**跨平台构建**:使用make统一构建流程,支持多平台 **跨平台构建**:使用make统一构建流程,支持多平台
+21 -21
View File
@@ -42,35 +42,35 @@ frontend:
backend-windows: frontend backend-windows: frontend
@echo [backend] Building Windows amd64 executable... @echo [backend] Building Windows amd64 executable...
ifeq ($(OS),Windows_NT) 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 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 endif
backend-linux: frontend backend-linux: frontend
@echo [backend] Building Linux amd64 executable... @echo [backend] Building Linux amd64 executable...
ifeq ($(OS),Windows_NT) 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 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) @chmod +x $(DIST_DIR)/$(BINARY_NAME)
endif endif
backend-darwin: frontend backend-darwin: frontend
@echo [backend] Building macOS amd64 executable... @echo [backend] Building macOS amd64 executable...
ifeq ($(OS),Windows_NT) 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 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) @chmod +x $(DIST_DIR)/$(BINARY_NAME)
endif endif
backend: frontend backend: frontend
@echo [backend] Building for the current platform... @echo [backend] Building for the current platform...
ifeq ($(OS),Windows_NT) 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 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) @chmod +x $(DIST_DIR)/$(BINARY_NAME)
endif endif
@@ -78,12 +78,12 @@ endif
build-windows: clean frontend build-windows: clean frontend
@echo [build] Building Windows amd64 package... @echo [build] Building Windows amd64 package...
ifeq ($(OS),Windows_NT) 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) @if exist $(DIST_DIR) rmdir /s /q $(DIST_DIR)
@mkdir $(DIST_DIR) @mkdir $(DIST_DIR)
@move /y $(WINDOWS_TEMP) $(DIST_DIR)\$(BINARY_NAME).exe >nul @move /y $(WINDOWS_TEMP) $(DIST_DIR)\$(BINARY_NAME).exe >nul
else 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) @rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)
@mv $(WINDOWS_TEMP) $(DIST_DIR)/$(BINARY_NAME).exe @mv $(WINDOWS_TEMP) $(DIST_DIR)/$(BINARY_NAME).exe
endif endif
@@ -92,12 +92,12 @@ endif
build-linux: clean frontend build-linux: clean frontend
@echo [build] Building Linux amd64 package... @echo [build] Building Linux amd64 package...
ifeq ($(OS),Windows_NT) 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) @if exist $(DIST_DIR) rmdir /s /q $(DIST_DIR)
@mkdir $(DIST_DIR) @mkdir $(DIST_DIR)
@move /y $(LINUX_TEMP) $(DIST_DIR)\$(BINARY_NAME) >nul @move /y $(LINUX_TEMP) $(DIST_DIR)\$(BINARY_NAME) >nul
else 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) @rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)
@mv $(LINUX_TEMP) $(DIST_DIR)/$(BINARY_NAME) @mv $(LINUX_TEMP) $(DIST_DIR)/$(BINARY_NAME)
@chmod +x $(DIST_DIR)/$(BINARY_NAME) @chmod +x $(DIST_DIR)/$(BINARY_NAME)
@@ -107,12 +107,12 @@ endif
build-darwin: clean frontend build-darwin: clean frontend
@echo [build] Building macOS amd64 package... @echo [build] Building macOS amd64 package...
ifeq ($(OS),Windows_NT) 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) @if exist $(DIST_DIR) rmdir /s /q $(DIST_DIR)
@mkdir $(DIST_DIR) @mkdir $(DIST_DIR)
@move /y $(DARWIN_TEMP) $(DIST_DIR)\$(BINARY_NAME) >nul @move /y $(DARWIN_TEMP) $(DIST_DIR)\$(BINARY_NAME) >nul
else 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) @rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)
@mv $(DARWIN_TEMP) $(DIST_DIR)/$(BINARY_NAME) @mv $(DARWIN_TEMP) $(DIST_DIR)/$(BINARY_NAME)
@chmod +x $(DIST_DIR)/$(BINARY_NAME) @chmod +x $(DIST_DIR)/$(BINARY_NAME)
@@ -122,22 +122,22 @@ endif
build: clean frontend build: clean frontend
@echo [build] Building Windows amd64 package... @echo [build] Building Windows amd64 package...
ifeq ($(OS),Windows_NT) 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... @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... @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) @if exist $(DIST_DIR) rmdir /s /q $(DIST_DIR)
@mkdir $(DIST_DIR) @mkdir $(DIST_DIR)
@move /y $(ALL_WINDOWS_TEMP) $(DIST_DIR)\$(BINARY_NAME)-windows-amd64.exe >nul @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_LINUX_TEMP) $(DIST_DIR)\$(BINARY_NAME)-linux-amd64 >nul
@move /y $(ALL_DARWIN_TEMP) $(DIST_DIR)\$(BINARY_NAME)-darwin-amd64 >nul @move /y $(ALL_DARWIN_TEMP) $(DIST_DIR)\$(BINARY_NAME)-darwin-amd64 >nul
else 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... @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... @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) @rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)
@mv $(ALL_WINDOWS_TEMP) $(DIST_DIR)/$(BINARY_NAME)-windows-amd64.exe @mv $(ALL_WINDOWS_TEMP) $(DIST_DIR)/$(BINARY_NAME)-windows-amd64.exe
@mv $(ALL_LINUX_TEMP) $(DIST_DIR)/$(BINARY_NAME)-linux-amd64 @mv $(ALL_LINUX_TEMP) $(DIST_DIR)/$(BINARY_NAME)-linux-amd64
@@ -150,7 +150,7 @@ endif
dist: build dist: build
run: run: frontend
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
@set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && $(GO) run . @set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && $(GO) run .
else else
+27 -12
View File
@@ -82,12 +82,12 @@ cd ../..
**开发模式:** **开发模式:**
需要打开两个终端窗口 生产运行只需要启动一个服务。需要前端热更新时,可额外启动 Vite 开发服务器
**终端1 - 启动Go后端** **终端1 - 启动统一服务**
```bash ```bash
# 在项目根目录运行 # 在项目根目录运行
go run main.go make run
``` ```
**终端2 - 启动前端开发服务器:** **终端2 - 启动前端开发服务器:**
@@ -97,11 +97,11 @@ npm run dev
``` ```
**说明:** **说明:**
- `go run main.go` 会编译并运行Go程序 - `make run` 会先构建前端,再启动同时提供前端、API和上传文件的统一服务
- 后端默认运行在 `http://localhost:1551` - 统一服务默认运行在 `http://localhost:1552`
- 前端默认运行在 `http://localhost:1552` - Vite 开发服务器运行在 `http://localhost:5173`,并将 API 请求代理到统一服务
- 首次运行会自动创建 `data/` 目录和数据库 - 首次运行会自动创建 `data/` 目录和数据库
- 可以通过环境变量 `PORT` 修改后端端口(默认1551 - 可以通过环境变量 `PORT` 修改统一服务端口(默认1552
**Windows用户注意:** 如果遇到中文乱码,在PowerShell中运行: **Windows用户注意:** 如果遇到中文乱码,在PowerShell中运行:
```powershell ```powershell
@@ -113,6 +113,7 @@ chcp 65001
- 前端:http://localhost:1552 - 前端:http://localhost:1552
- 管理界面:http://localhost:1552/admin - 管理界面:http://localhost:1552/admin
- 登录页面:http://localhost:1552/login - 登录页面:http://localhost:1552/login
- APIhttp://localhost:1552/api
**默认管理员账号:** **默认管理员账号:**
- 用户名:`admin` - 用户名:`admin`
@@ -146,8 +147,8 @@ npm run build
``` ```
构建完成后: 构建完成后:
- Go二进制文件:`./home-vue-go` (Linux) 或 `./home-vue-go.exe` (Windows) - `make build` 的发布包位于 `./dist/`
- 前端构建文件:`./dist` - 前端资源已经嵌入每个平台的二进制文件,部署时只需要对应的可执行文件
**说明:** **说明:**
- `go build` 编译Go程序为二进制文件 - `go build` 编译Go程序为二进制文件
@@ -159,7 +160,6 @@ npm run build
1. **上传文件到服务器:** 1. **上传文件到服务器:**
- 上传 `home-vue-go` 二进制文件 - 上传 `home-vue-go` 二进制文件
- 上传 `dist` 目录(前端构建文件)
2. **运行二进制文件:** 2. **运行二进制文件:**
```bash ```bash
@@ -173,7 +173,7 @@ npm run build
4. **环境变量(可选):** 4. **环境变量(可选):**
```bash ```bash
export PORT=1551 # 服务端口,默认1551 export PORT=1552 # 统一服务端口,默认1552
export JWT_SECRET=your-secret-key # JWT密钥,建议修改 export JWT_SECRET=your-secret-key # JWT密钥,建议修改
``` ```
@@ -239,7 +239,7 @@ go mod download
cd internal/ent && go generate ./... && cd ../.. cd internal/ent && go generate ./... && cd ../..
# 运行后端(开发模式) # 运行后端(开发模式)
go run main.go make run
# 构建后端(当前平台) # 构建后端(当前平台)
CGO_ENABLED=0 go build -o home-vue-go . CGO_ENABLED=0 go build -o home-vue-go .
@@ -269,6 +269,21 @@ npm run build
npm run preview npm run preview
``` ```
### GitHub Actions 构建与安装包
仓库内置 `.github/workflows/build.yml`,在推送到 `main` 或创建 Pull Request 时运行测试和跨平台构建。每次工作流运行都会产生三个可下载的安装包:
- Windows amd64ZIP
- Linux amd64TAR.GZ
- macOS amd64TAR.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 MIT License
+75 -250
View File
@@ -2,11 +2,10 @@ package main
import ( import (
"embed" "embed"
"io"
"io/fs" "io/fs"
"log" "log"
"mime"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -24,263 +23,120 @@ import (
var distFS embed.FS var distFS embed.FS
func main() { func main() {
// 设置亚洲/上海时区
loc, err := time.LoadLocation("Asia/Shanghai") loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil { if err != nil {
log.Fatal("无法加载时区:", err) log.Fatal("无法加载时区:", err)
} }
time.Local = loc time.Local = loc
// 获取可执行文件所在目录
exePath, err := os.Executable() exePath, err := os.Executable()
if err != nil { if err != nil {
log.Fatal("无法获取可执行文件路径:", err) log.Fatal("无法获取可执行文件路径:", err)
} }
exeDir := filepath.Dir(exePath) dataDir := filepath.Join(filepath.Dir(exePath), "data")
// 创建data目录
dataDir := filepath.Join(exeDir, "data")
if err := os.MkdirAll(dataDir, 0755); err != nil { if err := os.MkdirAll(dataDir, 0755); err != nil {
log.Fatal("无法创建data目录:", err) log.Fatal("无法创建data目录:", err)
} }
// 初始化配置
cfg := config.New(dataDir) cfg := config.New(dataDir)
// 初始化数据库
db, err := database.Init(cfg.DatabasePath, cfg) db, err := database.Init(cfg.DatabasePath, cfg)
if err != nil { if err != nil {
log.Fatal("数据库初始化失败:", err) log.Fatal("数据库初始化失败:", err)
} }
defer db.Close() defer db.Close()
// 设置Gin模式
if os.Getenv("GIN_MODE") == "" { if os.Getenv("GIN_MODE") == "" {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
// 创建Gin路由(不使用Default以避免重复日志) port := os.Getenv("PORT")
r := gin.New() if port == "" {
port = "1552"
// 添加恢复中间件
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.htmlSPA路由支持)
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文件夹中")
}
} }
// 配置CORS router := gin.New()
r.Use(corsMiddleware()) 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") distRoot, err := fs.Sub(distFS, "dist")
if err == nil { if err != nil {
// 使用嵌入的文件系统 log.Fatal("无法加载嵌入的前端文件:", err)
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.htmlSPA路由)
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("警告: 未找到前端文件,前端功能不可用")
}
} }
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") firstRunFile := filepath.Join(dataDir, ".first_run")
if _, err := os.Stat(firstRunFile); os.IsNotExist(err) { 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("默认管理员账号: admin, 密码: admin123")
log.Printf("提示: 首次启动后,请及时修改默认密码以确保安全") log.Printf("提示: 首次启动后,请及时修改默认密码以确保安全")
} }
log.Printf("========================================") log.Printf("========================================")
log.Printf("服务器启动成功!") log.Printf("服务器启动成功!")
log.Printf("后端API: http://localhost:%s", apiPort) log.Printf("访问端点: http://localhost:%s", port)
log.Printf("前端界面: http://localhost:%s", frontendPort)
log.Printf("========================================") log.Printf("========================================")
// 在goroutine中启动前端服务器 server := &http.Server{
go func() { Addr: ":" + port,
if err := frontendServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { Handler: router,
log.Fatalf("前端服务器启动失败: %v", err) }
} if err := server.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)
} }
} }
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{}) allowedOrigins := make(map[string]struct{})
for _, origin := range strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ",") { for _, origin := range strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ",") {
if value := strings.TrimSpace(origin); value != "" { if value := strings.TrimSpace(origin); value != "" {
@@ -288,8 +144,8 @@ func corsMiddleware() gin.HandlerFunc {
} }
} }
if len(allowedOrigins) == 0 { if len(allowedOrigins) == 0 {
allowedOrigins["http://localhost:1552"] = struct{}{} allowedOrigins["http://localhost:"+port] = struct{}{}
allowedOrigins["http://127.0.0.1:1552"] = struct{}{} allowedOrigins["http://127.0.0.1:"+port] = struct{}{}
} }
return func(c *gin.Context) { return func(c *gin.Context) {
origin := c.GetHeader("Origin") 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-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") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
if c.Request.Method == "OPTIONS" { if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(204) c.AbortWithStatus(http.StatusNoContent)
return return
} }
c.Next() 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
View File
@@ -1,38 +1,59 @@
package main package main
import ( import (
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"testing" "testing"
"testing/fstest"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func TestProxyAPIRequestUsesConfiguredPort(t *testing.T) { func TestAPIAndFrontendShareRouter(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)
}
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
router := gin.New() router := gin.New()
router.Any("/api/*path", proxyAPIRequest(port)) router.GET("/api/ping", func(c *gin.Context) {
recorder := httptest.NewRecorder() c.JSON(http.StatusOK, gin.H{"status": "ok"})
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/config?custom=1", nil)) })
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"}` { tests := []struct {
t.Fatalf("unexpected proxy response: status=%d body=%s", recorder.Code, recorder.Body.String()) 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
View File
@@ -1,7 +1,7 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
const apiPort = process.env.API_PORT || '1551'; const servicePort = process.env.PORT || '1552';
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
@@ -17,14 +17,14 @@ export default defineConfig({
}, },
}, },
server: { server: {
port: 1552, port: 5173,
proxy: { proxy: {
'/api': { '/api': {
target: `http://localhost:${apiPort}`, target: `http://localhost:${servicePort}`,
changeOrigin: true, changeOrigin: true,
}, },
'/uploads': { '/uploads': {
target: `http://localhost:${apiPort}`, target: `http://localhost:${servicePort}`,
changeOrigin: true, changeOrigin: true,
}, },
}, },