diff --git a/BUILD.md b/BUILD.md index 43aebc5..bf930c6 100644 --- a/BUILD.md +++ b/BUILD.md @@ -23,26 +23,17 @@ 如果你安装了 Git for Windows,make 工具通常已经包含在内,但可能不在 PATH 中。 -**方式1:使用提供的 make.bat 包装脚本(推荐)** -```bash -make.bat build-linux -make.bat build-windows -make.bat build -``` - -这个脚本会自动查找 Git 自带的 make 工具。 - -**方式2:将 Git 的 make 添加到 PATH** +**方式1:将 Git 的 make 添加到 PATH** 1. 找到 Git 安装目录(通常是 `C:\Program Files\Git`) 2. 将 `C:\Program Files\Git\usr\bin` 添加到系统 PATH 环境变量 3. 重启终端后即可直接使用 `make` 命令 -**方式3:使用 Chocolatey 安装 make** +**方式2:使用 Chocolatey 安装 make** ```powershell choco install make ``` -**方式4:使用 WSL** +**方式3:使用 WSL** 在 WSL 中运行 make 命令。 ### 构建命令 @@ -86,7 +77,7 @@ make backend-windows # Windows make backend-darwin # macOS ``` -**生成Ent代码(首次构建前需要):** +**生成 Ent 代码(仅修改 Schema 后需要):** ```bash make generate ``` @@ -185,32 +176,17 @@ export FRONTEND_PORT=8081 ## 注意事项 -1. **CGO依赖**:本项目使用SQLite数据库,需要启用CGO(`CGO_ENABLED=1`) +1. **SQLite驱动**:使用纯 Go SQLite 驱动,构建无需 CGO 或 GCC 2. **前端构建**:构建时必须先运行 `npm run build` 生成dist目录,Go编译时会嵌入这些文件 3. **Go版本**:需要Go 1.23或更高版本(支持embed功能) 4. **端口占用**:确保1551和1552端口未被占用 5. **单一文件**:构建完成后,只需一个可执行文件即可运行,无需其他依赖 6. **Make工具**:Windows用户需要安装make工具(Git for Windows自带,或使用Chocolatey安装) -7. **跨平台编译**:在Windows上交叉编译Linux版本需要gcc工具链,推荐使用WSL或在Linux系统上直接构建 +7. **跨平台编译**:Windows、Linux 和 macOS amd64 目标均可直接交叉编译 ## Windows 上使用 Make -### 方式1:使用提供的 make.bat 包装脚本(最简单) - -项目根目录提供了 `make.bat` 脚本,它会自动查找 Git 自带的 make 工具: - -```bash -# 构建 Linux 版本 -make.bat build-linux - -# 构建 Windows 版本 -make.bat build-windows - -# 构建当前平台版本 -make.bat build -``` - -### 方式2:将 Git 的 make 添加到 PATH(推荐) +### 方式1:将 Git 的 make 添加到 PATH(推荐) Git for Windows 自带 make 工具,通常位于: - `C:\Program Files\Git\usr\bin\make.exe` @@ -223,15 +199,15 @@ Git for Windows 自带 make 工具,通常位于: 4. 点击"确定"保存 5. 重启终端后即可直接使用 `make` 命令 -### 方式3:使用 Chocolatey 安装 make +### 方式2:使用 Chocolatey 安装 make ```powershell choco install make ``` -### 方式4:使用 WSL +### 方式3:使用 WSL 在 WSL 中运行 make 命令。 -### 方式5:手动下载 make for Windows +### 方式4:手动下载 make for Windows 从 https://sourceforge.net/projects/gnuwin32/files/make/ 下载并安装 ## 架构说明 @@ -312,14 +288,9 @@ make build-linux # 构建完成后,dist/home-vue-go 就是Linux可执行文件 ``` -**方式2:直接在Windows上构建(需要gcc工具链)** -```bash -# 如果遇到交叉编译错误,需要安装gcc工具链 -# 使用MSYS2安装: -# pacman -S mingw-w64-x86_64-gcc - -# 然后运行 -make build-linux +**方式2:直接在 Windows 上交叉构建** +```bash +make build-linux ``` **方式3:在Linux服务器上直接构建(最简单,推荐)** @@ -361,7 +332,7 @@ make build-linux - 端口映射:1551(后端API)、1552(前端界面) - 工作目录:可执行文件所在目录 -**注意**:由于项目使用SQLite(需要CGO),在Windows上交叉编译Linux版本需要额外的工具链。推荐使用WSL或在Linux系统上直接构建。 +**注意**:SQLite 使用纯 Go 驱动,因此从 Windows 交叉构建 Linux 版本不需要额外的 C 工具链。 ### 在 Linux 上构建 Windows 版本 diff --git a/Makefile b/Makefile index 9533c2d..274c334 100644 --- a/Makefile +++ b/Makefile @@ -1,189 +1,161 @@ -.PHONY: generate build build-linux build-windows build-darwin clean run dist frontend backend - -# 生成Ent代码 -generate: - cd internal/ent && go generate ./... - -# 构建前端 -frontend: - npm install - npm run build - -# 构建后端(Linux) -backend-linux: - CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dist/home-vue-go main.go - chmod +x dist/home-vue-go - -# 构建后端(Windows) -backend-windows: - CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o dist/home-vue-go.exe main.go - -# 构建后端(macOS) -backend-darwin: - CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o dist/home-vue-go main.go - chmod +x dist/home-vue-go - -# 构建后端(当前平台) -backend: - CGO_ENABLED=1 go build -ldflags="-s -w" -o dist/home-vue-go main.go - chmod +x dist/home-vue-go 2>/dev/null || true - -# 完整构建到dist目录(Linux)- 单一可执行文件 -build-linux: clean generate - @echo "========================================" - @echo "构建 Linux 版本" - @echo "========================================" -ifeq ($(OS),Windows_NT) - @if not exist dist mkdir dist - @call npm install && call npm run build - @if not exist dist\index.html (echo 错误: 前端构建失败 && exit 1) - @echo [提示] 在Windows上交叉编译Linux版本需要gcc工具链 - @echo [提示] 如果遇到错误,推荐使用WSL或在Linux系统上直接构建 - @echo [开始编译]... - @set CGO_ENABLED=1 - @set GOOS=linux - @set GOARCH=amd64 - @go build -ldflags="-s -w" -o dist\home-vue-go main.go - @if errorlevel 1 ( - @echo. - @echo [错误] 交叉编译失败 - @echo [原因] 在Windows上交叉编译Linux版本需要Linux的gcc工具链 - @echo. - @echo [解决方案1] 使用WSL(推荐): - @echo wsl - @echo cd /mnt/d/Desktop/Home-Vue-go - @echo make build-linux - @echo. - @echo [解决方案2] 在Linux服务器上直接构建: - @echo git clone ^ - @echo cd Home-Vue-go - @echo make build-linux - @echo. - @echo [解决方案3] 安装gcc工具链(复杂): - @echo - 使用MSYS2: pacman -S mingw-w64-x86_64-gcc - @echo - 或使用TDM-GCC - @exit /b 1 - ) - @if exist dist\static rmdir /s /q dist\static 2>nul - @if exist dist\index.html del /f /q dist\index.html 2>nul - @if exist dist\favicon.ico del /f /q dist\favicon.ico 2>nul - @echo. - @echo 构建完成!单一可执行文件: dist\home-vue-go - @echo 后端API: http://localhost:1551 - @echo 前端界面: http://localhost:1552 - @echo 1Panel配置: 只需配置1551和1552端口,运行命令: ./home-vue-go -else - @mkdir -p dist - @npm install && npm run build - @if [ ! -f "dist/index.html" ]; then echo "错误: 前端构建失败"; exit 1; fi - @CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dist/home-vue-go main.go - @chmod +x dist/home-vue-go - @rm -rf dist/static dist/index.html dist/favicon.ico 2>/dev/null || true - @echo "" - @echo "构建完成!单一可执行文件: dist/home-vue-go" - @echo "后端API: http://localhost:1551" - @echo "前端界面: http://localhost:1552" - @echo "1Panel配置: 只需配置1551和1552端口,运行命令: ./home-vue-go" -endif - -# 完整构建到dist目录(Windows)- 单一可执行文件 -build-windows: clean generate - @echo ======================================== - @echo 构建 Windows 版本 - @echo ======================================== - @if not exist dist mkdir dist - @npm install && npm run build - @if not exist dist\index.html (echo 错误: 前端构建失败 && exit 1) - @set CGO_ENABLED=1 && go build -ldflags="-s -w" -o dist\home-vue-go.exe main.go - @if exist dist\static rmdir /s /q dist\static - @if exist dist\index.html del /f /q dist\index.html - @if exist dist\favicon.ico del /f /q dist\favicon.ico - @echo. - @echo 构建完成!单一可执行文件: dist\home-vue-go.exe - @echo 后端API: http://localhost:1551 - @echo 前端界面: http://localhost:1552 - @echo 1Panel配置: 只需配置1551和1552端口,运行命令: ./home-vue-go.exe - -# 完整构建到dist目录(macOS)- 单一可执行文件 -build-darwin: clean generate - @echo "========================================" - @echo "构建 macOS 版本" - @echo "========================================" -ifeq ($(OS),Windows_NT) - @if not exist dist mkdir dist - @call npm install && call npm run build - @if not exist dist\index.html (echo 错误: 前端构建失败 && exit 1) - @set CGO_ENABLED=1 && set GOOS=darwin && set GOARCH=amd64 && go build -ldflags="-s -w" -o dist\home-vue-go main.go - @if exist dist\static rmdir /s /q dist\static 2>nul - @if exist dist\index.html del /f /q dist\index.html 2>nul - @if exist dist\favicon.ico del /f /q dist\favicon.ico 2>nul - @echo. - @echo 构建完成!单一可执行文件: dist\home-vue-go - @echo 后端API: http://localhost:1551 - @echo 前端界面: http://localhost:1552 -else - @mkdir -p dist - @npm install && npm run build - @if [ ! -f "dist/index.html" ]; then echo "错误: 前端构建失败"; exit 1; fi - @CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o dist/home-vue-go main.go - @chmod +x dist/home-vue-go - @rm -rf dist/static dist/index.html dist/favicon.ico 2>/dev/null || true - @echo "" - @echo "构建完成!单一可执行文件: dist/home-vue-go" - @echo "后端API: http://localhost:1551" - @echo "前端界面: http://localhost:1552" -endif - -# 完整构建到dist目录(当前平台)- 单一可执行文件 -build: clean generate - @echo "========================================" - @echo "构建当前平台版本" - @echo "========================================" -ifeq ($(OS),Windows_NT) - @if not exist dist mkdir dist - @call npm install && call npm run build - @if not exist dist\index.html (echo 错误: 前端构建失败 && exit 1) - @set CGO_ENABLED=1 && go build -ldflags="-s -w" -o dist\home-vue-go.exe main.go - @if exist dist\static rmdir /s /q dist\static 2>nul - @if exist dist\index.html del /f /q dist\index.html 2>nul - @if exist dist\favicon.ico del /f /q dist\favicon.ico 2>nul - @echo. - @echo 构建完成!单一可执行文件: dist\home-vue-go.exe - @echo 后端API: http://localhost:1551 - @echo 前端界面: http://localhost:1552 - @echo 1Panel配置: 只需配置1551和1552端口,运行命令: ./home-vue-go.exe -else - @mkdir -p dist - @npm install && npm run build - @if [ ! -f "dist/index.html" ]; then echo "错误: 前端构建失败"; exit 1; fi - @CGO_ENABLED=1 go build -ldflags="-s -w" -o dist/home-vue-go main.go - @chmod +x dist/home-vue-go 2>/dev/null || true - @rm -rf dist/static dist/index.html dist/favicon.ico 2>/dev/null || true - @echo "" - @echo "构建完成!单一可执行文件: dist/home-vue-go" - @echo "后端API: http://localhost:1551" - @echo "前端界面: http://localhost:1552" - @echo "1Panel配置: 只需配置1551和1552端口,运行命令: ./home-vue-go" -endif - -# 打包到dist目录(推荐使用) -dist: build - -# 运行开发服务器 -run: - go run main.go - -# 清理 -clean: - @echo "清理构建文件..." -ifeq ($(OS),Windows_NT) - @if exist home-vue-go.exe del /f /q home-vue-go.exe 2>nul - @if exist home-vue-go del /f /q home-vue-go 2>nul - @if exist dist rmdir /s /q dist 2>nul - @if exist node_modules rmdir /s /q node_modules 2>nul -else - @rm -f home-vue-go home-vue-go.exe 2>/dev/null || true - @rm -rf dist 2>/dev/null || true - @rm -rf node_modules 2>/dev/null || true -endif - @echo "清理完成" +.PHONY: generate frontend backend backend-linux backend-windows backend-darwin \ + build build-linux build-windows build-darwin clean clean-all dist run + +.NOTPARALLEL: + +GO ?= go +NPM ?= npm +GOPROXY ?= https://goproxy.cn,direct +DIST_DIR := dist +BINARY_NAME := home-vue-go +WINDOWS_TEMP := $(BINARY_NAME)-windows-amd64.exe +LINUX_TEMP := $(BINARY_NAME)-linux-amd64 +DARWIN_TEMP := $(BINARY_NAME)-darwin-amd64 + +ifeq ($(OS),Windows_NT) +NPM_CMD := npm.cmd +else +NPM_CMD := $(NPM) +endif + +# Generated Ent sources are committed. Run this target only after schema changes. +generate: + @echo [generate] Updating Ent sources... +ifeq ($(OS),Windows_NT) + @cd internal\ent && set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && set "GOARCH=amd64" && $(GO) generate ./... +else + @cd internal/ent && GOPROXY="$(GOPROXY)" $(GO) generate ./... +endif + @echo [generate] Done. + +# Install missing packages without deleting a usable local dependency tree. +frontend: + @echo [frontend] Installing dependencies... + @$(NPM_CMD) install --prefer-offline --no-audit --no-fund + @echo [frontend] Building production assets... + @$(NPM_CMD) run build + +# Backend-only targets keep frontend files beside the executable for debugging. +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 . +else + @GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build -trimpath -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) . +else + @GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -trimpath -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) . +else + @GOPROXY="$(GOPROXY)" CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build -trimpath -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 . +else + @GOPROXY="$(GOPROXY)" CGO_ENABLED=0 $(GO) build -trimpath -ldflags="-s -w" -o $(DIST_DIR)/$(BINARY_NAME) . + @chmod +x $(DIST_DIR)/$(BINARY_NAME) +endif + +# Full builds embed frontend assets, then remove the duplicate loose files. +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) . + @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) . + @rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR) + @mv $(WINDOWS_TEMP) $(DIST_DIR)/$(BINARY_NAME).exe +endif + @echo [build] Output: $(DIST_DIR)/$(BINARY_NAME).exe + +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) . + @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) . + @rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR) + @mv $(LINUX_TEMP) $(DIST_DIR)/$(BINARY_NAME) + @chmod +x $(DIST_DIR)/$(BINARY_NAME) +endif + @echo [build] Output: $(DIST_DIR)/$(BINARY_NAME) + +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) . + @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) . + @rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR) + @mv $(DARWIN_TEMP) $(DIST_DIR)/$(BINARY_NAME) + @chmod +x $(DIST_DIR)/$(BINARY_NAME) +endif + @echo [build] Output: $(DIST_DIR)/$(BINARY_NAME) + +ifeq ($(OS),Windows_NT) +build: build-windows +else +build: clean frontend + @echo [build] Building current platform package... + @GOPROXY="$(GOPROXY)" CGO_ENABLED=0 $(GO) build -trimpath -ldflags="-s -w" -o $(BINARY_NAME) . + @rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR) + @mv $(BINARY_NAME) $(DIST_DIR)/$(BINARY_NAME) + @chmod +x $(DIST_DIR)/$(BINARY_NAME) + @echo [build] Output: $(DIST_DIR)/$(BINARY_NAME) +endif + +dist: build + +run: +ifeq ($(OS),Windows_NT) + @set "GOPROXY=$(GOPROXY)" && set "CGO_ENABLED=0" && $(GO) run . +else + @GOPROXY="$(GOPROXY)" CGO_ENABLED=0 $(GO) run . +endif + +clean: + @echo [clean] Removing build artifacts... +ifeq ($(OS),Windows_NT) + @if exist $(DIST_DIR) rmdir /s /q $(DIST_DIR) + @if exist $(BINARY_NAME).exe del /f /q $(BINARY_NAME).exe + @if exist $(WINDOWS_TEMP) del /f /q $(WINDOWS_TEMP) + @if exist $(LINUX_TEMP) del /f /q $(LINUX_TEMP) + @if exist $(DARWIN_TEMP) del /f /q $(DARWIN_TEMP) +else + @rm -rf $(DIST_DIR) + @rm -f $(BINARY_NAME) $(BINARY_NAME).exe $(WINDOWS_TEMP) $(LINUX_TEMP) $(DARWIN_TEMP) +endif + @echo [clean] Done. + +clean-all: clean + @echo [clean] Removing frontend dependencies... +ifeq ($(OS),Windows_NT) + @if exist node_modules rmdir /s /q node_modules +else + @rm -rf node_modules +endif + @echo [clean] Dependency cleanup complete. diff --git a/README.md b/README.md index 636e22e..1be2f5b 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ npm install go mod download ``` -#### 3. 生成Ent代码(必须) +#### 3. 生成 Ent 代码(仅修改 Schema 后) -**重要:** 在运行项目之前,必须先生成Ent代码,否则Go代码无法编译。 +生成后的 Ent 源码已提交到仓库,普通运行和构建不需要重复生成。只有修改 `internal/ent/schema` 后才运行: ```bash # 进入ent目录 @@ -72,21 +72,11 @@ go generate ./... cd ../.. ``` -**如果遇到版本兼容性错误**(如 `invalid array length` 或 `Deprecated undefined`),使用: -```bash -cd internal/ent -# 使用与go.mod一致的版本(当前是v0.13.1) -go run -mod=mod entgo.io/ent/cmd/ent@v0.13.1 generate ./schema -cd ../.. -``` - -**重要**:版本号必须与 `go.mod` 中的 `entgo.io/ent` 版本一致。 - **说明:** - `go generate` 是Go的内置命令,用于运行代码生成工具 - `./...` 表示当前目录及所有子目录 - 这会根据 `schema/` 目录中的定义生成Ent ORM代码 -- 如果Go版本较新(如go1.25+),可能需要使用 `go run` 方式 +- 生成器使用 `go.mod` 中固定的 Ent 版本,不会使用 `@latest` #### 4. 运行项目 @@ -132,26 +122,14 @@ chcp 65001 #### 5. 构建部署 -**构建Go后端(Linux):** +**构建 Linux 单文件程序:** ```bash -# 生成Ent代码(必须) -cd internal/ent -go generate ./... -cd ../.. - -# 构建Linux二进制文件 -CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o home-vue-go main.go +make build-linux ``` -**构建Go后端(Windows):** -```bash -# 生成Ent代码(必须) -cd internal\ent -go generate ./... -cd ..\.. - -# 构建Windows二进制文件 -go build -o home-vue-go.exe main.go +**构建 Windows 单文件程序:** +```powershell +make build-windows ``` **构建前端:** @@ -166,7 +144,7 @@ npm run build **说明:** - `go build` 编译Go程序为二进制文件 -- `CGO_ENABLED=1` 启用CGO(SQLite需要) +- SQLite 使用纯 Go 驱动,构建不需要 CGO 或 GCC - `GOOS=linux GOARCH=amd64` 指定目标平台和架构 - `-o` 指定输出文件名 @@ -257,10 +235,10 @@ cd internal/ent && go generate ./... && cd ../.. go run main.go # 构建后端(当前平台) -go build -o home-vue-go main.go +CGO_ENABLED=0 go build -o home-vue-go . # 构建后端(Linux) -CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o home-vue-go main.go +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o home-vue-go . # 查看Go版本 go version diff --git a/go.mod b/go.mod index 8e930ed..13e47b7 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module home-vue-go -go 1.23 +go 1.23.0 require ( entgo.io/ent v0.14.5 github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/mattn/go-sqlite3 v1.14.22 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.33.0 + modernc.org/sqlite v1.36.3 ) require ( @@ -19,6 +19,7 @@ require ( github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/inflect v0.19.0 // indirect @@ -27,27 +28,41 @@ require ( github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl/v2 v2.18.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/zclconf/go-cty v1.14.4 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect golang.org/x/arch v0.8.0 // indirect + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.30.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.61.13 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect ) diff --git a/go.sum b/go.sum index db3a3c4..e473c1a 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,10 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= @@ -14,13 +18,19 @@ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -43,23 +53,42 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo= github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -70,27 +99,47 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -104,34 +153,95 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= +modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA= +modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= +modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= +modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= +modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.36.3 h1:qYMYlFR+rtLDUzuXoST1SDIdEPbX8xzuhdF90WsX1ss= +modernc.org/sqlite v1.36.3/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0 h1:xGFgVi5ZaWOnYdac2foDT3vg0ZZC9ErXFV57mr4OHrI= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/api/config.go b/internal/api/config.go index f429ae1..80a83ef 100644 --- a/internal/api/config.go +++ b/internal/api/config.go @@ -1,305 +1,308 @@ -package api - -import ( - "fmt" - "net/http" - "net/url" - "strings" - - "home-vue-go/internal/config" - "home-vue-go/internal/database" - - "github.com/gin-gonic/gin" -) - -type updateSiteConfigRequest struct { - SiteName *string `json:"siteName"` - SiteURL *string `json:"siteURL"` - SiteIcon *string `json:"siteIcon"` - SiteDescription *string `json:"siteDescription"` - SiteKeywords *string `json:"siteKeywords"` - UserName *string `json:"userName"` - ProfileImageURL *string `json:"profileImageURL"` - ICPNumber *string `json:"icpNumber"` - PoliceNumber *string `json:"policeNumber"` - PageTitle *string `json:"pageTitle"` - Favicon *string `json:"favicon"` - IconLibrary *string `json:"iconLibrary"` - FontLibrary *string `json:"fontLibrary"` - - FooterYearStart *string `json:"footerYearStart"` - FooterYearEnd *string `json:"footerYearEnd"` - ShowVisitTimer *bool `json:"showVisitTimer"` - RotatingTexts *[]string `json:"rotatingTexts"` - - GreetingText *string `json:"greetingText"` - OnlineStatusText *string `json:"onlineStatusText"` - FooterLabel *string `json:"footerLabel"` - ShowAbout *bool `json:"showAbout"` - ShowSites *bool `json:"showSites"` - ShowContacts *bool `json:"showContacts"` - ShowThemeToggle *bool `json:"showThemeToggle"` - ShowFooter *bool `json:"showFooter"` - AboutTitle *string `json:"aboutTitle"` - AboutDescription *string `json:"aboutDescription"` - AboutLinks *[]config.AboutLink `json:"aboutLinks"` - SitePageSize *int `json:"sitePageSize"` - OpenLinksNewTab *bool `json:"openLinksInNewTab"` - - AnalyticsProvider *string `json:"analyticsProvider"` - UmamiScript *string `json:"umamiScript"` - UmamiScriptURL *string `json:"umamiScriptUrl"` - UmamiWebsiteID *string `json:"umamiWebsiteId"` - UmamiAPIMode *string `json:"umamiApiMode"` - UmamiAPIURL *string `json:"umamiApiUrl"` - UmamiCredential *string `json:"umamiCredential"` - ClearUmamiCredential *bool `json:"clearUmamiCredential"` - UmamiDomains *string `json:"umamiDomains"` - UmamiDoNotTrack *bool `json:"umamiDoNotTrack"` - UmamiExcludeSearch *bool `json:"umamiExcludeSearch"` - UmamiExcludeHash *bool `json:"umamiExcludeHash"` - UmamiPerformance *bool `json:"umamiPerformance"` - UmamiTag *string `json:"umamiTag"` - UmamiTrackerOptions *struct { - Domains *string `json:"domains"` - DoNotTrack *bool `json:"doNotTrack"` - ExcludeSearch *bool `json:"excludeSearch"` - ExcludeHash *bool `json:"excludeHash"` - Performance *bool `json:"performance"` - Tag *string `json:"tag"` - } `json:"umamiTrackerOptions"` -} - -func GetSiteConfig(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - settings, err := db.LoadSiteSettings(c.Request.Context()) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"}) - return - } - c.JSON(http.StatusOK, siteConfigResponse(settings, false)) - } -} - -func GetAdminSiteConfig(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - settings, err := db.LoadSiteSettings(c.Request.Context()) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"}) - return - } - c.JSON(http.StatusOK, siteConfigResponse(settings, true)) - } -} - -func UpdateSiteConfig(db *database.Database, cfg *config.Config) gin.HandlerFunc { - return func(c *gin.Context) { - var req updateSiteConfigRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "配置参数格式错误"}) - return - } - - ctx := c.Request.Context() - settings, err := db.LoadSiteSettings(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "加载现有站点配置失败"}) - return - } - next := settings.Clone() - applyString(&next.SiteName, req.SiteName) - applyString(&next.SiteURL, req.SiteURL) - applyString(&next.SiteIcon, req.SiteIcon) - applyString(&next.SiteDescription, req.SiteDescription) - applyString(&next.SiteKeywords, req.SiteKeywords) - applyString(&next.UserName, req.UserName) - applyString(&next.ProfileImageURL, req.ProfileImageURL) - applyString(&next.ICPNumber, req.ICPNumber) - applyString(&next.PoliceNumber, req.PoliceNumber) - applyString(&next.PageTitle, req.PageTitle) - applyString(&next.Favicon, req.Favicon) - applyString(&next.IconLibrary, req.IconLibrary) - applyString(&next.FontLibrary, req.FontLibrary) - applyString(&next.FooterYearStart, req.FooterYearStart) - applyString(&next.FooterYearEnd, req.FooterYearEnd) - applyBool(&next.ShowVisitTimer, req.ShowVisitTimer) - applySlice(&next.RotatingTexts, req.RotatingTexts) - applyString(&next.GreetingText, req.GreetingText) - applyString(&next.OnlineStatusText, req.OnlineStatusText) - applyString(&next.FooterLabel, req.FooterLabel) - applyBool(&next.ShowAbout, req.ShowAbout) - applyBool(&next.ShowSites, req.ShowSites) - applyBool(&next.ShowContacts, req.ShowContacts) - applyBool(&next.ShowThemeToggle, req.ShowThemeToggle) - applyBool(&next.ShowFooter, req.ShowFooter) - applyString(&next.AboutTitle, req.AboutTitle) - applyString(&next.AboutDescription, req.AboutDescription) - applySlice(&next.AboutLinks, req.AboutLinks) - applyInt(&next.SitePageSize, req.SitePageSize) - applyBool(&next.OpenLinksNewTab, req.OpenLinksNewTab) - applyString(&next.AnalyticsProvider, req.AnalyticsProvider) - applyString(&next.UmamiScript, req.UmamiScript) - applyString(&next.UmamiScript, req.UmamiScriptURL) - applyString(&next.UmamiWebsiteID, req.UmamiWebsiteID) - applyString(&next.UmamiAPIMode, req.UmamiAPIMode) - applyString(&next.UmamiAPIURL, req.UmamiAPIURL) - applyString(&next.UmamiDomains, req.UmamiDomains) - applyBool(&next.UmamiDoNotTrack, req.UmamiDoNotTrack) - applyBool(&next.UmamiExcludeSearch, req.UmamiExcludeSearch) - applyBool(&next.UmamiExcludeHash, req.UmamiExcludeHash) - applyBool(&next.UmamiPerformance, req.UmamiPerformance) - applyString(&next.UmamiTag, req.UmamiTag) - if options := req.UmamiTrackerOptions; options != nil { - applyString(&next.UmamiDomains, options.Domains) - applyBool(&next.UmamiDoNotTrack, options.DoNotTrack) - applyBool(&next.UmamiExcludeSearch, options.ExcludeSearch) - applyBool(&next.UmamiExcludeHash, options.ExcludeHash) - applyBool(&next.UmamiPerformance, options.Performance) - applyString(&next.UmamiTag, options.Tag) - } - - if req.ClearUmamiCredential != nil && *req.ClearUmamiCredential { - next.UmamiCredential = "" - } else if req.UmamiCredential != nil { - if strings.TrimSpace(*req.UmamiCredential) == "" { - next.UmamiCredential = "" - } else { - if cfg == nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "加密配置不可用"}) - return - } - encrypted, err := cfg.EncryptSecret(strings.TrimSpace(*req.UmamiCredential)) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "保存统计凭据失败"}) - return - } - next.UmamiCredential = encrypted - } - } - - if err := validateSiteSettings(next); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - if err := db.SaveSiteSettings(ctx, next); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点配置失败"}) - return - } - c.JSON(http.StatusOK, siteConfigResponse(next, true)) - } -} - -func GetRotatingTexts(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - settings, err := db.LoadSiteSettings(c.Request.Context()) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "加载轮换文本配置失败"}) - return - } - c.JSON(http.StatusOK, gin.H{"texts": settings.RotatingTexts}) - } -} - -func UpdateRotatingTexts(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - var req struct { - Texts []string `json:"texts"` - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "轮换文本参数格式错误"}) - return - } - settings, err := db.LoadSiteSettings(c.Request.Context()) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"}) - return - } - settings.RotatingTexts = req.Texts - settings.Normalize() - if err := db.SaveSiteSettings(c.Request.Context(), settings); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "保存轮换文本配置失败"}) - return - } - c.JSON(http.StatusOK, gin.H{"message": "轮换文本配置已保存", "texts": settings.RotatingTexts}) - } -} - -func siteConfigResponse(settings *config.SiteSettings, admin bool) gin.H { - result := gin.H{ - "siteName": settings.SiteName, "siteURL": settings.SiteURL, "siteIcon": settings.SiteIcon, - "siteDescription": settings.SiteDescription, "siteKeywords": settings.SiteKeywords, "userName": settings.UserName, - "profileImageURL": settings.ProfileImageURL, "icpNumber": settings.ICPNumber, "policeNumber": settings.PoliceNumber, - "pageTitle": settings.PageTitle, "favicon": settings.Favicon, "iconLibrary": settings.IconLibrary, "fontLibrary": settings.FontLibrary, - "footerYearStart": settings.FooterYearStart, "footerYearEnd": settings.FooterYearEnd, "showVisitTimer": settings.ShowVisitTimer, - "rotatingTexts": settings.RotatingTexts, "greetingText": settings.GreetingText, "onlineStatusText": settings.OnlineStatusText, - "footerLabel": settings.FooterLabel, "showAbout": settings.ShowAbout, "showSites": settings.ShowSites, "showContacts": settings.ShowContacts, - "showThemeToggle": settings.ShowThemeToggle, "showFooter": settings.ShowFooter, "aboutTitle": settings.AboutTitle, - "aboutDescription": settings.AboutDescription, "aboutLinks": settings.AboutLinks, "sitePageSize": settings.SitePageSize, - "openLinksInNewTab": settings.OpenLinksNewTab, "analyticsProvider": settings.AnalyticsProvider, "umamiScript": settings.UmamiScript, "umamiScriptUrl": settings.UmamiScript, - "umamiWebsiteId": settings.UmamiWebsiteID, "umamiDomains": settings.UmamiDomains, "umamiDoNotTrack": settings.UmamiDoNotTrack, - "umamiExcludeSearch": settings.UmamiExcludeSearch, "umamiExcludeHash": settings.UmamiExcludeHash, - "umamiPerformance": settings.UmamiPerformance, "umamiTag": settings.UmamiTag, - "umamiTrackerOptions": gin.H{"domains": settings.UmamiDomains, "doNotTrack": settings.UmamiDoNotTrack, "excludeSearch": settings.UmamiExcludeSearch, "excludeHash": settings.UmamiExcludeHash, "performance": settings.UmamiPerformance, "tag": settings.UmamiTag}, - } - if admin { - result["umamiApiMode"] = settings.UmamiAPIMode - result["umamiApiUrl"] = settings.UmamiAPIURL - result["umamiCredentialConfigured"] = settings.UmamiCredential != "" - } - return result -} - -func validateSiteSettings(settings *config.SiteSettings) error { - for name, value := range map[string]string{"站点URL": settings.SiteURL, "Umami脚本地址": settings.UmamiScript, "Umami API地址": settings.UmamiAPIURL} { - if value == "" { - continue - } - parsed, err := url.ParseRequestURI(value) - if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { - return fmt.Errorf("%s必须是有效的 http/https 地址", name) - } - } - if settings.SitePageSize != 6 && settings.SitePageSize != 9 && settings.SitePageSize != 12 { - return fmt.Errorf("站点每页数量只能是 6、9 或 12") - } - if settings.AnalyticsProvider != "local" && settings.AnalyticsProvider != "umami" { - return fmt.Errorf("统计来源只能是 local 或 umami") - } - if settings.UmamiAPIMode != "selfhost" && settings.UmamiAPIMode != "cloud" { - return fmt.Errorf("Umami API 模式不正确") - } - if len(settings.AboutLinks) > 8 { - return fmt.Errorf("关于链接最多支持 8 条") - } - for _, link := range settings.AboutLinks { - if strings.TrimSpace(link.URL) == "" { - return fmt.Errorf("关于链接地址不能为空") - } - parsed, err := url.ParseRequestURI(link.URL) - if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { - return fmt.Errorf("关于链接必须是有效的 http/https 地址") - } - } - return nil -} - -func applyString(target *string, value *string) { - if value != nil { - *target = strings.TrimSpace(*value) - } -} -func applyBool(target *bool, value *bool) { - if value != nil { - *target = *value - } -} -func applyInt(target *int, value *int) { - if value != nil { - *target = *value - } -} -func applySlice[T any](target *[]T, value *[]T) { - if value != nil { - *target = *value - } -} +package api + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "home-vue-go/internal/config" + "home-vue-go/internal/database" + + "github.com/gin-gonic/gin" +) + +type updateSiteConfigRequest struct { + SiteName *string `json:"siteName"` + SiteURL *string `json:"siteURL"` + SiteIcon *string `json:"siteIcon"` + SiteDescription *string `json:"siteDescription"` + SiteKeywords *string `json:"siteKeywords"` + UserName *string `json:"userName"` + ProfileImageURL *string `json:"profileImageURL"` + ICPNumber *string `json:"icpNumber"` + PoliceNumber *string `json:"policeNumber"` + PageTitle *string `json:"pageTitle"` + Favicon *string `json:"favicon"` + IconLibrary *string `json:"iconLibrary"` + FontLibrary *string `json:"fontLibrary"` + + FooterYearStart *string `json:"footerYearStart"` + FooterYearEnd *string `json:"footerYearEnd"` + ShowVisitTimer *bool `json:"showVisitTimer"` + RotatingTexts *[]string `json:"rotatingTexts"` + + GreetingText *string `json:"greetingText"` + OnlineStatusText *string `json:"onlineStatusText"` + FooterLabel *string `json:"footerLabel"` + ShowAbout *bool `json:"showAbout"` + ShowSites *bool `json:"showSites"` + ShowContacts *bool `json:"showContacts"` + ShowThemeToggle *bool `json:"showThemeToggle"` + ShowFooter *bool `json:"showFooter"` + AboutTitle *string `json:"aboutTitle"` + AboutDescription *string `json:"aboutDescription"` + AboutLinks *[]config.AboutLink `json:"aboutLinks"` + SitePageSize *int `json:"sitePageSize"` + OpenLinksNewTab *bool `json:"openLinksInNewTab"` + + AnalyticsProvider *string `json:"analyticsProvider"` + UmamiScript *string `json:"umamiScript"` + UmamiScriptURL *string `json:"umamiScriptUrl"` + UmamiWebsiteID *string `json:"umamiWebsiteId"` + UmamiAPIMode *string `json:"umamiApiMode"` + UmamiAPIURL *string `json:"umamiApiUrl"` + UmamiCredential *string `json:"umamiCredential"` + ClearUmamiCredential *bool `json:"clearUmamiCredential"` + UmamiDomains *string `json:"umamiDomains"` + UmamiDoNotTrack *bool `json:"umamiDoNotTrack"` + UmamiExcludeSearch *bool `json:"umamiExcludeSearch"` + UmamiExcludeHash *bool `json:"umamiExcludeHash"` + UmamiPerformance *bool `json:"umamiPerformance"` + UmamiTag *string `json:"umamiTag"` + UmamiTrackerOptions *struct { + Domains *string `json:"domains"` + DoNotTrack *bool `json:"doNotTrack"` + ExcludeSearch *bool `json:"excludeSearch"` + ExcludeHash *bool `json:"excludeHash"` + Performance *bool `json:"performance"` + Tag *string `json:"tag"` + } `json:"umamiTrackerOptions"` +} + +func GetSiteConfig(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + settings, err := db.LoadSiteSettings(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"}) + return + } + c.JSON(http.StatusOK, siteConfigResponse(settings, false)) + } +} + +func GetAdminSiteConfig(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + settings, err := db.LoadSiteSettings(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"}) + return + } + c.JSON(http.StatusOK, siteConfigResponse(settings, true)) + } +} + +func UpdateSiteConfig(db *database.Database, cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + var req updateSiteConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "配置参数格式错误"}) + return + } + + ctx := c.Request.Context() + settings, err := db.LoadSiteSettings(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "加载现有站点配置失败"}) + return + } + next := settings.Clone() + applyString(&next.SiteName, req.SiteName) + applyString(&next.SiteURL, req.SiteURL) + applyString(&next.SiteIcon, req.SiteIcon) + applyString(&next.SiteDescription, req.SiteDescription) + applyString(&next.SiteKeywords, req.SiteKeywords) + applyString(&next.UserName, req.UserName) + applyString(&next.ProfileImageURL, req.ProfileImageURL) + applyString(&next.ICPNumber, req.ICPNumber) + applyString(&next.PoliceNumber, req.PoliceNumber) + applyString(&next.PageTitle, req.PageTitle) + applyString(&next.Favicon, req.Favicon) + applyString(&next.IconLibrary, req.IconLibrary) + applyString(&next.FontLibrary, req.FontLibrary) + applyString(&next.FooterYearStart, req.FooterYearStart) + applyString(&next.FooterYearEnd, req.FooterYearEnd) + applyBool(&next.ShowVisitTimer, req.ShowVisitTimer) + applySlice(&next.RotatingTexts, req.RotatingTexts) + applyString(&next.GreetingText, req.GreetingText) + applyString(&next.OnlineStatusText, req.OnlineStatusText) + applyString(&next.FooterLabel, req.FooterLabel) + applyBool(&next.ShowAbout, req.ShowAbout) + applyBool(&next.ShowSites, req.ShowSites) + applyBool(&next.ShowContacts, req.ShowContacts) + applyBool(&next.ShowThemeToggle, req.ShowThemeToggle) + applyBool(&next.ShowFooter, req.ShowFooter) + applyString(&next.AboutTitle, req.AboutTitle) + applyString(&next.AboutDescription, req.AboutDescription) + applySlice(&next.AboutLinks, req.AboutLinks) + applyInt(&next.SitePageSize, req.SitePageSize) + applyBool(&next.OpenLinksNewTab, req.OpenLinksNewTab) + applyString(&next.AnalyticsProvider, req.AnalyticsProvider) + applyString(&next.UmamiScript, req.UmamiScript) + applyString(&next.UmamiScript, req.UmamiScriptURL) + applyString(&next.UmamiWebsiteID, req.UmamiWebsiteID) + applyString(&next.UmamiAPIMode, req.UmamiAPIMode) + applyString(&next.UmamiAPIURL, req.UmamiAPIURL) + applyString(&next.UmamiDomains, req.UmamiDomains) + applyBool(&next.UmamiDoNotTrack, req.UmamiDoNotTrack) + applyBool(&next.UmamiExcludeSearch, req.UmamiExcludeSearch) + applyBool(&next.UmamiExcludeHash, req.UmamiExcludeHash) + applyBool(&next.UmamiPerformance, req.UmamiPerformance) + applyString(&next.UmamiTag, req.UmamiTag) + if options := req.UmamiTrackerOptions; options != nil { + applyString(&next.UmamiDomains, options.Domains) + applyBool(&next.UmamiDoNotTrack, options.DoNotTrack) + applyBool(&next.UmamiExcludeSearch, options.ExcludeSearch) + applyBool(&next.UmamiExcludeHash, options.ExcludeHash) + applyBool(&next.UmamiPerformance, options.Performance) + applyString(&next.UmamiTag, options.Tag) + } + + if req.ClearUmamiCredential != nil && *req.ClearUmamiCredential { + next.UmamiCredential = "" + } else if req.UmamiCredential != nil { + if strings.TrimSpace(*req.UmamiCredential) == "" { + next.UmamiCredential = "" + } else { + if cfg == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "加密配置不可用"}) + return + } + encrypted, err := cfg.EncryptSecret(strings.TrimSpace(*req.UmamiCredential)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存统计凭据失败"}) + return + } + next.UmamiCredential = encrypted + } + } + + if err := validateSiteSettings(next); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := db.SaveSiteSettings(ctx, next); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点配置失败"}) + return + } + c.JSON(http.StatusOK, siteConfigResponse(next, true)) + } +} + +func GetRotatingTexts(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + settings, err := db.LoadSiteSettings(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "加载轮换文本配置失败"}) + return + } + c.JSON(http.StatusOK, gin.H{"texts": settings.RotatingTexts}) + } +} + +func UpdateRotatingTexts(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + var req struct { + Texts []string `json:"texts"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "轮换文本参数格式错误"}) + return + } + settings, err := db.LoadSiteSettings(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "加载站点配置失败"}) + return + } + settings.RotatingTexts = req.Texts + settings.Normalize() + if err := db.SaveSiteSettings(c.Request.Context(), settings); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存轮换文本配置失败"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "轮换文本配置已保存", "texts": settings.RotatingTexts}) + } +} + +func siteConfigResponse(settings *config.SiteSettings, admin bool) gin.H { + result := gin.H{ + "siteName": settings.SiteName, "siteURL": settings.SiteURL, "siteIcon": settings.SiteIcon, + "siteDescription": settings.SiteDescription, "siteKeywords": settings.SiteKeywords, "userName": settings.UserName, + "profileImageURL": settings.ProfileImageURL, "icpNumber": settings.ICPNumber, "policeNumber": settings.PoliceNumber, + "pageTitle": settings.PageTitle, "favicon": settings.Favicon, "iconLibrary": settings.IconLibrary, "fontLibrary": settings.FontLibrary, + "footerYearStart": settings.FooterYearStart, "footerYearEnd": settings.FooterYearEnd, "showVisitTimer": settings.ShowVisitTimer, + "rotatingTexts": settings.RotatingTexts, "greetingText": settings.GreetingText, "onlineStatusText": settings.OnlineStatusText, + "footerLabel": settings.FooterLabel, "showAbout": settings.ShowAbout, "showSites": settings.ShowSites, "showContacts": settings.ShowContacts, + "showThemeToggle": settings.ShowThemeToggle, "showFooter": settings.ShowFooter, "aboutTitle": settings.AboutTitle, + "aboutDescription": settings.AboutDescription, "aboutLinks": settings.AboutLinks, "sitePageSize": settings.SitePageSize, + "openLinksInNewTab": settings.OpenLinksNewTab, "analyticsProvider": settings.AnalyticsProvider, "umamiScript": settings.UmamiScript, "umamiScriptUrl": settings.UmamiScript, + "umamiWebsiteId": settings.UmamiWebsiteID, "umamiDomains": settings.UmamiDomains, "umamiDoNotTrack": settings.UmamiDoNotTrack, + "umamiExcludeSearch": settings.UmamiExcludeSearch, "umamiExcludeHash": settings.UmamiExcludeHash, + "umamiPerformance": settings.UmamiPerformance, "umamiTag": settings.UmamiTag, + "umamiTrackerOptions": gin.H{"domains": settings.UmamiDomains, "doNotTrack": settings.UmamiDoNotTrack, "excludeSearch": settings.UmamiExcludeSearch, "excludeHash": settings.UmamiExcludeHash, "performance": settings.UmamiPerformance, "tag": settings.UmamiTag}, + } + if admin { + result["umamiApiMode"] = settings.UmamiAPIMode + result["umamiApiUrl"] = settings.UmamiAPIURL + result["umamiCredentialConfigured"] = settings.UmamiCredential != "" + } + return result +} + +func validateSiteSettings(settings *config.SiteSettings) error { + for name, value := range map[string]string{"站点URL": settings.SiteURL, "Umami脚本地址": settings.UmamiScript, "Umami API地址": settings.UmamiAPIURL} { + if value == "" { + continue + } + parsed, err := url.ParseRequestURI(value) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { + return fmt.Errorf("%s必须是有效的 http/https 地址", name) + } + } + if settings.SitePageSize != 6 && settings.SitePageSize != 9 && settings.SitePageSize != 12 { + return fmt.Errorf("站点每页数量只能是 6、9 或 12") + } + if settings.AnalyticsProvider != "local" && settings.AnalyticsProvider != "umami" { + return fmt.Errorf("统计来源只能是 local 或 umami") + } + if settings.UmamiAPIMode != "selfhost" && settings.UmamiAPIMode != "cloud" { + return fmt.Errorf("Umami API 模式不正确") + } + if len(settings.AboutLinks) > 8 { + return fmt.Errorf("关于链接最多支持 8 条") + } + for _, link := range settings.AboutLinks { + if strings.TrimSpace(link.URL) == "" { + return fmt.Errorf("关于链接地址不能为空") + } + parsed, err := url.ParseRequestURI(link.URL) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { + return fmt.Errorf("关于链接必须是有效的 http/https 地址") + } + if strings.TrimSpace(link.Icon) != "" && !validIconValue(link.Icon) { + return fmt.Errorf("关于链接图标格式不正确") + } + } + return nil +} + +func applyString(target *string, value *string) { + if value != nil { + *target = strings.TrimSpace(*value) + } +} +func applyBool(target *bool, value *bool) { + if value != nil { + *target = *value + } +} +func applyInt(target *int, value *int) { + if value != nil { + *target = *value + } +} +func applySlice[T any](target *[]T, value *[]T) { + if value != nil { + *target = *value + } +} diff --git a/internal/api/contacts.go b/internal/api/contacts.go index 862e496..487a9f2 100644 --- a/internal/api/contacts.go +++ b/internal/api/contacts.go @@ -1,319 +1,319 @@ -package api - -import ( - "net/http" - "strconv" - "strings" - - "home-vue-go/internal/database" - "home-vue-go/internal/ent" - "home-vue-go/internal/ent/contact" - - "github.com/gin-gonic/gin" -) - -func GetContacts(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - ctx := c.Request.Context() - contacts, err := db.Client.Contact.Query().Order(contact.BySortOrder(), contact.ByID()).All(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - result := make([]gin.H, len(contacts)) - for i, contact := range contacts { - result[i] = gin.H{ - "id": contact.ID, - "type": contact.Type, - "icon": contact.Icon, - "url": contact.URL, - "qrCode": contact.QrCode, - "hoverColor": contact.HoverColor, - "sortOrder": contact.SortOrder, - } - } - - c.JSON(http.StatusOK, result) - } -} - -func ReorderContacts(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - var req struct { - IDs []int `json:"ids" binding:"required"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - if len(req.IDs) == 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表不能为空"}) - return - } - - seen := make(map[int]bool, len(req.IDs)) - for _, id := range req.IDs { - if id <= 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含无效ID"}) - return - } - if seen[id] { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含重复ID"}) - return - } - seen[id] = true - } - - ctx := c.Request.Context() - existingIDs, err := db.Client.Contact.Query().IDs(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if len(existingIDs) != len(req.IDs) { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表与当前联系方式数量不一致"}) - return - } - existing := make(map[int]bool, len(existingIDs)) - for _, id := range existingIDs { - existing[id] = true - } - for _, id := range req.IDs { - if !existing[id] { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含不存在的联系方式"}) - return - } - } - - tx, err := db.Client.Tx(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - for index, id := range req.IDs { - if _, err := tx.Contact.UpdateOneID(id).SetSortOrder((index + 1) * 10).Save(ctx); err != nil { - _ = tx.Rollback() - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - } - - if err := tx.Commit(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - contacts, err := db.Client.Contact.Query().Order(contact.BySortOrder(), contact.ByID()).All(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - result := make([]gin.H, len(contacts)) - for i, contact := range contacts { - result[i] = gin.H{ - "id": contact.ID, - "type": contact.Type, - "icon": contact.Icon, - "url": contact.URL, - "qrCode": contact.QrCode, - "hoverColor": contact.HoverColor, - "sortOrder": contact.SortOrder, - } - } - - c.JSON(http.StatusOK, result) - } -} - -func CreateContact(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - var req struct { - Type string `json:"type" binding:"required"` - Icon string `json:"icon" binding:"required"` - URL string `json:"url"` - QrCode string `json:"qrCode"` - HoverColor string `json:"hoverColor"` - SortOrder int `json:"sortOrder"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - req.Type = strings.TrimSpace(req.Type) - req.Icon = strings.TrimSpace(req.Icon) - req.URL = strings.TrimSpace(req.URL) - req.QrCode = strings.TrimSpace(req.QrCode) - req.HoverColor = strings.TrimSpace(req.HoverColor) - if req.Type == "" || req.Icon == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "类型和图标不能为空"}) - return - } - - if req.URL != "" && !validContactURL(req.URL) { - c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式URL必须是有效的http/https/mailto/tel地址"}) - return - } - if req.QrCode != "" && !validQRCode(req.QrCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": "二维码必须使用本地上传路径或安全的http/https图片地址"}) - return - } - if req.URL != "" && req.QrCode != "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式不能同时设置链接和二维码"}) - return - } - if req.URL == "" && req.QrCode == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "链接或二维码不能为空"}) - return - } - - ctx := c.Request.Context() - create := db.Client.Contact.Create(). - SetType(req.Type). - SetIcon(req.Icon). - SetSortOrder(req.SortOrder) - - if req.URL != "" { - create.SetURL(req.URL) - } - if req.QrCode != "" { - create.SetQrCode(req.QrCode) - } - if req.HoverColor != "" { - create.SetHoverColor(req.HoverColor) - } - - contact, err := create.Save(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "id": contact.ID, - "type": contact.Type, - "icon": contact.Icon, - "url": contact.URL, - "qrCode": contact.QrCode, - "hoverColor": contact.HoverColor, - "sortOrder": contact.SortOrder, - }) - } -} - -func UpdateContact(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) - return - } - - var req struct { - Type string `json:"type" binding:"required"` - Icon string `json:"icon" binding:"required"` - URL string `json:"url"` - QrCode string `json:"qrCode"` - HoverColor string `json:"hoverColor"` - SortOrder int `json:"sortOrder"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - req.Type = strings.TrimSpace(req.Type) - req.Icon = strings.TrimSpace(req.Icon) - req.URL = strings.TrimSpace(req.URL) - req.QrCode = strings.TrimSpace(req.QrCode) - req.HoverColor = strings.TrimSpace(req.HoverColor) - if req.Type == "" || req.Icon == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "类型和图标不能为空"}) - return - } - - if req.URL != "" && !validContactURL(req.URL) { - c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式URL必须是有效的http/https/mailto/tel地址"}) - return - } - if req.QrCode != "" && !validQRCode(req.QrCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": "二维码必须使用本地上传路径或安全的http/https图片地址"}) - return - } - if req.URL != "" && req.QrCode != "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式不能同时设置链接和二维码"}) - return - } - if req.URL == "" && req.QrCode == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "链接或二维码不能为空"}) - return - } - - ctx := c.Request.Context() - update := db.Client.Contact.UpdateOneID(id) - update.SetType(req.Type) - update.SetIcon(req.Icon) - if req.URL != "" { - update.SetURL(req.URL) - } else { - update.ClearURL() - } - if req.QrCode != "" { - update.SetQrCode(req.QrCode) - } else { - update.ClearQrCode() - } - if req.HoverColor != "" { - update.SetHoverColor(req.HoverColor) - } else { - update.ClearHoverColor() - } - update.SetSortOrder(req.SortOrder) - - contact, err := update.Save(ctx) - if err != nil { - if ent.IsNotFound(err) { - c.JSON(http.StatusNotFound, gin.H{"error": "联系方式不存在"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "id": contact.ID, - "type": contact.Type, - "icon": contact.Icon, - "url": contact.URL, - "qrCode": contact.QrCode, - "hoverColor": contact.HoverColor, - "sortOrder": contact.SortOrder, - }) - } -} - -func DeleteContact(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) - return - } - - ctx := c.Request.Context() - if err := db.Client.Contact.DeleteOneID(id).Exec(ctx); err != nil { - if ent.IsNotFound(err) { - c.JSON(http.StatusNotFound, gin.H{"error": "联系方式不存在"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) - } -} +package api + +import ( + "net/http" + "strconv" + "strings" + + "home-vue-go/internal/database" + "home-vue-go/internal/ent" + "home-vue-go/internal/ent/contact" + + "github.com/gin-gonic/gin" +) + +func GetContacts(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + ctx := c.Request.Context() + contacts, err := db.Client.Contact.Query().Order(contact.BySortOrder(), contact.ByID()).All(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + result := make([]gin.H, len(contacts)) + for i, contact := range contacts { + result[i] = gin.H{ + "id": contact.ID, + "type": contact.Type, + "icon": contact.Icon, + "url": contact.URL, + "qrCode": contact.QrCode, + "hoverColor": contact.HoverColor, + "sortOrder": contact.SortOrder, + } + } + + c.JSON(http.StatusOK, result) + } +} + +func ReorderContacts(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + var req struct { + IDs []int `json:"ids" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if len(req.IDs) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表不能为空"}) + return + } + + seen := make(map[int]bool, len(req.IDs)) + for _, id := range req.IDs { + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含无效ID"}) + return + } + if seen[id] { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含重复ID"}) + return + } + seen[id] = true + } + + ctx := c.Request.Context() + existingIDs, err := db.Client.Contact.Query().IDs(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if len(existingIDs) != len(req.IDs) { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表与当前联系方式数量不一致"}) + return + } + existing := make(map[int]bool, len(existingIDs)) + for _, id := range existingIDs { + existing[id] = true + } + for _, id := range req.IDs { + if !existing[id] { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含不存在的联系方式"}) + return + } + } + + tx, err := db.Client.Tx(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + for index, id := range req.IDs { + if _, err := tx.Contact.UpdateOneID(id).SetSortOrder((index + 1) * 10).Save(ctx); err != nil { + _ = tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } + + if err := tx.Commit(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + contacts, err := db.Client.Contact.Query().Order(contact.BySortOrder(), contact.ByID()).All(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + result := make([]gin.H, len(contacts)) + for i, contact := range contacts { + result[i] = gin.H{ + "id": contact.ID, + "type": contact.Type, + "icon": contact.Icon, + "url": contact.URL, + "qrCode": contact.QrCode, + "hoverColor": contact.HoverColor, + "sortOrder": contact.SortOrder, + } + } + + c.JSON(http.StatusOK, result) + } +} + +func CreateContact(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + var req struct { + Type string `json:"type" binding:"required"` + Icon string `json:"icon" binding:"required"` + URL string `json:"url"` + QrCode string `json:"qrCode"` + HoverColor string `json:"hoverColor"` + SortOrder int `json:"sortOrder"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + req.Type = strings.TrimSpace(req.Type) + req.Icon = strings.TrimSpace(req.Icon) + req.URL = strings.TrimSpace(req.URL) + req.QrCode = strings.TrimSpace(req.QrCode) + req.HoverColor = strings.TrimSpace(req.HoverColor) + if req.Type == "" || !validIconValue(req.Icon) { + c.JSON(http.StatusBadRequest, gin.H{"error": "类型和图标不能为空"}) + return + } + + if req.URL != "" && !validContactURL(req.URL) { + c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式URL必须是有效的http/https/mailto/tel地址"}) + return + } + if req.QrCode != "" && !validQRCode(req.QrCode) { + c.JSON(http.StatusBadRequest, gin.H{"error": "二维码必须使用本地上传路径或安全的http/https图片地址"}) + return + } + if req.URL != "" && req.QrCode != "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式不能同时设置链接和二维码"}) + return + } + if req.URL == "" && req.QrCode == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "链接或二维码不能为空"}) + return + } + + ctx := c.Request.Context() + create := db.Client.Contact.Create(). + SetType(req.Type). + SetIcon(req.Icon). + SetSortOrder(req.SortOrder) + + if req.URL != "" { + create.SetURL(req.URL) + } + if req.QrCode != "" { + create.SetQrCode(req.QrCode) + } + if req.HoverColor != "" { + create.SetHoverColor(req.HoverColor) + } + + contact, err := create.Save(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "id": contact.ID, + "type": contact.Type, + "icon": contact.Icon, + "url": contact.URL, + "qrCode": contact.QrCode, + "hoverColor": contact.HoverColor, + "sortOrder": contact.SortOrder, + }) + } +} + +func UpdateContact(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) + return + } + + var req struct { + Type string `json:"type" binding:"required"` + Icon string `json:"icon" binding:"required"` + URL string `json:"url"` + QrCode string `json:"qrCode"` + HoverColor string `json:"hoverColor"` + SortOrder int `json:"sortOrder"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + req.Type = strings.TrimSpace(req.Type) + req.Icon = strings.TrimSpace(req.Icon) + req.URL = strings.TrimSpace(req.URL) + req.QrCode = strings.TrimSpace(req.QrCode) + req.HoverColor = strings.TrimSpace(req.HoverColor) + if req.Type == "" || !validIconValue(req.Icon) { + c.JSON(http.StatusBadRequest, gin.H{"error": "类型和图标不能为空"}) + return + } + + if req.URL != "" && !validContactURL(req.URL) { + c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式URL必须是有效的http/https/mailto/tel地址"}) + return + } + if req.QrCode != "" && !validQRCode(req.QrCode) { + c.JSON(http.StatusBadRequest, gin.H{"error": "二维码必须使用本地上传路径或安全的http/https图片地址"}) + return + } + if req.URL != "" && req.QrCode != "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "联系方式不能同时设置链接和二维码"}) + return + } + if req.URL == "" && req.QrCode == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "链接或二维码不能为空"}) + return + } + + ctx := c.Request.Context() + update := db.Client.Contact.UpdateOneID(id) + update.SetType(req.Type) + update.SetIcon(req.Icon) + if req.URL != "" { + update.SetURL(req.URL) + } else { + update.ClearURL() + } + if req.QrCode != "" { + update.SetQrCode(req.QrCode) + } else { + update.ClearQrCode() + } + if req.HoverColor != "" { + update.SetHoverColor(req.HoverColor) + } else { + update.ClearHoverColor() + } + update.SetSortOrder(req.SortOrder) + + contact, err := update.Save(ctx) + if err != nil { + if ent.IsNotFound(err) { + c.JSON(http.StatusNotFound, gin.H{"error": "联系方式不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "id": contact.ID, + "type": contact.Type, + "icon": contact.Icon, + "url": contact.URL, + "qrCode": contact.QrCode, + "hoverColor": contact.HoverColor, + "sortOrder": contact.SortOrder, + }) + } +} + +func DeleteContact(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) + return + } + + ctx := c.Request.Context() + if err := db.Client.Contact.DeleteOneID(id).Exec(ctx); err != nil { + if ent.IsNotFound(err) { + c.JSON(http.StatusNotFound, gin.H{"error": "联系方式不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) + } +} diff --git a/internal/api/sites.go b/internal/api/sites.go index 0009469..eabd9ee 100644 --- a/internal/api/sites.go +++ b/internal/api/sites.go @@ -1,242 +1,242 @@ -package api - -import ( - "net/http" - "strconv" - "strings" - - "home-vue-go/internal/database" - "home-vue-go/internal/ent" - "home-vue-go/internal/ent/site" - - "github.com/gin-gonic/gin" -) - -func GetSites(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - ctx := c.Request.Context() - sites, err := db.Client.Site.Query().Order(site.BySortOrder(), site.ByID()).All(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - result := make([]gin.H, len(sites)) - for i, site := range sites { - result[i] = gin.H{ - "id": site.ID, - "name": site.Name, - "url": site.URL, - "icon": site.Icon, - "sortOrder": site.SortOrder, - } - } - - c.JSON(http.StatusOK, result) - } -} - -func ReorderSites(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - var req struct { - IDs []int `json:"ids" binding:"required"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - if len(req.IDs) == 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表不能为空"}) - return - } - - seen := make(map[int]bool, len(req.IDs)) - for _, id := range req.IDs { - if id <= 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含无效ID"}) - return - } - if seen[id] { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含重复ID"}) - return - } - seen[id] = true - } - - ctx := c.Request.Context() - existingIDs, err := db.Client.Site.Query().IDs(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if len(existingIDs) != len(req.IDs) { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表与当前站点数量不一致"}) - return - } - existing := make(map[int]bool, len(existingIDs)) - for _, id := range existingIDs { - existing[id] = true - } - for _, id := range req.IDs { - if !existing[id] { - c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含不存在的站点"}) - return - } - } - - tx, err := db.Client.Tx(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - for index, id := range req.IDs { - if _, err := tx.Site.UpdateOneID(id).SetSortOrder((index + 1) * 10).Save(ctx); err != nil { - _ = tx.Rollback() - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - } - - if err := tx.Commit(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - sites, err := db.Client.Site.Query().Order(site.BySortOrder(), site.ByID()).All(ctx) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - result := make([]gin.H, len(sites)) - for i, site := range sites { - result[i] = gin.H{ - "id": site.ID, - "name": site.Name, - "url": site.URL, - "icon": site.Icon, - "sortOrder": site.SortOrder, - } - } - - c.JSON(http.StatusOK, result) - } -} - -func CreateSite(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - var req struct { - Name string `json:"name" binding:"required"` - URL string `json:"url" binding:"required"` - Icon string `json:"icon" binding:"required"` - SortOrder int `json:"sortOrder"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - req.Name, req.URL, req.Icon = strings.TrimSpace(req.Name), strings.TrimSpace(req.URL), strings.TrimSpace(req.Icon) - if req.Name == "" || req.URL == "" || req.Icon == "" || !validHTTPURL(req.URL) { - c.JSON(http.StatusBadRequest, gin.H{"error": "站点名称、图标不能为空,URL必须是有效的http/https地址"}) - return - } - - ctx := c.Request.Context() - site, err := db.Client.Site.Create(). - SetName(req.Name). - SetURL(req.URL). - SetIcon(req.Icon). - SetSortOrder(req.SortOrder). - Save(ctx) - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "id": site.ID, - "name": site.Name, - "url": site.URL, - "icon": site.Icon, - "sortOrder": site.SortOrder, - }) - } -} - -func UpdateSite(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) - return - } - - var req struct { - Name string `json:"name"` - URL string `json:"url"` - Icon string `json:"icon"` - SortOrder int `json:"sortOrder"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - req.Name, req.URL, req.Icon = strings.TrimSpace(req.Name), strings.TrimSpace(req.URL), strings.TrimSpace(req.Icon) - if req.Name == "" || req.URL == "" || req.Icon == "" || !validHTTPURL(req.URL) { - c.JSON(http.StatusBadRequest, gin.H{"error": "站点名称、图标不能为空,URL必须是有效的http/https地址"}) - return - } - - ctx := c.Request.Context() - update := db.Client.Site.UpdateOneID(id) - update.SetName(req.Name) - update.SetURL(req.URL) - update.SetIcon(req.Icon) - update.SetSortOrder(req.SortOrder) - - site, err := update.Save(ctx) - if err != nil { - if ent.IsNotFound(err) { - c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "id": site.ID, - "name": site.Name, - "url": site.URL, - "icon": site.Icon, - "sortOrder": site.SortOrder, - }) - } -} - -func DeleteSite(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) - return - } - - ctx := c.Request.Context() - if err := db.Client.Site.DeleteOneID(id).Exec(ctx); err != nil { - if ent.IsNotFound(err) { - c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) - } -} +package api + +import ( + "net/http" + "strconv" + "strings" + + "home-vue-go/internal/database" + "home-vue-go/internal/ent" + "home-vue-go/internal/ent/site" + + "github.com/gin-gonic/gin" +) + +func GetSites(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + ctx := c.Request.Context() + sites, err := db.Client.Site.Query().Order(site.BySortOrder(), site.ByID()).All(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + result := make([]gin.H, len(sites)) + for i, site := range sites { + result[i] = gin.H{ + "id": site.ID, + "name": site.Name, + "url": site.URL, + "icon": site.Icon, + "sortOrder": site.SortOrder, + } + } + + c.JSON(http.StatusOK, result) + } +} + +func ReorderSites(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + var req struct { + IDs []int `json:"ids" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if len(req.IDs) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表不能为空"}) + return + } + + seen := make(map[int]bool, len(req.IDs)) + for _, id := range req.IDs { + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含无效ID"}) + return + } + if seen[id] { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含重复ID"}) + return + } + seen[id] = true + } + + ctx := c.Request.Context() + existingIDs, err := db.Client.Site.Query().IDs(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if len(existingIDs) != len(req.IDs) { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表与当前站点数量不一致"}) + return + } + existing := make(map[int]bool, len(existingIDs)) + for _, id := range existingIDs { + existing[id] = true + } + for _, id := range req.IDs { + if !existing[id] { + c.JSON(http.StatusBadRequest, gin.H{"error": "排序列表包含不存在的站点"}) + return + } + } + + tx, err := db.Client.Tx(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + for index, id := range req.IDs { + if _, err := tx.Site.UpdateOneID(id).SetSortOrder((index + 1) * 10).Save(ctx); err != nil { + _ = tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } + + if err := tx.Commit(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + sites, err := db.Client.Site.Query().Order(site.BySortOrder(), site.ByID()).All(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + result := make([]gin.H, len(sites)) + for i, site := range sites { + result[i] = gin.H{ + "id": site.ID, + "name": site.Name, + "url": site.URL, + "icon": site.Icon, + "sortOrder": site.SortOrder, + } + } + + c.JSON(http.StatusOK, result) + } +} + +func CreateSite(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + var req struct { + Name string `json:"name" binding:"required"` + URL string `json:"url" binding:"required"` + Icon string `json:"icon" binding:"required"` + SortOrder int `json:"sortOrder"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + req.Name, req.URL, req.Icon = strings.TrimSpace(req.Name), strings.TrimSpace(req.URL), strings.TrimSpace(req.Icon) + if req.Name == "" || req.URL == "" || !validIconValue(req.Icon) || !validHTTPURL(req.URL) { + c.JSON(http.StatusBadRequest, gin.H{"error": "站点名称、图标不能为空,URL必须是有效的http/https地址"}) + return + } + + ctx := c.Request.Context() + site, err := db.Client.Site.Create(). + SetName(req.Name). + SetURL(req.URL). + SetIcon(req.Icon). + SetSortOrder(req.SortOrder). + Save(ctx) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "id": site.ID, + "name": site.Name, + "url": site.URL, + "icon": site.Icon, + "sortOrder": site.SortOrder, + }) + } +} + +func UpdateSite(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) + return + } + + var req struct { + Name string `json:"name"` + URL string `json:"url"` + Icon string `json:"icon"` + SortOrder int `json:"sortOrder"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + req.Name, req.URL, req.Icon = strings.TrimSpace(req.Name), strings.TrimSpace(req.URL), strings.TrimSpace(req.Icon) + if req.Name == "" || req.URL == "" || !validIconValue(req.Icon) || !validHTTPURL(req.URL) { + c.JSON(http.StatusBadRequest, gin.H{"error": "站点名称、图标不能为空,URL必须是有效的http/https地址"}) + return + } + + ctx := c.Request.Context() + update := db.Client.Site.UpdateOneID(id) + update.SetName(req.Name) + update.SetURL(req.URL) + update.SetIcon(req.Icon) + update.SetSortOrder(req.SortOrder) + + site, err := update.Save(ctx) + if err != nil { + if ent.IsNotFound(err) { + c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "id": site.ID, + "name": site.Name, + "url": site.URL, + "icon": site.Icon, + "sortOrder": site.SortOrder, + }) + } +} + +func DeleteSite(db *database.Database) gin.HandlerFunc { + return func(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) + return + } + + ctx := c.Request.Context() + if err := db.Client.Site.DeleteOneID(id).Exec(ctx); err != nil { + if ent.IsNotFound(err) { + c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) + } +} diff --git a/internal/api/validation.go b/internal/api/validation.go index 9e50648..0ebc391 100644 --- a/internal/api/validation.go +++ b/internal/api/validation.go @@ -3,9 +3,17 @@ package api import ( "net/url" "path" + "regexp" "strings" ) +const maxIconValueLength = 160 + +var ( + legacyIconPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+(?:\s+[A-Za-z0-9_-]+)*$`) + iconifyValuePattern = regexp.MustCompile(`^iconify:[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$`) +) + func validHTTPURL(value string) bool { parsed, err := url.ParseRequestURI(strings.TrimSpace(value)) return err == nil && parsed.Host != "" && (parsed.Scheme == "http" || parsed.Scheme == "https") @@ -30,3 +38,14 @@ func validQRCode(value string) bool { } return validHTTPURL(value) } + +func validIconValue(value string) bool { + value = strings.TrimSpace(value) + if value == "" || len(value) > maxIconValueLength { + return false + } + if strings.HasPrefix(value, "iconify:") { + return iconifyValuePattern.MatchString(value) + } + return legacyIconPattern.MatchString(value) +} diff --git a/internal/api/validation_test.go b/internal/api/validation_test.go index f61af6e..41d62a2 100644 --- a/internal/api/validation_test.go +++ b/internal/api/validation_test.go @@ -1,6 +1,11 @@ package api -import "testing" +import ( + "strings" + "testing" + + "home-vue-go/internal/config" +) func TestURLValidation(t *testing.T) { for _, value := range []string{"https://example.com", "http://localhost:8080"} { @@ -30,6 +35,49 @@ func TestURLValidation(t *testing.T) { } } +func TestIconValueValidation(t *testing.T) { + for _, value := range []string{ + "fas fa-home", + "fab fa-github", + "iconify:mdi:home", + "iconify:material-symbols:add-home-outline", + } { + if !validIconValue(value) { + t.Errorf("expected valid icon value: %s", value) + } + } + for _, value := range []string{ + "", + "iconify:MDI:home", + "iconify:mdi:", + "iconify:mdi:home/../../x", + "fas fa-home\"> + + diff --git a/src/components/Home.vue b/src/components/Home.vue index 3eb902a..51b0936 100644 --- a/src/components/Home.vue +++ b/src/components/Home.vue @@ -46,7 +46,7 @@ :style="{ '--hover-color': contact.hoverColor || 'var(--hover-link-color)' }" :aria-label="contact.type" > - +