From 3f50316ad06b0f8a4bdf32bcffd0583fb2db7173 Mon Sep 17 00:00:00 2001 From: freefire Date: Sat, 29 Aug 2026 04:01:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E6=88=90=20Gitea=20=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=94=B9=E7=BB=84=E6=94=B6=E5=B0=BE=EF=BC=88import?= =?UTF-8?q?=E3=80=81=E6=9E=84=E5=BB=BA=E4=B8=8E=20LICENSE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同步包路径与路由,去掉 SPA 构建步骤,对齐 Gitea 式 LICENSE,并更新规格/规则与占位 SSR。 Co-authored-by: Cursor --- .cursor/rules/build-scripts.mdc | 8 +- .cursor/rules/rebuild-gitea-ssr.mdc | 7 +- .dockerignore | 8 +- .gitignore | 18 +- Dockerfile | 18 +- LICENSE | 12 +- Makefile | 50 ++-- README.md | 102 +++---- build.ps1 | 71 ++--- cmd/jiang13/main.go | 6 +- cmd/jiang13/program.go | 32 +-- docs/rebuild-spec/02-features.md | 2 +- docs/rebuild-spec/03-data-model.md | 14 +- docs/rebuild-spec/04-api.md | 8 +- docs/rebuild-spec/05-business-rules.md | 14 +- docs/rebuild-spec/06-pages-ux.md | 6 +- docs/rebuild-spec/07-config-ops.md | 8 +- .../rebuild-spec/08-gitea-ssr-architecture.md | 28 +- docs/rebuild-spec/README.md | 20 +- models/db.go | 2 +- models/gitea.go | 2 +- models/level.go | 2 +- models/models.go | 2 +- models/oauth.go | 2 +- models/user_view.go | 2 +- modules/auth/auth.go | 22 +- modules/seo/pagemeta.go | 16 ++ routers/api/api.go | 160 +++++------ routers/api/economy.go | 18 +- routers/api/friend_link.go | 14 +- routers/api/handlers.go | 108 ++++---- routers/api/media.go | 2 +- routers/api/message.go | 18 +- routers/api/oidc.go | 22 +- routers/api/report.go | 24 +- routers/api/seo.go | 145 +++++----- routers/api/seo_bot.go | 26 +- routers/api/special.go | 28 +- routers/api/thumb.go | 6 +- routers/setup.go | 159 +++++------ routers/web/home.go | 30 +- services/aside_widgets_test.go | 2 +- services/auth.go | 38 +-- services/backup.go | 2 +- services/badge.go | 104 +++---- services/board.go | 36 +-- services/board_icon.go | 2 +- services/bounty.go | 66 ++--- services/bounty_test.go | 46 +-- services/captcha.go | 2 +- services/comment.go | 192 ++++++------- services/common.go | 2 +- services/content.go | 2 +- services/content_test.go | 2 +- services/crawler.go | 2 +- services/email_code.go | 8 +- services/filter_io.go | 2 +- services/filter_words.go | 2 +- services/friend_link.go | 96 +++---- services/friend_link_enrich.go | 10 +- services/friend_link_reciprocal.go | 2 +- services/friend_link_reciprocal_async.go | 8 +- services/friend_link_reciprocal_test.go | 2 +- services/gitea.go | 68 ++--- services/image_webp.go | 2 +- services/lottery_post.go | 50 ++-- services/mail.go | 2 +- services/mail_template.go | 2 +- services/media.go | 38 +-- services/mention.go | 10 +- services/mention_test.go | 2 +- services/message.go | 72 ++--- services/notify.go | 72 ++--- services/oauth_clients.go | 34 +-- services/oidc.go | 40 +-- services/permalink.go | 2 +- services/points.go | 52 ++-- services/poll.go | 54 ++-- services/post.go | 262 +++++++++--------- services/post_special.go | 18 +- services/ratelimit.go | 2 +- services/report.go | 98 +++---- services/sanitize_html.go | 2 +- services/sanitize_html_test.go | 2 +- services/seo.go | 6 +- services/settings.go | 54 ++-- services/settings_util.go | 2 +- services/site_page.go | 50 ++-- services/storage.go | 2 +- services/thumb.go | 2 +- services/unlock.go | 40 +-- services/upload.go | 2 +- services/user.go | 82 +++--- 93 files changed, 1472 insertions(+), 1522 deletions(-) create mode 100644 modules/seo/pagemeta.go diff --git a/.cursor/rules/build-scripts.mdc b/.cursor/rules/build-scripts.mdc index 6d78454..199efaa 100644 --- a/.cursor/rules/build-scripts.mdc +++ b/.cursor/rules/build-scripts.mdc @@ -5,7 +5,8 @@ alwaysApply: true # 编译与构建脚本 -Go 单二进制 + `go:embed` 前端;产物在 `dist/`。 +本分支(`rebuild/gitea-ssr`):Go 单二进制 + `go:embed` 的 **templates** 与 **public/assets**(由 `web_src` 构建);产物在 `dist/`。 +对照 SPA 仅在 `main` 分支。 ## 运行编译(优先用封装命令,不要猜命令) @@ -18,15 +19,16 @@ Go 单二进制 + `go:embed` 前端;产物在 `dist/`。 Windows 上**不要**让用户直接 `.\build.ps1`(默认 ExecutionPolicy 会拦截);应通过 `build.bat`(内部 `-ExecutionPolicy Bypass`)调用。 -常用 target:`build`(默认)、`dev`、`run`、`frontend`、`clean`、`build-all`、`build-windows`、`build-linux`、`tidy`、`help`。 +常用 target:`build`(默认)、`dev`、`run`、`web-src`、`clean`、`build-all`、`build-windows`、`build-linux`、`tidy`、`help`。 ## 修改构建脚本时的约定 1. **双轨同步**:`build.ps1` 与 `Makefile` 目标与行为保持一致;改其一须同步另一份。 2. **`build.bat` 仅用 ASCII 注释**:`.bat` 会被 cmd 按 GBK 解析,UTF-8 中文注释会导致整行乱码、`powershell` 无法执行。 3. **`build.ps1` 可用 UTF-8**:由 PowerShell 执行,中文注释无妨。 -4. **构建顺序**:先 `frontend` 内 `npm run build`,再 `go build -trimpath -ldflags "-s -w -X main.version=..." -o dist/jiang13 ./cmd/jiang13`。 +4. **构建顺序**:先 `web_src` 内 `npm run build`(产出 `public/assets`),再 `go build -trimpath -ldflags "-s -w -X main.version=..." -o dist/jiang13 ./cmd/jiang13`。 5. **入口包**:`./cmd/jiang13`;Windows 产物带 `.exe`。 +6. **本分支无 `frontend/`**:勿再添加 SPA 构建步骤;需要对照 UI 时 `git checkout main`。 ## 新增 target 检查清单 diff --git a/.cursor/rules/rebuild-gitea-ssr.mdc b/.cursor/rules/rebuild-gitea-ssr.mdc index 38bd0de..8318764 100644 --- a/.cursor/rules/rebuild-gitea-ssr.mdc +++ b/.cursor/rules/rebuild-gitea-ssr.mdc @@ -21,9 +21,14 @@ alwaysApply: true 对齐 [Gitea](https://github.com/go-gitea/gitea) 职责划分: - `routers/web` — HTML 页面路由 +- `routers/api` — JSON API(原 `handler/`) +- `routers/setup.go` — 路由总装 - `templates/` — 模板 - `web_src/` → `public/assets/` — 渐进增强 CSS/JS -- 现有 `service/`、`model/` 可复用业务逻辑 +- `models/`、`services/` — 数据与业务 +- `modules/auth`、`modules/webrender`、`modules/seo` — 横切 + +本分支**已删除** `frontend/` 与 `embed_static/`;勿再恢复 SPA 生产回落。 细节见 [`docs/rebuild-spec/08-gitea-ssr-architecture.md`](docs/rebuild-spec/08-gitea-ssr-architecture.md)。 diff --git a/.dockerignore b/.dockerignore index 96abb60..ac6b70b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,17 +4,17 @@ .idea .vscode .cursor +.trae # 运行时数据与本地配置 data/ app.ini tmp-cookie.txt -# 编译产物与前端缓存 +# 编译产物与依赖缓存 dist/ -frontend/node_modules/ -frontend/dist/ -embed_static/static/spa/ +web_src/node_modules/ +public/assets/ node_modules/ .vite/ diff --git a/.gitignore b/.gitignore index 0f5d4c2..c5e2f31 100644 --- a/.gitignore +++ b/.gitignore @@ -4,22 +4,28 @@ # 本地配置(保留 app.ini.example) /app.ini -# 前端依赖与构建缓存 +# Node / 构建缓存 /node_modules/ -/frontend/node_modules/ -/frontend/dist/ -/embed_static/static/spa/ +/web_src/node_modules/ +.vite/ +*.tsbuildinfo -# Go 编译产物 +# Go 编译产物(统一进 dist/;勿把二进制扔在仓库根目录) /dist/ *.exe +/jiang13 +/jiang13-* +!/cmd/jiang13/ # 临时文件 tmp-cookie.txt +*-err.txt +*-out.txt -# 编辑器 / OS +# 编辑器 / AI 草稿 / OS .idea/ .vscode/ +.trae/ *.swp Thumbs.db .DS_Store diff --git a/Dockerfile b/Dockerfile index 61cedfc..d9c8a00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# 姜十三论坛 — 多阶段构建:Node 前端 → Go 单二进制 → Alpine 运行镜像 +# 姜十三论坛 — 多阶段构建:web_src → Go 单二进制 → Alpine 运行镜像 # 不使用 # syntax=docker/dockerfile:1,避免构建前额外拉取 docker.io/docker/dockerfile # # 国内网络:默认经 DaoCloud 拉取基础镜像,npm/go 走国内代理 @@ -8,14 +8,12 @@ ARG IMAGE_PREFIX=docker.m.daocloud.io/library/ ARG VERSION=dev -# ── Stage 1: 前端构建(Vite → embed_static/static/spa)──────────────────── -FROM ${IMAGE_PREFIX}node:22-bookworm-slim AS frontend -WORKDIR /src/frontend -COPY frontend/package.json frontend/package-lock.json ./ -RUN npm config set registry https://registry.npmmirror.com \ - && npm ci -COPY frontend/ ./ -RUN npm run build +# ── Stage 1: SSR 渐进资源(web_src → public/assets)──────────────────────── +FROM ${IMAGE_PREFIX}node:22-bookworm-slim AS websrc +WORKDIR /src/web_src +COPY web_src/package.json ./ +COPY web_src/ ./ +RUN node build.mjs # ── Stage 2: Go 编译(纯 Go SQLite,CGO_ENABLED=0)──────────────────────── FROM ${IMAGE_PREFIX}golang:1.26-bookworm AS builder @@ -25,7 +23,7 @@ WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . -COPY --from=frontend /src/embed_static/static/spa ./embed_static/static/spa +COPY --from=websrc /src/public/assets ./public/assets RUN CGO_ENABLED=0 go build -trimpath \ -ldflags "-s -w -X main.version=${VERSION}" \ -o /out/jiang13 ./cmd/jiang13 diff --git a/LICENSE b/LICENSE index 2ae8e80..3bbab44 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,4 @@ -MIT License - -Copyright (c) 2026 freefire +Copyright (c) 2026 The Jiang13 Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +7,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile b/Makefile index 8acfd7b..c4df5dc 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # 姜十三论坛 Jiang13 Forum - Makefile -# Go 1.26 单二进制编译,与 Gitea 打包方式一致 +# Go 1.26 单二进制:templates SSR + web_src 渐进资源(本分支无 React SPA) APP_NAME := jiang13 MAIN_PKG := ./cmd/jiang13 @@ -12,42 +12,39 @@ REGISTRY_IMAGE := hangzhang714128/jiang13-forum GO := go GOFLAGS := -trimpath -.PHONY: all build build-windows build-linux build-darwin clean run dev tidy help frontend frontend-build web-src-build docker compose-up compose-down +.PHONY: all build build-windows build-linux build-darwin build-all clean run dev tidy help web-src-build docker compose-up compose-down all: build web-src-build: cd web_src && npm run build -frontend-build: - cd frontend && npm install && npm run build - ## 编译当前平台二进制(纯 Go SQLite,无需 CGO) -build: web-src-build frontend-build +build: web-src-build @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PKG) @echo "✓ 编译完成: $(BUILD_DIR)/$(APP_NAME)" -## Windows amd64(先打包前端再 embed) -build-windows: web-src-build frontend-build +## Windows amd64 +build-windows: web-src-build @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME).exe $(MAIN_PKG) @echo "✓ Windows: $(BUILD_DIR)/$(APP_NAME).exe" -## Linux amd64(先打包前端再 embed) -build-linux: web-src-build frontend-build +## Linux amd64 +build-linux: web-src-build @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PKG) @echo "✓ Linux: $(BUILD_DIR)/$(APP_NAME)-linux-amd64" -## macOS arm64 (Apple Silicon)(先打包前端再 embed) -build-darwin: web-src-build frontend-build +## macOS arm64 (Apple Silicon) +build-darwin: web-src-build @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME)-darwin-arm64 $(MAIN_PKG) @echo "✓ macOS: $(BUILD_DIR)/$(APP_NAME)-darwin-arm64" -## 跨平台全量编译(web-src + frontend 只跑一次) -build-all: web-src-build frontend-build +## 跨平台全量编译(web_src 只跑一次) +build-all: web-src-build @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME).exe $(MAIN_PKG) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PKG) @@ -59,20 +56,13 @@ build-all: web-src-build frontend-build tidy: $(GO) mod tidy -## 本地运行(仅后端,使用已 embed 的前端;数据目录与 dist 二进制一致) -run: +## 本地运行 SSR(先构建 web_src) +run: web-src-build @mkdir -p $(DEV_DATA_DIR) - $(GO) run $(MAIN_PKG) --data $(DEV_DATA_DIR) + $(GO) run $(MAIN_PKG) --work-path . --data $(DEV_DATA_DIR) -## 前端热更新开发(后端 :3000 + Vite :5173,Ctrl+C 同时退出;数据目录与 dist 二进制一致) -dev: - @echo "前端热更新: http://localhost:5173" - @echo "后端 API : http://localhost:3000" - @echo "数据目录 : $(DEV_DATA_DIR) (与 dist 二进制一致)" - @mkdir -p $(DEV_DATA_DIR) - @trap 'kill 0' INT; \ - $(GO) run $(MAIN_PKG) --dev --data $(DEV_DATA_DIR) & \ - cd frontend && (test -d node_modules || npm install) && npm run dev +## 同 run(SPA 对照请 git checkout main) +dev: run ## 清理编译产物 clean: @@ -91,15 +81,15 @@ compose-down: docker compose down help: - @echo "姜十三论坛编译命令:" + @echo "姜十三论坛编译命令 (rebuild/gitea-ssr):" @echo " make web-src-build - 构建 SSR 渐进资源 (web_src)" - @echo " make build - 编译当前平台" + @echo " make build - web_src + 编译当前平台" @echo " make build-windows - 编译 Windows" @echo " make build-linux - 编译 Linux" @echo " make build-darwin - 编译 macOS" @echo " make build-all - 编译全部平台" - @echo " make run - 启动后端 SSR(:3000)" - @echo " make dev - 后端 + 旧 SPA Vite 对照(:5173 + :3000)" + @echo " make run / make dev - 启动 SSR(:3000)" @echo " make docker - 构建 Docker 镜像" @echo " make compose-up - Docker Compose 启动" @echo " make compose-down - Docker Compose 停止" + @echo " SPA 对照: git checkout main" diff --git a/README.md b/README.md index 6cd2576..4db81b9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ **能聊 · 好看 · 好装** 面向小圈子、团队与同好社群的轻量现代化论坛。 -编译为单个 Go 二进制,前端 SPA(单页应用)内嵌,内置 SQLite,拷到服务器即可运行。 +本分支(`rebuild/gitea-ssr`):Go 模板真 SSR + `web_src` 渐进增强,单二进制 + SQLite。 +对照 React SPA 请见 `main` 分支。
@@ -13,7 +14,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-18a058?style=flat-square)](LICENSE) [![Docker](https://img.shields.io/badge/Docker-hangzhang714128%2Fjiang13--forum-2496ED?style=flat-square&logo=docker&logoColor=white)](https://hub.docker.com/r/hangzhang714128/jiang13-forum) [![Go](https://img.shields.io/badge/Go-1.26-00ADD8?style=flat-square&logo=go&logoColor=white)](go.mod) -[![React](https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=white)](frontend/package.json) +[![SSR](https://img.shields.io/badge/SSR-Go_html%2Ftemplate-00ADD8?style=flat-square&logo=go&logoColor=white)](docs/rebuild-spec/08-gitea-ssr-architecture.md) [![SQLite](https://img.shields.io/badge/SQLite-内置-003B57?style=flat-square&logo=sqlite&logoColor=white)](#) [在线演示](https://bbs.iioio.com/) · @@ -31,8 +32,8 @@
-> **演示站点:** [https://bbs.iioio.com/](https://bbs.iioio.com/) -> 项目积极开发中。管理后台已统一为 React SPA(`/admin`),欢迎提 Issue / PR 共建。 +> **演示站点:** [https://bbs.iioio.com/](https://bbs.iioio.com/)(现网多为 `main` SPA) +> 本分支按 [Gitea 式 SSR 规格](docs/rebuild-spec/08-gitea-ssr-architecture.md) 重构;欢迎提 Issue / PR 共建。 @@ -142,7 +143,7 @@ make build **手动分步(全平台):** ```bash -cd frontend && npm install && npm run build +cd web_src && npm run build cd .. && go build -trimpath -ldflags "-s -w" -o dist/jiang13 ./cmd/jiang13 ``` @@ -325,80 +326,61 @@ C:\jiang13\jiang13.exe --service start | 层级 | 技术 | |------|------| -| **后端** | Go 1.26 · Gin · GORM · SQLite | -| **前端** | React 18 · TipTap · Radix UI · Tailwind CSS · TanStack Virtual | -| **构建** | Vite → `go:embed` 内嵌 SPA,单二进制发布 | +| **后端 / SSR** | Go 1.26 · Gin · GORM · SQLite · `html/template` | +| **渐进资源** | `web_src/`(构建到 `public/assets/`,URL `/ssr-assets/`) | +| **构建** | `web_src` → `go:embed` templates + assets,单二进制发布 | | **认证** | bcrypt · JWT Cookie · 可选 OIDC Provider | +| **对照 SPA** | 仅 `main` 分支(React 18 · TipTap · Vite) | --- -## 前端开发 - -日常改前端不需要重新完整构建,Vite 支持秒级热更新(HMR,热模块替换): +## 本地开发(SSR) ```bat -build.bat -Target dev +build.bat -Target run ``` ```bash -make dev +make run ``` -浏览器访问 `http://localhost:5173`,API 自动代理到 `http://localhost:3000`。 +浏览器访问 `http://localhost:3000`。数据目录默认 `dist/data`。 -开发后端与 `dist/jiang13` 共用数据目录 `dist/data`(SQLite、上传、JWT 密钥等),避免 dev 与 dist 运行数据不一致。 +改模板 / Go 后重启进程;改 `web_src` 后需再跑 `build.bat -Target web-src`(或完整 `build`)。 -**何时需要完整构建:** - -- 修改 Go 代码或要发布单二进制 → `build.bat` / `make build` -- 更新 README 界面截图 → 见下方「更新截图」 - -> 直接访问 `:3000` 看到的是上次 build 嵌入的前端;开发时请用 `:5173`。 - -### 更新截图 - -默认从演示站抓取到 `docs/screenshots/`(需本机已安装 Playwright): - -```bash -npm install -D playwright -npx playwright install chromium -node scripts/capture-screenshots.mjs -``` - -| 环境变量 | 说明 | 默认 | -|----------|------|------| -| `J13_URL` | 抓取目标 | `https://bbs.iioio.com` | -| `J13_POST_ID` | 详情页帖子 ID | `1` | -| `J13_RICH_POST_ID` | 富文本展示帖 ID | `8` | -| `J13_USER` / `J13_PASS` | 发帖页登录(可选) | `admin` / `admin123` | - -本地站点示例:`J13_URL=http://localhost:3000 node scripts/capture-screenshots.mjs` +需要对照旧 SPA UI:`git checkout main` 或 `git worktree add ../jiang13-spa main`。 --- ## 项目结构 ``` -jiang13-forum/ -├── cmd/jiang13/ # 程序入口(含系统服务注册) -├── config/ # app.ini 与命令行配置 -├── app.ini.example # 配置文件示例 -├── Dockerfile # 多阶段 Docker 构建 -├── docker-compose.yml # 单容器 Compose 部署 -├── docker-entrypoint.sh # 容器启动脚本(修正 /data 卷权限) -├── .dockerignore -├── model/ # GORM 模型与数据库迁移 -├── service/ # 业务逻辑 -├── handler/ # HTTP 处理器(前台 + 后台) -├── middleware/ # JWT 鉴权等 -├── router/ # 路由注册 -├── embed_static/ # go:embed 内嵌的 SPA -├── frontend/ # React 源码(Vite 构建) -├── docs/screenshots/ # README 界面截图 -├── ROADMAP.md # 路线图与已知问题 -└── scripts/ # 开发辅助脚本(含截图) +jiang13-forum/ # 分支 rebuild/gitea-ssr +├── cmd/jiang13/ # 程序入口(含系统服务注册) +├── config/ # app.ini 与命令行配置 +├── app.ini.example +├── Dockerfile # web_src → Go → Alpine +├── docker-compose.yml +├── models/ # GORM 模型 +├── services/ # 业务逻辑 +├── routers/ +│ ├── setup.go # 路由总装 +│ ├── web/ # HTML SSR +│ └── api/ # JSON API +├── modules/ +│ ├── auth/ # JWT / 限流 +│ ├── webrender/ # 模板渲染 +│ └── seo/ +├── templates/ # Go html/template(embed) +├── web_src/ # 渐进 CSS/JS 源码 +├── public/assets/ # web_src 构建产物(embed) +├── docs/rebuild-spec/ # 产品规格与 SSR 架构 +├── docs/screenshots/ +└── ROADMAP.md ``` +> SPA 源码树仅存在于 `main`(`frontend/`、`embed_static/`)。 + --- ## 数据目录 @@ -424,7 +406,7 @@ data/ |------|------| | ✅ 已可用 | 三栏布局、暗色主题、虚拟滚动、Feed 排序、楼层评论 | | ✅ 发帖体验 | TipTap 富文本、图片上传、修订历史、回复可见等门控 | -| ✅ 管理后台 | React SPA:仪表盘、置顶 / 精华、禁言、系统设置 | +| ✅ 管理后台 | JSON API 已就绪;本分支管理 UI 为 SSR 占位(完整后台见 `main` SPA) | | 📋 计划中 | 通知动态优化、邮件提醒 | --- @@ -439,4 +421,4 @@ data/ ## 许可证 -[MIT](LICENSE) — 自由使用、修改与分发。 +[MIT](LICENSE)(与 [Gitea](https://github.com/go-gitea/gitea) 相同的 Expat 文本格式)— 自由使用、修改与分发。 diff --git a/build.ps1 b/build.ps1 index 661d4c0..e1e7498 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,9 +1,10 @@ # Jiang13 Forum - Windows build script (replaces GNU Make) # Usage: .\build.ps1 # .\build.ps1 -Target build-windows +# Branch rebuild/gitea-ssr: Go templates SSR + web_src (no React SPA) param( - [ValidateSet('build', 'build-windows', 'build-linux', 'build-darwin', 'build-all', 'frontend', 'web-src', 'tidy', 'run', 'dev', 'clean', 'docker', 'compose-up', 'compose-down', 'help')] + [ValidateSet('build', 'build-windows', 'build-linux', 'build-darwin', 'build-all', 'web-src', 'tidy', 'run', 'dev', 'clean', 'docker', 'compose-up', 'compose-down', 'help')] [string]$Target = 'build' ) @@ -33,20 +34,6 @@ function Build-WebSrc { } } -function Build-Frontend { - Write-Host '[frontend] npm run build...' -ForegroundColor Cyan - Push-Location frontend - try { - if (-not (Test-Path node_modules)) { - npm install - } - npm run build - if ($LASTEXITCODE -ne 0) { throw 'frontend build failed' } - } finally { - Pop-Location - } -} - function Build-Go([string]$OutFile, [string]$GoOS = '', [string]$GoArch = '') { Ensure-Dir $BuildDir if ($GoOS) { $env:GOOS = $GoOS } else { Remove-Item Env:GOOS -ErrorAction SilentlyContinue } @@ -80,23 +67,22 @@ function Build-Go([string]$OutFile, [string]$GoOS = '', [string]$GoArch = '') { switch ($Target) { 'help' { - Write-Host '.\build.ps1 build current platform' - Write-Host '.\build.ps1 -Target frontend SPA frontend only (legacy)' - Write-Host '.\build.ps1 -Target web-src SSR progressive assets' + Write-Host '.\build.ps1 build current platform (web_src + go)' + Write-Host '.\build.ps1 -Target web-src SSR progressive assets only' Write-Host '.\build.ps1 -Target build-windows' Write-Host '.\build.ps1 -Target build-linux' Write-Host '.\build.ps1 -Target build-all' - Write-Host '.\build.ps1 -Target run backend (SSR on :3000)' - Write-Host '.\build.ps1 -Target dev backend + Vite SPA对照' + Write-Host '.\build.ps1 -Target run SSR on :3000' + Write-Host '.\build.ps1 -Target dev same as run (SSR; SPA is on main)' Write-Host '.\build.ps1 -Target tidy' Write-Host '.\build.ps1 -Target clean' - Write-Host '.\build.ps1 -Target docker build Docker image' - Write-Host '.\build.ps1 -Target compose-up docker compose up -d --build' - Write-Host '.\build.ps1 -Target compose-down docker compose down' + Write-Host '.\build.ps1 -Target docker' + Write-Host '.\build.ps1 -Target compose-up' + Write-Host '.\build.ps1 -Target compose-down' Write-Host '' Write-Host 'Note: Windows "make" is often Embarcadero MAKE, not GNU Make.' + Write-Host 'SPA reference: git checkout main (or origin/main).' } - 'frontend' { Build-Frontend } 'web-src' { Build-WebSrc } 'tidy' { go mod tidy } 'clean' { @@ -105,54 +91,33 @@ switch ($Target) { } 'run' { Ensure-Dir $DevDataDir - go run $MainPkg --data $DevDataDir + Build-WebSrc + go run $MainPkg --work-path . --data $DevDataDir } 'dev' { - $root = (Get-Location).Path Ensure-Dir $DevDataDir - Write-Host '' - Write-Host '[dev] 前端开发 : http://localhost:5173 (Vite HMR)' -ForegroundColor Green - Write-Host '[dev] 后端 API : http://localhost:3000 (Go)' -ForegroundColor Green - Write-Host "[dev] 数据目录 : $DevDataDir (与 dist 二进制一致)" -ForegroundColor Green - Write-Host '[dev] 提示 : 请访问 5173 端口,Vite 会自动代理 API 到 3000' -ForegroundColor Yellow - Write-Host '[dev] 正在新窗口启动 Go 后端 (仅 API)...' -ForegroundColor Cyan - Start-Process powershell -ArgumentList @( - '-NoExit', '-Command', - "Set-Location '$root'; Write-Host '[backend] Go API on :3000' -ForegroundColor Cyan; go run $MainPkg --dev --data '$DevDataDir'" - ) | Out-Null - Start-Sleep -Seconds 2 - Push-Location frontend - try { - if (-not (Test-Path node_modules)) { npm install } - npm run dev - } finally { - Pop-Location - } + Build-WebSrc + Write-Host '[dev] SSR: http://localhost:3000 (SPA 对照请 checkout main)' -ForegroundColor Green + go run $MainPkg --work-path . --data $DevDataDir } 'build' { Build-WebSrc - Build-Frontend Build-Go -OutFile $AppName } 'build-windows' { Build-WebSrc - Build-Frontend Build-Go -OutFile $AppName -GoOS 'windows' -GoArch 'amd64' } 'build-linux' { - Write-Host '[build-linux] will build web_src + SPA then go:embed' -ForegroundColor Yellow Build-WebSrc - Build-Frontend Build-Go -OutFile "$AppName-linux-amd64" -GoOS 'linux' -GoArch 'amd64' } 'build-darwin' { Build-WebSrc - Build-Frontend Build-Go -OutFile "$AppName-darwin-arm64" -GoOS 'darwin' -GoArch 'arm64' } 'build-all' { Build-WebSrc - Build-Frontend Build-Go -OutFile $AppName -GoOS 'windows' -GoArch 'amd64' Build-Go -OutFile "$AppName-linux-amd64" -GoOS 'linux' -GoArch 'amd64' Build-Go -OutFile "$AppName-darwin-arm64" -GoOS 'darwin' -GoArch 'arm64' @@ -166,12 +131,10 @@ switch ($Target) { } 'compose-up' { docker compose up -d --build - if ($LASTEXITCODE -ne 0) { throw 'docker compose up failed' } - Write-Host '[ok] compose started' -ForegroundColor Green + if ($LASTEXITCODE -ne 0) { throw 'compose up failed' } } 'compose-down' { docker compose down - if ($LASTEXITCODE -ne 0) { throw 'docker compose down failed' } - Write-Host '[ok] compose stopped' -ForegroundColor Green + if ($LASTEXITCODE -ne 0) { throw 'compose down failed' } } } diff --git a/cmd/jiang13/main.go b/cmd/jiang13/main.go index 1fd57d9..81f0ed2 100644 --- a/cmd/jiang13/main.go +++ b/cmd/jiang13/main.go @@ -1,11 +1,11 @@ -package main +package main import ( "fmt" "log" "os" - "github.com/kardianos/service" + kardsvc "github.com/kardianos/service" "git.iioio.com/freefire/jiang13-forum/config" ) @@ -25,7 +25,7 @@ func main() { } prg := &program{cfg: cfg} - svc, err := service.New(prg, svcCfg) + svc, err := kardsvc.New(prg, svcCfg) if err != nil { log.Fatalf("创建系统服务失败: %v", err) } diff --git a/cmd/jiang13/program.go b/cmd/jiang13/program.go index b68391d..d10a03c 100644 --- a/cmd/jiang13/program.go +++ b/cmd/jiang13/program.go @@ -1,4 +1,4 @@ -package main +package main import ( "context" @@ -9,11 +9,11 @@ import ( "os" "time" - "github.com/kardianos/service" + kardsvc "github.com/kardianos/service" "git.iioio.com/freefire/jiang13-forum/config" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/router" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/routers" ) const ( @@ -28,7 +28,7 @@ type program struct { server *http.Server } -func (p *program) Start(s service.Service) error { +func (p *program) Start(s kardsvc.Service) error { if err := p.setup(); err != nil { return err } @@ -40,7 +40,7 @@ func (p *program) Start(s service.Service) error { return nil } -func (p *program) Stop(s service.Service) error { +func (p *program) Stop(s kardsvc.Service) error { log.Println("收到关机信号,正在优雅关闭...") if p.server == nil { return nil @@ -63,7 +63,7 @@ func (p *program) setup() error { return fmt.Errorf("打开日志文件失败: %w", err) } // 服务模式下 stdout 可能不可用,仅写文件;前台运行则双写 - if service.Interactive() { + if kardsvc.Interactive() { log.SetOutput(io.MultiWriter(os.Stdout, logFile)) } else { log.SetOutput(logFile) @@ -75,11 +75,11 @@ func (p *program) setup() error { log.Printf(" 版本: %s", version) log.Println("========================================") - if err := model.InitDB(cfg.DBPath()); err != nil { + if err := models.InitDB(cfg.DBPath()); err != nil { return fmt.Errorf("数据库初始化失败: %w", err) } - engine, err := router.Setup(cfg) + engine, err := routers.Setup(cfg) if err != nil { return fmt.Errorf("路由初始化失败: %w", err) } @@ -98,9 +98,9 @@ func (p *program) setup() error { return nil } -func buildServiceConfig(cfg *config.Config) (*service.Config, error) { +func buildServiceConfig(cfg *config.Config) (*kardsvc.Config, error) { // 服务只绑定工作目录与配置文件;端口/数据目录改 app.ini 后重启即可,无需重装服务 - return &service.Config{ + return &kardsvc.Config{ Name: svcName, DisplayName: svcDisplayName, Description: svcDescription, @@ -109,7 +109,7 @@ func buildServiceConfig(cfg *config.Config) (*service.Config, error) { "--work-path", cfg.WorkPath, "--config", cfg.ConfigFile, }, - Option: service.KeyValue{ + Option: kardsvc.KeyValue{ // systemd:异常退出后自动拉起 "Restart": "always", // Windows:崩溃后重启 @@ -118,16 +118,16 @@ func buildServiceConfig(cfg *config.Config) (*service.Config, error) { }, nil } -func runServiceControl(s service.Service, action string) error { +func runServiceControl(s kardsvc.Service, action string) error { if action == "status" { st, err := s.Status() if err != nil { return err } switch st { - case service.StatusRunning: + case kardsvc.StatusRunning: fmt.Println("服务状态: 运行中 (running)") - case service.StatusStopped: + case kardsvc.StatusStopped: fmt.Println("服务状态: 已停止 (stopped)") default: fmt.Println("服务状态: 未知 (unknown)") @@ -135,7 +135,7 @@ func runServiceControl(s service.Service, action string) error { return nil } - if err := service.Control(s, action); err != nil { + if err := kardsvc.Control(s, action); err != nil { return err } diff --git a/docs/rebuild-spec/02-features.md b/docs/rebuild-spec/02-features.md index 3cc0313..d985f4f 100644 --- a/docs/rebuild-spec/02-features.md +++ b/docs/rebuild-spec/02-features.md @@ -3,7 +3,7 @@ > **读者**:实现与验收 > **前置**:[01-product.md](01-product.md) > **交叉**:[05-business-rules.md](05-business-rules.md)、[06-pages-ux.md](06-pages-ux.md) -> **源码对照**:[`frontend/src/App.tsx`](../../frontend/src/App.tsx)、[`router/router.go`](../../router/router.go)、[`README.md`](../../README.md) +> **源码对照**:[`frontend/src/App.tsx`]((仅 main)frontend/src/App.tsx)、[`router/router.go`](../../routers/setup.go)、[`README.md`](../../README.md) 用复选框做验收;重构完成时应全部可勾选(或书面声明砍掉的功能)。 diff --git a/docs/rebuild-spec/03-data-model.md b/docs/rebuild-spec/03-data-model.md index ed2f882..e7c7123 100644 --- a/docs/rebuild-spec/03-data-model.md +++ b/docs/rebuild-spec/03-data-model.md @@ -3,7 +3,7 @@ > **读者**:实现数据库与领域层的 AI > **前置**:[01-product.md](01-product.md) > **后续**:[04-api.md](04-api.md)、[05-business-rules.md](05-business-rules.md) -> **源码**:[`model/models.go`](../../model/models.go)、[`model/oauth.go`](../../model/oauth.go)、[`model/gitea.go`](../../model/gitea.go)、[`model/level.go`](../../model/level.go)、[`model/db.go`](../../model/db.go)、[`model/user_view.go`](../../model/user_view.go)、[`service/settings.go`](../../service/settings.go) +> **源码**:[`model/models.go`](../../models/models.go)、[`model/oauth.go`](../../models/oauth.go)、[`model/gitea.go`](../../models/gitea.go)、[`model/level.go`](../../models/level.go)、[`model/db.go`](../../models/db.go)、[`model/user_view.go`](../../models/user_view.go)、[`service/settings.go`](../../services/settings.go) 当前无独立 SQL migration;表由 GORM `AutoMigrate` 创建。新站可用正式 migration,但**字段语义应对齐**。 @@ -72,7 +72,7 @@ erDiagram **非落库展示字段**:`level`(由 Exp 推导)、`badges`(附加)。 -视图结构:`UserPublic` / `UserSelf` / `UserAdmin`(见 [`model/user_view.go`](../../model/user_view.go))。 +视图结构:`UserPublic` / `UserSelf` / `UserAdmin`(见 [`model/user_view.go`](../../models/user_view.go))。 ### 2.2 boards @@ -278,7 +278,7 @@ Metric:`tenure_days` | `likes_received` | `creator_income` ## 4. 等级(Exp → Level) -源:[`model/level.go`](../../model/level.go) +源:[`model/level.go`](../../models/level.go) | Level | 最低 Exp | |-------|----------| @@ -299,7 +299,7 @@ Metric:`tenure_days` | `likes_received` | `creator_income` ## 5. 内置自动徽章(seed) -源:[`model/db.go`](../../model/db.go) `seedDefaultBadges` +源:[`model/db.go`](../../models/db.go) `seedDefaultBadges` | code | 名称 | metric | threshold | |------|------|--------|-----------| @@ -317,7 +317,7 @@ Metric:`tenure_days` | `likes_received` | `creator_income` ## 6. forum_settings 键与默认值 -源:[`service/settings.go`](../../service/settings.go)、[`service/permalink.go`](../../service/permalink.go) +源:[`service/settings.go`](../../services/settings.go)、[`service/permalink.go`](../../services/permalink.go) ### 6.1 论坛限制 @@ -429,7 +429,7 @@ Metric:`tenure_days` | `likes_received` | `creator_income` ## 7. 升级兼容补丁(现网 InitDB) -[`model/db.go`](../../model/db.go) 在 AutoMigrate 后: +[`model/db.go`](../../models/db.go) 在 AutoMigrate 后: - 空 `status` 的帖/评 → `published` - 空 `post_type` → `normal` @@ -449,4 +449,4 @@ Metric:`tenure_days` | `likes_received` | `creator_income` ... ``` -积分解锁 `block_key` = `sha256(innerHTML)[:16]`(hex),见 [`service/unlock.go`](../../service/unlock.go)。 +积分解锁 `block_key` = `sha256(innerHTML)[:16]`(hex),见 [`service/unlock.go`](../../services/unlock.go)。 diff --git a/docs/rebuild-spec/04-api.md b/docs/rebuild-spec/04-api.md index 8a8340c..cad9958 100644 --- a/docs/rebuild-spec/04-api.md +++ b/docs/rebuild-spec/04-api.md @@ -2,7 +2,7 @@ > **读者**:实现后端 / BFF / 前端数据层的 AI > **前置**:[03-data-model.md](03-data-model.md) -> **源码**:[`router/router.go`](../../router/router.go)、[`frontend/src/api/client.ts`](../../frontend/src/api/client.ts)、[`frontend/src/api/types.ts`](../../frontend/src/api/types.ts)、[`middleware/auth.go`](../../middleware/auth.go) +> **源码**:[`router/router.go`](../../routers/setup.go)、[`frontend/src/api/client.ts`]((仅 main)frontend/src/api/client.ts)、[`frontend/src/api/types.ts`]((仅 main)frontend/src/api/types.ts)、[`middleware/auth.go`](../../modules/auth/auth.go) 不要求 OpenAPI YAML;以下表格 + JSON 形状即为合约。新站可加 `/v1` 前缀,但**字段名建议保持**以便对照迁移。 @@ -54,7 +54,7 @@ | GET/POST | `/oauth/userinfo` | Bearer | 用户信息 | | GET/POST | `/oauth/logout` | 视实现 | 登出 | -细节以 [`service/oidc.go`](../../service/oidc.go) / [`handler/oidc.go`](../../handler/oidc.go) 为准。 +细节以 [`service/oidc.go`](../../services/oidc.go) / [`handler/oidc.go`](../../routers/api/oidc.go) 为准。 --- @@ -356,7 +356,7 @@ ## 7. 核心类型速查(与前端对齐) -详见 [`frontend/src/api/types.ts`](../../frontend/src/api/types.ts)。实现时至少对齐: +详见 [`frontend/src/api/types.ts`]((仅 main)frontend/src/api/types.ts)。实现时至少对齐: - `User` / `UserPublic` / `UserActivityStats` - `Board` / `PostItem` / `PostDetailResponse` / `Comment` @@ -373,4 +373,4 @@ 中间件对未登录 / 过期 / 禁言返回 JSON error(并可能清 cookie)。前端统一 `throw new Error(data.error)`。新站应保持可区分的错误文案或错误码,避免前端无法提示。 -源:[`middleware/auth.go`](../../middleware/auth.go)。 +源:[`middleware/auth.go`](../../modules/auth/auth.go)。 diff --git a/docs/rebuild-spec/05-business-rules.md b/docs/rebuild-spec/05-business-rules.md index 283e5f2..8a4ff45 100644 --- a/docs/rebuild-spec/05-business-rules.md +++ b/docs/rebuild-spec/05-business-rules.md @@ -2,13 +2,13 @@ > **读者**:实现领域逻辑的 AI(最易「看起来像但算错」) > **前置**:[03-data-model.md](03-data-model.md)、[04-api.md](04-api.md) -> **源码**:[`service/`](../../service/)、[`model/models.go`](../../model/models.go) +> **源码**:[`service/`](../../services/)、[`model/models.go`](../../models/models.go) --- ## 1. 注册与引导 -源:[`handler/handlers.go`](../../handler/handlers.go) `APIRegisterConfig`、[`service/auth.go`](../../service/auth.go) +源:[`handler/handlers.go`](../../routers/api/handlers.go) `APIRegisterConfig`、[`service/auth.go`](../../services/auth.go) | 规则 | 细节 | |------|------| @@ -41,7 +41,7 @@ stateDiagram-v2 | 待审提醒 | 通知管理员(kind=`moderation`) | | 游客评论 | 通常直接或按实现进入审核;勿假设与登录用户完全相同 | -源:[`service/post.go`](../../service/post.go) `CanViewPost`、[`service/comment.go`](../../service/comment.go)。 +源:[`service/post.go`](../../services/post.go) `CanViewPost`、[`service/comment.go`](../../services/comment.go)。 --- @@ -68,7 +68,7 @@ stateDiagram-v2 | 评论成功 | +2 | | 帖子被点赞 | +1(作者) | -源:[`service/post.go`](../../service/post.go)、[`service/comment.go`](../../service/comment.go)、[`service/badge.go`](../../service/badge.go) `AddExp`。 +源:[`service/post.go`](../../services/post.go)、[`service/comment.go`](../../services/comment.go)、[`service/badge.go`](../../services/badge.go) `AddExp`。 等级门槛见 [03-data-model.md](03-data-model.md) §4。管理员设 level 时应同步 Exp 到门槛值。 @@ -76,7 +76,7 @@ stateDiagram-v2 ## 5. 内容门控(红action) -源:[`service/content.go`](../../service/content.go)、[`handler/api.go`](../../handler/api.go) `APIPostDetail`、[`service/unlock.go`](../../service/unlock.go) +源:[`service/content.go`](../../services/content.go)、[`handler/api.go`](../../routers/api/api.go) `APIPostDetail`、[`service/unlock.go`](../../services/unlock.go) ### 5.1 出口顺序(详情) @@ -148,7 +148,7 @@ stateDiagram-v2 ## 7. 签到与每日抽奖 -源:[`service/points.go`](../../service/points.go) +源:[`service/points.go`](../../services/points.go) ### 签到 @@ -225,4 +225,4 @@ stateDiagram-v2 | `board_pinned` | **不**抬升 | 抬升 | | `featured` | 标记展示,不一定改变排序 | 同左 | -具体 SQL/排序实现见 [`service/post.go`](../../service/post.go) ListItems。 +具体 SQL/排序实现见 [`service/post.go`](../../services/post.go) ListItems。 diff --git a/docs/rebuild-spec/06-pages-ux.md b/docs/rebuild-spec/06-pages-ux.md index 3f86133..87d0a07 100644 --- a/docs/rebuild-spec/06-pages-ux.md +++ b/docs/rebuild-spec/06-pages-ux.md @@ -2,7 +2,7 @@ > **读者**:实现前台 / 后台 UI 的 AI > **前置**:[02-features.md](02-features.md) -> **源码**:[`frontend/src/App.tsx`](../../frontend/src/App.tsx)、[`frontend/src/pages/`](../../frontend/src/pages/)、[`frontend/src/components/`](../../frontend/src/components/)、[`frontend/src/layouts/`](../../frontend/src/layouts/) +> **源码**:[`frontend/src/App.tsx`]((仅 main)frontend/src/App.tsx)、[`frontend/src/pages/`]((仅 main)frontend/src/pages/)、[`frontend/src/components/`]((仅 main)frontend/src/components/)、[`frontend/src/layouts/`]((仅 main)frontend/src/layouts/) 视觉可重设;**信息架构与关键操作流应对齐**。新站建议 SSR 直出同等信息,而不是先空壳再 fetch。 @@ -119,7 +119,7 @@ ### 3.2 编辑器能力(应对齐) -源:[`ArticleEditor.tsx`](../../frontend/src/components/ArticleEditor.tsx) 与 `editor/` 扩展 +源:[`ArticleEditor.tsx`]((仅 main)frontend/src/components/ArticleEditor.tsx) 与 `editor/` 扩展 - 标题 h2–h6(无 h1,避免与帖标题冲突) - 粗体/斜体/删除线等基础标记 @@ -129,7 +129,7 @@ - 图片上传 + 图片组布局 + 浮动/清除浮动 - 表情 / 贴纸选择器(多套:bilibili/douyin/tieba/weibo 等静态资源) - **登录可见** / **回复可见** / **积分可见**(价格 1–9999)节点 -- 富文本 ↔ Markdown 双模(门控块有 markdown 约定,见 [`utils/markdownContent.ts`](../../frontend/src/utils/markdownContent.ts)) +- 富文本 ↔ Markdown 双模(门控块有 markdown 约定,见 [`utils/markdownContent.ts`]((仅 main)frontend/src/utils/markdownContent.ts)) - Tab 缩进 未保存离开:`UnsavedChangesDialog`。 diff --git a/docs/rebuild-spec/07-config-ops.md b/docs/rebuild-spec/07-config-ops.md index 7878f25..896e424 100644 --- a/docs/rebuild-spec/07-config-ops.md +++ b/docs/rebuild-spec/07-config-ops.md @@ -2,7 +2,7 @@ > **读者**:部署与运维、以及实现配置层的 AI > **前置**:[README.md](README.md) -> **源码**:[`app.ini.example`](../../app.ini.example)、[`config/`](../../config/)、[`README.md`](../../README.md)、[`handler/seo.go`](../../handler/seo.go)、[`handler/seo_bot.go`](../../handler/seo_bot.go)、[`embed_static/`](../../embed_static/) +> **源码**:[`app.ini.example`](../../app.ini.example)、[`config/`](../../config/)、[`README.md`](../../README.md)、[`handler/seo.go`](../../routers/api/seo.go)、[`handler/seo_bot.go`](../../routers/api/seo_bot.go)、[`embed_static/`]((仅 main 分支)embed_static/) 运维形态可改;下列描述**现网**行为,便于迁移数据与对齐环境变量语义。 @@ -91,8 +91,8 @@ data/ | 机制 | 说明 | |------|------| -| SPA 壳注入 | [`embed_static`](../../embed_static/) 注入 title / branding JSON,**无帖文 DOM** | -| 爬虫 HTML | User-Agent 命中时 [`seo_bot.go`](../../handler/seo_bot.go) 返回简易 HTML | +| SPA 壳注入 | `embed_static`(仅 `main` 分支) 注入 title / branding JSON,**无帖文 DOM** | +| 爬虫 HTML | User-Agent 命中时 [`seo_bot.go`](../../routers/api/seo_bot.go) 返回简易 HTML | | robots.txt / sitemap.xml | 动态生成 | 重构验收:用普通浏览器「查看网页源代码」应能看到帖文正文,而不仅是空 div + script。 @@ -110,7 +110,7 @@ data/ - `/board/2.html` - `/page/about.html` -路由应同时接受无后缀与有后缀形式。解析逻辑见 [`service/permalink.go`](../../service/permalink.go)。 +路由应同时接受无后缀与有后缀形式。解析逻辑见 [`service/permalink.go`](../../services/permalink.go)。 --- diff --git a/docs/rebuild-spec/08-gitea-ssr-architecture.md b/docs/rebuild-spec/08-gitea-ssr-architecture.md index 2fb8743..27436d7 100644 --- a/docs/rebuild-spec/08-gitea-ssr-architecture.md +++ b/docs/rebuild-spec/08-gitea-ssr-architecture.md @@ -35,19 +35,29 @@ git worktree add ../jiang13-spa main --- -## 目录职责(演进中) +## 目录职责(本分支已落地) ```text -routers/web/ # 返回 HTML 的页面路由 -templates/ # Go 模板源文件(嵌入) -web_src/ # CSS/JS 源码 -public/assets/ # web_src 构建产物(嵌入,URL 前缀 `/ssr-assets/`) -modules/ # 横切(模板渲染等) -docs/rebuild-spec/ # 产品规格 -.cursor/rules/ # AI 开发规则 +cmd/jiang13/ # 入口 +config/ # 配置 +models/ # GORM 模型(原 model/) +services/ # 业务逻辑(原 service/) +routers/ + setup.go # 路由总装(原 router/) + web/ # HTML SSR + api/ # JSON API(原 handler/) +modules/ + auth/ # JWT / 限流等(原 middleware/) + webrender/ # 模板渲染 + seo/ # PageMeta 等 +templates/ # Go 模板(embed) +web_src/ # CSS/JS 源码 +public/assets/ # 构建产物(URL 前缀 `/ssr-assets/`) +docs/rebuild-spec/ # 产品规格 +.cursor/rules/ # AI 开发规则 ``` -现有 `model/`、`service/`、`handler/`(JSON API)可先复用,公开页出口改为模板。 +**已删除(勿恢复):** `frontend/`、`embed_static/`。SPA 对照仅看 `main`。 --- diff --git a/docs/rebuild-spec/README.md b/docs/rebuild-spec/README.md index 2d3eccd..068bf89 100644 --- a/docs/rebuild-spec/README.md +++ b/docs/rebuild-spec/README.md @@ -61,9 +61,9 @@ flowchart LR | 痛点 | 现状 | 对用户的影响 | |------|------|----------------| -| 非真 SSR | 生产入口 [`embed_static`](../../embed_static/) 只注入 title / branding / Open Graph,**不渲染帖文 DOM** | 刷新先出壳再灌数据,体验不如 SSR | -| 爬虫双轨 | [`handler/seo_bot.go`](../../handler/seo_bot.go) 对爬虫返回独立 HTML | 用户与爬虫看到的不是同一套渲染路径 | -| 无正式 migration | Schema 靠 GORM `AutoMigrate`([`model/db.go`](../../model/db.go)) | 升级靠「加字段」,难做破坏性迁移与审计 | +| 非真 SSR | 生产入口(`main` 的 `embed_static`)只注入 title / branding / Open Graph,**不渲染帖文 DOM** | 刷新先出壳再灌数据,体验不如 SSR | +| 爬虫双轨 | [`routers/api/seo_bot.go`](../../routers/api/seo_bot.go) 对爬虫返回独立 HTML | 用户与爬虫看到的不是同一套渲染路径 | +| 无正式 migration | Schema 靠 GORM `AutoMigrate`([`models/db.go`](../../models/db.go)) | 升级靠「加字段」,难做破坏性迁移与审计 | | Cookie JWT | 无 session 表,密钥在 `data/.jwt_secret` | 可保留语义,实现可换成更好的会话方案 | **新站目标**:用户首屏即可看到帖文 / 列表的服务端渲染(SSR)HTML;SEO meta 与正文同源。技术选型自定(Next.js / Nuxt / Remix / 其它均可)。 @@ -113,13 +113,13 @@ flowchart LR | 主题 | 路径 | |------|------| -| 路由总表 | [`router/router.go`](../../router/router.go) | -| GORM 模型 | [`model/models.go`](../../model/models.go) | -| AutoMigrate | [`model/db.go`](../../model/db.go) | -| 论坛设置键 | [`service/settings.go`](../../service/settings.go) | -| 前端 API 客户端 | [`frontend/src/api/client.ts`](../../frontend/src/api/client.ts) | -| 前端类型 | [`frontend/src/api/types.ts`](../../frontend/src/api/types.ts) | -| 页面路由 | [`frontend/src/App.tsx`](../../frontend/src/App.tsx) | +| 路由总装 | [`routers/setup.go`](../../routers/setup.go) | +| GORM 模型 | [`models/models.go`](../../models/models.go) | +| AutoMigrate | [`models/db.go`](../../models/db.go) | +| 论坛设置键 | [`services/settings.go`](../../services/settings.go) | +| SSR 页面路由 | [`routers/web/`](../../routers/web/) | +| JSON API | [`routers/api/`](../../routers/api/) | +| 前端 API / 页面(对照) | 仅 `main`:`frontend/src/api/`、`frontend/src/App.tsx` | | 产品介绍 | [`docs/introduction.md`](../introduction.md)、[`README.md`](../../README.md) | --- diff --git a/models/db.go b/models/db.go index 94a1ad2..e091cce 100644 --- a/models/db.go +++ b/models/db.go @@ -1,4 +1,4 @@ -package model +package models import ( "fmt" diff --git a/models/gitea.go b/models/gitea.go index 5ceacbe..a8590b7 100644 --- a/models/gitea.go +++ b/models/gitea.go @@ -1,4 +1,4 @@ -package model +package models import "time" diff --git a/models/level.go b/models/level.go index a2644f5..66fb398 100644 --- a/models/level.go +++ b/models/level.go @@ -1,4 +1,4 @@ -package model +package models // LevelThresholds 各等级所需最低 Exp(下标 0 对应 Lv1) var LevelThresholds = []int{0, 20, 50, 100, 200, 400, 800, 1500, 3000, 5000} diff --git a/models/models.go b/models/models.go index 7fdd7c8..615e43e 100644 --- a/models/models.go +++ b/models/models.go @@ -1,4 +1,4 @@ -package model +package models import ( "time" diff --git a/models/oauth.go b/models/oauth.go index a8bdbd4..2ebfb09 100644 --- a/models/oauth.go +++ b/models/oauth.go @@ -1,4 +1,4 @@ -package model +package models import "time" diff --git a/models/user_view.go b/models/user_view.go index 4025521..9328c98 100644 --- a/models/user_view.go +++ b/models/user_view.go @@ -1,4 +1,4 @@ -package model +package models import "time" diff --git a/modules/auth/auth.go b/modules/auth/auth.go index 2df0ac4..863de2f 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -1,4 +1,4 @@ -package middleware +package auth import ( "fmt" @@ -6,8 +6,8 @@ import ( "strings" "github.com/gin-gonic/gin" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/services" ) const ( @@ -18,10 +18,10 @@ const ( ) type AuthMiddleware struct { - auth *service.AuthService + auth *services.AuthService } -func NewAuthMiddleware(auth *service.AuthService) *AuthMiddleware { +func NewAuthMiddleware(auth *services.AuthService) *AuthMiddleware { return &AuthMiddleware{auth: auth} } @@ -32,8 +32,8 @@ func (m *AuthMiddleware) OptionalAuth() gin.HandlerFunc { token := extractToken(c) if token != "" { if claims, err := m.auth.ParseToken(token); err == nil { - var user model.User - if err := model.DB.Select("id", "username", "role").First(&user, claims.UserID).Error; err != nil { + var user models.User + if err := models.DB.Select("id", "username", "role").First(&user, claims.UserID).Error; err != nil { c.SetCookie(CookieName, "", -1, "/", "", false, true) } else { c.Set(CtxUserID, user.ID) @@ -63,8 +63,8 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc { return } // 检查禁言 - var user model.User - if err := model.DB.First(&user, claims.UserID).Error; err != nil || user.Banned { + var user models.User + if err := models.DB.First(&user, claims.UserID).Error; err != nil || user.Banned { respondBanned(c) c.Abort() return @@ -81,7 +81,7 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc { func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc { return func(c *gin.Context) { role, exists := c.Get(CtxRole) - if !exists || role != model.RoleAdmin { + if !exists || role != models.RoleAdmin { if isAPI(c) { c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) } else { @@ -147,7 +147,7 @@ func respondBanned(c *gin.Context) { } // RateLimitMiddleware 限流中间件 -func RateLimitMiddleware(limiter *service.RateLimiter, action string) gin.HandlerFunc { +func RateLimitMiddleware(limiter *services.RateLimiter, action string) gin.HandlerFunc { return func(c *gin.Context) { key := c.ClientIP() if uid, ok := c.Get(CtxUserID); ok { diff --git a/modules/seo/pagemeta.go b/modules/seo/pagemeta.go new file mode 100644 index 0000000..fb49bb7 --- /dev/null +++ b/modules/seo/pagemeta.go @@ -0,0 +1,16 @@ +package seo + +// PageMeta 页面级 SEO / 社交预览元数据(SSR 与爬虫 HTML 共用) +type PageMeta struct { + Title string // 完整 + Description string + Keywords string // meta keywords + Canonical string + OGType string // 默认 website + OGImage string + SiteName string // og:site_name + Locale string // og:locale,默认 zh_CN + Robots string // 如 noindex,nofollow + JSONLD string // 已序列化的 JSON-LD 对象(不含 script 标签) + Status int // HTTP 状态码,0 视为 200 +} diff --git a/routers/api/api.go b/routers/api/api.go index 3887d5d..80af074 100644 --- a/routers/api/api.go +++ b/routers/api/api.go @@ -1,4 +1,4 @@ -package handler +package api import ( "errors" @@ -10,9 +10,9 @@ import ( "time" "github.com/gin-gonic/gin" - "git.iioio.com/freefire/jiang13-forum/middleware" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/modules/auth" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/services" ) // APIMe 当前登录用户 @@ -25,7 +25,7 @@ func (h *Handlers) APIMe(c *gin.Context) { user, err := h.User.GetByID(uid) if err != nil { // 账号已删或不存在:清掉失效 cookie,与未登录态一致 - c.SetCookie(middleware.CookieName, "", -1, "/", "", false, true) + c.SetCookie(auth.CookieName, "", -1, "/", "", false, true) c.JSON(http.StatusOK, gin.H{"user": nil}) return } @@ -35,7 +35,7 @@ func (h *Handlers) APIMe(c *gin.Context) { view := user.ToSelf() if h.Badge != nil { if badges, bErr := h.Badge.ListUserBadges(user.ID); bErr == nil { - view.Badges = service.BadgeViews(badges, 0) + view.Badges = services.BadgeViews(badges, 0) } } c.JSON(http.StatusOK, gin.H{ @@ -51,14 +51,14 @@ func (h *Handlers) APIBoards(c *gin.Context) { return } if boards == nil { - boards = []service.BoardWithStats{} + boards = []services.BoardWithStats{} } c.JSON(http.StatusOK, gin.H{"boards": boards}) } // APIHealth 健康检查(容器探活 / 负载均衡) func (h *Handlers) APIHealth(c *gin.Context) { - if err := model.PingDB(); err != nil { + if err := models.PingDB(); err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{ "status": "unavailable", "error": err.Error(), @@ -71,10 +71,10 @@ func (h *Handlers) APIHealth(c *gin.Context) { // APIStats 论坛概览统计 func (h *Handlers) APIStats(c *gin.Context) { var userCount, postCount, boardCount, commentCount int64 - model.DB.Model(&model.User{}).Count(&userCount) - model.DB.Model(&model.Post{}).Where("status = ?", model.ContentStatusPublished).Count(&postCount) - model.DB.Model(&model.Board{}).Count(&boardCount) - model.DB.Model(&model.Comment{}).Where("status = ?", model.ContentStatusPublished).Count(&commentCount) + models.DB.Model(&models.User{}).Count(&userCount) + models.DB.Model(&models.Post{}).Where("status = ?", models.ContentStatusPublished).Count(&postCount) + models.DB.Model(&models.Board{}).Count(&boardCount) + models.DB.Model(&models.Comment{}).Where("status = ?", models.ContentStatusPublished).Count(&commentCount) c.JSON(http.StatusOK, gin.H{ "users": userCount, "posts": postCount, @@ -144,19 +144,19 @@ func (h *Handlers) APIAdminDeleteBoard(c *gin.Context) { // APIAdminDashboard 管理后台概览 func (h *Handlers) APIAdminDashboard(c *gin.Context) { var userCount, postCount, boardCount, commentCount int64 - model.DB.Model(&model.User{}).Count(&userCount) - model.DB.Model(&model.Post{}).Where("status = ?", model.ContentStatusPublished).Count(&postCount) - model.DB.Model(&model.Board{}).Count(&boardCount) - model.DB.Model(&model.Comment{}).Where("status = ?", model.ContentStatusPublished).Count(&commentCount) + models.DB.Model(&models.User{}).Count(&userCount) + models.DB.Model(&models.Post{}).Where("status = ?", models.ContentStatusPublished).Count(&postCount) + models.DB.Model(&models.Board{}).Count(&boardCount) + models.DB.Model(&models.Comment{}).Where("status = ?", models.ContentStatusPublished).Count(&commentCount) pendingPosts, _ := h.Post.PendingPostCount() pendingComments, _ := h.Comment.PendingCommentCount() pendingReports, _ := h.Report.PendingCount() pendingFriendLinks, _ := h.FriendLinkApply.PendingCount() - recentPosts, _, _ := h.Post.List(service.PostListQuery{ + recentPosts, _, _ := h.Post.List(services.PostListQuery{ Page: 1, Size: 8, ViewerIsAdmin: true, Status: "all", }) if recentPosts == nil { - recentPosts = []model.Post{} + recentPosts = []models.Post{} } c.JSON(http.StatusOK, gin.H{ "users": userCount, "posts": postCount, "boards": boardCount, @@ -175,7 +175,7 @@ func (h *Handlers) APIAdminPosts(c *gin.Context) { size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) keyword := strings.TrimSpace(c.Query("keyword")) status := strings.TrimSpace(c.DefaultQuery("status", "all")) - posts, total, err := h.Post.ListItems(service.PostListQuery{ + posts, total, err := h.Post.ListItems(services.PostListQuery{ Page: page, Size: size, Keyword: keyword, ViewerIsAdmin: true, Status: status, }) @@ -184,7 +184,7 @@ func (h *Handlers) APIAdminPosts(c *gin.Context) { return } if posts == nil { - posts = []service.PostListItem{} + posts = []services.PostListItem{} } pending, _ := h.Post.PendingPostCount() c.JSON(http.StatusOK, gin.H{ @@ -321,7 +321,7 @@ func (h *Handlers) APIAdminTrashPosts(c *gin.Context) { return } if posts == nil { - posts = []service.TrashPostItem{} + posts = []services.TrashPostItem{} } c.JSON(http.StatusOK, gin.H{ "posts": posts, "total": total, "page": page, @@ -360,7 +360,7 @@ func (h *Handlers) APIAdminComments(c *gin.Context) { return } if comments == nil { - comments = []model.Comment{} + comments = []models.Comment{} } pending, _ := h.Comment.PendingCommentCount() c.JSON(http.StatusOK, gin.H{ @@ -374,18 +374,18 @@ func (h *Handlers) APIAdminComments(c *gin.Context) { // APIAdminApproveComment 通过评论审核 func (h *Handlers) APIAdminApproveComment(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if err := h.Comment.SetStatus(uint(id), model.ContentStatusPublished); err != nil { + if err := h.Comment.SetStatus(uint(id), models.ContentStatusPublished); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if h.Notify != nil { if comment, err := h.Comment.GetByID(uint(id)); err == nil { - comment.Status = model.ContentStatusPublished + comment.Status = models.ContentStatusPublished h.Notify.AsyncNotifyCommentPublished(comment) h.Notify.AsyncNotifyCommentMentions(comment) } } - c.JSON(http.StatusOK, gin.H{"message": "评论已通过审核", "status": model.ContentStatusPublished}) + c.JSON(http.StatusOK, gin.H{"message": "评论已通过审核", "status": models.ContentStatusPublished}) } // APIAdminRejectComment 拒绝评论并私信通知 @@ -404,7 +404,7 @@ func (h *Handlers) APIAdminRejectComment(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := h.Comment.SetStatus(uint(id), model.ContentStatusRejected); err != nil { + if err := h.Comment.SetStatus(uint(id), models.ContentStatusRejected); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -417,13 +417,13 @@ func (h *Handlers) APIAdminRejectComment(c *gin.Context) { _, _ = h.Message.SendSystem( comment.UserID, "评论未通过审核", - service.FormatCommentRejectContent(title, comment.PostID, comment.Floor, reason), - model.MessageKindReject, + services.FormatCommentRejectContent(title, comment.PostID, comment.Floor, reason), + models.MessageKindReject, &pid, nil, ) } - c.JSON(http.StatusOK, gin.H{"message": "已拒绝该评论并通知作者", "status": model.ContentStatusRejected}) + c.JSON(http.StatusOK, gin.H{"message": "已拒绝该评论并通知作者", "status": models.ContentStatusRejected}) } // APIAdminDeleteComment 管理员软删除评论(进入回收站) @@ -447,7 +447,7 @@ func (h *Handlers) APIAdminTrashComments(c *gin.Context) { return } if comments == nil { - comments = []service.TrashCommentItem{} + comments = []services.TrashCommentItem{} } c.JSON(http.StatusOK, gin.H{ "comments": comments, "total": total, "page": page, @@ -492,7 +492,7 @@ func (h *Handlers) APIAdminUsers(c *gin.Context) { size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) keyword := strings.TrimSpace(c.Query("keyword")) filter := strings.TrimSpace(c.DefaultQuery("filter", "all")) - users, total, err := h.User.ListUsers(service.UserListQuery{ + users, total, err := h.User.ListUsers(services.UserListQuery{ Page: page, Size: size, Keyword: keyword, Filter: filter, }) if err != nil { @@ -500,10 +500,10 @@ func (h *Handlers) APIAdminUsers(c *gin.Context) { return } if users == nil { - users = []model.User{} + users = []models.User{} } c.JSON(http.StatusOK, gin.H{ - "users": model.UsersToAdmin(users), "total": total, "page": page, + "users": models.UsersToAdmin(users), "total": total, "page": page, "total_pages": calcTotalPages(total, size), "keyword": keyword, "filter": filter, @@ -560,7 +560,7 @@ func (h *Handlers) APIAdminDownloadBackup(c *gin.Context) { // APIAdminSettings 系统设置信息 func (h *Handlers) APIAdminSettings(c *gin.Context) { limits := h.Settings.Limits() - filterContent, _ := service.ReadFilterWordsFile(h.Cfg.FilterWordsPath()) + filterContent, _ := services.ReadFilterWordsFile(h.Cfg.FilterWordsPath()) clients, _ := h.Settings.ListOAuthClients() c.JSON(http.StatusOK, gin.H{ "filter_path": h.Cfg.FilterWordsPath(), @@ -575,7 +575,7 @@ func (h *Handlers) APIAdminSettings(c *gin.Context) { "storage": h.Settings.StorageConfigPublic(), "branding": h.Settings.SiteBranding(), "filter_words": filterContent, - "filter_word_count": service.CountFilterWords(filterContent), + "filter_word_count": services.CountFilterWords(filterContent), }) } @@ -588,7 +588,7 @@ func (h *Handlers) APISiteBranding(c *gin.Context) { // APIAdminUpdateBranding 更新站点品牌文案 func (h *Handlers) APIAdminUpdateBranding(c *gin.Context) { - var req service.SiteBranding + var req services.SiteBranding if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return @@ -620,7 +620,7 @@ func (h *Handlers) APIAdminUploadBrandingAsset(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "图片不能超过 2MB"}) return } - url, err := service.SaveUploadedImage(h.Store, file, service.UploadCategorySite, kind) + url, err := services.SaveUploadedImage(h.Store, file, services.UploadCategorySite, kind) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -677,7 +677,7 @@ func (h *Handlers) APIAdminClearBrandingAsset(c *gin.Context) { // APIAdminUpdateForumSettings 更新论坛设置 func (h *Handlers) APIAdminUpdateForumSettings(c *gin.Context) { - var req service.ForumLimits + var req services.ForumLimits if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return @@ -694,7 +694,7 @@ func (h *Handlers) APIAdminUpdateForumSettings(c *gin.Context) { // APIAdminUpdateMailSettings 更新邮件 SMTP 配置 func (h *Handlers) APIAdminUpdateMailSettings(c *gin.Context) { - var req service.MailConfig + var req services.MailConfig if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return @@ -711,7 +711,7 @@ func (h *Handlers) APIAdminUpdateMailSettings(c *gin.Context) { // APIAdminUpdateOIDCSettings 更新 OIDC Provider 全局配置 func (h *Handlers) APIAdminUpdateOIDCSettings(c *gin.Context) { - var req service.OIDCConfig + var req services.OIDCConfig if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return @@ -749,7 +749,7 @@ func (h *Handlers) APIProjects(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "读取失败"}) return } - list = service.AttachGiteaOwners(list, h.Badge) + list = services.AttachGiteaOwners(list, h.Badge) c.JSON(http.StatusOK, gin.H{ "projects": list, "total": total, @@ -760,7 +760,7 @@ func (h *Handlers) APIProjects(c *gin.Context) { // APIAdminUpdateGiteaSettings 更新 Gitea 同步配置 func (h *Handlers) APIAdminUpdateGiteaSettings(c *gin.Context) { - var req service.GiteaSyncConfig + var req services.GiteaSyncConfig if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return @@ -777,7 +777,7 @@ func (h *Handlers) APIAdminUpdateGiteaSettings(c *gin.Context) { // APIAdminUpdateStorageSettings 更新上传存储(本地 / S3 兼容),保存后立即热切换 func (h *Handlers) APIAdminUpdateStorageSettings(c *gin.Context) { - var req service.StorageConfig + var req services.StorageConfig if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return @@ -799,7 +799,7 @@ func (h *Handlers) APIAdminUpdateStorageSettings(c *gin.Context) { // APIAdminSyncGitea 立即同步 Gitea 公开仓库 func (h *Handlers) APIAdminSyncGitea(c *gin.Context) { if h.Gitea == nil { - c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrGiteaNotConfigured.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrGiteaNotConfigured.Error()}) return } n, err := h.Gitea.SyncRepos() @@ -826,7 +826,7 @@ func (h *Handlers) APIAdminListOAuthClients(c *gin.Context) { // APIAdminCreateOAuthClient 创建 OAuth 应用 func (h *Handlers) APIAdminCreateOAuthClient(c *gin.Context) { - var req service.OAuthClientInput + var req services.OAuthClientInput if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return @@ -850,7 +850,7 @@ func (h *Handlers) APIAdminUpdateOAuthClient(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "无效 ID"}) return } - var req service.OAuthClientInput + var req services.OAuthClientInput if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return @@ -897,16 +897,16 @@ func (h *Handlers) APIAdminTestMail(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "请填写收件邮箱"}) return } - if err := service.ValidateEmail(req.To); err != nil { + if err := services.ValidateEmail(req.To); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if !h.Settings.MailReady() { - c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrMailNotConfigured.Error()}) return } siteName := h.Settings.SiteBranding().Name - err := h.Mail.Send(service.NormalizeEmail(req.To), "邮件配置测试", + err := h.Mail.Send(services.NormalizeEmail(req.To), "邮件配置测试", fmt.Sprintf("这是一封来自%s的测试邮件,说明 SMTP 配置正常。", siteName)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -917,14 +917,14 @@ func (h *Handlers) APIAdminTestMail(c *gin.Context) { // APIAdminFilterWords 读取敏感词配置 func (h *Handlers) APIAdminFilterWords(c *gin.Context) { - content, err := service.ReadFilterWordsFile(h.Cfg.FilterWordsPath()) + content, err := services.ReadFilterWordsFile(h.Cfg.FilterWordsPath()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "读取敏感词配置失败"}) return } c.JSON(http.StatusOK, gin.H{ "content": content, - "word_count": service.CountFilterWords(content), + "word_count": services.CountFilterWords(content), "path": h.Cfg.FilterWordsPath(), }) } @@ -938,13 +938,13 @@ func (h *Handlers) APIAdminUpdateFilterWords(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } - if err := service.WriteFilterWordsFile(h.Cfg.FilterWordsPath(), req.Content, h.Filter); err != nil { + if err := services.WriteFilterWordsFile(h.Cfg.FilterWordsPath(), req.Content, h.Filter); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "保存敏感词配置失败"}) return } c.JSON(http.StatusOK, gin.H{ "message": "敏感词已保存并生效", - "word_count": service.CountFilterWords(req.Content), + "word_count": services.CountFilterWords(req.Content), }) } @@ -959,7 +959,7 @@ func (h *Handlers) APIPosts(c *gin.Context) { author := strings.TrimSpace(c.Query("author")) titleOnly := c.Query("title_only") == "1" || strings.EqualFold(c.Query("title_only"), "true") - q := service.PostListQuery{ + q := services.PostListQuery{ BoardID: uint(boardID), UserID: uint(userID), Page: page, @@ -982,10 +982,10 @@ func (h *Handlers) APIPosts(c *gin.Context) { return } if items == nil { - items = []service.PostListItem{} + items = []services.PostListItem{} } if h.Badge != nil { - users := make([]*model.User, 0, len(items)) + users := make([]*models.User, 0, len(items)) for i := range items { if items[i].User.ID > 0 { users = append(users, &items[i].User) @@ -1012,34 +1012,34 @@ func (h *Handlers) APIPostDetail(c *gin.Context) { } uid := h.currentUserID(c) isAdmin := h.isAdmin(c) - if !service.CanViewPost(post, uid, isAdmin) { + if !services.CanViewPost(post, uid, isAdmin) { c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"}) return } - if c.Query("skip_view") != "1" && post.Status == model.ContentStatusPublished { + if c.Query("skip_view") != "1" && post.Status == models.ContentStatusPublished { h.Post.RecordView(uint(id)) } // 出口再消毒:兼容库内历史脏 HTML(如 <style>),避免旧帖污染整页 - post.Content = service.SanitizePostHTML(post.Content) + post.Content = services.SanitizePostHTML(post.Content) hasReplied := uid > 0 && h.Comment.HasUserReplied(uint(id), uid) if uid == 0 { - post.Content = service.RedactMembersOnlyHTML(post.Content) - post.Content = service.RedactReplyOnlyHTML(post.Content) + post.Content = services.RedactMembersOnlyHTML(post.Content) + post.Content = services.RedactReplyOnlyHTML(post.Content) } else if !isAdmin && post.UserID != uid && !hasReplied { // 作者与管理员始终可见;其他用户需已回复 - post.Content = service.RedactReplyOnlyHTML(post.Content) + post.Content = services.RedactReplyOnlyHTML(post.Content) } // 积分解锁块:作者/站长全文;其他人按解锁记录 redact if isAdmin || post.UserID == uid { - post.Content = service.RevealAllPointsOnly(post.Content) + post.Content = services.RevealAllPointsOnly(post.Content) } else { - unlocked, _ := service.ListUnlockedKeys(uid, uint(id)) - post.Content = service.RedactPointsOnlyHTML(post.Content, unlocked) + unlocked, _ := services.ListUnlockedKeys(uid, uint(id)) + post.Content = services.RedactPointsOnlyHTML(post.Content, unlocked) } comments, _ := h.Comment.ListByPost(uint(id), uid, isAdmin, post.UserID, h.parseGuestCommentIDs(c)) if h.Badge != nil { if post.User.ID > 0 { - h.Badge.AttachBadgeSummaries([]*model.User{&post.User}, 3) + h.Badge.AttachBadgeSummaries([]*models.User{&post.User}, 3) } h.Badge.AttachBadgeSummariesOnComments(comments, 2) } @@ -1060,21 +1060,21 @@ func (h *Handlers) APIPostDetail(c *gin.Context) { "is_edited": isEdited, "post_edit_window_hours": h.Settings.PostEditWindowHours(), } - if post.PostType == model.PostTypePoll { - if poll, err := service.GetPollView(uint(id), uid); err == nil { + if post.PostType == models.PostTypePoll { + if poll, err := services.GetPollView(uint(id), uid); err == nil { resp["poll"] = poll } } - if post.PostType == model.PostTypeLottery { - if lottery, err := service.GetPostLotteryView(post); err == nil && lottery != nil { + if post.PostType == models.PostTypeLottery { + if lottery, err := services.GetPostLotteryView(post); err == nil && lottery != nil { resp["lottery"] = lottery } } - if post.PostType == model.PostTypeBounty && post.BountyStatus == model.BountyStatusOpen && post.BountyPoints > 0 { - canRefund, blockReason := service.CanRefundBounty(post, isAdmin) + if post.PostType == models.PostTypeBounty && post.BountyStatus == models.BountyStatusOpen && post.BountyPoints > 0 { + canRefund, blockReason := services.CanRefundBounty(post, isAdmin) resp["bounty_can_refund"] = canRefund resp["bounty_refund_block_reason"] = blockReason - if n, err := service.CountEligibleBountyReplies(model.DB, post.ID, post.UserID); err == nil { + if n, err := services.CountEligibleBountyReplies(models.DB, post.ID, post.UserID); err == nil { resp["bounty_eligible_reply_count"] = n } } @@ -1091,7 +1091,7 @@ func (h *Handlers) APIPostComments(c *gin.Context) { } uid := h.currentUserID(c) isAdmin := h.isAdmin(c) - if !service.CanViewPost(post, uid, isAdmin) { + if !services.CanViewPost(post, uid, isAdmin) { c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"}) return } @@ -1101,7 +1101,7 @@ func (h *Handlers) APIPostComments(c *gin.Context) { return } if comments == nil { - comments = []model.Comment{} + comments = []models.Comment{} } if h.Badge != nil { h.Badge.AttachBadgeSummariesOnComments(comments, 2) @@ -1138,7 +1138,7 @@ func (h *Handlers) APIRecentComments(c *gin.Context) { return } if list == nil { - list = []service.RecentCommentItem{} + list = []services.RecentCommentItem{} } c.JSON(http.StatusOK, gin.H{"comments": list}) } @@ -1151,7 +1151,7 @@ func (h *Handlers) APIRecentUsers(c *gin.Context) { return } if list == nil { - list = []service.RecentUserItem{} + list = []services.RecentUserItem{} } c.JSON(http.StatusOK, gin.H{"users": list}) } @@ -1166,7 +1166,7 @@ func (h *Handlers) APIFavorites(c *gin.Context) { return } if favs == nil { - favs = []model.PostFavorite{} + favs = []models.PostFavorite{} } c.JSON(http.StatusOK, gin.H{"favorites": favs, "total": total, "page": page}) } @@ -1217,6 +1217,6 @@ func (h *Handlers) APIPostRevisionDetail(c *gin.Context) { } func isClientLimitError(err error) bool { - return errors.Is(err, service.ErrSearchKeywordTooShort) || - errors.Is(err, service.ErrSearchKeywordTooLong) + return errors.Is(err, services.ErrSearchKeywordTooShort) || + errors.Is(err, services.ErrSearchKeywordTooLong) } diff --git a/routers/api/economy.go b/routers/api/economy.go index db9b96c..d9acbb0 100644 --- a/routers/api/economy.go +++ b/routers/api/economy.go @@ -1,4 +1,4 @@ -package handler +package api import ( "errors" @@ -6,8 +6,8 @@ import ( "strconv" "github.com/gin-gonic/gin" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/services" ) // APIMePoints 余额与流水 @@ -53,7 +53,7 @@ func (h *Handlers) APIMeCheckInGet(c *gin.Context) { func (h *Handlers) APIMeCheckIn(c *gin.Context) { st, err := h.Points.CheckIn(h.currentUserID(c)) if err != nil { - if errors.Is(err, service.ErrAlreadyCheckedIn) { + if errors.Is(err, services.ErrAlreadyCheckedIn) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -102,7 +102,7 @@ func (h *Handlers) APIUnlockPostBlock(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 block_key"}) return } - res, err := service.UnlockPointsBlock(h.currentUserID(c), uint(id), req.BlockKey) + res, err := services.UnlockPointsBlock(h.currentUserID(c), uint(id), req.BlockKey) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -120,7 +120,7 @@ func (h *Handlers) APIAdminVerifyUser(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } - if err := service.SetVerified(uint(id), req.Verified); err != nil { + if err := services.SetVerified(uint(id), req.Verified); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -141,11 +141,11 @@ func (h *Handlers) APIAdminSetUserLevel(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } - if err := service.SetUserLevel(uint(id), req.Level); err != nil { + if err := services.SetUserLevel(uint(id), req.Level); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, gin.H{"message": "等级已更新", "level": req.Level, "exp": model.ExpForLevel(req.Level)}) + c.JSON(http.StatusOK, gin.H{"message": "等级已更新", "level": req.Level, "exp": models.ExpForLevel(req.Level)}) } // APIAdminAdjustPoints 调积分 @@ -179,7 +179,7 @@ func (h *Handlers) APIAdminListBadges(c *gin.Context) { // APIAdminUpsertBadge 创建/更新徽章定义 func (h *Handlers) APIAdminUpsertBadge(c *gin.Context) { - var def model.BadgeDef + var def models.BadgeDef if err := c.ShouldBindJSON(&def); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return diff --git a/routers/api/friend_link.go b/routers/api/friend_link.go index a1beca5..9766f9d 100644 --- a/routers/api/friend_link.go +++ b/routers/api/friend_link.go @@ -1,4 +1,4 @@ -package handler +package api import ( "fmt" @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/services" "github.com/gin-gonic/gin" ) @@ -28,7 +28,7 @@ func (h *Handlers) APIApplyFriendLink(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } - result, err := h.FriendLinkApply.Create(service.FriendLinkApplyInput{ + result, err := h.FriendLinkApply.Create(services.FriendLinkApplyInput{ UserID: uid, Name: req.Name, URL: req.URL, @@ -65,10 +65,10 @@ func (h *Handlers) APIUploadFriendLinkLogo(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "图片文件过大"}) return } - url, err := service.SaveUploadedImage( + url, err := services.SaveUploadedImage( h.Store, file, - service.UploadCategorySite, + services.UploadCategorySite, fmt.Sprintf("fl_%d", uid), ) if err != nil { @@ -83,7 +83,7 @@ func (h *Handlers) APIAdminFriendLinkApplies(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) status := strings.TrimSpace(c.DefaultQuery("status", "pending")) - list, total, err := h.FriendLinkApply.ListAdmin(service.FriendLinkApplyListQuery{ + list, total, err := h.FriendLinkApply.ListAdmin(services.FriendLinkApplyListQuery{ Page: page, Size: size, Status: status, }) if err != nil { @@ -236,7 +236,7 @@ func (h *Handlers) APIUpdateFriendLinkApply(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } - result, err := h.FriendLinkApply.Update(uid, uint(id), service.FriendLinkApplyInput{ + result, err := h.FriendLinkApply.Update(uid, uint(id), services.FriendLinkApplyInput{ UserID: uid, Name: req.Name, URL: req.URL, diff --git a/routers/api/handlers.go b/routers/api/handlers.go index dbb47c9..8a2c417 100644 --- a/routers/api/handlers.go +++ b/routers/api/handlers.go @@ -1,4 +1,4 @@ -package handler +package api import ( "encoding/base64" @@ -10,58 +10,58 @@ import ( "github.com/gin-gonic/gin" "git.iioio.com/freefire/jiang13-forum/config" - "git.iioio.com/freefire/jiang13-forum/middleware" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/modules/auth" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/services" ) // Handlers 聚合所有 HTTP 处理器 type Handlers struct { Cfg *config.Config - Store *service.UploadStore - Auth *service.AuthService - User *service.UserService - Board *service.BoardService - Post *service.PostService - Comment *service.CommentService - Message *service.MessageService - Notify *service.NotifyService - Report *service.ReportService - Backup *service.BackupService - Filter *service.SensitiveFilter - Limiter *service.RateLimiter - Settings *service.ForumSettingsService - Captcha *service.CaptchaService - Mail *service.MailService - EmailCode *service.EmailCodeService - OIDC *service.OIDCService - Gitea *service.GiteaService - Points *service.PointsService - Badge *service.BadgeService - SitePage *service.SitePageService - FriendLinkApply *service.FriendLinkApplyService + Store *services.UploadStore + Auth *services.AuthService + User *services.UserService + Board *services.BoardService + Post *services.PostService + Comment *services.CommentService + Message *services.MessageService + Notify *services.NotifyService + Report *services.ReportService + Backup *services.BackupService + Filter *services.SensitiveFilter + Limiter *services.RateLimiter + Settings *services.ForumSettingsService + Captcha *services.CaptchaService + Mail *services.MailService + EmailCode *services.EmailCodeService + OIDC *services.OIDCService + Gitea *services.GiteaService + Points *services.PointsService + Badge *services.BadgeService + SitePage *services.SitePageService + FriendLinkApply *services.FriendLinkApplyService } func (h *Handlers) setAuthCookie(c *gin.Context, token string) { - c.SetCookie(middleware.CookieName, token, int(service.TokenExpire.Seconds()), "/", "", false, true) + c.SetCookie(auth.CookieName, token, int(services.TokenExpire.Seconds()), "/", "", false, true) } func (h *Handlers) currentUserID(c *gin.Context) uint { - if v, ok := c.Get(middleware.CtxUserID); ok { + if v, ok := c.Get(auth.CtxUserID); ok { return v.(uint) } return 0 } func (h *Handlers) isAdmin(c *gin.Context) bool { - if v, ok := c.Get(middleware.CtxRole); ok { - return v == model.RoleAdmin + if v, ok := c.Get(auth.CtxRole); ok { + return v == models.RoleAdmin } return false } // loadCurrentUser 加载当前登录用户完整资料(含认证/积分) -func (h *Handlers) loadCurrentUser(c *gin.Context) (*model.User, error) { +func (h *Handlers) loadCurrentUser(c *gin.Context) (*models.User, error) { uid := h.currentUserID(c) if uid == 0 { return nil, errors.New("未登录") @@ -133,7 +133,7 @@ func (h *Handlers) APIRegisterConfig(c *gin.Context) { "mail_ready": mailReady, "require_email_code": mailReady, "register_open": userCount == 0 || mailReady, - "email_code_len": service.EmailCodeLen, + "email_code_len": services.EmailCodeLen, }) } @@ -147,7 +147,7 @@ func (h *Handlers) APISendRegisterEmailCode(c *gin.Context) { return } if !h.Settings.MailReady() { - c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrMailNotConfigured.Error()}) return } if err := h.EmailCode.SendRegisterCode(req.Email); err != nil { @@ -167,7 +167,7 @@ func (h *Handlers) APISendResetEmailCode(c *gin.Context) { return } if !h.Settings.MailReady() { - c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrMailNotConfigured.Error()}) return } if err := h.EmailCode.SendResetCode(req.Email); err != nil { @@ -189,11 +189,11 @@ func (h *Handlers) APIResetPassword(c *gin.Context) { return } if !h.Settings.MailReady() { - c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrMailNotConfigured.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrMailNotConfigured.Error()}) return } - if !h.EmailCode.VerifyPurpose(service.EmailCodePurposeReset, req.Email, req.EmailCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrEmailCodeInvalid.Error()}) + if !h.EmailCode.VerifyPurpose(services.EmailCodePurposeReset, req.Email, req.EmailCode) { + c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrEmailCodeInvalid.Error()}) return } if err := h.User.ResetPasswordByEmail(req.Email, req.NewPassword); err != nil { @@ -244,12 +244,12 @@ func (h *Handlers) APIRegister(c *gin.Context) { userCount := h.Auth.UserCount() mailReady := h.Settings.MailReady() if userCount > 0 && !mailReady { - c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrRegisterClosed.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrRegisterClosed.Error()}) return } if mailReady { if !h.EmailCode.Verify(req.Email, req.EmailCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": service.ErrEmailCodeInvalid.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": services.ErrEmailCodeInvalid.Error()}) return } } @@ -283,7 +283,7 @@ func (h *Handlers) APILogin(c *gin.Context) { } func (h *Handlers) APILogout(c *gin.Context) { - c.SetCookie(middleware.CookieName, "", -1, "/", "", false, true) + c.SetCookie(auth.CookieName, "", -1, "/", "", false, true) c.JSON(http.StatusOK, gin.H{"message": "已退出"}) } @@ -323,7 +323,7 @@ func (h *Handlers) APIUserPublic(c *gin.Context) { if h.Badge != nil { _ = h.Badge.EvaluateAuto(user.ID) if badges, bErr := h.Badge.ListUserBadges(user.ID); bErr == nil { - view.Badges = service.BadgeViews(badges, 0) + view.Badges = services.BadgeViews(badges, 0) } } c.JSON(http.StatusOK, gin.H{ @@ -396,10 +396,10 @@ func (h *Handlers) APIUploadPostImage(c *gin.Context) { return } uid := h.currentUserID(c) - url, err := service.SaveUploadedImage( + url, err := services.SaveUploadedImage( h.Store, file, - service.UploadCategoryPosts, + services.UploadCategoryPosts, fmt.Sprintf("%d", uid), ) if err != nil { @@ -421,20 +421,20 @@ func (h *Handlers) APICreatePost(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - extras := service.ParsePostExtrasFromForm( + extras := services.ParsePostExtrasFromForm( c.PostForm("poll_options"), c.PostForm("bounty_points"), c.PostForm("lottery_winner_count"), ) - if post.PostType == model.PostTypePoll || post.PostType == model.PostTypeBounty || post.PostType == model.PostTypeLottery { - if err := service.FinalizeSpecialPostCreate(post, h.currentUserID(c), extras); err != nil { + if post.PostType == models.PostTypePoll || post.PostType == models.PostTypeBounty || post.PostType == models.PostTypeLottery { + if err := services.FinalizeSpecialPostCreate(post, h.currentUserID(c), extras); err != nil { _ = h.Post.Delete(h.currentUserID(c), post.ID, true) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } msg := "发帖成功" - if post.Status == model.ContentStatusPending { + if post.Status == models.ContentStatusPending { msg = "已提交审核,通过后将公开显示" if h.Notify != nil { h.Notify.AsyncNotifyPendingPost(post) @@ -479,8 +479,8 @@ func (h *Handlers) APIToggleLike(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - var post model.Post - model.DB.First(&post, id) + var post models.Post + models.DB.First(&post, id) c.JSON(http.StatusOK, gin.H{"liked": liked, "like_count": post.LikeCount}) } @@ -535,7 +535,7 @@ func (h *Handlers) APICreateComment(c *gin.Context) { return } - in := service.CommentCreateInput{ + in := services.CommentCreateInput{ UserID: uid, PostID: uint(postID), Content: content, @@ -551,14 +551,14 @@ func (h *Handlers) APICreateComment(c *gin.Context) { msg := "评论成功" if h.Notify != nil { switch comment.Status { - case model.ContentStatusPublished: + case models.ContentStatusPublished: h.Notify.AsyncNotifyCommentPublished(comment) h.Notify.AsyncNotifyCommentMentions(comment) - case model.ContentStatusPending: + case models.ContentStatusPending: msg = "评论已提交,审核通过后公开显示" h.Notify.AsyncNotifyPendingComment(comment) } - } else if comment.Status == model.ContentStatusPending { + } else if comment.Status == models.ContentStatusPending { msg = "评论已提交,审核通过后公开显示" } c.JSON(http.StatusOK, gin.H{"message": msg, "floor": comment.Floor, "id": comment.ID, "status": comment.Status}) @@ -586,7 +586,7 @@ func (h *Handlers) APIUpdateComment(c *gin.Context) { status := "" if comment, e := h.Comment.GetByID(uint(id)); e == nil { status = comment.Status - if status == model.ContentStatusPending && !h.isAdmin(c) { + if status == models.ContentStatusPending && !h.isAdmin(c) { msg = "评论已更新,审核通过后公开显示" } if enteredPending && h.Notify != nil { diff --git a/routers/api/media.go b/routers/api/media.go index 0fcf320..f655d49 100644 --- a/routers/api/media.go +++ b/routers/api/media.go @@ -1,4 +1,4 @@ -package handler +package api import ( "net/http" diff --git a/routers/api/message.go b/routers/api/message.go index d8b8835..56549c7 100644 --- a/routers/api/message.go +++ b/routers/api/message.go @@ -1,19 +1,19 @@ -package handler +package api import ( "net/http" "strconv" "github.com/gin-gonic/gin" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/services" ) // APIMessageConversations 会话列表(按对方聚合) func (h *Handlers) APIMessageConversations(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("size", "30")) - list, total, err := h.Message.ListConversations(service.ConversationListQuery{ + list, total, err := h.Message.ListConversations(services.ConversationListQuery{ UserID: h.currentUserID(c), Page: page, Size: size, @@ -41,7 +41,7 @@ func (h *Handlers) APIConversationMessages(c *gin.Context) { before, _ := strconv.ParseUint(c.DefaultQuery("before", "0"), 10, 64) uid := h.currentUserID(c) - list, total, err := h.Message.ListConversationMessages(service.ConversationMessagesQuery{ + list, total, err := h.Message.ListConversationMessages(services.ConversationMessagesQuery{ UserID: uid, PeerID: uint(peerID), Page: page, @@ -63,10 +63,10 @@ func (h *Handlers) APIConversationMessages(c *gin.Context) { } } - var peer *model.User + var peer *models.User if peerID > 0 { - var u model.User - if err := model.DB.First(&u, uint(peerID)).Error; err == nil { + var u models.User + if err := models.DB.First(&u, uint(peerID)).Error; err == nil { peer = &u } } @@ -147,7 +147,7 @@ func (h *Handlers) APISendMessage(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } - msg, err := h.Message.Send(service.MessageSendInput{ + msg, err := h.Message.Send(services.MessageSendInput{ FromUserID: h.currentUserID(c), ToUserID: req.ToUserID, Subject: req.Subject, diff --git a/routers/api/oidc.go b/routers/api/oidc.go index 311b0d9..8974661 100644 --- a/routers/api/oidc.go +++ b/routers/api/oidc.go @@ -1,4 +1,4 @@ -package handler +package api import ( "encoding/base64" @@ -8,8 +8,8 @@ import ( "strings" "github.com/gin-gonic/gin" - "git.iioio.com/freefire/jiang13-forum/middleware" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/modules/auth" + "git.iioio.com/freefire/jiang13-forum/services" ) // OIDCDiscovery OpenID Provider 元数据 @@ -47,7 +47,7 @@ func (h *Handlers) OIDCAuthorize(c *gin.Context) { return } - req := service.AuthorizeRequest{ + req := services.AuthorizeRequest{ ClientID: c.Query("client_id"), RedirectURI: c.Query("redirect_uri"), ResponseType: c.Query("response_type"), @@ -60,7 +60,7 @@ func (h *Handlers) OIDCAuthorize(c *gin.Context) { if err := h.OIDC.ValidateAuthorize(req); err != nil { // redirect_uri 未通过校验时不能重定向,避免开放重定向 - if errors.Is(err, service.ErrOIDCInvalidRedirect) || errors.Is(err, service.ErrOIDCInvalidClient) { + if errors.Is(err, services.ErrOIDCInvalidRedirect) || errors.Is(err, services.ErrOIDCInvalidClient) { c.String(http.StatusBadRequest, err.Error()) return } @@ -77,7 +77,7 @@ func (h *Handlers) OIDCAuthorize(c *gin.Context) { callback, err := h.OIDC.IssueAuthCode(uid, req) if err != nil { - if errors.Is(err, service.ErrOIDCUserBanned) { + if errors.Is(err, services.ErrOIDCUserBanned) { h.oidcErrorRedirect(c, req.RedirectURI, req.State, "access_denied", "账号已被禁言") return } @@ -121,7 +121,7 @@ func (h *Handlers) OIDCToken(c *gin.Context) { } } - resp, err := h.OIDC.ExchangeCode(service.TokenRequest{ + resp, err := h.OIDC.ExchangeCode(services.TokenRequest{ GrantType: c.PostForm("grant_type"), Code: c.PostForm("code"), RedirectURI: c.PostForm("redirect_uri"), @@ -133,12 +133,12 @@ func (h *Handlers) OIDCToken(c *gin.Context) { status := http.StatusBadRequest code := "invalid_grant" switch { - case errors.Is(err, service.ErrOIDCInvalidClient): + case errors.Is(err, services.ErrOIDCInvalidClient): status = http.StatusUnauthorized code = "invalid_client" - case errors.Is(err, service.ErrOIDCInvalidRequest): + case errors.Is(err, services.ErrOIDCInvalidRequest): code = "invalid_request" - case errors.Is(err, service.ErrOIDCPKCEFailed): + case errors.Is(err, services.ErrOIDCPKCEFailed): code = "invalid_grant" } c.JSON(status, gin.H{"error": code, "error_description": err.Error()}) @@ -179,7 +179,7 @@ func (h *Handlers) OIDCLogout(c *gin.Context) { state = c.PostForm("state") } - c.SetCookie(middleware.CookieName, "", -1, "/", "", false, true) + c.SetCookie(auth.CookieName, "", -1, "/", "", false, true) if h.OIDC == nil { c.Redirect(http.StatusFound, "/") diff --git a/routers/api/report.go b/routers/api/report.go index 8e0a49f..294dbc3 100644 --- a/routers/api/report.go +++ b/routers/api/report.go @@ -1,4 +1,4 @@ -package handler +package api import ( "net/http" @@ -6,8 +6,8 @@ import ( "strings" "github.com/gin-gonic/gin" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/services" ) // APICreatePostReport 举报帖子 @@ -53,7 +53,7 @@ func (h *Handlers) APIAdminReports(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) status := c.DefaultQuery("status", "pending") - list, total, err := h.Report.ListAdmin(service.ReportListQuery{ + list, total, err := h.Report.ListAdmin(services.ReportListQuery{ Status: status, Page: page, Size: size, @@ -84,7 +84,7 @@ func (h *Handlers) APIAdminHandleReport(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } - rep, err := h.Report.Handle(service.HandleReportInput{ + rep, err := h.Report.Handle(services.HandleReportInput{ ReportID: uint(id), HandlerID: h.currentUserID(c), Action: req.Action, @@ -101,11 +101,11 @@ func (h *Handlers) APIAdminHandleReport(c *gin.Context) { // APIAdminApprovePost 通过帖子审核 func (h *Handlers) APIAdminApprovePost(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if err := h.Post.SetStatus(uint(id), model.ContentStatusPublished); err != nil { + if err := h.Post.SetStatus(uint(id), models.ContentStatusPublished); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, gin.H{"message": "帖子已通过审核", "status": model.ContentStatusPublished}) + c.JSON(http.StatusOK, gin.H{"message": "帖子已通过审核", "status": models.ContentStatusPublished}) } // APIAdminRejectPost 拒绝帖子并私信通知作者(标记为 rejected,不进回收站) @@ -133,7 +133,7 @@ func (h *Handlers) APIAdminRejectPost(c *gin.Context) { title := post.Title postID := post.ID - if err := h.Post.SetStatus(postID, model.ContentStatusRejected); err != nil { + if err := h.Post.SetStatus(postID, models.ContentStatusRejected); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -142,8 +142,8 @@ func (h *Handlers) APIAdminRejectPost(c *gin.Context) { _, msgErr := h.Message.SendSystem( authorID, "帖子《"+title+"》未通过审核", - service.FormatRejectContent(title, postID, reason), - model.MessageKindReject, + services.FormatRejectContent(title, postID, reason), + models.MessageKindReject, &pid, nil, ) @@ -151,13 +151,13 @@ func (h *Handlers) APIAdminRejectPost(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "帖子已拒绝,但私信通知失败:" + msgErr.Error(), "notified": false, - "status": model.ContentStatusRejected, + "status": models.ContentStatusRejected, }) return } c.JSON(http.StatusOK, gin.H{ "message": "已拒绝该帖并私信通知作者", "notified": true, - "status": model.ContentStatusRejected, + "status": models.ContentStatusRejected, }) } diff --git a/routers/api/seo.go b/routers/api/seo.go index 3e789e7..30dc413 100644 --- a/routers/api/seo.go +++ b/routers/api/seo.go @@ -1,4 +1,4 @@ -package handler +package api import ( "encoding/json" @@ -11,9 +11,9 @@ import ( "time" "github.com/gin-gonic/gin" - "git.iioio.com/freefire/jiang13-forum/embed_static" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/modules/seo" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/services" ) var ( @@ -60,7 +60,7 @@ func (h *Handlers) SitemapXML(c *gin.Context) { now := time.Now().UTC() permalink := h.Settings.Permalink() - urls := []service.SitemapURL{ + urls := []services.SitemapURL{ {Loc: base + "/", LastMod: now, ChangeFreq: "hourly", Priority: "1.0"}, {Loc: base + "/projects", LastMod: now, ChangeFreq: "daily", Priority: "0.6"}, {Loc: base + "/links", LastMod: now, ChangeFreq: "weekly", Priority: "0.6"}, @@ -68,8 +68,8 @@ func (h *Handlers) SitemapXML(c *gin.Context) { if boards, err := h.Board.List(); err == nil { for _, board := range boards { - urls = append(urls, service.SitemapURL{ - Loc: base + service.QueryBoardHome(board.ID, permalink), + urls = append(urls, services.SitemapURL{ + Loc: base + services.QueryBoardHome(board.ID, permalink), LastMod: board.UpdatedAt.UTC(), ChangeFreq: "daily", Priority: "0.7", @@ -83,7 +83,7 @@ func (h *Handlers) SitemapXML(c *gin.Context) { if lm.IsZero() { lm = p.CreatedAt } - urls = append(urls, service.SitemapURL{ + urls = append(urls, services.SitemapURL{ Loc: base + permalink.PostPath(p.ID), LastMod: lm.UTC(), ChangeFreq: "weekly", @@ -94,7 +94,7 @@ func (h *Handlers) SitemapXML(c *gin.Context) { if users, e2 := h.User.ListSitemap(seoSitemapLimit); e2 == nil { for _, u := range users { - urls = append(urls, service.SitemapURL{ + urls = append(urls, services.SitemapURL{ Loc: base + permalink.UserPath(u.ID), LastMod: u.UpdatedAt.UTC(), ChangeFreq: "weekly", @@ -109,7 +109,7 @@ func (h *Handlers) SitemapXML(c *gin.Context) { if lm.IsZero() { lm = p.CreatedAt } - urls = append(urls, service.SitemapURL{ + urls = append(urls, services.SitemapURL{ Loc: base + permalink.PagePath(p.Slug), LastMod: lm.UTC(), ChangeFreq: "monthly", @@ -160,14 +160,14 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) { if siteName == "" { siteName = "姜十三论坛" } - defaultImage := service.AbsoluteURL(base, brand.DefaultShareImage()) + defaultImage := services.AbsoluteURL(base, brand.DefaultShareImage()) siteKeywords := brand.MetaKeywords() permalink := h.Settings.Permalink() // 旧版 /?board=id → 规范板块路径 if path == "/" || path == "" { if boardID, err := strconv.ParseUint(c.Query("board"), 10, 64); err == nil && boardID > 0 { - target := service.QueryBoardHome(uint(boardID), permalink) + target := services.QueryBoardHome(uint(boardID), permalink) if q := c.Request.URL.RawQuery; q != "" { // 保留 sort/keyword 等 query,去掉 board vals := c.Request.URL.Query() @@ -181,7 +181,7 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) { } } - isBot := service.IsSEOCrawler(c.Request.UserAgent()) + isBot := services.IsSEOCrawler(c.Request.UserAgent()) if isBot { c.Header("Vary", "User-Agent") } @@ -201,11 +201,11 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) { if desc == "" { desc = brand.MetaDescription() } - meta := attachSiteSEO(&embed_static.SPAPageMeta{ + meta := attachSiteSEO(&seo.PageMeta{ Title: pageTitle(board.Name, siteName), - Description: service.TruncateRunes(desc, seoDescMax), - Keywords: service.JoinSEOKeywords(board.Name, siteKeywords), - Canonical: service.AbsoluteURL(base, bm.Canonical), + Description: services.TruncateRunes(desc, seoDescMax), + Keywords: services.JoinSEOKeywords(board.Name, siteKeywords), + Canonical: services.AbsoluteURL(base, bm.Canonical), OGType: "website", OGImage: defaultImage, }, siteName, siteKeywords) @@ -213,7 +213,7 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(h.botBoardHTML(meta, *board))) return } - embed_static.ServeSPAWithMeta(c, meta) + servePendingSSR(c, meta.Title, `<p>板块页 SSR 迁移中,请先从 <a href="/">首页</a> 浏览。</p>`) return } @@ -224,16 +224,16 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) { return } post, err := h.Post.FindByID(pm.ID) - if err != nil || !service.CanViewPost(post, h.currentUserID(c), h.isAdmin(c)) { + if err != nil || !services.CanViewPost(post, h.currentUserID(c), h.isAdmin(c)) { h.serveNotFound(c, base, siteName, siteKeywords, path, isBot) return } - postKeywords := service.JoinSEOKeywords(post.Board.Name, siteKeywords) + postKeywords := services.JoinSEOKeywords(post.Board.Name, siteKeywords) if isBot { c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(h.botPostHTML(base, siteName, defaultImage, postKeywords, post))) return } - embed_static.ServeSPAWithMeta(c, attachSiteSEO(h.postPageMeta(base, siteName, defaultImage, post), siteName, postKeywords)) + servePendingSSR(c, pageTitle(post.Title, siteName), `<p>帖子详情 SSR 迁移中,请先从 <a href="/">首页</a> 返回。</p>`) return } @@ -252,7 +252,7 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(h.botUserHTML(base, siteName, defaultImage, siteKeywords, user))) return } - embed_static.ServeSPAWithMeta(c, attachSiteSEO(h.userPageMeta(base, siteName, defaultImage, user), siteName, siteKeywords)) + servePendingSSR(c, pageTitle(user.Nickname, siteName), `<p>用户主页 SSR 迁移中,请先从 <a href="/">首页</a> 返回。</p>`) return } @@ -267,12 +267,12 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) { h.serveNotFound(c, base, siteName, siteKeywords, path, isBot) return } - desc := service.ExcerptFromHTML(page.Content, seoDescMax) - meta := attachSiteSEO(&embed_static.SPAPageMeta{ + desc := services.ExcerptFromHTML(page.Content, seoDescMax) + meta := attachSiteSEO(&seo.PageMeta{ Title: pageTitle(page.Title, siteName), Description: desc, - Keywords: service.JoinSEOKeywords(page.Title, siteKeywords), - Canonical: service.AbsoluteURL(base, pg.Canonical), + Keywords: services.JoinSEOKeywords(page.Title, siteKeywords), + Canonical: services.AbsoluteURL(base, pg.Canonical), OGType: "article", OGImage: defaultImage, }, siteName, siteKeywords) @@ -281,7 +281,7 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(renderBotHTML(meta, body))) return } - embed_static.ServeSPAWithMeta(c, meta) + servePendingSSR(c, meta.Title, page.Content) return } @@ -291,13 +291,23 @@ func (h *Handlers) ServePublicSPA(c *gin.Context) { return } - // 其余已知路由:SPA + head meta;首页对爬虫额外返回可读正文 + // 其余已知路由:爬虫可读首页;用户走占位页(首页本身已由 routers/web SSR) meta := h.buildSPAPageMeta(c, path, brand, base, siteName, defaultImage) if isBot && (path == "/" || path == "") { c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(h.botHomeHTML(meta, brand))) return } - embed_static.ServeSPAWithMeta(c, meta) + servePendingSSR(c, meta.Title, `<p>该页面 SSR 迁移中。<a href="/">返回首页</a></p>`) +} + +func servePendingSSR(c *gin.Context, title, bodyHTML string) { + if strings.TrimSpace(title) == "" { + title = "姜十三论坛" + } + page := fmt.Sprintf(`<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>%s
%s
`, + html.EscapeString(title), bodyHTML) + c.Header("Cache-Control", "no-cache") + c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(page)) } func (h *Handlers) serveNotFound(c *gin.Context, base, siteName, keywords, path string, isBot bool) { @@ -306,14 +316,17 @@ func (h *Handlers) serveNotFound(c *gin.Context, base, siteName, keywords, path c.Data(http.StatusNotFound, "text/html; charset=utf-8", []byte(botNotFoundHTML(base, siteName, keywords, path))) return } - embed_static.ServeSPAWithMeta(c, notFoundPageMeta(base, siteName, keywords, path)) + page := fmt.Sprintf(`%s

404

页面不存在。

返回首页

`, + html.EscapeString(pageTitle("页面不存在", siteName))) + c.Header("Cache-Control", "no-cache") + c.Data(http.StatusNotFound, "text/html; charset=utf-8", []byte(page)) } -func notFoundPageMeta(base, siteName, keywords, path string) *embed_static.SPAPageMeta { - return attachSiteSEO(&embed_static.SPAPageMeta{ +func notFoundPageMeta(base, siteName, keywords, path string) *seo.PageMeta { + return attachSiteSEO(&seo.PageMeta{ Title: pageTitle("页面不存在", siteName), Description: "您访问的页面不存在或已删除", - Canonical: service.AbsoluteURL(base, path), + Canonical: services.AbsoluteURL(base, path), OGType: "website", Robots: "noindex,follow", Status: http.StatusNotFound, @@ -321,7 +334,7 @@ func notFoundPageMeta(base, siteName, keywords, path string) *embed_static.SPAPa } // attachSiteSEO 填充站点级 keywords / og:site_name / og:locale -func attachSiteSEO(meta *embed_static.SPAPageMeta, siteName, keywords string) *embed_static.SPAPageMeta { +func attachSiteSEO(meta *seo.PageMeta, siteName, keywords string) *seo.PageMeta { if meta == nil { return nil } @@ -341,7 +354,7 @@ func isKnownPublicPath(path string) bool { if seoPostEditRe.MatchString(path) { return true } - permalink := service.PermalinkConfig{} + permalink := services.PermalinkConfig{} if permalink.MatchBoardPath(path).OK { return true } @@ -351,15 +364,15 @@ func isKnownPublicPath(path string) bool { return false } -func (h *Handlers) buildSPAPageMeta(c *gin.Context, path string, brand service.SiteBranding, base, siteName, defaultImage string) *embed_static.SPAPageMeta { +func (h *Handlers) buildSPAPageMeta(c *gin.Context, path string, brand services.SiteBranding, base, siteName, defaultImage string) *seo.PageMeta { siteTitle := brand.DocumentTitle() - homeDesc := service.TruncateRunes(brand.MetaDescription(), seoDescMax) + homeDesc := services.TruncateRunes(brand.MetaDescription(), seoDescMax) siteKeywords := brand.MetaKeywords() - meta := attachSiteSEO(&embed_static.SPAPageMeta{ + meta := attachSiteSEO(&seo.PageMeta{ Title: siteTitle, Description: homeDesc, Keywords: siteKeywords, - Canonical: service.AbsoluteURL(base, pathWithQuery(c)), + Canonical: services.AbsoluteURL(base, pathWithQuery(c)), OGType: "website", OGImage: defaultImage, }, siteName, siteKeywords) @@ -379,9 +392,9 @@ func (h *Handlers) buildSPAPageMeta(c *gin.Context, path string, brand service.S desc = brand.MetaDescription() } meta.Title = pageTitle(board.Name, siteName) - meta.Description = service.TruncateRunes(desc, seoDescMax) - meta.Canonical = service.AbsoluteURL(base, service.QueryBoardHome(board.ID, h.Settings.Permalink())) - meta.Keywords = service.JoinSEOKeywords(board.Name, siteKeywords) + meta.Description = services.TruncateRunes(desc, seoDescMax) + meta.Canonical = services.AbsoluteURL(base, services.QueryBoardHome(board.ID, h.Settings.Permalink())) + meta.Keywords = services.JoinSEOKeywords(board.Name, siteKeywords) return meta } // 无效板块 id:仍显示首页,但可标记 noindex @@ -393,38 +406,38 @@ func (h *Handlers) buildSPAPageMeta(c *gin.Context, path string, brand service.S "@type": "WebSite", "name": siteName, "description": meta.Description, - "url": service.AbsoluteURL(base, "/"), + "url": services.AbsoluteURL(base, "/"), }) } if path == "/projects" { meta.Title = pageTitle("项目", siteName) - meta.Description = service.TruncateRunes(siteName+" 的公开项目列表", seoDescMax) - meta.Keywords = service.JoinSEOKeywords("项目", siteKeywords) + meta.Description = services.TruncateRunes(siteName+" 的公开项目列表", seoDescMax) + meta.Keywords = services.JoinSEOKeywords("项目", siteKeywords) } if path == "/links" { meta.Title = pageTitle("友情链接", siteName) - meta.Description = service.TruncateRunes(siteName+" 的友情链接与申请入口", seoDescMax) - meta.Keywords = service.JoinSEOKeywords("友情链接", siteKeywords) + meta.Description = services.TruncateRunes(siteName+" 的友情链接与申请入口", seoDescMax) + meta.Keywords = services.JoinSEOKeywords("友情链接", siteKeywords) } return meta } -func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *model.Post) *embed_static.SPAPageMeta { +func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *models.Post) *seo.PageMeta { permalink := h.Settings.Permalink() - content := service.RedactGatedPostHTML(post.Content) + content := services.RedactGatedPostHTML(post.Content) plain := post.ContentPlain if plain == "" { - plain = service.StripHTMLForSearch(content) + plain = services.StripHTMLForSearch(content) } - desc := service.TruncateRunes(plain, seoDescMax) - author := service.DisplayName(&post.User) - canonical := service.AbsoluteURL(base, permalink.PostPath(post.ID)) - ogImage := service.AbsoluteURL(base, service.FirstImageURL(content)) + desc := services.TruncateRunes(plain, seoDescMax) + author := services.DisplayName(&post.User) + canonical := services.AbsoluteURL(base, permalink.PostPath(post.ID)) + ogImage := services.AbsoluteURL(base, services.FirstImageURL(content)) if ogImage == "" { - ogImage = service.AbsoluteURL(base, post.User.Avatar) + ogImage = services.AbsoluteURL(base, post.User.Avatar) } if ogImage == "" { ogImage = defaultImage @@ -442,7 +455,7 @@ func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *model "author": map[string]any{ "@type": "Person", "name": author, - "url": service.AbsoluteURL(base, permalink.UserPath(post.UserID)), + "url": services.AbsoluteURL(base, permalink.UserPath(post.UserID)), }, "interactionStatistic": map[string]any{ "@type": "InteractionCounter", @@ -456,12 +469,12 @@ func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *model if ogImage != "" { jsonld["image"] = []string{ogImage} } - body := service.TruncateRunes(plain, seoPrerenderMax) + body := services.TruncateRunes(plain, seoPrerenderMax) if body != "" { jsonld["articleBody"] = body } - return &embed_static.SPAPageMeta{ + return &seo.PageMeta{ Title: pageTitle(post.Title, siteName), Description: desc, Canonical: canonical, @@ -471,16 +484,16 @@ func (h *Handlers) postPageMeta(base, siteName, defaultImage string, post *model } } -func (h *Handlers) userPageMeta(base, siteName, defaultImage string, user *model.User) *embed_static.SPAPageMeta { +func (h *Handlers) userPageMeta(base, siteName, defaultImage string, user *models.User) *seo.PageMeta { permalink := h.Settings.Permalink() - name := service.DisplayName(user) + name := services.DisplayName(user) desc := strings.TrimSpace(user.Signature) if desc == "" { desc = name + " 的主页" } - desc = service.TruncateRunes(desc, seoDescMax) - canonical := service.AbsoluteURL(base, permalink.UserPath(user.ID)) - ogImage := service.AbsoluteURL(base, user.Avatar) + desc = services.TruncateRunes(desc, seoDescMax) + canonical := services.AbsoluteURL(base, permalink.UserPath(user.ID)) + ogImage := services.AbsoluteURL(base, user.Avatar) if ogImage == "" { ogImage = defaultImage } @@ -500,7 +513,7 @@ func (h *Handlers) userPageMeta(base, siteName, defaultImage string, user *model jsonld["mainEntity"].(map[string]any)["image"] = ogImage } - return &embed_static.SPAPageMeta{ + return &seo.PageMeta{ Title: pageTitle(name+" 的主页", siteName), Description: desc, Canonical: canonical, @@ -538,13 +551,13 @@ func pathWithQuery(c *gin.Context) string { if path == "" { path = "/" } - permalink := service.PermalinkConfig{} // 由调用方在 buildSPAPageMeta 中单独处理 board + permalink := services.PermalinkConfig{} // 由调用方在 buildSPAPageMeta 中单独处理 board if q := c.Request.URL.RawQuery; q != "" { if path == "/" { board := c.Query("board") if board != "" { _ = permalink - return service.LegacyQueryBoardHome(uint(parseUintOrZero(board))) + return services.LegacyQueryBoardHome(uint(parseUintOrZero(board))) } return "/" } diff --git a/routers/api/seo_bot.go b/routers/api/seo_bot.go index c633136..60c0626 100644 --- a/routers/api/seo_bot.go +++ b/routers/api/seo_bot.go @@ -1,4 +1,4 @@ -package handler +package api import ( "fmt" @@ -6,16 +6,16 @@ import ( "strings" "time" - "git.iioio.com/freefire/jiang13-forum/embed_static" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/modules/seo" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/services" ) // 爬虫专用伪静态 HTML(无 SPA;仅 User-Agent 命中爬虫时返回,避免用户刷新闪屏) -func renderBotHTML(meta *embed_static.SPAPageMeta, bodyInner string) string { +func renderBotHTML(meta *seo.PageMeta, bodyInner string) string { if meta == nil { - meta = &embed_static.SPAPageMeta{} + meta = &seo.PageMeta{} } ogType := strings.TrimSpace(meta.OGType) if ogType == "" { @@ -87,7 +87,7 @@ func writeEscapedMeta(b *strings.Builder, attr, key, content string) { b.WriteString("") } -func (h *Handlers) botBoardHTML(meta *embed_static.SPAPageMeta, board model.Board) string { +func (h *Handlers) botBoardHTML(meta *seo.PageMeta, board models.Board) string { desc := strings.TrimSpace(board.Description) if desc == "" { desc = meta.Description @@ -99,7 +99,7 @@ func (h *Handlers) botBoardHTML(meta *embed_static.SPAPageMeta, board model.Boar return renderBotHTML(meta, body) } -func (h *Handlers) botHomeHTML(meta *embed_static.SPAPageMeta, brand service.SiteBranding) string { +func (h *Handlers) botHomeHTML(meta *seo.PageMeta, brand services.SiteBranding) string { name := strings.TrimSpace(brand.Name) if name == "" { name = "姜十三论坛" @@ -117,10 +117,10 @@ func (h *Handlers) botHomeHTML(meta *embed_static.SPAPageMeta, brand service.Sit return renderBotHTML(meta, body.String()) } -func (h *Handlers) botPostHTML(base, siteName, defaultImage, keywords string, post *model.Post) string { +func (h *Handlers) botPostHTML(base, siteName, defaultImage, keywords string, post *models.Post) string { meta := attachSiteSEO(h.postPageMeta(base, siteName, defaultImage, post), siteName, keywords) - content := service.SanitizePostHTML(service.RedactGatedPostHTML(post.Content)) - author := service.DisplayName(&post.User) + content := services.SanitizePostHTML(services.RedactGatedPostHTML(post.Content)) + author := services.DisplayName(&post.User) var body strings.Builder body.WriteString("
") body.WriteString("

" + html.EscapeString(post.Title) + "

") @@ -138,9 +138,9 @@ func (h *Handlers) botPostHTML(base, siteName, defaultImage, keywords string, po return renderBotHTML(meta, body.String()) } -func (h *Handlers) botUserHTML(base, siteName, defaultImage, keywords string, user *model.User) string { +func (h *Handlers) botUserHTML(base, siteName, defaultImage, keywords string, user *models.User) string { meta := attachSiteSEO(h.userPageMeta(base, siteName, defaultImage, user), siteName, keywords) - name := service.DisplayName(user) + name := services.DisplayName(user) sig := strings.TrimSpace(user.Signature) var body strings.Builder body.WriteString("

" + html.EscapeString(name) + " 的主页

") diff --git a/routers/api/special.go b/routers/api/special.go index e21d56a..f944eb3 100644 --- a/routers/api/special.go +++ b/routers/api/special.go @@ -1,12 +1,12 @@ -package handler +package api import ( "net/http" "strconv" "github.com/gin-gonic/gin" - "git.iioio.com/freefire/jiang13-forum/model" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/models" + "git.iioio.com/freefire/jiang13-forum/services" ) // APIPages 已发布单页摘要列表 @@ -17,7 +17,7 @@ func (h *Handlers) APIPages(c *gin.Context) { return } if pages == nil { - pages = []service.SitePageSummary{} + pages = []services.SitePageSummary{} } c.JSON(http.StatusOK, gin.H{"pages": pages}) } @@ -57,14 +57,14 @@ func (h *Handlers) APIAdminPages(c *gin.Context) { return } if pages == nil { - pages = []model.SitePage{} + pages = []models.SitePage{} } c.JSON(http.StatusOK, gin.H{"pages": pages}) } // APIAdminCreatePage 创建单页 func (h *Handlers) APIAdminCreatePage(c *gin.Context) { - var in service.SitePageInput + var in services.SitePageInput if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"}) return @@ -80,7 +80,7 @@ func (h *Handlers) APIAdminCreatePage(c *gin.Context) { // APIAdminUpdatePage 更新单页 func (h *Handlers) APIAdminUpdatePage(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - var in service.SitePageInput + var in services.SitePageInput if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"}) return @@ -137,11 +137,11 @@ func (h *Handlers) APIPollVote(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式无效"}) return } - if err := service.VotePoll(uint(id), h.currentUserID(c), body.OptionIDs); err != nil { + if err := services.VotePoll(uint(id), h.currentUserID(c), body.OptionIDs); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - poll, _ := service.GetPollView(uint(id), h.currentUserID(c)) + poll, _ := services.GetPollView(uint(id), h.currentUserID(c)) c.JSON(http.StatusOK, gin.H{"message": "投票成功", "poll": poll}) } @@ -153,11 +153,11 @@ func (h *Handlers) APIPollClose(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"}) return } - if err := service.ClosePoll(uint(id), h.currentUserID(c), h.isAdmin(c), post.UserID); err != nil { + if err := services.ClosePoll(uint(id), h.currentUserID(c), h.isAdmin(c), post.UserID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - poll, _ := service.GetPollView(uint(id), h.currentUserID(c)) + poll, _ := services.GetPollView(uint(id), h.currentUserID(c)) c.JSON(http.StatusOK, gin.H{"message": "投票已结束", "poll": poll}) } @@ -169,7 +169,7 @@ func (h *Handlers) APIBountyAward(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择评论"}) return } - if err := service.AwardBounty(uint(id), h.currentUserID(c), h.isAdmin(c), uint(commentID)); err != nil { + if err := services.AwardBounty(uint(id), h.currentUserID(c), h.isAdmin(c), uint(commentID)); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -179,7 +179,7 @@ func (h *Handlers) APIBountyAward(c *gin.Context) { // APIBountyRefund 退回悬赏 func (h *Handlers) APIBountyRefund(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if err := service.RefundBounty(uint(id), h.currentUserID(c), h.isAdmin(c)); err != nil { + if err := services.RefundBounty(uint(id), h.currentUserID(c), h.isAdmin(c)); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -189,7 +189,7 @@ func (h *Handlers) APIBountyRefund(c *gin.Context) { // APILotteryDraw 帖内抽奖开奖 func (h *Handlers) APILotteryDraw(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - view, err := service.DrawPostLottery(uint(id), h.currentUserID(c), h.isAdmin(c)) + view, err := services.DrawPostLottery(uint(id), h.currentUserID(c), h.isAdmin(c)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return diff --git a/routers/api/thumb.go b/routers/api/thumb.go index e7c349f..98b0b70 100644 --- a/routers/api/thumb.go +++ b/routers/api/thumb.go @@ -1,4 +1,4 @@ -package handler +package api import ( "net/http" @@ -7,7 +7,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/services" ) // ServeImageThumb 帖子图片缩略图(按需生成并缓存) @@ -15,7 +15,7 @@ import ( func (h *Handlers) ServeImageThumb(c *gin.Context) { rel := strings.TrimPrefix(c.Param("filepath"), "/") uploadsRoot := filepath.Join(h.Cfg.DataDir, "uploads") - thumbPath, err := service.EnsureUploadThumb(uploadsRoot, rel) + thumbPath, err := services.EnsureUploadThumb(uploadsRoot, rel) if err != nil { // 生成失败时回退原图,避免正文裂图 orig := filepath.Join(uploadsRoot, filepath.FromSlash(rel)) diff --git a/routers/setup.go b/routers/setup.go index e939d9a..ad0f991 100644 --- a/routers/setup.go +++ b/routers/setup.go @@ -1,7 +1,6 @@ -package router +package routers import ( - "encoding/json" "fmt" "io/fs" "net/http" @@ -9,12 +8,11 @@ import ( "path/filepath" "git.iioio.com/freefire/jiang13-forum/config" - "git.iioio.com/freefire/jiang13-forum/embed_static" - "git.iioio.com/freefire/jiang13-forum/handler" - "git.iioio.com/freefire/jiang13-forum/middleware" + "git.iioio.com/freefire/jiang13-forum/modules/auth" webpublic "git.iioio.com/freefire/jiang13-forum/public" + "git.iioio.com/freefire/jiang13-forum/routers/api" webpages "git.iioio.com/freefire/jiang13-forum/routers/web" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/services" "github.com/gin-gonic/gin" ) @@ -24,7 +22,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { r.Use(gin.Recovery()) r.Use(gin.Logger()) - // SSR 静态资源(独立前缀,避免与 SPA /assets/* 冲突) + // SSR 静态资源 if sub, err := fs.Sub(webpublic.Assets, "assets"); err == nil { ssrFiles := http.StripPrefix("/ssr-assets", http.FileServer(http.FS(sub))) r.GET("/ssr-assets/*filepath", func(c *gin.Context) { @@ -32,59 +30,42 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { ssrFiles.ServeHTTP(c.Writer, c.Request) }) } - - // 生产:内嵌 React SPA(未迁移路径仍走 SPA);dev:Vite 可对照旧前台 - if !cfg.DevMode { - if err := embed_static.SetupEmbed(r); err != nil { - return nil, err - } - } else { - fmt.Fprintf(os.Stderr, "[dev] SSR 页面请访问 http://localhost:3000 ;旧 SPA 对照可用 Vite :5173\n") + if cfg.DevMode { + fmt.Fprintf(os.Stderr, "[dev] SSR 请访问 http://localhost:%d (对照 SPA 请 checkout main)\n", cfg.Port) } - filter := service.NewSensitiveFilter() - _ = service.WriteDefaultFilterWords(cfg.FilterWordsPath()) + filter := services.NewSensitiveFilter() + _ = services.WriteDefaultFilterWords(cfg.FilterWordsPath()) filter.LoadFromFile(cfg.FilterWordsPath()) - settingsSvc := service.NewForumSettingsService() - // SPA 入口 HTML 注入标题与品牌 JSON,避免刷新时先闪默认文案 - embed_static.SetSPADocumentTitle(func() string { - return settingsSvc.SiteBranding().DocumentTitle() - }) - embed_static.SetSPABrandingJSON(func() []byte { - b, err := json.Marshal(settingsSvc.SiteBranding()) - if err != nil { - return nil - } - return b - }) - authSvc := service.NewAuthService(cfg.JWTSecret, filter, settingsSvc) - userSvc := service.NewUserService(filter, settingsSvc) - boardSvc := service.NewBoardService() + settingsSvc := services.NewForumSettingsService() + authSvc := services.NewAuthService(cfg.JWTSecret, filter, settingsSvc) + userSvc := services.NewUserService(filter, settingsSvc) + boardSvc := services.NewBoardService() boardSvc.EnsureDefaultBoard() - postSvc := service.NewPostService(filter, settingsSvc) - commentSvc := service.NewCommentService(filter, settingsSvc) - messageSvc := service.NewMessageService(filter, settingsSvc) - reportSvc := service.NewReportService(filter, settingsSvc, messageSvc, postSvc, commentSvc) - backupSvc := service.NewBackupService(cfg.DBPath(), cfg.DataDir) - limiter := service.NewRateLimiter(settingsSvc) - captchaSvc := service.NewCaptchaService() - mailSvc := service.NewMailService(settingsSvc) - emailCodeSvc := service.NewEmailCodeService(mailSvc) - notifySvc := service.NewNotifyService(messageSvc, mailSvc, settingsSvc) - friendLinkApplySvc := service.NewFriendLinkApplyService(settingsSvc, messageSvc) - oidcSvc, err := service.NewOIDCService(cfg, settingsSvc) + postSvc := services.NewPostService(filter, settingsSvc) + commentSvc := services.NewCommentService(filter, settingsSvc) + messageSvc := services.NewMessageService(filter, settingsSvc) + reportSvc := services.NewReportService(filter, settingsSvc, messageSvc, postSvc, commentSvc) + backupSvc := services.NewBackupService(cfg.DBPath(), cfg.DataDir) + limiter := services.NewRateLimiter(settingsSvc) + captchaSvc := services.NewCaptchaService() + mailSvc := services.NewMailService(settingsSvc) + emailCodeSvc := services.NewEmailCodeService(mailSvc) + notifySvc := services.NewNotifyService(messageSvc, mailSvc, settingsSvc) + friendLinkApplySvc := services.NewFriendLinkApplyService(settingsSvc, messageSvc) + oidcSvc, err := services.NewOIDCService(cfg, settingsSvc) if err != nil { return nil, err } - giteaSvc := service.NewGiteaService(settingsSvc) + giteaSvc := services.NewGiteaService(settingsSvc) giteaSvc.StartBackgroundSync() - uploadStore := service.NewUploadStore(cfg.DataDir, settingsSvc) + uploadStore := services.NewUploadStore(cfg.DataDir, settingsSvc) if err := uploadStore.ReloadFromSettings(settingsSvc); err != nil { // 配置不完整时保持本地磁盘,避免进程无法启动;管理员可在后台修正后热切换 fmt.Fprintf(os.Stderr, "警告: 对象存储初始化失败,暂用本地磁盘: %v\n", err) - _ = uploadStore.Apply(service.StorageConfig{Type: "local"}) + _ = uploadStore.Apply(services.StorageConfig{Type: "local"}) } // 后台同步存量文件到媒体索引,避免列表依赖实时扫盘 go func() { @@ -95,18 +76,18 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { } }() - h := &handler.Handlers{ + h := &api.Handlers{ Cfg: cfg, Store: uploadStore, Auth: authSvc, User: userSvc, Board: boardSvc, Post: postSvc, Comment: commentSvc, Message: messageSvc, Notify: notifySvc, Report: reportSvc, Backup: backupSvc, Filter: filter, Limiter: limiter, Settings: settingsSvc, Captcha: captchaSvc, Mail: mailSvc, EmailCode: emailCodeSvc, OIDC: oidcSvc, Gitea: giteaSvc, - Points: service.NewPointsService(), Badge: service.NewBadgeService(), - SitePage: service.NewSitePageService(filter), + Points: services.NewPointsService(), Badge: services.NewBadgeService(), + SitePage: services.NewSitePageService(filter), FriendLinkApply: friendLinkApplySvc, } - authMW := middleware.NewAuthMiddleware(authSvc) + authMW := auth.NewAuthMiddleware(authSvc) // Gitea 式 SSR 公开页(优先于 SPA) webpages.Register(r, webpages.Deps{ @@ -148,9 +129,9 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { pubAPI.GET("/pages/:slug", h.APIPageDetail) pubAPI.GET("/captcha", h.APICaptcha) pubAPI.GET("/register/config", h.APIRegisterConfig) - pubAPI.POST("/register/email-code", middleware.RateLimitMiddleware(limiter, "register"), h.APISendRegisterEmailCode) - pubAPI.POST("/password-reset/email-code", middleware.RateLimitMiddleware(limiter, "register"), h.APISendResetEmailCode) - pubAPI.POST("/password-reset", middleware.RateLimitMiddleware(limiter, "login"), h.APIResetPassword) + pubAPI.POST("/register/email-code", auth.RateLimitMiddleware(limiter, "register"), h.APISendRegisterEmailCode) + pubAPI.POST("/password-reset/email-code", auth.RateLimitMiddleware(limiter, "register"), h.APISendResetEmailCode) + pubAPI.POST("/password-reset", auth.RateLimitMiddleware(limiter, "login"), h.APIResetPassword) pubAPI.GET("/posts", h.APIPosts) pubAPI.GET("/posts/hot", h.APIHotPosts) pubAPI.GET("/tags", h.APITags) @@ -161,10 +142,10 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { pubAPI.GET("/users/:id", h.APIUserPublic) pubAPI.GET("/posts/:id", h.APIPostDetail) pubAPI.GET("/posts/:id/comments", h.APIPostComments) - pubAPI.POST("/posts/:id/comments", middleware.RateLimitMiddleware(limiter, "comment"), h.APICreateComment) + pubAPI.POST("/posts/:id/comments", auth.RateLimitMiddleware(limiter, "comment"), h.APICreateComment) pubAPI.GET("/projects", h.APIProjects) - pubAPI.POST("/register", middleware.RateLimitMiddleware(limiter, "register"), h.APIRegister) - pubAPI.POST("/login", middleware.RateLimitMiddleware(limiter, "login"), h.APILogin) + pubAPI.POST("/register", auth.RateLimitMiddleware(limiter, "register"), h.APIRegister) + pubAPI.POST("/login", auth.RateLimitMiddleware(limiter, "login"), h.APILogin) } // 需登录 API @@ -178,7 +159,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { api.POST("/profile/password", h.APIUpdatePassword) api.POST("/profile/avatar", h.APIUploadAvatar) api.POST("/uploads/image", h.APIUploadPostImage) - api.POST("/posts", middleware.RateLimitMiddleware(limiter, "post"), h.APICreatePost) + api.POST("/posts", auth.RateLimitMiddleware(limiter, "post"), h.APICreatePost) api.PUT("/posts/:id", h.APIUpdatePost) api.DELETE("/posts/:id", h.APIDeletePost) api.GET("/posts/:id/revisions", h.APIPostRevisions) @@ -191,17 +172,17 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { api.POST("/posts/:id/bounty/award", h.APIBountyAward) api.POST("/posts/:id/bounty/refund", h.APIBountyRefund) api.POST("/posts/:id/lottery/draw", h.APILotteryDraw) - api.POST("/posts/:id/report", middleware.RateLimitMiddleware(limiter, "report"), h.APICreatePostReport) + api.POST("/posts/:id/report", auth.RateLimitMiddleware(limiter, "report"), h.APICreatePostReport) api.GET("/messages/unread-count", h.APIMessageUnreadCount) api.GET("/messages/notifications", h.APIMessageNotifications) api.POST("/messages/notifications/read", h.APIMarkNotificationsRead) api.GET("/messages/conversations", h.APIMessageConversations) api.GET("/messages/conversations/:peerId", h.APIConversationMessages) api.POST("/messages/conversations/:peerId/read", h.APIMarkConversationRead) - api.POST("/messages", middleware.RateLimitMiddleware(limiter, "message"), h.APISendMessage) + api.POST("/messages", auth.RateLimitMiddleware(limiter, "message"), h.APISendMessage) api.POST("/messages/read-all", h.APIMarkAllMessagesRead) api.POST("/comments/:id/like", h.APIToggleCommentLike) - api.POST("/comments/:id/report", middleware.RateLimitMiddleware(limiter, "report"), h.APICreateCommentReport) + api.POST("/comments/:id/report", auth.RateLimitMiddleware(limiter, "report"), h.APICreateCommentReport) api.DELETE("/comments/:id", h.APIDeleteComment) api.PUT("/comments/:id", h.APIUpdateComment) api.GET("/me/points", h.APIMePoints) @@ -209,11 +190,11 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { api.POST("/me/check-in", h.APIMeCheckIn) api.GET("/me/lottery", h.APIMeLotteryGet) api.POST("/me/lottery", h.APIMeLotteryDraw) - api.POST("/posts/:id/unlock", middleware.RateLimitMiddleware(limiter, "post"), h.APIUnlockPostBlock) - api.POST("/friend-links/apply", middleware.RateLimitMiddleware(limiter, "friend_link"), h.APIApplyFriendLink) - api.POST("/friend-links/logo", middleware.RateLimitMiddleware(limiter, "post"), h.APIUploadFriendLinkLogo) + api.POST("/posts/:id/unlock", auth.RateLimitMiddleware(limiter, "post"), h.APIUnlockPostBlock) + api.POST("/friend-links/apply", auth.RateLimitMiddleware(limiter, "friend_link"), h.APIApplyFriendLink) + api.POST("/friend-links/logo", auth.RateLimitMiddleware(limiter, "post"), h.APIUploadFriendLinkLogo) api.GET("/friend-links/my-applies", h.APIMyFriendLinkApplies) - api.PUT("/friend-links/applies/:id", middleware.RateLimitMiddleware(limiter, "friend_link"), h.APIUpdateFriendLinkApply) + api.PUT("/friend-links/applies/:id", auth.RateLimitMiddleware(limiter, "friend_link"), h.APIUpdateFriendLinkApply) api.DELETE("/friend-links/applies/:id", h.APICancelFriendLinkApply) } @@ -288,41 +269,27 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { adminAPI.GET("/backup/download/:name", h.APIAdminDownloadBackup) } - // 后台管理页面由 React SPA 渲染(JSON API 见上方 /api/admin) - // dev 模式下前端由 Vite 提供,后台页面路由不在此注册 - if !cfg.DevMode { - admin := r.Group("/admin") + // 管理后台 HTML:SSR 尚未迁移;勿用 /*filepath(与 /admin/login 冲突) + adminPendingHTML := `管理后台

管理后台 SSR 迁移中

API 仍可用;UI 请暂时对照 main 分支 SPA,或等待后续模板页。

返回首页

` + adminPending := func(c *gin.Context) { + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, adminPendingHTML) + } + admin := r.Group("/admin") + { + admin.GET("/login", func(c *gin.Context) { + c.Redirect(http.StatusFound, "/login") + }) + adminAuth := admin.Group("/", authMW.RequireAuth(), authMW.RequireAdmin()) { - admin.GET("/login", func(c *gin.Context) { - c.Redirect(http.StatusFound, "/login") - }) - - adminAuth := admin.Group("/", authMW.RequireAuth(), authMW.RequireAdmin()) - { - adminAuth.GET("/", func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/dashboard") }) - for _, page := range []string{"dashboard", "boards", "pages", "links", "posts", "comments", "reports", "users", "badges", "media", "settings"} { - adminAuth.GET("/"+page, embed_static.ServeSPANoIndex) - } - } + adminAuth.GET("/", func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/dashboard") }) + adminAuth.GET("/dashboard", adminPending) + adminAuth.GET("/:page", adminPending) } } - // 未迁移路径:生产仍回落 React SPA;/ 与 /board/:id 已由 routers/web 接管 - if cfg.DevMode { - r.NoRoute(func(c *gin.Context) { - c.JSON(http.StatusNotFound, gin.H{ - "error": "页面未找到。SSR 首页请打开 / ;旧 SPA 请用 Vite http://localhost:5173", - }) - }) - } else { - r.NoRoute(func(c *gin.Context) { - if embed_static.IsSPARoute(c.Request.URL.Path) { - h.ServePublicSPA(c) - return - } - c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) - }) - } + // 未迁移公开路径:爬虫可读 HTML / 用户占位(首页与板块已由 routers/web 接管) + r.NoRoute(h.ServePublicSPA) return r, nil } diff --git a/routers/web/home.go b/routers/web/home.go index 8abd064..afe8f37 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -1,4 +1,4 @@ -package web +package web import ( "net/http" @@ -6,18 +6,18 @@ import ( "strings" "time" - "git.iioio.com/freefire/jiang13-forum/middleware" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/modules/auth" + "git.iioio.com/freefire/jiang13-forum/models" "git.iioio.com/freefire/jiang13-forum/modules/webrender" - "git.iioio.com/freefire/jiang13-forum/service" + "git.iioio.com/freefire/jiang13-forum/services" "github.com/gin-gonic/gin" ) // Deps 页面路由依赖(复用现有 service,避免 Phase 1 大搬家) type Deps struct { - Settings *service.ForumSettingsService - Board *service.BoardService - Post *service.PostService + Settings *services.ForumSettingsService + Board *services.BoardService + Post *services.PostService } // BoardView 侧栏板块 @@ -61,7 +61,7 @@ type HomePageData struct { } // Register 注册已迁移的 SSR 页面(优先于 SPA) -func Register(r *gin.Engine, deps Deps, authMW *middleware.AuthMiddleware) { +func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) { g := r.Group("/", authMW.OptionalAuth()) g.GET("/", deps.Home) g.GET("/board/:id", deps.Home) @@ -99,21 +99,21 @@ func (d Deps) Home(c *gin.Context) { } var uid uint - if v, ok := c.Get(middleware.CtxUserID); ok { + if v, ok := c.Get(auth.CtxUserID); ok { uid, _ = v.(uint) } isAdmin := false - if v, ok := c.Get(middleware.CtxRole); ok { + if v, ok := c.Get(auth.CtxRole); ok { switch r := v.(type) { - case model.Role: - isAdmin = r == model.RoleAdmin + case models.Role: + isAdmin = r == models.RoleAdmin case string: - isAdmin = r == string(model.RoleAdmin) + isAdmin = r == string(models.RoleAdmin) } } - username, _ := c.Get(middleware.CtxUsername) + username, _ := c.Get(auth.CtxUsername) - q := service.PostListQuery{ + q := services.PostListQuery{ BoardID: boardID, Page: page, Size: size, diff --git a/services/aside_widgets_test.go b/services/aside_widgets_test.go index 996cba5..fb56ced 100644 --- a/services/aside_widgets_test.go +++ b/services/aside_widgets_test.go @@ -1,4 +1,4 @@ -package service +package services import "testing" diff --git a/services/auth.go b/services/auth.go index a57c286..c45733c 100644 --- a/services/auth.go +++ b/services/auth.go @@ -1,4 +1,4 @@ -package service +package services import ( "errors" @@ -6,7 +6,7 @@ import ( "time" "github.com/golang-jwt/jwt/v5" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) // 最近访问写入节流,避免每次 API 都打库 @@ -19,7 +19,7 @@ const TokenExpire = 7 * 24 * time.Hour type Claims struct { UserID uint `json:"user_id"` Username string `json:"username"` - Role model.Role `json:"role"` + Role models.Role `json:"role"` jwt.RegisteredClaims } @@ -36,12 +36,12 @@ func NewAuthService(jwtSecret string, filter *SensitiveFilter, settings *ForumSe // UserCount 当前用户数 func (s *AuthService) UserCount() int64 { var n int64 - model.DB.Model(&model.User{}).Count(&n) + models.DB.Model(&models.User{}).Count(&n) return n } // Register 用户注册 -func (s *AuthService) Register(username, password, nickname, email string) (*model.User, error) { +func (s *AuthService) Register(username, password, nickname, email string) (*models.User, error) { if err := ValidateUsername(username); err != nil { return nil, err } @@ -53,11 +53,11 @@ func (s *AuthService) Register(username, password, nickname, email string) (*mod return nil, err } - var exist model.User - if err := model.DB.Where("username = ?", username).First(&exist).Error; err == nil { + var exist models.User + if err := models.DB.Where("username = ?", username).First(&exist).Error; err == nil { return nil, ErrUserExists } - if err := model.DB.Where("email = ?", email).First(&exist).Error; err == nil { + if err := models.DB.Where("email = ?", email).First(&exist).Error; err == nil { return nil, ErrEmailExists } @@ -71,28 +71,28 @@ func (s *AuthService) Register(username, password, nickname, email string) (*mod nickname = s.filter.Filter(nickname) // 首个注册用户自动成为管理员 - role := model.RoleUser + role := models.RoleUser if s.UserCount() == 0 { - role = model.RoleAdmin + role = models.RoleAdmin } - user := &model.User{ + user := &models.User{ Username: username, Email: email, Password: hash, Nickname: nickname, Role: role, } - if err := model.DB.Create(user).Error; err != nil { + if err := models.DB.Create(user).Error; err != nil { return nil, err } return user, nil } // Login 用户登录,返回 JWT token;clientIP 写入上次登录记录 -func (s *AuthService) Login(username, password, clientIP string) (string, *model.User, error) { - var user model.User - if err := model.DB.Where("username = ?", username).First(&user).Error; err != nil { +func (s *AuthService) Login(username, password, clientIP string) (string, *models.User, error) { + var user models.User + if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil { return "", nil, ErrInvalidCred } if user.Banned { @@ -107,13 +107,13 @@ func (s *AuthService) Login(username, password, clientIP string) (string, *model } // recordLogin 记录上次登录时间与 IP;登录同时视为一次访问(失败不影响登录) -func (s *AuthService) recordLogin(user *model.User, clientIP string) { +func (s *AuthService) recordLogin(user *models.User, clientIP string) { now := time.Now() ip := clientIP if len(ip) > 45 { ip = ip[:45] } - _ = model.DB.Model(user).Updates(map[string]interface{}{ + _ = models.DB.Model(user).Updates(map[string]interface{}{ "last_login_at": now, "last_login_ip": ip, "last_access_at": now, @@ -136,11 +136,11 @@ func (s *AuthService) TouchLastAccess(userID uint) { } } lastAccessTouchCache.Store(userID, now) - _ = model.DB.Model(&model.User{}).Where("id = ?", userID).Update("last_access_at", now).Error + _ = models.DB.Model(&models.User{}).Where("id = ?", userID).Update("last_access_at", now).Error } // GenerateToken 生成 JWT -func (s *AuthService) GenerateToken(user *model.User) (string, error) { +func (s *AuthService) GenerateToken(user *models.User) (string, error) { claims := Claims{ UserID: user.ID, Username: user.Username, diff --git a/services/backup.go b/services/backup.go index 527df60..d02ee21 100644 --- a/services/backup.go +++ b/services/backup.go @@ -1,4 +1,4 @@ -package service +package services import ( "fmt" diff --git a/services/badge.go b/services/badge.go index 7af58b1..c61bcb1 100644 --- a/services/badge.go +++ b/services/badge.go @@ -1,10 +1,10 @@ -package service +package services import ( "errors" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -14,34 +14,34 @@ type BadgeService struct{} func NewBadgeService() *BadgeService { return &BadgeService{} } // ListDefs 列出徽章定义 -func (s *BadgeService) ListDefs(includeDisabled bool) ([]model.BadgeDef, error) { - q := model.DB.Order("sort_order asc, id asc") +func (s *BadgeService) ListDefs(includeDisabled bool) ([]models.BadgeDef, error) { + q := models.DB.Order("sort_order asc, id asc") if !includeDisabled { q = q.Where("enabled = ?", true) } - var rows []model.BadgeDef + var rows []models.BadgeDef err := q.Find(&rows).Error return rows, err } // UpsertDef 创建或更新徽章定义(按 code) -func (s *BadgeService) UpsertDef(def *model.BadgeDef) error { +func (s *BadgeService) UpsertDef(def *models.BadgeDef) error { if def.Code == "" || def.Name == "" { return errors.New("徽章代码与名称不能为空") } - if def.Kind != model.BadgeKindAuto && def.Kind != model.BadgeKindLimited { + if def.Kind != models.BadgeKindAuto && def.Kind != models.BadgeKindLimited { return errors.New("无效的徽章类型") } - var existing model.BadgeDef - err := model.DB.Where("code = ?", def.Code).Limit(1).Find(&existing).Error + var existing models.BadgeDef + err := models.DB.Where("code = ?", def.Code).Limit(1).Find(&existing).Error if err != nil { return err } if existing.ID == 0 { - return model.DB.Create(def).Error + return models.DB.Create(def).Error } def.ID = existing.ID - return model.DB.Model(&existing).Updates(map[string]interface{}{ + return models.DB.Model(&existing).Updates(map[string]interface{}{ "name": def.Name, "description": def.Description, "icon": def.Icon, @@ -55,22 +55,22 @@ func (s *BadgeService) UpsertDef(def *model.BadgeDef) error { // AwardLimited 站长颁发限定徽章 func (s *BadgeService) AwardLimited(userID, badgeID, adminID uint) error { - var def model.BadgeDef - if err := model.DB.First(&def, badgeID).Error; err != nil { + var def models.BadgeDef + if err := models.DB.First(&def, badgeID).Error; err != nil { return errors.New("徽章不存在") } - if def.Kind != model.BadgeKindLimited { + if def.Kind != models.BadgeKindLimited { return errors.New("仅可颁发限定徽章") } if !def.Enabled { return errors.New("徽章已停用") } var n int64 - model.DB.Model(&model.UserBadge{}).Where("user_id = ? AND badge_id = ?", userID, badgeID).Count(&n) + models.DB.Model(&models.UserBadge{}).Where("user_id = ? AND badge_id = ?", userID, badgeID).Count(&n) if n > 0 { return errors.New("用户已拥有该徽章") } - return model.DB.Create(&model.UserBadge{ + return models.DB.Create(&models.UserBadge{ UserID: userID, BadgeID: badgeID, AwardedAt: time.Now(), @@ -80,7 +80,7 @@ func (s *BadgeService) AwardLimited(userID, badgeID, adminID uint) error { // Revoke 收回徽章 func (s *BadgeService) Revoke(userID, badgeID uint) error { - res := model.DB.Where("user_id = ? AND badge_id = ?", userID, badgeID).Delete(&model.UserBadge{}) + res := models.DB.Where("user_id = ? AND badge_id = ?", userID, badgeID).Delete(&models.UserBadge{}) if res.Error != nil { return res.Error } @@ -91,21 +91,21 @@ func (s *BadgeService) Revoke(userID, badgeID uint) error { } // ListUserBadges 用户已获徽章(含定义) -func (s *BadgeService) ListUserBadges(userID uint) ([]model.UserBadge, error) { - var rows []model.UserBadge - err := model.DB.Preload("Badge").Where("user_id = ?", userID). +func (s *BadgeService) ListUserBadges(userID uint) ([]models.UserBadge, error) { + var rows []models.UserBadge + err := models.DB.Preload("Badge").Where("user_id = ?", userID). Order("awarded_at desc").Find(&rows).Error return rows, err } // BadgeViews 转为展示视图(最多 limit 枚,0=全部) -func BadgeViews(rows []model.UserBadge, limit int) []model.UserBadgeView { - out := make([]model.UserBadgeView, 0, len(rows)) +func BadgeViews(rows []models.UserBadge, limit int) []models.UserBadgeView { + out := make([]models.UserBadgeView, 0, len(rows)) for _, r := range rows { if r.Badge.ID == 0 || !r.Badge.Enabled { continue } - out = append(out, model.UserBadgeView{ + out = append(out, models.UserBadgeView{ Code: r.Badge.Code, Name: r.Badge.Name, Description: r.Badge.Description, @@ -121,25 +121,25 @@ func BadgeViews(rows []model.UserBadge, limit int) []model.UserBadgeView { // EvaluateAuto 检查并授予符合条件的自动徽章 func (s *BadgeService) EvaluateAuto(userID uint) error { - var user model.User - if err := model.DB.First(&user, userID).Error; err != nil { + var user models.User + if err := models.DB.First(&user, userID).Error; err != nil { return err } - var defs []model.BadgeDef - if err := model.DB.Where("kind = ? AND enabled = ?", model.BadgeKindAuto, true).Find(&defs).Error; err != nil { + var defs []models.BadgeDef + if err := models.DB.Where("kind = ? AND enabled = ?", models.BadgeKindAuto, true).Find(&defs).Error; err != nil { return err } tenureDays := int(time.Since(user.CreatedAt).Hours() / 24) var likes int64 - _ = model.DB.Model(&model.Post{}). + _ = models.DB.Model(&models.Post{}). Select("COALESCE(SUM(like_count), 0)"). - Where("user_id = ? AND status = ?", userID, model.ContentStatusPublished). + Where("user_id = ? AND status = ?", userID, models.ContentStatusPublished). Scan(&likes).Error income := user.CreatorIncomeTotal owned := map[uint]bool{} - var existing []model.UserBadge - _ = model.DB.Where("user_id = ?", userID).Find(&existing).Error + var existing []models.UserBadge + _ = models.DB.Where("user_id = ?", userID).Find(&existing).Error for _, e := range existing { owned[e.BadgeID] = true } @@ -150,17 +150,17 @@ func (s *BadgeService) EvaluateAuto(userID uint) error { } ok := false switch d.Metric { - case model.BadgeMetricTenureDays: + case models.BadgeMetricTenureDays: ok = tenureDays >= d.Threshold - case model.BadgeMetricLikesReceived: + case models.BadgeMetricLikesReceived: ok = int(likes) >= d.Threshold - case model.BadgeMetricCreatorIncome: + case models.BadgeMetricCreatorIncome: ok = income >= d.Threshold } if !ok { continue } - _ = model.DB.Create(&model.UserBadge{ + _ = models.DB.Create(&models.UserBadge{ UserID: userID, BadgeID: d.ID, AwardedAt: time.Now(), @@ -171,7 +171,7 @@ func (s *BadgeService) EvaluateAuto(userID uint) error { } // AttachBadgeSummaries 批量为用户填充展示用徽章(最多 perUser 枚) -func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) { +func (s *BadgeService) AttachBadgeSummaries(users []*models.User, perUser int) { if len(users) == 0 { return } @@ -184,7 +184,7 @@ func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) { if u == nil || u.ID == 0 { continue } - u.Level = model.LevelFromExp(u.Exp) + u.Level = models.LevelFromExp(u.Exp) if !seen[u.ID] { seen[u.ID] = true ids = append(ids, u.ID) @@ -193,10 +193,10 @@ func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) { if len(ids) == 0 { return } - var rows []model.UserBadge - _ = model.DB.Preload("Badge").Where("user_id IN ?", ids). + var rows []models.UserBadge + _ = models.DB.Preload("Badge").Where("user_id IN ?", ids). Order("awarded_at desc").Find(&rows).Error - grouped := map[uint][]model.UserBadgeView{} + grouped := map[uint][]models.UserBadgeView{} for _, r := range rows { if r.Badge.ID == 0 || !r.Badge.Enabled { continue @@ -205,7 +205,7 @@ func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) { if len(list) >= perUser { continue } - list = append(list, model.UserBadgeView{ + list = append(list, models.UserBadgeView{ Code: r.Badge.Code, Name: r.Badge.Name, Description: r.Badge.Description, @@ -223,8 +223,8 @@ func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) { } // AttachBadgeSummariesOnPosts 给帖子作者填充徽章摘要 -func (s *BadgeService) AttachBadgeSummariesOnPosts(posts []model.Post, perUser int) { - users := make([]*model.User, 0, len(posts)) +func (s *BadgeService) AttachBadgeSummariesOnPosts(posts []models.Post, perUser int) { + users := make([]*models.User, 0, len(posts)) for i := range posts { if posts[i].User.ID > 0 { users = append(users, &posts[i].User) @@ -234,8 +234,8 @@ func (s *BadgeService) AttachBadgeSummariesOnPosts(posts []model.Post, perUser i } // AttachBadgeSummariesOnComments 给评论作者填充徽章摘要 -func (s *BadgeService) AttachBadgeSummariesOnComments(comments []model.Comment, perUser int) { - users := make([]*model.User, 0, len(comments)) +func (s *BadgeService) AttachBadgeSummariesOnComments(comments []models.Comment, perUser int) { + users := make([]*models.User, 0, len(comments)) for i := range comments { if comments[i].User.ID > 0 { users = append(users, &comments[i].User) @@ -249,24 +249,24 @@ func AddExp(userID uint, delta int) { if userID == 0 || delta <= 0 { return } - _ = model.DB.Model(&model.User{}).Where("id = ?", userID). + _ = models.DB.Model(&models.User{}).Where("id = ?", userID). UpdateColumn("exp", gorm.Expr("exp + ?", delta)).Error } // SetUserLevel 站长设等级(调整 Exp 到门槛) func SetUserLevel(userID uint, level int) error { - if level < 1 || level > model.MaxLevel() { + if level < 1 || level > models.MaxLevel() { return errors.New("等级须在 1–10") } - exp := model.ExpForLevel(level) - return model.DB.Model(&model.User{}).Where("id = ?", userID).Update("exp", exp).Error + exp := models.ExpForLevel(level) + return models.DB.Model(&models.User{}).Where("id = ?", userID).Update("exp", exp).Error } // SetVerified 设置认证 func SetVerified(userID uint, verified bool) error { - var user model.User - if err := model.DB.First(&user, userID).Error; err != nil { + var user models.User + if err := models.DB.First(&user, userID).Error; err != nil { return errors.New("用户不存在") } - return model.DB.Model(&user).Update("verified", verified).Error + return models.DB.Model(&user).Update("verified", verified).Error } diff --git a/services/board.go b/services/board.go index 127f979..07a197c 100644 --- a/services/board.go +++ b/services/board.go @@ -1,9 +1,9 @@ -package service +package services import ( "errors" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) type BoardService struct{} @@ -14,13 +14,13 @@ func NewBoardService() *BoardService { // BoardWithStats 板块及帖子数量 type BoardWithStats struct { - model.Board + models.Board PostCount int `json:"post_count"` } -func (s *BoardService) List() ([]model.Board, error) { - var boards []model.Board - err := model.DB.Order("sort_order asc, id asc").Find(&boards).Error +func (s *BoardService) List() ([]models.Board, error) { + var boards []models.Board + err := models.DB.Order("sort_order asc, id asc").Find(&boards).Error return boards, err } @@ -32,34 +32,34 @@ func (s *BoardService) ListWithStats() ([]BoardWithStats, error) { result := make([]BoardWithStats, len(boards)) for i, b := range boards { var count int64 - model.DB.Model(&model.Post{}). - Where("board_id = ? AND status = ?", b.ID, model.ContentStatusPublished). + models.DB.Model(&models.Post{}). + Where("board_id = ? AND status = ?", b.ID, models.ContentStatusPublished). Count(&count) result[i] = BoardWithStats{Board: b, PostCount: int(count)} } return result, nil } -func (s *BoardService) GetByID(id uint) (*model.Board, error) { - var board model.Board - if err := model.DB.First(&board, id).Error; err != nil { +func (s *BoardService) GetByID(id uint) (*models.Board, error) { + var board models.Board + if err := models.DB.First(&board, id).Error; err != nil { return nil, ErrBoardNotFound } return &board, nil } -func (s *BoardService) Create(name, desc, icon string, colorIndex, sortOrder int) (*model.Board, error) { - board := &model.Board{ +func (s *BoardService) Create(name, desc, icon string, colorIndex, sortOrder int) (*models.Board, error) { + board := &models.Board{ Name: name, Description: desc, Icon: NormalizeBoardIcon(icon), ColorIndex: NormalizeBoardColorIndex(colorIndex), SortOrder: sortOrder, } - return board, model.DB.Create(board).Error + return board, models.DB.Create(board).Error } func (s *BoardService) Update(id uint, name, desc, icon string, colorIndex, sortOrder int) error { - return model.DB.Model(&model.Board{}).Where("id = ?", id).Updates(map[string]interface{}{ + return models.DB.Model(&models.Board{}).Where("id = ?", id).Updates(map[string]interface{}{ "name": name, "description": desc, "icon": NormalizeBoardIcon(icon), @@ -70,17 +70,17 @@ func (s *BoardService) Update(id uint, name, desc, icon string, colorIndex, sort func (s *BoardService) Delete(id uint) error { var count int64 - model.DB.Model(&model.Post{}).Where("board_id = ?", id).Count(&count) + models.DB.Model(&models.Post{}).Where("board_id = ?", id).Count(&count) if count > 0 { return errors.New("该板块下还有帖子,无法删除") } - return model.DB.Delete(&model.Board{}, id).Error + return models.DB.Delete(&models.Board{}, id).Error } // EnsureDefaultBoard 若尚无板块则创建默认「综合讨论」,便于全新安装后直接发帖 func (s *BoardService) EnsureDefaultBoard() { var n int64 - if err := model.DB.Model(&model.Board{}).Count(&n).Error; err != nil || n > 0 { + if err := models.DB.Model(&models.Board{}).Count(&n).Error; err != nil || n > 0 { return } _, _ = s.Create("综合讨论", "默认板块,欢迎发帖交流", "message-square", 0, 0) diff --git a/services/board_icon.go b/services/board_icon.go index 5256918..c3262bb 100644 --- a/services/board_icon.go +++ b/services/board_icon.go @@ -1,4 +1,4 @@ -package service +package services import "strings" diff --git a/services/bounty.go b/services/bounty.go index 0a49fb7..e80e977 100644 --- a/services/bounty.go +++ b/services/bounty.go @@ -1,9 +1,9 @@ -package service +package services import ( "errors" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -19,27 +19,27 @@ const bountyRefundBlockReason = "已有用户回复,无法自行取消悬赏 // CountEligibleBountyReplies 统计他人已发布的有效回复数(不含楼主) func CountEligibleBountyReplies(db *gorm.DB, postID, authorID uint) (int64, error) { if db == nil { - db = model.DB + db = models.DB } var n int64 - err := db.Model(&model.Comment{}). - Where("post_id = ? AND status = ? AND user_id != ?", postID, model.ContentStatusPublished, authorID). + err := db.Model(&models.Comment{}). + Where("post_id = ? AND status = ? AND user_id != ?", postID, models.ContentStatusPublished, authorID). Count(&n).Error return n, err } // CanRefundBounty 当前查看者是否可取消悬赏(管理员始终可强制取消) -func CanRefundBounty(post *model.Post, viewerIsAdmin bool) (bool, string) { - if post == nil || post.PostType != model.PostTypeBounty { +func CanRefundBounty(post *models.Post, viewerIsAdmin bool) (bool, string) { + if post == nil || post.PostType != models.PostTypeBounty { return false, "" } - if post.BountyStatus != model.BountyStatusOpen || post.BountyPoints < 1 { + if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 { return false, "" } if viewerIsAdmin { return true, "" } - n, err := CountEligibleBountyReplies(model.DB, post.ID, post.UserID) + n, err := CountEligibleBountyReplies(models.DB, post.ID, post.UserID) if err != nil { return false, "" } @@ -54,42 +54,42 @@ func EscrowBounty(tx *gorm.DB, userID, postID uint, points int) error { if points < 1 { return ErrBountyInvalidPoint } - _, err := AdjustPointsTx(tx, userID, -points, model.PointReasonBountyEscrow, "post", postID, "发布悬赏帖") + _, err := AdjustPointsTx(tx, userID, -points, models.PointReasonBountyEscrow, "post", postID, "发布悬赏帖") return err } // AwardBounty 采纳评论并发放悬赏 func AwardBounty(postID, operatorID uint, isAdmin bool, commentID uint) error { - var post model.Post - if err := model.DB.First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.First(&post, postID).Error; err != nil { return ErrPostNotFound } - if post.PostType != model.PostTypeBounty { + if post.PostType != models.PostTypeBounty { return errors.New("非悬赏帖") } if !isAdmin && post.UserID != operatorID { return ErrPermissionDenied } - if post.BountyStatus != model.BountyStatusOpen || post.BountyPoints < 1 { + if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 { return ErrBountyNotOpen } - var comment model.Comment - if err := model.DB.First(&comment, commentID).Error; err != nil { + var comment models.Comment + if err := models.DB.First(&comment, commentID).Error; err != nil { return errors.New("评论不存在") } - if comment.PostID != postID || comment.Status != model.ContentStatusPublished { + if comment.PostID != postID || comment.Status != models.ContentStatusPublished { return errors.New("评论无效") } if comment.UserID == post.UserID { return ErrBountySelfAward } points := post.BountyPoints - return model.DB.Transaction(func(tx *gorm.DB) error { - if _, err := AdjustPointsTx(tx, comment.UserID, points, model.PointReasonBountyAward, "post", postID, "悬赏采纳"); err != nil { + return models.DB.Transaction(func(tx *gorm.DB) error { + if _, err := AdjustPointsTx(tx, comment.UserID, points, models.PointReasonBountyAward, "post", postID, "悬赏采纳"); err != nil { return err } return tx.Model(&post).Updates(map[string]interface{}{ - "bounty_status": model.BountyStatusAwarded, + "bounty_status": models.BountyStatusAwarded, "bounty_comment_id": commentID, }).Error }) @@ -97,21 +97,21 @@ func AwardBounty(postID, operatorID uint, isAdmin bool, commentID uint) error { // RefundBounty 取消悬赏并退回积分 func RefundBounty(postID, operatorID uint, isAdmin bool) error { - var post model.Post - if err := model.DB.First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.First(&post, postID).Error; err != nil { return ErrPostNotFound } - if post.PostType != model.PostTypeBounty { + if post.PostType != models.PostTypeBounty { return errors.New("非悬赏帖") } if !isAdmin && post.UserID != operatorID { return ErrPermissionDenied } - if post.BountyStatus != model.BountyStatusOpen || post.BountyPoints < 1 { + if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 { return ErrBountyNotOpen } if !isAdmin { - n, err := CountEligibleBountyReplies(model.DB, post.ID, post.UserID) + n, err := CountEligibleBountyReplies(models.DB, post.ID, post.UserID) if err != nil { return err } @@ -120,31 +120,31 @@ func RefundBounty(postID, operatorID uint, isAdmin bool) error { } } points := post.BountyPoints - return model.DB.Transaction(func(tx *gorm.DB) error { - if _, err := AdjustPointsTx(tx, post.UserID, points, model.PointReasonBountyRefund, "post", postID, "悬赏退回"); err != nil { + return models.DB.Transaction(func(tx *gorm.DB) error { + if _, err := AdjustPointsTx(tx, post.UserID, points, models.PointReasonBountyRefund, "post", postID, "悬赏退回"); err != nil { return err } return tx.Model(&post).Updates(map[string]interface{}{ - "bounty_status": model.BountyStatusRefunded, + "bounty_status": models.BountyStatusRefunded, "bounty_points": 0, }).Error }) } // RefundBountyIfOpen 删帖时自动退回未采纳悬赏 -func RefundBountyIfOpen(tx *gorm.DB, post *model.Post) error { - if post == nil || post.PostType != model.PostTypeBounty { +func RefundBountyIfOpen(tx *gorm.DB, post *models.Post) error { + if post == nil || post.PostType != models.PostTypeBounty { return nil } - if post.BountyStatus != model.BountyStatusOpen || post.BountyPoints < 1 { + if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 { return nil } points := post.BountyPoints - if _, err := AdjustPointsTx(tx, post.UserID, points, model.PointReasonBountyRefund, "post", post.ID, "删帖退回悬赏"); err != nil { + if _, err := AdjustPointsTx(tx, post.UserID, points, models.PointReasonBountyRefund, "post", post.ID, "删帖退回悬赏"); err != nil { return err } return tx.Model(post).Updates(map[string]interface{}{ - "bounty_status": model.BountyStatusRefunded, + "bounty_status": models.BountyStatusRefunded, "bounty_points": 0, }).Error } diff --git a/services/bounty_test.go b/services/bounty_test.go index b5fe17f..81bef76 100644 --- a/services/bounty_test.go +++ b/services/bounty_test.go @@ -1,11 +1,11 @@ -package service +package services import ( "errors" "testing" "github.com/glebarez/sqlite" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -15,26 +15,26 @@ func setupBountyTestDB(t *testing.T) *gorm.DB { if err != nil { t.Fatal(err) } - if err := db.AutoMigrate(&model.User{}, &model.Post{}, &model.Comment{}, &model.PointLedger{}); err != nil { + if err := db.AutoMigrate(&models.User{}, &models.Post{}, &models.Comment{}, &models.PointLedger{}); err != nil { t.Fatal(err) } - prev := model.DB - model.DB = db - t.Cleanup(func() { model.DB = prev }) + prev := models.DB + models.DB = db + t.Cleanup(func() { models.DB = prev }) return db } -func seedBountyPost(t *testing.T, db *gorm.DB, authorID uint, points int) model.Post { +func seedBountyPost(t *testing.T, db *gorm.DB, authorID uint, points int) models.Post { t.Helper() - post := model.Post{ + post := models.Post{ UserID: authorID, BoardID: 1, Title: "悬赏测试", Content: "内容", - PostType: model.PostTypeBounty, + PostType: models.PostTypeBounty, BountyPoints: points, - BountyStatus: model.BountyStatusOpen, - Status: model.ContentStatusPublished, + BountyStatus: models.BountyStatusOpen, + Status: models.ContentStatusPublished, } if err := db.Create(&post).Error; err != nil { t.Fatal(err) @@ -44,7 +44,7 @@ func seedBountyPost(t *testing.T, db *gorm.DB, authorID uint, points int) model. func seedUser(t *testing.T, db *gorm.DB, id uint, points int) { t.Helper() - u := model.User{ + u := models.User{ ID: id, Username: "user" + string(rune('0'+id)), Password: "hash", @@ -58,7 +58,7 @@ func seedUser(t *testing.T, db *gorm.DB, id uint, points int) { func seedComment(t *testing.T, db *gorm.DB, postID, userID uint, floor int, status string) { t.Helper() - c := model.Comment{ + c := models.Comment{ PostID: postID, UserID: userID, Floor: floor, @@ -79,25 +79,25 @@ func TestCountEligibleBountyReplies(t *testing.T) { t.Fatalf("无回复时期望 0,得到 %d err=%v", n, err) } - seedComment(t, db, post.ID, 1, 1, model.ContentStatusPublished) + seedComment(t, db, post.ID, 1, 1, models.ContentStatusPublished) n, err = CountEligibleBountyReplies(db, post.ID, 1) if err != nil || n != 0 { t.Fatalf("楼主自己的回复不应计入,得到 %d", n) } - seedComment(t, db, post.ID, 2, 2, model.ContentStatusPublished) + seedComment(t, db, post.ID, 2, 2, models.ContentStatusPublished) n, err = CountEligibleBountyReplies(db, post.ID, 1) if err != nil || n != 1 { t.Fatalf("他人 published 回复期望 1,得到 %d", n) } - seedComment(t, db, post.ID, 3, 3, model.ContentStatusPending) + seedComment(t, db, post.ID, 3, 3, models.ContentStatusPending) n, err = CountEligibleBountyReplies(db, post.ID, 1) if err != nil || n != 1 { t.Fatalf("pending 回复不应增加计数,得到 %d", n) } - seedComment(t, db, post.ID, 0, 4, model.ContentStatusPublished) + seedComment(t, db, post.ID, 0, 4, models.ContentStatusPublished) n, err = CountEligibleBountyReplies(db, post.ID, 1) if err != nil || n != 2 { t.Fatalf("游客回复应计入,得到 %d", n) @@ -113,7 +113,7 @@ func TestCanRefundBounty(t *testing.T) { t.Fatalf("无回复时楼主应可退,can=%v reason=%q", can, reason) } - seedComment(t, db, post.ID, 2, 1, model.ContentStatusPublished) + seedComment(t, db, post.ID, 2, 1, models.ContentStatusPublished) can, reason = CanRefundBounty(&post, false) if can || reason != bountyRefundBlockReason { t.Fatalf("有他人回复时楼主不可退,can=%v reason=%q", can, reason) @@ -130,7 +130,7 @@ func TestRefundBountyBlockedForAuthorWithReplies(t *testing.T) { seedUser(t, db, 1, 0) seedUser(t, db, 2, 0) post := seedBountyPost(t, db, 1, 8) - seedComment(t, db, post.ID, 2, 1, model.ContentStatusPublished) + seedComment(t, db, post.ID, 2, 1, models.ContentStatusPublished) err := RefundBounty(post.ID, 1, false) if !errors.Is(err, ErrBountyRefundBlocked) { @@ -146,14 +146,14 @@ func TestRefundBountyAllowedWithoutReplies(t *testing.T) { if err := RefundBounty(post.ID, 1, false); err != nil { t.Fatalf("无回复时楼主应可退回,err=%v", err) } - var updated model.Post + var updated models.Post if err := db.First(&updated, post.ID).Error; err != nil { t.Fatal(err) } - if updated.BountyStatus != model.BountyStatusRefunded || updated.BountyPoints != 0 { + if updated.BountyStatus != models.BountyStatusRefunded || updated.BountyPoints != 0 { t.Fatalf("状态应为 refunded 且积分为 0,得到 status=%s points=%d", updated.BountyStatus, updated.BountyPoints) } - var author model.User + var author models.User if err := db.First(&author, 1).Error; err != nil { t.Fatal(err) } @@ -167,7 +167,7 @@ func TestRefundBountyAdminBypassWithReplies(t *testing.T) { seedUser(t, db, 1, 0) seedUser(t, db, 2, 0) post := seedBountyPost(t, db, 1, 4) - seedComment(t, db, post.ID, 2, 1, model.ContentStatusPublished) + seedComment(t, db, post.ID, 2, 1, models.ContentStatusPublished) if err := RefundBounty(post.ID, 99, true); err != nil { t.Fatalf("管理员应可强制退回,err=%v", err) diff --git a/services/captcha.go b/services/captcha.go index 74cd43c..fc746f4 100644 --- a/services/captcha.go +++ b/services/captcha.go @@ -1,4 +1,4 @@ -package service +package services import ( "crypto/rand" diff --git a/services/comment.go b/services/comment.go index 4b094ce..9eaeeb1 100644 --- a/services/comment.go +++ b/services/comment.go @@ -1,4 +1,4 @@ -package service +package services import ( "errors" @@ -7,7 +7,7 @@ import ( "gorm.io/gorm" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) type CommentService struct { @@ -25,9 +25,9 @@ func (s *CommentService) HasUserReplied(postID, userID uint) bool { return false } var count int64 - err := model.DB.Model(&model.Comment{}). + err := models.DB.Model(&models.Comment{}). Where("post_id = ? AND user_id = ? AND status IN ?", postID, userID, - []string{model.ContentStatusPublished, model.ContentStatusPending}). + []string{models.ContentStatusPublished, models.ContentStatusPending}). Limit(1). Count(&count).Error return err == nil && count > 0 @@ -44,7 +44,7 @@ type CommentCreateInput struct { IsPrivate bool } -func (s *CommentService) canViewPrivate(c model.Comment, viewerID uint, isAdmin bool, postAuthorID uint, guestSet map[uint]struct{}) bool { +func (s *CommentService) canViewPrivate(c models.Comment, viewerID uint, isAdmin bool, postAuthorID uint, guestSet map[uint]struct{}) bool { if !c.IsPrivate { return true } @@ -63,8 +63,8 @@ func (s *CommentService) canViewPrivate(c model.Comment, viewerID uint, isAdmin return false } -func (s *CommentService) fillReplyTargets(comments []model.Comment, loadMissing bool) { - idMap := make(map[uint]model.Comment, len(comments)) +func (s *CommentService) fillReplyTargets(comments []models.Comment, loadMissing bool) { + idMap := make(map[uint]models.Comment, len(comments)) for _, c := range comments { idMap[c.ID] = c } @@ -78,27 +78,27 @@ func (s *CommentService) fillReplyTargets(comments []model.Comment, loadMissing continue } if loadMissing { - var target model.Comment - if model.DB.Preload("User").First(&target, *comments[i].ReplyTo).Error == nil { + var target models.Comment + if models.DB.Preload("User").First(&target, *comments[i].ReplyTo).Error == nil { comments[i].ReplyTarget = &target } } } } -func canViewComment(c model.Comment, viewerID uint, isAdmin bool) bool { - if isAdmin || c.Status == model.ContentStatusPublished || c.Status == "" { +func canViewComment(c models.Comment, viewerID uint, isAdmin bool) bool { + if isAdmin || c.Status == models.ContentStatusPublished || c.Status == "" { return true } - if c.Status == model.ContentStatusPending || c.Status == model.ContentStatusRejected { + if c.Status == models.ContentStatusPending || c.Status == models.ContentStatusRejected { return viewerID > 0 && c.UserID == viewerID } return false } -func (s *CommentService) ListByPost(postID, viewerID uint, isAdmin bool, postAuthorID uint, visibleGuestIDs []uint) ([]model.Comment, error) { - var comments []model.Comment - err := model.DB.Preload("User").Where("post_id = ?", postID).Order("floor asc").Find(&comments).Error +func (s *CommentService) ListByPost(postID, viewerID uint, isAdmin bool, postAuthorID uint, visibleGuestIDs []uint) ([]models.Comment, error) { + var comments []models.Comment + err := models.DB.Preload("User").Where("post_id = ?", postID).Order("floor asc").Find(&comments).Error if err != nil { return nil, err } @@ -108,12 +108,12 @@ func (s *CommentService) ListByPost(postID, viewerID uint, isAdmin bool, postAut guestSet[id] = struct{}{} } - allByID := make(map[uint]model.Comment, len(comments)) + allByID := make(map[uint]models.Comment, len(comments)) for _, c := range comments { allByID[c.ID] = c } - visible := make([]model.Comment, 0, len(comments)) + visible := make([]models.Comment, 0, len(comments)) visibleIDs := make(map[uint]struct{}, len(comments)) for i := range comments { if !canViewComment(comments[i], viewerID, isAdmin) { @@ -145,7 +145,7 @@ func (s *CommentService) ListByPost(postID, viewerID uint, isAdmin bool, postAut } // resolveThreadParent 计算嵌套展示父节点:优先直接父评论,否则沿 reply_to 向上找到最近可见祖先 -func resolveThreadParent(replyTo *uint, visibleIDs map[uint]struct{}, allByID map[uint]model.Comment) *uint { +func resolveThreadParent(replyTo *uint, visibleIDs map[uint]struct{}, allByID map[uint]models.Comment) *uint { if replyTo == nil { return nil } @@ -169,7 +169,7 @@ func resolveThreadParent(replyTo *uint, visibleIDs map[uint]struct{}, allByID ma return nil } -func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) { +func (s *CommentService) Create(in CommentCreateInput) (*models.Comment, error) { content := SanitizePostHTML(strings.TrimSpace(in.Content)) content = s.filter.Filter(content) if content == "" { @@ -179,16 +179,16 @@ func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) { return nil, err } - var post model.Post - if err := model.DB.First(&post, in.PostID).Error; err != nil { + var post models.Post + if err := models.DB.First(&post, in.PostID).Error; err != nil { return nil, ErrPostNotFound } if in.UserID == 0 { return nil, errors.New("请登录后评论") } - var user model.User - if err := model.DB.First(&user, in.UserID).Error; err != nil { + var user models.User + if err := models.DB.First(&user, in.UserID).Error; err != nil { return nil, errors.New("用户不存在") } if user.Banned { @@ -201,31 +201,31 @@ func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) { } // 未公开帖仅作者/管理员可评论 - if post.Status != model.ContentStatusPublished && post.Status != "" { - if user.Role != model.RoleAdmin && post.UserID != in.UserID { + if post.Status != models.ContentStatusPublished && post.Status != "" { + if user.Role != models.RoleAdmin && post.UserID != in.UserID { return nil, errors.New("帖子审核中,暂不可评论") } } var maxFloor int - model.DB.Model(&model.Comment{}).Where("post_id = ?", in.PostID).Select("COALESCE(MAX(floor), 0)").Scan(&maxFloor) + models.DB.Model(&models.Comment{}).Where("post_id = ?", in.PostID).Select("COALESCE(MAX(floor), 0)").Scan(&maxFloor) if in.ReplyTo != nil { - var target model.Comment - if err := model.DB.Where("id = ? AND post_id = ?", *in.ReplyTo, in.PostID).First(&target).Error; err != nil { + var target models.Comment + if err := models.DB.Where("id = ? AND post_id = ?", *in.ReplyTo, in.PostID).First(&target).Error; err != nil { return nil, ErrCommentNotFound } - if !canViewComment(target, in.UserID, user.Role == model.RoleAdmin) { + if !canViewComment(target, in.UserID, user.Role == models.RoleAdmin) { return nil, ErrCommentNotFound } } - status := model.ContentStatusPending + status := models.ContentStatusPending if user.SkipsModeration() { - status = model.ContentStatusPublished + status = models.ContentStatusPublished } - comment := &model.Comment{ + comment := &models.Comment{ PostID: in.PostID, UserID: in.UserID, Floor: maxFloor + 1, @@ -237,10 +237,10 @@ func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) { IsPrivate: in.IsPrivate, Status: status, } - if err := model.DB.Create(comment).Error; err != nil { + if err := models.DB.Create(comment).Error; err != nil { return nil, err } - if status == model.ContentStatusPublished { + if status == models.ContentStatusPublished { AddExp(in.UserID, 2) } return comment, nil @@ -249,39 +249,39 @@ func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) { // SetStatus 设置评论审核状态 func (s *CommentService) SetStatus(commentID uint, status string) error { switch status { - case model.ContentStatusPending, model.ContentStatusPublished, model.ContentStatusRejected: + case models.ContentStatusPending, models.ContentStatusPublished, models.ContentStatusRejected: default: return errors.New("无效的审核状态") } - var comment model.Comment - if err := model.DB.Select("id", "user_id", "status").First(&comment, commentID).Error; err != nil { + var comment models.Comment + if err := models.DB.Select("id", "user_id", "status").First(&comment, commentID).Error; err != nil { return ErrCommentNotFound } prev := comment.Status - res := model.DB.Model(&model.Comment{}).Where("id = ?", commentID).Update("status", status) + res := models.DB.Model(&models.Comment{}).Where("id = ?", commentID).Update("status", status) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return ErrCommentNotFound } - if status == model.ContentStatusPublished && prev != model.ContentStatusPublished && comment.UserID > 0 { + if status == models.ContentStatusPublished && prev != models.ContentStatusPublished && comment.UserID > 0 { AddExp(comment.UserID, 2) } return nil } // GetByID 获取评论 -func (s *CommentService) GetByID(id uint) (*model.Comment, error) { - var c model.Comment - if err := model.DB.Preload("User").Preload("Post").First(&c, id).Error; err != nil { +func (s *CommentService) GetByID(id uint) (*models.Comment, error) { + var c models.Comment + if err := models.DB.Preload("User").Preload("Post").First(&c, id).Error; err != nil { return nil, ErrCommentNotFound } return &c, nil } // fillLiked 批量标记当前用户是否已点赞 -func (s *CommentService) fillLiked(comments []model.Comment, viewerID uint) { +func (s *CommentService) fillLiked(comments []models.Comment, viewerID uint) { if viewerID == 0 || len(comments) == 0 { return } @@ -289,8 +289,8 @@ func (s *CommentService) fillLiked(comments []model.Comment, viewerID uint) { for _, c := range comments { ids = append(ids, c.ID) } - var likes []model.CommentLike - model.DB.Where("user_id = ? AND comment_id IN ?", viewerID, ids).Find(&likes) + var likes []models.CommentLike + models.DB.Where("user_id = ? AND comment_id IN ?", viewerID, ids).Find(&likes) likedSet := make(map[uint]struct{}, len(likes)) for _, l := range likes { likedSet[l.CommentID] = struct{}{} @@ -302,29 +302,29 @@ func (s *CommentService) fillLiked(comments []model.Comment, viewerID uint) { // ToggleLike 切换评论点赞 func (s *CommentService) ToggleLike(userID, commentID uint) (liked bool, likeCount int, err error) { - var comment model.Comment - if err := model.DB.Select("id", "like_count").First(&comment, commentID).Error; err != nil { + var comment models.Comment + if err := models.DB.Select("id", "like_count").First(&comment, commentID).Error; err != nil { return false, 0, ErrCommentNotFound } - var like model.CommentLike - result := model.DB.Where("comment_id = ? AND user_id = ?", commentID, userID).Limit(1).Find(&like) + var like models.CommentLike + result := models.DB.Where("comment_id = ? AND user_id = ?", commentID, userID).Limit(1).Find(&like) if result.Error != nil { return false, 0, result.Error } if result.RowsAffected > 0 { - if err := model.DB.Delete(&like).Error; err != nil { + if err := models.DB.Delete(&like).Error; err != nil { return false, 0, err } - model.DB.Model(&model.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("CASE WHEN like_count > 0 THEN like_count - 1 ELSE 0 END")) - _ = model.DB.Select("like_count").First(&comment, commentID) + models.DB.Model(&models.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("CASE WHEN like_count > 0 THEN like_count - 1 ELSE 0 END")) + _ = models.DB.Select("like_count").First(&comment, commentID) return false, comment.LikeCount, nil } - like = model.CommentLike{CommentID: commentID, UserID: userID} - if err := model.DB.Create(&like).Error; err != nil { + like = models.CommentLike{CommentID: commentID, UserID: userID} + if err := models.DB.Create(&like).Error; err != nil { return false, 0, err } - model.DB.Model(&model.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("like_count + 1")) - _ = model.DB.Select("like_count").First(&comment, commentID) + models.DB.Model(&models.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("like_count + 1")) + _ = models.DB.Select("like_count").First(&comment, commentID) return true, comment.LikeCount, nil } @@ -334,14 +334,14 @@ func (s *CommentService) IsLiked(userID, commentID uint) bool { return false } var count int64 - model.DB.Model(&model.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count) + models.DB.Model(&models.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count) return count > 0 } // PendingCommentCount 待审评论数 func (s *CommentService) PendingCommentCount() (int64, error) { var n int64 - err := model.DB.Model(&model.Comment{}).Where("status = ?", model.ContentStatusPending).Count(&n).Error + err := models.DB.Model(&models.Comment{}).Where("status = ?", models.ContentStatusPending).Count(&n).Error return n, err } @@ -353,8 +353,8 @@ func (s *CommentService) Delete(userID, commentID uint, isAdmin bool) error { } func (s *CommentService) Update(userID, commentID uint, isAdmin, skipModeration bool, content string) (string, bool, error) { - var comment model.Comment - if err := model.DB.First(&comment, commentID).Error; err != nil { + var comment models.Comment + if err := models.DB.First(&comment, commentID).Error; err != nil { return "", false, ErrCommentNotFound } if !isAdmin && (comment.UserID == 0 || comment.UserID != userID) { @@ -380,8 +380,8 @@ func (s *CommentService) Update(userID, commentID uint, isAdmin, skipModeration } enteredPending := false - err := model.DB.Transaction(func(tx *gorm.DB) error { - rev := model.CommentRevision{ + err := models.DB.Transaction(func(tx *gorm.DB) error { + rev := models.CommentRevision{ CommentID: commentID, EditorID: userID, Content: comment.Content, @@ -391,7 +391,7 @@ func (s *CommentService) Update(userID, commentID uint, isAdmin, skipModeration } updates := map[string]interface{}{"content": content} if !skipModeration { - updates["status"] = model.ContentStatusPending + updates["status"] = models.ContentStatusPending enteredPending = true } return tx.Model(&comment).Updates(updates).Error @@ -413,11 +413,11 @@ func collectReplySubtreeIDs(db *gorm.DB, rootID uint, softDeletedOnly bool) ([]u seen := map[uint]struct{}{rootID: {}} frontier := []uint{rootID} for len(frontier) > 0 { - childQ := q.Model(&model.Comment{}).Select("id").Where("reply_to IN ?", frontier) + childQ := q.Model(&models.Comment{}).Select("id").Where("reply_to IN ?", frontier) if softDeletedOnly { childQ = childQ.Where("deleted_at IS NOT NULL") } - var children []model.Comment + var children []models.Comment if err := childQ.Find(&children).Error; err != nil { return nil, err } @@ -436,20 +436,20 @@ func collectReplySubtreeIDs(db *gorm.DB, rootID uint, softDeletedOnly bool) ([]u // AdminDelete 软删除评论及其回复树(进入回收站);修订与点赞保留以便恢复 func (s *CommentService) AdminDelete(commentID uint) error { - var root model.Comment - if err := model.DB.First(&root, commentID).Error; err != nil { + var root models.Comment + if err := models.DB.First(&root, commentID).Error; err != nil { return ErrCommentNotFound } - ids, err := collectReplySubtreeIDs(model.DB, commentID, false) + ids, err := collectReplySubtreeIDs(models.DB, commentID, false) if err != nil { return err } - return model.DB.Where("id IN ?", ids).Delete(&model.Comment{}).Error + return models.DB.Where("id IN ?", ids).Delete(&models.Comment{}).Error } // TrashCommentItem 评论回收站列表项 type TrashCommentItem struct { - model.Comment + models.Comment DeletedAt time.Time `json:"deleted_at"` } @@ -459,7 +459,7 @@ func (s *CommentService) ListTrash(page, size int, keyword string) ([]TrashComme page = 1 } size = s.settings.NormalizePageSize(size) - db := model.DB.Unscoped().Model(&model.Comment{}). + db := models.DB.Unscoped().Model(&models.Comment{}). Where("comments.deleted_at IS NOT NULL"). Joins("JOIN posts ON posts.id = comments.post_id AND posts.deleted_at IS NULL"). Preload("User").Preload("Post") @@ -475,7 +475,7 @@ func (s *CommentService) ListTrash(page, size int, keyword string) ([]TrashComme if err := db.Count(&total).Error; err != nil { return nil, 0, err } - var comments []model.Comment + var comments []models.Comment if err := db.Order("comments.deleted_at DESC").Offset((page - 1) * size).Limit(size).Find(&comments).Error; err != nil { return nil, 0, err } @@ -491,65 +491,65 @@ func (s *CommentService) ListTrash(page, size int, keyword string) ([]TrashComme // Restore 从回收站恢复评论及其已软删的回复树 func (s *CommentService) Restore(commentID uint) error { - var comment model.Comment - if err := model.DB.Unscoped().First(&comment, commentID).Error; err != nil { + var comment models.Comment + if err := models.DB.Unscoped().First(&comment, commentID).Error; err != nil { return ErrCommentNotFound } if !comment.DeletedAt.Valid { return errors.New("评论未被删除") } // 所属帖子必须仍存在且未删除 - var post model.Post - if err := model.DB.First(&post, comment.PostID).Error; err != nil { + var post models.Post + if err := models.DB.First(&post, comment.PostID).Error; err != nil { return errors.New("所属帖子不存在或已在回收站,请先恢复帖子") } - ids, err := collectReplySubtreeIDs(model.DB, commentID, true) + ids, err := collectReplySubtreeIDs(models.DB, commentID, true) if err != nil { return err } - return model.DB.Unscoped().Model(&model.Comment{}). + return models.DB.Unscoped().Model(&models.Comment{}). Where("id IN ?", ids). Update("deleted_at", nil).Error } // Purge 永久删除回收站中的评论及其已软删回复(含修订、点赞) func (s *CommentService) Purge(commentID uint) error { - var comment model.Comment - if err := model.DB.Unscoped().First(&comment, commentID).Error; err != nil { + var comment models.Comment + if err := models.DB.Unscoped().First(&comment, commentID).Error; err != nil { return ErrCommentNotFound } if !comment.DeletedAt.Valid { return errors.New("仅可彻底删除回收站中的评论,请先删除评论") } - ids, err := collectReplySubtreeIDs(model.DB, commentID, true) + ids, err := collectReplySubtreeIDs(models.DB, commentID, true) if err != nil { return err } - return model.DB.Transaction(func(tx *gorm.DB) error { - if err := tx.Where("comment_id IN ?", ids).Delete(&model.CommentRevision{}).Error; err != nil { + return models.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("comment_id IN ?", ids).Delete(&models.CommentRevision{}).Error; err != nil { return err } - if err := tx.Where("comment_id IN ?", ids).Delete(&model.CommentLike{}).Error; err != nil { + if err := tx.Where("comment_id IN ?", ids).Delete(&models.CommentLike{}).Error; err != nil { return err } - return tx.Unscoped().Where("id IN ?", ids).Delete(&model.Comment{}).Error + return tx.Unscoped().Where("id IN ?", ids).Delete(&models.Comment{}).Error }) } // ListRevisions 评论编辑历史(管理员查看) -func (s *CommentService) ListRevisions(commentID uint) ([]model.CommentRevision, error) { +func (s *CommentService) ListRevisions(commentID uint) ([]models.CommentRevision, error) { if _, err := s.GetByID(commentID); err != nil { return nil, err } - var revs []model.CommentRevision - err := model.DB.Preload("Editor"). + var revs []models.CommentRevision + err := models.DB.Preload("Editor"). Where("comment_id = ?", commentID). Order("id desc").Find(&revs).Error if err != nil { return nil, err } if revs == nil { - revs = []model.CommentRevision{} + revs = []models.CommentRevision{} } return revs, nil } @@ -572,9 +572,9 @@ func (s *CommentService) ListRecentPublic(limit int) ([]RecentCommentItem, error if limit < 1 { limit = 8 } - var comments []model.Comment - err := model.DB.Preload("User").Preload("Post"). - Where("is_private = ? AND status = ?", false, model.ContentStatusPublished). + var comments []models.Comment + err := models.DB.Preload("User").Preload("Post"). + Where("is_private = ? AND status = ?", false, models.ContentStatusPublished). Order("id desc").Limit(limit * 2). // 多取一些以跳过已删帖 Find(&comments).Error if err != nil { @@ -630,21 +630,21 @@ func truncateRunes(s string, n int) string { } // ListRecent 管理员查看最近评论 -func (s *CommentService) ListRecent(page, size int, status string) ([]model.Comment, int64, error) { +func (s *CommentService) ListRecent(page, size int, status string) ([]models.Comment, int64, error) { if page < 1 { page = 1 } if size < 1 { size = 20 } - db := model.DB.Model(&model.Comment{}) + db := models.DB.Model(&models.Comment{}) switch status { - case model.ContentStatusPending, model.ContentStatusPublished, model.ContentStatusRejected: + case models.ContentStatusPending, models.ContentStatusPublished, models.ContentStatusRejected: db = db.Where("status = ?", status) } var total int64 db.Count(&total) - var comments []model.Comment + var comments []models.Comment err := db.Preload("User").Preload("Post"). Order("CASE WHEN status = 'pending' THEN 0 ELSE 1 END, id DESC"). Offset((page - 1) * size).Limit(size).Find(&comments).Error diff --git a/services/common.go b/services/common.go index 082a2b1..314b669 100644 --- a/services/common.go +++ b/services/common.go @@ -1,4 +1,4 @@ -package service +package services import ( "errors" diff --git a/services/content.go b/services/content.go index 7704f9d..8765bf5 100644 --- a/services/content.go +++ b/services/content.go @@ -1,4 +1,4 @@ -package service +package services import ( "regexp" diff --git a/services/content_test.go b/services/content_test.go index 9b40231..379781f 100644 --- a/services/content_test.go +++ b/services/content_test.go @@ -1,4 +1,4 @@ -package service +package services import ( "strings" diff --git a/services/crawler.go b/services/crawler.go index d3ce1ed..afee307 100644 --- a/services/crawler.go +++ b/services/crawler.go @@ -1,4 +1,4 @@ -package service +package services import "strings" diff --git a/services/email_code.go b/services/email_code.go index b2f4a4e..8730f1a 100644 --- a/services/email_code.go +++ b/services/email_code.go @@ -1,4 +1,4 @@ -package service +package services import ( "crypto/rand" @@ -8,7 +8,7 @@ import ( "sync" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) const ( @@ -65,8 +65,8 @@ func (s *EmailCodeService) sendCode(purpose, email string) error { return err } - var exist model.User - found := model.DB.Where("email = ?", email).First(&exist).Error == nil + var exist models.User + found := models.DB.Where("email = ?", email).First(&exist).Error == nil switch purpose { case EmailCodePurposeRegister: if found { diff --git a/services/filter_io.go b/services/filter_io.go index 2221b46..42c97cf 100644 --- a/services/filter_io.go +++ b/services/filter_io.go @@ -1,4 +1,4 @@ -package service +package services import "os" diff --git a/services/filter_words.go b/services/filter_words.go index ea861be..e67d2f5 100644 --- a/services/filter_words.go +++ b/services/filter_words.go @@ -1,4 +1,4 @@ -package service +package services import ( "os" diff --git a/services/friend_link.go b/services/friend_link.go index 1a765f2..b8f5f37 100644 --- a/services/friend_link.go +++ b/services/friend_link.go @@ -1,4 +1,4 @@ -package service +package services import ( "errors" @@ -8,7 +8,7 @@ import ( "time" "unicode/utf8" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -41,7 +41,7 @@ type FriendLinkApplyInput struct { } type FriendLinkApplyCreateResult struct { - Apply *model.FriendLinkApply + Apply *models.FriendLinkApply } type FriendLinkApplyService struct { @@ -106,41 +106,41 @@ func (s *FriendLinkApplyService) Create(in FriendLinkApplyInput) (*FriendLinkApp return nil, ErrFriendLinkApplyPending } - apply := &model.FriendLinkApply{ + apply := &models.FriendLinkApply{ UserID: in.UserID, Name: name, URL: href, Logo: logo, ReciprocalPageURL: reciprocal, LinkOnHomepage: in.LinkOnHomepage, - Status: model.FriendLinkApplyStatusPending, + Status: models.FriendLinkApplyStatusPending, } - if err := model.DB.Create(apply).Error; err != nil { + if err := models.DB.Create(apply).Error; err != nil { return nil, err } s.startReciprocalCheck(apply.ID, reciprocal, in.OurSiteURL) - _ = model.DB.Preload("User").First(apply, apply.ID).Error + _ = models.DB.Preload("User").First(apply, apply.ID).Error return &FriendLinkApplyCreateResult{Apply: apply}, nil } // PendingCount 待审数量 func (s *FriendLinkApplyService) PendingCount() (int64, error) { var n int64 - err := model.DB.Model(&model.FriendLinkApply{}). - Where("status = ?", model.FriendLinkApplyStatusPending). + err := models.DB.Model(&models.FriendLinkApply{}). + Where("status = ?", models.FriendLinkApplyStatusPending). Count(&n).Error return n, err } // ListAdmin 管理员列表 -func (s *FriendLinkApplyService) ListAdmin(q FriendLinkApplyListQuery) ([]model.FriendLinkApply, int64, error) { +func (s *FriendLinkApplyService) ListAdmin(q FriendLinkApplyListQuery) ([]models.FriendLinkApply, int64, error) { if q.Page < 1 { q.Page = 1 } if q.Size < 1 || q.Size > 50 { q.Size = 20 } - db := model.DB.Model(&model.FriendLinkApply{}) + db := models.DB.Model(&models.FriendLinkApply{}) status := strings.TrimSpace(q.Status) if status != "" && status != "all" { db = db.Where("status = ?", status) @@ -149,7 +149,7 @@ func (s *FriendLinkApplyService) ListAdmin(q FriendLinkApplyListQuery) ([]model. if err := db.Count(&total).Error; err != nil { return nil, 0, err } - var list []model.FriendLinkApply + var list []models.FriendLinkApply err := db.Preload("User"). Order("CASE WHEN status = 'pending' THEN 0 ELSE 1 END, id DESC"). Offset((q.Page - 1) * q.Size). @@ -161,22 +161,22 @@ func (s *FriendLinkApplyService) ListAdmin(q FriendLinkApplyListQuery) ([]model. return list, total, nil } -func (s *FriendLinkApplyService) getPending(id uint) (*model.FriendLinkApply, error) { - var apply model.FriendLinkApply - if err := model.DB.Preload("User").First(&apply, id).Error; err != nil { +func (s *FriendLinkApplyService) getPending(id uint) (*models.FriendLinkApply, error) { + var apply models.FriendLinkApply + if err := models.DB.Preload("User").First(&apply, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrFriendLinkApplyNotFound } return nil, err } - if apply.Status != model.FriendLinkApplyStatusPending { + if apply.Status != models.FriendLinkApplyStatusPending { return nil, ErrFriendLinkApplyHandled } return &apply, nil } // Approve 通过申请并写入友链 -func (s *FriendLinkApplyService) Approve(id uint) (*model.FriendLinkApply, error) { +func (s *FriendLinkApplyService) Approve(id uint) (*models.FriendLinkApply, error) { apply, err := s.getPending(id) if err != nil { return nil, err @@ -211,13 +211,13 @@ func (s *FriendLinkApplyService) Approve(id uint) (*model.FriendLinkApply, error } now := time.Now() - if err := model.DB.Model(apply).Updates(map[string]interface{}{ - "status": model.FriendLinkApplyStatusApproved, + if err := models.DB.Model(apply).Updates(map[string]interface{}{ + "status": models.FriendLinkApplyStatusApproved, "reviewed_at": now, }).Error; err != nil { return nil, err } - apply.Status = model.FriendLinkApplyStatusApproved + apply.Status = models.FriendLinkApplyStatusApproved apply.ReviewedAt = &now if s.messages != nil && apply.UserID > 0 { @@ -226,27 +226,27 @@ func (s *FriendLinkApplyService) Approve(id uint) (*model.FriendLinkApply, error "你申请的友情链接「%s」(%s)已通过审核,现已展示在友情链接页面。\n\n如有疑问,可回复本私信联系管理员。", apply.Name, apply.URL, ) - _, _ = s.messages.SendSystem(apply.UserID, subject, content, model.MessageKindSystem, nil, nil) + _, _ = s.messages.SendSystem(apply.UserID, subject, content, models.MessageKindSystem, nil, nil) } return apply, nil } // Reject 拒绝申请 -func (s *FriendLinkApplyService) Reject(id uint, note string) (*model.FriendLinkApply, error) { +func (s *FriendLinkApplyService) Reject(id uint, note string) (*models.FriendLinkApply, error) { apply, err := s.getPending(id) if err != nil { return nil, err } note = strings.TrimSpace(note) now := time.Now() - if err := model.DB.Model(apply).Updates(map[string]interface{}{ - "status": model.FriendLinkApplyStatusRejected, + if err := models.DB.Model(apply).Updates(map[string]interface{}{ + "status": models.FriendLinkApplyStatusRejected, "review_note": note, "reviewed_at": now, }).Error; err != nil { return nil, err } - apply.Status = model.FriendLinkApplyStatusRejected + apply.Status = models.FriendLinkApplyStatusRejected apply.ReviewNote = note apply.ReviewedAt = &now @@ -260,7 +260,7 @@ func (s *FriendLinkApplyService) Reject(id uint, note string) (*model.FriendLink "你申请的友情链接「%s」(%s)未通过审核。\n\n原因:\n%s\n\n如有疑问,可回复本私信联系管理员。", apply.Name, apply.URL, reason, ) - _, _ = s.messages.SendSystem(apply.UserID, subject, content, model.MessageKindReject, nil, nil) + _, _ = s.messages.SendSystem(apply.UserID, subject, content, models.MessageKindReject, nil, nil) } return apply, nil } @@ -302,8 +302,8 @@ func (s *FriendLinkApplyService) prepareApplyFields(in FriendLinkApplyInput, all } func (s *FriendLinkApplyService) hasPendingApplyForURL(userID, excludeID uint, href string) (bool, error) { - db := model.DB.Model(&model.FriendLinkApply{}). - Where("user_id = ? AND status = ? AND url = ?", userID, model.FriendLinkApplyStatusPending, href) + db := models.DB.Model(&models.FriendLinkApply{}). + Where("user_id = ? AND status = ? AND url = ?", userID, models.FriendLinkApplyStatusPending, href) if excludeID > 0 { db = db.Where("id <> ?", excludeID) } @@ -346,8 +346,8 @@ func (s *FriendLinkApplyService) removePublishedFriendLink(href string) error { // Update 修改并重新提交友链申请(待审 / 已拒绝 / 已通过) func (s *FriendLinkApplyService) Update(userID, id uint, in FriendLinkApplyInput) (*FriendLinkApplyCreateResult, error) { - var apply model.FriendLinkApply - if err := model.DB.First(&apply, id).Error; err != nil { + var apply models.FriendLinkApply + if err := models.DB.First(&apply, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrFriendLinkApplyNotFound } @@ -356,13 +356,13 @@ func (s *FriendLinkApplyService) Update(userID, id uint, in FriendLinkApplyInput if apply.UserID != userID { return nil, errors.New("无权操作该申请") } - if apply.Status != model.FriendLinkApplyStatusPending && - apply.Status != model.FriendLinkApplyStatusRejected && - apply.Status != model.FriendLinkApplyStatusApproved { + if apply.Status != models.FriendLinkApplyStatusPending && + apply.Status != models.FriendLinkApplyStatusRejected && + apply.Status != models.FriendLinkApplyStatusApproved { return nil, errors.New("该申请不可修改") } - wasApproved := apply.Status == model.FriendLinkApplyStatusApproved + wasApproved := apply.Status == models.FriendLinkApplyStatusApproved allowPublishedURL := "" if wasApproved { allowPublishedURL = apply.URL @@ -395,25 +395,25 @@ func (s *FriendLinkApplyService) Update(userID, id uint, in FriendLinkApplyInput "reciprocal_verified": false, "reciprocal_check_note": "", "reciprocal_checked_at": nil, - "status": model.FriendLinkApplyStatusPending, + "status": models.FriendLinkApplyStatusPending, "review_note": "", "reviewed_at": nil, } - if err := model.DB.Model(&apply).Updates(updates).Error; err != nil { + if err := models.DB.Model(&apply).Updates(updates).Error; err != nil { return nil, err } s.startReciprocalCheck(apply.ID, reciprocal, in.OurSiteURL) - _ = model.DB.Preload("User").First(&apply, apply.ID).Error + _ = models.DB.Preload("User").First(&apply, apply.ID).Error return &FriendLinkApplyCreateResult{Apply: &apply}, nil } // RecheckReciprocal 管理员触发重新检测回链 -func (s *FriendLinkApplyService) RecheckReciprocal(id uint, ourSiteURL string) (*model.FriendLinkApply, error) { +func (s *FriendLinkApplyService) RecheckReciprocal(id uint, ourSiteURL string) (*models.FriendLinkApply, error) { if !s.settings.FriendLinkReciprocalCheckEnabled() { return nil, errors.New("回链检测已关闭") } - var apply model.FriendLinkApply - if err := model.DB.Preload("User").First(&apply, id).Error; err != nil { + var apply models.FriendLinkApply + if err := models.DB.Preload("User").First(&apply, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrFriendLinkApplyNotFound } @@ -437,7 +437,7 @@ func (s *FriendLinkApplyService) startReciprocalCheck(applyID uint, pageURL, our return } now := time.Now() - _ = model.DB.Model(&model.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{ + _ = models.DB.Model(&models.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{ "reciprocal_verified": false, "reciprocal_check_note": "", "reciprocal_checked_at": now, @@ -445,9 +445,9 @@ func (s *FriendLinkApplyService) startReciprocalCheck(applyID uint, pageURL, our } // ListMine 当前用户的友链申请 -func (s *FriendLinkApplyService) ListMine(userID uint) ([]model.FriendLinkApply, error) { - var list []model.FriendLinkApply - err := model.DB.Where("user_id = ?", userID). +func (s *FriendLinkApplyService) ListMine(userID uint) ([]models.FriendLinkApply, error) { + var list []models.FriendLinkApply + err := models.DB.Where("user_id = ?", userID). Order("id DESC"). Limit(50). Find(&list).Error @@ -456,8 +456,8 @@ func (s *FriendLinkApplyService) ListMine(userID uint) ([]model.FriendLinkApply, // Cancel 撤销待审申请 func (s *FriendLinkApplyService) Cancel(userID, id uint) error { - var apply model.FriendLinkApply - if err := model.DB.First(&apply, id).Error; err != nil { + var apply models.FriendLinkApply + if err := models.DB.First(&apply, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrFriendLinkApplyNotFound } @@ -466,8 +466,8 @@ func (s *FriendLinkApplyService) Cancel(userID, id uint) error { if apply.UserID != userID { return errors.New("无权操作该申请") } - if apply.Status != model.FriendLinkApplyStatusPending { + if apply.Status != models.FriendLinkApplyStatusPending { return ErrFriendLinkApplyHandled } - return model.DB.Delete(&apply).Error + return models.DB.Delete(&apply).Error } diff --git a/services/friend_link_enrich.go b/services/friend_link_enrich.go index d571656..2b16034 100644 --- a/services/friend_link_enrich.go +++ b/services/friend_link_enrich.go @@ -1,10 +1,10 @@ -package service +package services import ( "encoding/json" "strings" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) // EnrichFriendLinksLogos 为缺少 LOGO 的已发布友链,从已通过申请中按 URL 回填 @@ -27,9 +27,9 @@ func EnrichFriendLinksLogos(links []FriendLink) []FriendLink { return links } - var applies []model.FriendLinkApply - _ = model.DB. - Where("status = ? AND logo <> ''", model.FriendLinkApplyStatusApproved). + var applies []models.FriendLinkApply + _ = models.DB. + Where("status = ? AND logo <> ''", models.FriendLinkApplyStatusApproved). Order("id DESC"). Find(&applies).Error diff --git a/services/friend_link_reciprocal.go b/services/friend_link_reciprocal.go index 9ac06c9..3d8b50f 100644 --- a/services/friend_link_reciprocal.go +++ b/services/friend_link_reciprocal.go @@ -1,4 +1,4 @@ -package service +package services import ( "context" diff --git a/services/friend_link_reciprocal_async.go b/services/friend_link_reciprocal_async.go index 66b963b..db3a80c 100644 --- a/services/friend_link_reciprocal_async.go +++ b/services/friend_link_reciprocal_async.go @@ -1,10 +1,10 @@ -package service +package services import ( "sync" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) const reciprocalCheckConcurrency = 3 @@ -45,7 +45,7 @@ func runReciprocalCheck(applyID uint, gen uint64, pageURL, ourSiteURL string) { } reciprocalCheckMu.Unlock() - _ = model.DB.Model(&model.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{ + _ = models.DB.Model(&models.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{ "reciprocal_verified": verified, "reciprocal_check_note": note, "reciprocal_checked_at": now, @@ -54,7 +54,7 @@ func runReciprocalCheck(applyID uint, gen uint64, pageURL, ourSiteURL string) { // ResetReciprocalCheckState 重置为检测中,供重新检测使用 func ResetReciprocalCheckState(applyID uint) { - _ = model.DB.Model(&model.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{ + _ = models.DB.Model(&models.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{ "reciprocal_verified": false, "reciprocal_check_note": "", "reciprocal_checked_at": nil, diff --git a/services/friend_link_reciprocal_test.go b/services/friend_link_reciprocal_test.go index 95cfe1b..d35ccab 100644 --- a/services/friend_link_reciprocal_test.go +++ b/services/friend_link_reciprocal_test.go @@ -1,4 +1,4 @@ -package service +package services import ( "net/url" diff --git a/services/gitea.go b/services/gitea.go index 77a9526..888082e 100644 --- a/services/gitea.go +++ b/services/gitea.go @@ -1,4 +1,4 @@ -package service +package services import ( "encoding/json" @@ -13,7 +13,7 @@ import ( "sync" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) var ( @@ -26,11 +26,11 @@ type GiteaOwnerView struct { ID uint `json:"id"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` - Role model.Role `json:"role"` + Role models.Role `json:"role"` Verified bool `json:"verified"` Exp int `json:"exp"` Level int `json:"level"` - Badges []model.UserBadgeView `json:"badges,omitempty"` + Badges []models.UserBadgeView `json:"badges,omitempty"` } // GiteaRepoView 前台展示 @@ -123,7 +123,7 @@ func (g *GiteaService) ListPublic(page, size int, q string) ([]GiteaRepoView, in BackfillForumUserIDs() q = strings.TrimSpace(q) - db := model.DB.Model(&model.GiteaRepo{}).Where("private = ? AND forum_user_id IS NOT NULL AND forum_user_id > 0", false) + db := models.DB.Model(&models.GiteaRepo{}).Where("private = ? AND forum_user_id IS NOT NULL AND forum_user_id > 0", false) if q != "" { like := "%" + escapeLikePattern(q) + "%" db = db.Where( @@ -138,7 +138,7 @@ func (g *GiteaService) ListPublic(page, size int, q string) ([]GiteaRepoView, in if err := db.Count(&total).Error; err != nil { return nil, 0, err } - var rows []model.GiteaRepo + var rows []models.GiteaRepo err := db.Order("updated_at_remote desc, id desc"). Offset((page - 1) * size). Limit(size). @@ -155,13 +155,13 @@ func (g *GiteaService) ListPublic(page, size int, q string) ([]GiteaRepoView, in // BackfillForumUserIDs 将缺失 forum_user_id 的公开仓按 owner_login(忽略大小写)匹配论坛 username func BackfillForumUserIDs() int { - var rows []model.GiteaRepo - if err := model.DB.Where("private = ? AND (forum_user_id IS NULL OR forum_user_id = 0)", false). + var rows []models.GiteaRepo + if err := models.DB.Where("private = ? AND (forum_user_id IS NULL OR forum_user_id = 0)", false). Find(&rows).Error; err != nil || len(rows) == 0 { return 0 } - var users []model.User - if err := model.DB.Select("id", "username").Where("banned = ?", false).Find(&users).Error; err != nil || len(users) == 0 { + var users []models.User + if err := models.DB.Select("id", "username").Where("banned = ?", false).Find(&users).Error; err != nil || len(users) == 0 { return 0 } byLogin := make(map[string]uint, len(users)) @@ -179,7 +179,7 @@ func BackfillForumUserIDs() int { if !ok || uid == 0 { continue } - if err := model.DB.Model(&rows[i]).Update("forum_user_id", uid).Error; err != nil { + if err := models.DB.Model(&rows[i]).Update("forum_user_id", uid).Error; err != nil { log.Printf("[gitea] 回填 forum_user_id 失败 repo=%s: %v", rows[i].FullName, err) continue } @@ -222,13 +222,13 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie logins = append(logins, key) } - byID := make(map[uint]*model.User) - byLogin := make(map[string]*model.User) - ptrs := make([]*model.User, 0, len(ids)+len(logins)) + byID := make(map[uint]*models.User) + byLogin := make(map[string]*models.User) + ptrs := make([]*models.User, 0, len(ids)+len(logins)) if len(ids) > 0 { - var users []model.User - if err := model.DB.Where("id IN ? AND banned = ?", ids, false).Find(&users).Error; err == nil { + var users []models.User + if err := models.DB.Where("id IN ? AND banned = ?", ids, false).Find(&users).Error; err == nil { for i := range users { u := &users[i] byID[u.ID] = u @@ -241,8 +241,8 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie } } if len(logins) > 0 { - var users []model.User - if err := model.DB.Where("banned = ? AND LOWER(username) IN ?", false, logins).Find(&users).Error; err == nil { + var users []models.User + if err := models.DB.Where("banned = ? AND LOWER(username) IN ?", false, logins).Find(&users).Error; err == nil { for i := range users { u := &users[i] key := strings.ToLower(strings.TrimSpace(u.Username)) @@ -264,7 +264,7 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie } // 去重 ptrs seenPtr := make(map[uint]struct{}, len(ptrs)) - uniq := make([]*model.User, 0, len(ptrs)) + uniq := make([]*models.User, 0, len(ptrs)) for _, u := range ptrs { if u == nil || u.ID == 0 { continue @@ -279,14 +279,14 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie badge.AttachBadgeSummaries(uniq, 3) } else { for _, u := range uniq { - u.Level = model.LevelFromExp(u.Exp) + u.Level = models.LevelFromExp(u.Exp) } } out := make([]GiteaRepoView, 0, len(list)) for i := range list { item := list[i] - var u *model.User + var u *models.User if item.ForumUserID != nil && *item.ForumUserID > 0 { u = byID[*item.ForumUserID] } @@ -297,7 +297,7 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie uid := u.ID item.ForumUserID = &uid // 回写缺失关联,便于下次列表过滤命中 - _ = model.DB.Model(&model.GiteaRepo{}).Where("id = ?", item.ID). + _ = models.DB.Model(&models.GiteaRepo{}).Where("id = ?", item.ID). Update("forum_user_id", uid).Error } } @@ -315,7 +315,7 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie Role: u.Role, Verified: u.Verified, Exp: u.Exp, - Level: model.LevelFromExp(u.Exp), + Level: models.LevelFromExp(u.Exp), Badges: u.Badges, } out = append(out, item) @@ -346,8 +346,8 @@ func (g *GiteaService) SyncRepos() (int, error) { // 同步前先回填历史缺失关联 BackfillForumUserIDs() - var users []model.User - if err := model.DB.Where("banned = ?", false).Select("id", "username").Find(&users).Error; err != nil { + var users []models.User + if err := models.DB.Where("banned = ?", false).Select("id", "username").Find(&users).Error; err != nil { return 0, err } @@ -378,7 +378,7 @@ func (g *GiteaService) SyncRepos() (int, error) { if owner == "" { owner = username } - row := model.GiteaRepo{ + row := models.GiteaRepo{ GiteaID: gr.ID, OwnerLogin: owner, Name: gr.Name, @@ -393,16 +393,16 @@ func (g *GiteaService) SyncRepos() (int, error) { ForumUserID: &uid, SyncedAt: now, } - var existing model.GiteaRepo - err := model.DB.Where("gitea_id = ?", gr.ID).First(&existing).Error + var existing models.GiteaRepo + err := models.DB.Where("gitea_id = ?", gr.ID).First(&existing).Error if err != nil { - if err := model.DB.Create(&row).Error; err != nil { + if err := models.DB.Create(&row).Error; err != nil { log.Printf("[gitea] 创建仓库失败 %s: %v", gr.FullName, err) continue } } else { row.ID = existing.ID - if err := model.DB.Model(&existing).Updates(map[string]any{ + if err := models.DB.Model(&existing).Updates(map[string]any{ "owner_login": row.OwnerLogin, "name": row.Name, "full_name": row.FullName, @@ -426,14 +426,14 @@ func (g *GiteaService) SyncRepos() (int, error) { // 仅清理本次成功同步到的 owner 下、却未再出现的旧记录 if len(syncedOwners) > 0 { - var all []model.GiteaRepo - if err := model.DB.Where("private = ?", false).Find(&all).Error; err == nil { + var all []models.GiteaRepo + if err := models.DB.Where("private = ?", false).Find(&all).Error; err == nil { for _, r := range all { if _, ok := syncedOwners[strings.ToLower(r.OwnerLogin)]; !ok { continue } if _, ok := seen[r.GiteaID]; !ok { - _ = model.DB.Delete(&r).Error + _ = models.DB.Delete(&r).Error } } } @@ -515,7 +515,7 @@ func (g *GiteaService) fetchUserPublicRepos(baseURL, token, username string) ([] return all, nil } -func toGiteaRepoView(r model.GiteaRepo) GiteaRepoView { +func toGiteaRepoView(r models.GiteaRepo) GiteaRepoView { return GiteaRepoView{ ID: r.ID, GiteaID: r.GiteaID, diff --git a/services/image_webp.go b/services/image_webp.go index 33a92c5..f66783f 100644 --- a/services/image_webp.go +++ b/services/image_webp.go @@ -1,4 +1,4 @@ -package service +package services import ( "bytes" diff --git a/services/lottery_post.go b/services/lottery_post.go index 83533be..5c99b75 100644 --- a/services/lottery_post.go +++ b/services/lottery_post.go @@ -1,11 +1,11 @@ -package service +package services import ( "crypto/rand" "errors" "math/big" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -34,15 +34,15 @@ func InitPostLottery(postID uint, winnerCount int) error { if winnerCount < 1 || winnerCount > 20 { return errors.New("开奖人数需 1-20") } - return model.DB.Model(&model.Post{}).Where("id = ?", postID).Updates(map[string]interface{}{ + return models.DB.Model(&models.Post{}).Where("id = ?", postID).Updates(map[string]interface{}{ "lottery_winner_count": winnerCount, - "lottery_status": model.PostLotteryStatusOpen, + "lottery_status": models.PostLotteryStatusOpen, }).Error } // GetPostLotteryView 获取抽奖视图 -func GetPostLotteryView(post *model.Post) (*PostLotteryView, error) { - if post == nil || post.PostType != model.PostTypeLottery { +func GetPostLotteryView(post *models.Post) (*PostLotteryView, error) { + if post == nil || post.PostType != models.PostTypeLottery { return nil, nil } participants, err := lotteryParticipants(post.ID, post.UserID) @@ -54,9 +54,9 @@ func GetPostLotteryView(post *model.Post) (*PostLotteryView, error) { Status: post.LotteryStatus, ParticipantCount: len(participants), } - if post.LotteryStatus == model.PostLotteryStatusDrawn { - var winners []model.PostLotteryWinner - model.DB.Preload("User").Where("post_id = ?", post.ID).Find(&winners) + if post.LotteryStatus == models.PostLotteryStatusDrawn { + var winners []models.PostLotteryWinner + models.DB.Preload("User").Where("post_id = ?", post.ID).Find(&winners) for _, w := range winners { view.Winners = append(view.Winners, PostLotteryWinnerView{ UserID: w.UserID, Username: w.User.Username, Nickname: w.User.Nickname, @@ -67,15 +67,15 @@ func GetPostLotteryView(post *model.Post) (*PostLotteryView, error) { return view, nil } -func lotteryParticipants(postID, authorID uint) ([]model.Comment, error) { - var comments []model.Comment - err := model.DB.Where("post_id = ? AND status = ? AND user_id <> ?", postID, model.ContentStatusPublished, authorID). +func lotteryParticipants(postID, authorID uint) ([]models.Comment, error) { + var comments []models.Comment + err := models.DB.Where("post_id = ? AND status = ? AND user_id <> ?", postID, models.ContentStatusPublished, authorID). Order("id ASC").Find(&comments).Error if err != nil { return nil, err } seen := map[uint]bool{} - var unique []model.Comment + var unique []models.Comment for _, c := range comments { if seen[c.UserID] { continue @@ -88,17 +88,17 @@ func lotteryParticipants(postID, authorID uint) ([]model.Comment, error) { // DrawPostLottery 开奖 func DrawPostLottery(postID, operatorID uint, isAdmin bool) (*PostLotteryView, error) { - var post model.Post - if err := model.DB.First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.First(&post, postID).Error; err != nil { return nil, ErrPostNotFound } - if post.PostType != model.PostTypeLottery { + if post.PostType != models.PostTypeLottery { return nil, errors.New("非抽奖帖") } if !isAdmin && post.UserID != operatorID { return nil, ErrPermissionDenied } - if post.LotteryStatus == model.PostLotteryStatusDrawn { + if post.LotteryStatus == models.PostLotteryStatusDrawn { return nil, ErrLotteryAlreadyDrawn } participants, err := lotteryParticipants(postID, post.UserID) @@ -113,25 +113,25 @@ func DrawPostLottery(postID, operatorID uint, isAdmin bool) (*PostLotteryView, e return nil, ErrLotteryNotEnough } picked := randomPickComments(participants, need) - err = model.DB.Transaction(func(tx *gorm.DB) error { + err = models.DB.Transaction(func(tx *gorm.DB) error { for _, c := range picked { - w := model.PostLotteryWinner{PostID: postID, UserID: c.UserID, CommentID: c.ID} + w := models.PostLotteryWinner{PostID: postID, UserID: c.UserID, CommentID: c.ID} if err := tx.Create(&w).Error; err != nil { return err } } - return tx.Model(&post).Update("lottery_status", model.PostLotteryStatusDrawn).Error + return tx.Model(&post).Update("lottery_status", models.PostLotteryStatusDrawn).Error }) if err != nil { return nil, err } - post.LotteryStatus = model.PostLotteryStatusDrawn + post.LotteryStatus = models.PostLotteryStatusDrawn return GetPostLotteryView(&post) } -func randomPickComments(comments []model.Comment, n int) []model.Comment { - pool := append([]model.Comment{}, comments...) - out := make([]model.Comment, 0, n) +func randomPickComments(comments []models.Comment, n int) []models.Comment { + pool := append([]models.Comment{}, comments...) + out := make([]models.Comment, 0, n) for i := 0; i < n && len(pool) > 0; i++ { idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(pool)))) if err != nil { @@ -146,5 +146,5 @@ func randomPickComments(comments []model.Comment, n int) []model.Comment { // DeleteLotteryData 删帖清理 func DeleteLotteryData(tx *gorm.DB, postID uint) { - tx.Where("post_id = ?", postID).Delete(&model.PostLotteryWinner{}) + tx.Where("post_id = ?", postID).Delete(&models.PostLotteryWinner{}) } diff --git a/services/mail.go b/services/mail.go index 6eefb6e..9cdc0b6 100644 --- a/services/mail.go +++ b/services/mail.go @@ -1,4 +1,4 @@ -package service +package services import ( "crypto/tls" diff --git a/services/mail_template.go b/services/mail_template.go index f637bf6..f70182c 100644 --- a/services/mail_template.go +++ b/services/mail_template.go @@ -1,4 +1,4 @@ -package service +package services import ( "fmt" diff --git a/services/media.go b/services/media.go index 06156e4..4d1546b 100644 --- a/services/media.go +++ b/services/media.go @@ -1,4 +1,4 @@ -package service +package services import ( "context" @@ -12,7 +12,7 @@ import ( "github.com/minio/minio-go/v7" "git.iioio.com/freefire/jiang13-forum/config" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) // MediaItem 管理端媒体资源条目 @@ -47,7 +47,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL if s == nil { return nil, errors.New("上传存储未初始化") } - if model.DB == nil { + if models.DB == nil { return nil, errors.New("数据库未初始化") } if page < 1 { @@ -69,7 +69,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL // 索引为空时先同步一次,避免升级后首次打开空白 var indexed int64 - _ = model.DB.Model(&model.Media{}).Count(&indexed).Error + _ = models.DB.Model(&models.Media{}).Count(&indexed).Error if indexed == 0 { _, _ = s.SyncMediaIndex() } @@ -84,7 +84,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL Cnt int } var rows []catCount - if err := model.DB.Model(&model.Media{}). + if err := models.DB.Model(&models.Media{}). Select("category, count(*) as cnt"). Group("category"). Scan(&rows).Error; err != nil { @@ -94,7 +94,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL counts[r.Category] = r.Cnt } - dbq := model.DB.Model(&model.Media{}) + dbq := models.DB.Model(&models.Media{}) if category != "all" { dbq = dbq.Where("category = ?", category) } @@ -115,7 +115,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL page = totalPages } - var records []model.Media + var records []models.Media offset := (page - 1) * size if err := dbq.Order("created_at desc, id desc").Offset(offset).Limit(size).Find(&records).Error; err != nil { return nil, err @@ -178,7 +178,7 @@ func (s *UploadStore) DeleteMedia(urls []string) (int, error) { // SyncMediaIndex 扫描当前存储后端,回填/校正媒体索引;返回写入或更新条数 func (s *UploadStore) SyncMediaIndex() (int, error) { - if s == nil || model.DB == nil { + if s == nil || models.DB == nil { return 0, errors.New("存储或数据库未初始化") } mode, _, _, _ := s.snapshot() @@ -206,19 +206,19 @@ func (s *UploadStore) SyncMediaIndex() (int, error) { } // 清理当前后端下已不存在的索引(其它后端记录保留) - var stale []model.Media - _ = model.DB.Where("storage_type = ?", storageType).Find(&stale).Error + var stale []models.Media + _ = models.DB.Where("storage_type = ?", storageType).Find(&stale).Error for _, row := range stale { if _, ok := seen[row.URL]; ok { continue } - _ = model.DB.Delete(&model.Media{}, row.ID).Error + _ = models.DB.Delete(&models.Media{}, row.ID).Error } return n, nil } func (s *UploadStore) upsertMediaRecord(category, name, url string, size int64, contentType, storageType string, userID *uint) error { - if model.DB == nil || strings.TrimSpace(url) == "" { + if models.DB == nil || strings.TrimSpace(url) == "" { return nil } category = strings.TrimSpace(category) @@ -231,8 +231,8 @@ func (s *UploadStore) upsertMediaRecord(category, name, url string, size int64, contentType = imageContentType(strings.ToLower(filepath.Ext(name))) } - var existing model.Media - err := model.DB.Where("url = ?", url).First(&existing).Error + var existing models.Media + err := models.DB.Where("url = ?", url).First(&existing).Error if err == nil { updates := map[string]interface{}{ "category": category, @@ -244,10 +244,10 @@ func (s *UploadStore) upsertMediaRecord(category, name, url string, size int64, if userID != nil { updates["user_id"] = *userID } - return model.DB.Model(&existing).Updates(updates).Error + return models.DB.Model(&existing).Updates(updates).Error } - rec := model.Media{ + rec := models.Media{ Category: category, Name: name, URL: url, @@ -256,11 +256,11 @@ func (s *UploadStore) upsertMediaRecord(category, name, url string, size int64, StorageType: storageType, UserID: userID, } - return model.DB.Create(&rec).Error + return models.DB.Create(&rec).Error } func (s *UploadStore) deleteMediaRecords(urls []string) { - if model.DB == nil || len(urls) == 0 { + if models.DB == nil || len(urls) == 0 { return } clean := make([]string, 0, len(urls)) @@ -276,7 +276,7 @@ func (s *UploadStore) deleteMediaRecords(urls []string) { if len(clean) == 0 { return } - _ = model.DB.Where("url IN ?", clean).Delete(&model.Media{}).Error + _ = models.DB.Where("url IN ?", clean).Delete(&models.Media{}).Error } func (s *UploadStore) resolveSiblingPublicURLs(rawURL string) []string { diff --git a/services/mention.go b/services/mention.go index cdfd165..8071068 100644 --- a/services/mention.go +++ b/services/mention.go @@ -1,10 +1,10 @@ -package service +package services import ( "regexp" "strings" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) const maxMentionsPerContent = 10 @@ -47,10 +47,10 @@ func ResolveMentionUserIDs(names []string, excludeUserID uint) []uint { ids := make([]uint, 0, len(names)) seen := make(map[uint]struct{}, len(names)) for _, name := range names { - var u model.User - err := model.DB.Select("id").Where("username = ?", name).First(&u).Error + var u models.User + err := models.DB.Select("id").Where("username = ?", name).First(&u).Error if err != nil { - err = model.DB.Select("id").Where("nickname = ?", name).First(&u).Error + err = models.DB.Select("id").Where("nickname = ?", name).First(&u).Error } if err != nil || u.ID == 0 || u.ID == excludeUserID { continue diff --git a/services/mention_test.go b/services/mention_test.go index 4161806..d3de930 100644 --- a/services/mention_test.go +++ b/services/mention_test.go @@ -1,4 +1,4 @@ -package service +package services import ( "reflect" diff --git a/services/message.go b/services/message.go index da46952..13863d5 100644 --- a/services/message.go +++ b/services/message.go @@ -1,4 +1,4 @@ -package service +package services import ( "errors" @@ -7,7 +7,7 @@ import ( "time" "unicode/utf8" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) var ( @@ -34,7 +34,7 @@ type MessageSendInput struct { } // Send 发送私信(用户互发或系统通知) -func (s *MessageService) Send(in MessageSendInput) (*model.PrivateMessage, error) { +func (s *MessageService) Send(in MessageSendInput) (*models.PrivateMessage, error) { if in.ToUserID == 0 { return nil, errors.New("收件人不存在") } @@ -42,8 +42,8 @@ func (s *MessageService) Send(in MessageSendInput) (*model.PrivateMessage, error return nil, ErrCannotMessageSelf } if in.FromUserID > 0 { - var to model.User - if err := model.DB.Select("id", "banned").First(&to, in.ToUserID).Error; err != nil { + var to models.User + if err := models.DB.Select("id", "banned").First(&to, in.ToUserID).Error; err != nil { return nil, errors.New("收件人不存在") } if to.Banned { @@ -75,13 +75,13 @@ func (s *MessageService) Send(in MessageSendInput) (*model.PrivateMessage, error kind := in.Kind if kind == "" { if in.FromUserID == 0 { - kind = model.MessageKindSystem + kind = models.MessageKindSystem } else { - kind = model.MessageKindUser + kind = models.MessageKindUser } } - msg := &model.PrivateMessage{ + msg := &models.PrivateMessage{ FromUserID: in.FromUserID, ToUserID: in.ToUserID, Subject: subject, @@ -91,17 +91,17 @@ func (s *MessageService) Send(in MessageSendInput) (*model.PrivateMessage, error RelatedReportID: in.RelatedReportID, IsRead: false, } - if err := model.DB.Create(msg).Error; err != nil { + if err := models.DB.Create(msg).Error; err != nil { return nil, err } - _ = model.DB.Preload("FromUser").Preload("ToUser").First(msg, msg.ID).Error + _ = models.DB.Preload("FromUser").Preload("ToUser").First(msg, msg.ID).Error return msg, nil } // SendSystem 系统私信(管理员/系统 → 用户) -func (s *MessageService) SendSystem(toUserID uint, subject, content, kind string, relatedPostID, relatedReportID *uint) (*model.PrivateMessage, error) { +func (s *MessageService) SendSystem(toUserID uint, subject, content, kind string, relatedPostID, relatedReportID *uint) (*models.PrivateMessage, error) { if kind == "" { - kind = model.MessageKindSystem + kind = models.MessageKindSystem } return s.Send(MessageSendInput{ FromUserID: 0, @@ -116,7 +116,7 @@ func (s *MessageService) SendSystem(toUserID uint, subject, content, kind string // MarkAllRead 全部标为已读 func (s *MessageService) MarkAllRead(userID uint) error { - return model.DB.Model(&model.PrivateMessage{}). + return models.DB.Model(&models.PrivateMessage{}). Where("to_user_id = ? AND is_read = ?", userID, false). Update("is_read", true).Error } @@ -124,7 +124,7 @@ func (s *MessageService) MarkAllRead(userID uint) error { // UnreadCount 未读数 func (s *MessageService) UnreadCount(userID uint) (int64, error) { var n int64 - err := model.DB.Model(&model.PrivateMessage{}). + err := models.DB.Model(&models.PrivateMessage{}). Where("to_user_id = ? AND is_read = ?", userID, false). Count(&n).Error return n, err @@ -132,13 +132,13 @@ func (s *MessageService) UnreadCount(userID uint) (int64, error) { // UnreadCounts 未读总数,以及私信 / 系统通知分项 func (s *MessageService) UnreadCounts(userID uint) (total, dm, notify int64, err error) { - err = model.DB.Model(&model.PrivateMessage{}). + err = models.DB.Model(&models.PrivateMessage{}). Where("to_user_id = ? AND is_read = ?", userID, false). Count(&total).Error if err != nil { return 0, 0, 0, err } - err = model.DB.Model(&model.PrivateMessage{}). + err = models.DB.Model(&models.PrivateMessage{}). Where("to_user_id = ? AND is_read = ? AND from_user_id = 0", userID, false). Count(¬ify).Error if err != nil { @@ -152,12 +152,12 @@ func (s *MessageService) UnreadCounts(userID uint) (total, dm, notify int64, err } // ListNotifications 系统通知列表(按时间倒序,非聊天气泡) -func (s *MessageService) ListNotifications(userID uint, page, size int, kind string) ([]model.PrivateMessage, int64, error) { +func (s *MessageService) ListNotifications(userID uint, page, size int, kind string) ([]models.PrivateMessage, int64, error) { if page < 1 { page = 1 } size = s.settings.NormalizePageSize(size) - db := model.DB.Model(&model.PrivateMessage{}). + db := models.DB.Model(&models.PrivateMessage{}). Where("from_user_id = 0 AND to_user_id = ?", userID) kind = strings.TrimSpace(kind) if kind != "" && kind != "all" { @@ -167,13 +167,13 @@ func (s *MessageService) ListNotifications(userID uint, page, size int, kind str if err := db.Count(&total).Error; err != nil { return nil, 0, err } - var list []model.PrivateMessage + var list []models.PrivateMessage err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&list).Error if err != nil { return nil, 0, err } if list == nil { - list = []model.PrivateMessage{} + list = []models.PrivateMessage{} } return list, total, nil } @@ -186,9 +186,9 @@ func (s *MessageService) MarkNotificationsRead(userID uint) error { // MessageConversation 按对方聚合的会话摘要 type MessageConversation struct { PeerUserID uint `json:"peer_user_id"` // 0 = 系统通知 - PeerUser *model.User `json:"peer_user,omitempty"` + PeerUser *models.User `json:"peer_user,omitempty"` IsSystem bool `json:"is_system"` - LastMessage *model.PrivateMessage `json:"last_message,omitempty"` + LastMessage *models.PrivateMessage `json:"last_message,omitempty"` UnreadCount int64 `json:"unread_count"` UpdatedAt time.Time `json:"updated_at"` } @@ -220,7 +220,7 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo } var rows []peerRow // peer_id:系统通知为 0;否则为对话另一方 - err := model.DB.Raw(` + err := models.DB.Raw(` SELECT CASE WHEN from_user_id = 0 THEN 0 @@ -239,7 +239,7 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo } var total int64 - err = model.DB.Raw(` + err = models.DB.Raw(` SELECT COUNT(*) FROM ( SELECT CASE @@ -268,20 +268,20 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo } } - var lastMsgs []model.PrivateMessage - if err := model.DB.Preload("FromUser").Preload("ToUser"). + var lastMsgs []models.PrivateMessage + if err := models.DB.Preload("FromUser").Preload("ToUser"). Where("id IN ?", lastIDs).Find(&lastMsgs).Error; err != nil { return nil, 0, err } - msgByID := make(map[uint]model.PrivateMessage, len(lastMsgs)) + msgByID := make(map[uint]models.PrivateMessage, len(lastMsgs)) for i := range lastMsgs { msgByID[lastMsgs[i].ID] = lastMsgs[i] } - usersByID := make(map[uint]model.User) + usersByID := make(map[uint]models.User) if len(peerIDs) > 0 { - var users []model.User - if err := model.DB.Where("id IN ?", peerIDs).Find(&users).Error; err != nil { + var users []models.User + if err := models.DB.Where("id IN ?", peerIDs).Find(&users).Error; err != nil { return nil, 0, err } for i := range users { @@ -294,7 +294,7 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo Cnt int64 } var unreadRows []unreadRow - _ = model.DB.Raw(` + _ = models.DB.Raw(` SELECT CASE WHEN from_user_id = 0 THEN 0 ELSE from_user_id END AS peer_id, COUNT(*) AS cnt @@ -332,13 +332,13 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo } // ListConversationMessages 某会话内消息(时间正序,支持 Before 向上翻页) -func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) ([]model.PrivateMessage, int64, error) { +func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) ([]models.PrivateMessage, int64, error) { if q.Page < 1 { q.Page = 1 } q.Size = s.settings.NormalizePageSize(q.Size) - countDB := model.DB.Model(&model.PrivateMessage{}) + countDB := models.DB.Model(&models.PrivateMessage{}) if q.PeerID == 0 { countDB = countDB.Where("from_user_id = 0 AND to_user_id = ?", q.UserID) } else { @@ -353,7 +353,7 @@ func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) ( return nil, 0, err } - qdb := model.DB.Preload("FromUser").Preload("ToUser") + qdb := models.DB.Preload("FromUser").Preload("ToUser") if q.PeerID == 0 { qdb = qdb.Where("from_user_id = 0 AND to_user_id = ?", q.UserID) } else { @@ -366,7 +366,7 @@ func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) ( qdb = qdb.Where("id < ?", q.Before) } - var list []model.PrivateMessage + var list []models.PrivateMessage // 先按 id desc 取一页,再反转为正序(聊天从旧到新) err := qdb.Order("id desc").Limit(q.Size).Find(&list).Error if err != nil { @@ -380,7 +380,7 @@ func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) ( // MarkConversationRead 将会话内未读标为已读 func (s *MessageService) MarkConversationRead(userID, peerID uint) error { - db := model.DB.Model(&model.PrivateMessage{}). + db := models.DB.Model(&models.PrivateMessage{}). Where("to_user_id = ? AND is_read = ?", userID, false) if peerID == 0 { db = db.Where("from_user_id = 0") diff --git a/services/notify.go b/services/notify.go index b2800b0..84c8a0d 100644 --- a/services/notify.go +++ b/services/notify.go @@ -1,11 +1,11 @@ -package service +package services import ( "fmt" "strings" "unicode/utf8" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) // NotifyService 站内消息 + 邮件提醒编排 @@ -35,7 +35,7 @@ func (s *NotifyService) goNotify(fn func()) { } // AsyncNotifyCommentPublished 异步:评论公开后通知被回复者或楼主 -func (s *NotifyService) AsyncNotifyCommentPublished(comment *model.Comment) { +func (s *NotifyService) AsyncNotifyCommentPublished(comment *models.Comment) { if s == nil || comment == nil { return } @@ -44,7 +44,7 @@ func (s *NotifyService) AsyncNotifyCommentPublished(comment *model.Comment) { } // AsyncNotifyCommentMentions 异步:评论公开后通知被 @ 的用户 -func (s *NotifyService) AsyncNotifyCommentMentions(comment *model.Comment) { +func (s *NotifyService) AsyncNotifyCommentMentions(comment *models.Comment) { if s == nil || comment == nil { return } @@ -53,7 +53,7 @@ func (s *NotifyService) AsyncNotifyCommentMentions(comment *model.Comment) { } // AsyncNotifyPendingPost 异步:待审帖通知管理员 -func (s *NotifyService) AsyncNotifyPendingPost(post *model.Post) { +func (s *NotifyService) AsyncNotifyPendingPost(post *models.Post) { if s == nil || post == nil { return } @@ -62,7 +62,7 @@ func (s *NotifyService) AsyncNotifyPendingPost(post *model.Post) { } // AsyncNotifyPendingComment 异步:待审评论通知管理员 -func (s *NotifyService) AsyncNotifyPendingComment(comment *model.Comment) { +func (s *NotifyService) AsyncNotifyPendingComment(comment *models.Comment) { if s == nil || comment == nil { return } @@ -71,8 +71,8 @@ func (s *NotifyService) AsyncNotifyPendingComment(comment *model.Comment) { } // NotifyCommentPublished 评论公开后通知被回复者或楼主 -func (s *NotifyService) NotifyCommentPublished(comment *model.Comment) { - if s == nil || comment == nil || comment.Status != model.ContentStatusPublished { +func (s *NotifyService) NotifyCommentPublished(comment *models.Comment) { + if s == nil || comment == nil || comment.Status != models.ContentStatusPublished { return } @@ -96,14 +96,14 @@ func (s *NotifyService) NotifyCommentPublished(comment *model.Comment) { subject := "收到新回复" content := FormatReplyContent(authorName, title, displayFloor, isNested) pid := comment.PostID - _, _ = s.messages.SendSystem(toUserID, subject, content, model.MessageKindReply, &pid, nil) + _, _ = s.messages.SendSystem(toUserID, subject, content, models.MessageKindReply, &pid, nil) s.sendReplyMail(toUserID, authorName, title, comment.PostID, displayFloor, isNested, comment.Content) } // NotifyCommentMentions 评论公开后通知被 @提及的用户(跳过已收到回复通知的人) -func (s *NotifyService) NotifyCommentMentions(comment *model.Comment) { - if s == nil || comment == nil || comment.Status != model.ContentStatusPublished { +func (s *NotifyService) NotifyCommentMentions(comment *models.Comment) { + if s == nil || comment == nil || comment.Status != models.ContentStatusPublished { return } names := ExtractMentionNames(comment.Content) @@ -132,13 +132,13 @@ func (s *NotifyService) NotifyCommentMentions(comment *model.Comment) { if uid == 0 || uid == comment.UserID || uid == replyTo { continue } - _, _ = s.messages.SendSystem(uid, subject, content, model.MessageKindMention, &pid, nil) + _, _ = s.messages.SendSystem(uid, subject, content, models.MessageKindMention, &pid, nil) } } // NotifyPendingPost 新帖进入待审时通知全部管理员 -func (s *NotifyService) NotifyPendingPost(post *model.Post) { - if s == nil || post == nil || post.Status != model.ContentStatusPending { +func (s *NotifyService) NotifyPendingPost(post *models.Post) { + if s == nil || post == nil || post.Status != models.ContentStatusPending { return } title := strings.TrimSpace(post.Title) @@ -149,14 +149,14 @@ func (s *NotifyService) NotifyPendingPost(post *model.Post) { subject := "新的待审核帖子" content := FormatPendingPostContent(authorName, title, post.ID) pid := post.ID - s.notifyAdmins(subject, content, model.MessageKindModeration, &pid, func(siteName, baseURL string) (string, string, string) { + s.notifyAdmins(subject, content, models.MessageKindModeration, &pid, func(siteName, baseURL string) (string, string, string) { return BuildModerationMail(siteName, "帖子", authorName, title, post.ID, 0, false, AbsoluteURL(baseURL, "/admin/posts")) }) } // NotifyPendingComment 新评论进入待审时通知全部管理员 -func (s *NotifyService) NotifyPendingComment(comment *model.Comment) { - if s == nil || comment == nil || comment.Status != model.ContentStatusPending { +func (s *NotifyService) NotifyPendingComment(comment *models.Comment) { + if s == nil || comment == nil || comment.Status != models.ContentStatusPending { return } post, err := s.loadPost(comment.PostID) @@ -173,7 +173,7 @@ func (s *NotifyService) NotifyPendingComment(comment *model.Comment) { isNested := comment.ReplyTo != nil && *comment.ReplyTo > 0 content := FormatPendingCommentContent(authorName, title, displayFloor, isNested) pid := comment.PostID - s.notifyAdmins(subject, content, model.MessageKindModeration, &pid, func(siteName, baseURL string) (string, string, string) { + s.notifyAdmins(subject, content, models.MessageKindModeration, &pid, func(siteName, baseURL string) (string, string, string) { return BuildModerationMail(siteName, "评论", authorName, title, comment.PostID, displayFloor, isNested, AbsoluteURL(baseURL, "/admin/comments")) }) } @@ -215,8 +215,8 @@ func (s *NotifyService) sendReplyMail(toUserID uint, authorName, postTitle strin if s.mail == nil || !s.settings.MailReady() { return } - var user model.User - if err := model.DB.Select("id", "email", "nickname", "username").First(&user, toUserID).Error; err != nil { + var user models.User + if err := models.DB.Select("id", "email", "nickname", "username").First(&user, toUserID).Error; err != nil { return } email := strings.TrimSpace(user.Email) @@ -232,10 +232,10 @@ func (s *NotifyService) sendReplyMail(toUserID uint, authorName, postTitle strin _ = s.mail.SendHTML(email, subj, text, html) } -func (s *NotifyService) resolveReplyRecipient(comment *model.Comment, post *model.Post) (uint, error) { +func (s *NotifyService) resolveReplyRecipient(comment *models.Comment, post *models.Post) (uint, error) { if comment.ReplyTo != nil && *comment.ReplyTo > 0 { - var target model.Comment - if err := model.DB.Select("id", "user_id", "post_id"). + var target models.Comment + if err := models.DB.Select("id", "user_id", "post_id"). Where("id = ? AND post_id = ?", *comment.ReplyTo, comment.PostID). First(&target).Error; err != nil { return 0, err @@ -249,7 +249,7 @@ func (s *NotifyService) resolveReplyRecipient(comment *model.Comment, post *mode } // resolveDisplayFloor 解析页面可见的顶层楼号(子回复沿 reply_to 上溯) -func (s *NotifyService) resolveDisplayFloor(comment *model.Comment) int { +func (s *NotifyService) resolveDisplayFloor(comment *models.Comment) int { if comment == nil { return 0 } @@ -264,8 +264,8 @@ func (s *NotifyService) resolveDisplayFloor(comment *model.Comment) int { break } seen[curID] = struct{}{} - var ancestor model.Comment - if err := model.DB.Select("id", "floor", "reply_to"). + var ancestor models.Comment + if err := models.DB.Select("id", "floor", "reply_to"). Where("id = ? AND post_id = ?", curID, comment.PostID). First(&ancestor).Error; err != nil { return comment.Floor @@ -278,18 +278,18 @@ func (s *NotifyService) resolveDisplayFloor(comment *model.Comment) int { return comment.Floor } -func (s *NotifyService) loadPost(postID uint) (*model.Post, error) { - var post model.Post - if err := model.DB.Select("id", "user_id", "title", "status").First(&post, postID).Error; err != nil { +func (s *NotifyService) loadPost(postID uint) (*models.Post, error) { + var post models.Post + if err := models.DB.Select("id", "user_id", "title", "status").First(&post, postID).Error; err != nil { return nil, err } return &post, nil } -func (s *NotifyService) listAdmins() ([]model.User, error) { - var admins []model.User - err := model.DB.Select("id", "email", "nickname", "username"). - Where("role = ? AND banned = ?", model.RoleAdmin, false). +func (s *NotifyService) listAdmins() ([]models.User, error) { + var admins []models.User + err := models.DB.Select("id", "email", "nickname", "username"). + Where("role = ? AND banned = ?", models.RoleAdmin, false). Find(&admins).Error return admins, err } @@ -302,7 +302,7 @@ func (s *NotifyService) siteName() string { return name } -func (s *NotifyService) commentAuthorName(comment *model.Comment) string { +func (s *NotifyService) commentAuthorName(comment *models.Comment) string { if comment.UserID > 0 { if comment.User.ID == comment.UserID { if n := DisplayName(&comment.User); n != "" { @@ -321,8 +321,8 @@ func (s *NotifyService) userDisplayName(userID uint) string { if userID == 0 { return "用户" } - var u model.User - if err := model.DB.Select("id", "nickname", "username").First(&u, userID).Error; err != nil { + var u models.User + if err := models.DB.Select("id", "nickname", "username").First(&u, userID).Error; err != nil { return fmt.Sprintf("用户 #%d", userID) } if n := DisplayName(&u); n != "" { diff --git a/services/oauth_clients.go b/services/oauth_clients.go index 56c3e55..d17a26c 100644 --- a/services/oauth_clients.go +++ b/services/oauth_clients.go @@ -1,4 +1,4 @@ -package service +package services import ( "crypto/rand" @@ -7,7 +7,7 @@ import ( "strings" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) var ( @@ -44,8 +44,8 @@ type OAuthClientInput struct { // ListOAuthClients 列出全部 OAuth 应用 func (s *ForumSettingsService) ListOAuthClients() ([]OAuthClientView, error) { - var rows []model.OAuthClient - if err := model.DB.Order("id asc").Find(&rows).Error; err != nil { + var rows []models.OAuthClient + if err := models.DB.Order("id asc").Find(&rows).Error; err != nil { return nil, err } out := make([]OAuthClientView, 0, len(rows)) @@ -64,7 +64,7 @@ func (s *ForumSettingsService) CreateOAuthClient(in OAuthClientInput) (*OAuthCli return nil, ErrOAuthClientInvalid } var n int64 - model.DB.Model(&model.OAuthClient{}).Where("client_id = ?", clientID).Count(&n) + models.DB.Model(&models.OAuthClient{}).Where("client_id = ?", clientID).Count(&n) if n > 0 { return nil, ErrOAuthClientExists } @@ -85,14 +85,14 @@ func (s *ForumSettingsService) CreateOAuthClient(in OAuthClientInput) (*OAuthCli if in.Enabled != nil { enabled = *in.Enabled } - row := model.OAuthClient{ + row := models.OAuthClient{ ClientID: clientID, ClientSecretHash: hash, Name: name, RedirectURIs: uris, Enabled: enabled, } - if err := model.DB.Create(&row).Error; err != nil { + if err := models.DB.Create(&row).Error; err != nil { return nil, err } v := toOAuthClientView(row, plain) @@ -101,8 +101,8 @@ func (s *ForumSettingsService) CreateOAuthClient(in OAuthClientInput) (*OAuthCli // UpdateOAuthClient 更新应用 func (s *ForumSettingsService) UpdateOAuthClient(id uint, in OAuthClientInput) (*OAuthClientView, error) { - var row model.OAuthClient - if err := model.DB.First(&row, id).Error; err != nil { + var row models.OAuthClient + if err := models.DB.First(&row, id).Error; err != nil { return nil, ErrOAuthClientNotFound } name := strings.TrimSpace(in.Name) @@ -137,7 +137,7 @@ func (s *ForumSettingsService) UpdateOAuthClient(id uint, in OAuthClientInput) ( row.ClientSecretHash = hash } - if err := model.DB.Save(&row).Error; err != nil { + if err := models.DB.Save(&row).Error; err != nil { return nil, err } v := toOAuthClientView(row, plain) @@ -146,7 +146,7 @@ func (s *ForumSettingsService) UpdateOAuthClient(id uint, in OAuthClientInput) ( // DeleteOAuthClient 删除应用 func (s *ForumSettingsService) DeleteOAuthClient(id uint) error { - res := model.DB.Delete(&model.OAuthClient{}, id) + res := models.DB.Delete(&models.OAuthClient{}, id) if res.Error != nil { return res.Error } @@ -157,23 +157,23 @@ func (s *ForumSettingsService) DeleteOAuthClient(id uint) error { } // FindEnabledOAuthClient 按 client_id 查找已启用应用 -func FindEnabledOAuthClient(clientID string) (*model.OAuthClient, error) { - var row model.OAuthClient - if err := model.DB.Where("client_id = ? AND enabled = ?", clientID, true).First(&row).Error; err != nil { +func FindEnabledOAuthClient(clientID string) (*models.OAuthClient, error) { + var row models.OAuthClient + if err := models.DB.Where("client_id = ? AND enabled = ?", clientID, true).First(&row).Error; err != nil { return nil, ErrOIDCInvalidClient } return &row, nil } // VerifyOAuthClientSecret 校验客户端密钥(bcrypt 哈希) -func VerifyOAuthClientSecret(row *model.OAuthClient, secret string) bool { +func VerifyOAuthClientSecret(row *models.OAuthClient, secret string) bool { if row == nil || secret == "" || row.ClientSecretHash == "" { return false } return CheckPassword(row.ClientSecretHash, secret) } -func toOAuthClientView(row model.OAuthClient, plainSecret string) OAuthClientView { +func toOAuthClientView(row models.OAuthClient, plainSecret string) OAuthClientView { return OAuthClientView{ ID: row.ID, ClientID: row.ClientID, @@ -198,6 +198,6 @@ func generateClientSecret() (string, error) { // CountEnabledOAuthClients 已启用客户端数量 func CountEnabledOAuthClients() int64 { var n int64 - model.DB.Model(&model.OAuthClient{}).Where("enabled = ?", true).Count(&n) + models.DB.Model(&models.OAuthClient{}).Where("enabled = ?", true).Count(&n) return n } diff --git a/services/oidc.go b/services/oidc.go index 190ddd0..2df1254 100644 --- a/services/oidc.go +++ b/services/oidc.go @@ -1,4 +1,4 @@ -package service +package services import ( "crypto/rand" @@ -20,7 +20,7 @@ import ( "github.com/golang-jwt/jwt/v5" "git.iioio.com/freefire/jiang13-forum/config" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) const ( @@ -242,8 +242,8 @@ func (s *OIDCService) IssueAuthCode(userID uint, req AuthorizeRequest) (string, if err := s.ValidateAuthorize(req); err != nil { return "", err } - var user model.User - if err := model.DB.First(&user, userID).Error; err != nil { + var user models.User + if err := models.DB.First(&user, userID).Error; err != nil { return "", ErrOIDCInvalidRequest } if user.Banned { @@ -258,7 +258,7 @@ func (s *OIDCService) IssueAuthCode(userID uint, req AuthorizeRequest) (string, if req.CodeChallenge != "" && method == "" { method = "PLAIN" } - rec := &model.OAuthAuthCode{ + rec := &models.OAuthAuthCode{ Code: code, ClientID: req.ClientID, UserID: user.ID, @@ -269,7 +269,7 @@ func (s *OIDCService) IssueAuthCode(userID uint, req AuthorizeRequest) (string, CodeChallengeMethod: method, ExpiresAt: time.Now().Add(oidcAuthCodeTTL), } - if err := model.DB.Create(rec).Error; err != nil { + if err := models.DB.Create(rec).Error; err != nil { return "", err } @@ -320,14 +320,14 @@ func (s *OIDCService) ExchangeCode(req TokenRequest) (*TokenResponse, error) { return nil, ErrOIDCInvalidClient } - var rec model.OAuthAuthCode - if err := model.DB.Where("code = ?", req.Code).First(&rec).Error; err != nil { + var rec models.OAuthAuthCode + if err := models.DB.Where("code = ?", req.Code).First(&rec).Error; err != nil { return nil, ErrOIDCInvalidGrant } if rec.Used || time.Now().After(rec.ExpiresAt) { // 重放:作废同用户同客户端未过期码 if rec.Used { - _ = model.DB.Model(&model.OAuthAuthCode{}). + _ = models.DB.Model(&models.OAuthAuthCode{}). Where("client_id = ? AND user_id = ? AND used = ? AND expires_at > ?", rec.ClientID, rec.UserID, false, time.Now()). Update("used", true).Error @@ -342,10 +342,10 @@ func (s *OIDCService) ExchangeCode(req TokenRequest) (*TokenResponse, error) { } rec.Used = true - _ = model.DB.Save(&rec).Error + _ = models.DB.Save(&rec).Error - var user model.User - if err := model.DB.First(&user, rec.UserID).Error; err != nil || user.Banned { + var user models.User + if err := models.DB.First(&user, rec.UserID).Error; err != nil || user.Banned { return nil, ErrOIDCInvalidGrant } @@ -408,7 +408,7 @@ type oidcIDClaims struct { jwt.RegisteredClaims } -func (s *OIDCService) signAccessToken(user *model.User, scope, clientID string) (string, error) { +func (s *OIDCService) signAccessToken(user *models.User, scope, clientID string) (string, error) { now := time.Now() issuer := s.Issuer() claims := oidcAccessClaims{ @@ -429,7 +429,7 @@ func (s *OIDCService) signAccessToken(user *model.User, scope, clientID string) return t.SignedString(s.privateKey) } -func (s *OIDCService) signIDToken(user *model.User, scope, clientID, nonce string) (string, error) { +func (s *OIDCService) signIDToken(user *models.User, scope, clientID, nonce string) (string, error) { now := time.Now() issuer := s.Issuer() claims := oidcIDClaims{ @@ -462,13 +462,13 @@ func (s *OIDCService) signIDToken(user *model.User, scope, clientID, nonce strin return t.SignedString(s.privateKey) } -func (s *OIDCService) userGroups(user *model.User) []string { +func (s *OIDCService) userGroups(user *models.User) []string { rt := s.runtime() groups := make([]string, 0, 2) if rt.UserGroup != "" { groups = append(groups, rt.UserGroup) } - if user.Role == model.RoleAdmin && rt.AdminGroup != "" { + if user.Role == models.RoleAdmin && rt.AdminGroup != "" { groups = append(groups, rt.AdminGroup) } return groups @@ -484,8 +484,8 @@ func (s *OIDCService) UserInfo(accessToken string) (map[string]any, error) { if err != nil { return nil, ErrOIDCInvalidToken } - var user model.User - if err := model.DB.First(&user, uint(uid)).Error; err != nil || user.Banned { + var user models.User + if err := models.DB.First(&user, uint(uid)).Error; err != nil || user.Banned { return nil, ErrOIDCInvalidToken } rt := s.runtime() @@ -536,8 +536,8 @@ func (s *OIDCService) ResolveLogoutRedirect(postLogoutRedirectURI, state string) if uri == "" { return "/", nil } - var clients []model.OAuthClient - if err := model.DB.Where("enabled = ?", true).Find(&clients).Error; err != nil { + var clients []models.OAuthClient + if err := models.DB.Where("enabled = ?", true).Find(&clients).Error; err != nil { return "", err } allowed := false diff --git a/services/permalink.go b/services/permalink.go index 987d1f7..b4d975b 100644 --- a/services/permalink.go +++ b/services/permalink.go @@ -1,4 +1,4 @@ -package service +package services import ( "fmt" diff --git a/services/points.go b/services/points.go index cd96907..fb00c37 100644 --- a/services/points.go +++ b/services/points.go @@ -1,4 +1,4 @@ -package service +package services import ( "crypto/rand" @@ -7,7 +7,7 @@ import ( "math/big" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -31,13 +31,13 @@ func todayLocal() string { // AdjustPointsTx 在已有事务内调整积分并写流水;返回变动后余额 func AdjustPointsTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID uint, note string) (int, error) { if delta == 0 { - var u model.User + var u models.User if err := tx.Select("points").First(&u, userID).Error; err != nil { return 0, err } return u.Points, nil } - var user model.User + var user models.User if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil { return 0, err } @@ -48,7 +48,7 @@ func AdjustPointsTx(tx *gorm.DB, userID uint, delta int, reason, refType string, if err := tx.Model(&user).Update("points", newBal).Error; err != nil { return 0, err } - led := model.PointLedger{ + led := models.PointLedger{ UserID: userID, Delta: delta, Balance: newBal, @@ -66,7 +66,7 @@ func AdjustPointsTx(tx *gorm.DB, userID uint, delta int, reason, refType string, // AdjustPoints 独立事务调整积分 func (s *PointsService) AdjustPoints(userID uint, delta int, reason, refType string, refID uint, note string) (int, error) { var bal int - err := model.DB.Transaction(func(tx *gorm.DB) error { + err := models.DB.Transaction(func(tx *gorm.DB) error { var e error bal, e = AdjustPointsTx(tx, userID, delta, reason, refType, refID, note) return e @@ -79,7 +79,7 @@ func (s *PointsService) AdminAdjust(userID uint, delta int, note string) (int, e if delta == 0 { return 0, ErrInvalidPointsDelta } - return s.AdjustPoints(userID, delta, model.PointReasonAdminAdjust, "admin", 0, note) + return s.AdjustPoints(userID, delta, models.PointReasonAdminAdjust, "admin", 0, note) } // CheckInStatus 今日签到状态 @@ -93,8 +93,8 @@ type CheckInStatus struct { func (s *PointsService) GetCheckInStatus(userID uint) (CheckInStatus, error) { day := todayLocal() st := CheckInStatus{Day: day} - var row model.CheckIn - err := model.DB.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&row).Error + var row models.CheckIn + err := models.DB.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&row).Error if err != nil { return st, err } @@ -112,8 +112,8 @@ func (s *PointsService) GetCheckInStatus(userID uint) (CheckInStatus, error) { func (s *PointsService) computeNextStreak(userID uint, today string) int { yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") - var prev model.CheckIn - model.DB.Where("user_id = ? AND day = ?", userID, yesterday).Limit(1).Find(&prev) + var prev models.CheckIn + models.DB.Where("user_id = ? AND day = ?", userID, yesterday).Limit(1).Find(&prev) if prev.ID > 0 { return prev.Streak + 1 } @@ -136,8 +136,8 @@ func checkInReward(streak int) int { func (s *PointsService) CheckIn(userID uint) (CheckInStatus, error) { day := todayLocal() var out CheckInStatus - err := model.DB.Transaction(func(tx *gorm.DB) error { - var existing model.CheckIn + err := models.DB.Transaction(func(tx *gorm.DB) error { + var existing models.CheckIn if err := tx.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&existing).Error; err != nil { return err } @@ -145,18 +145,18 @@ func (s *PointsService) CheckIn(userID uint) (CheckInStatus, error) { return ErrAlreadyCheckedIn } yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") - var prev model.CheckIn + var prev models.CheckIn _ = tx.Where("user_id = ? AND day = ?", userID, yesterday).Limit(1).Find(&prev).Error streak := 1 if prev.ID > 0 { streak = prev.Streak + 1 } pts := checkInReward(streak) - row := model.CheckIn{UserID: userID, Day: day, Points: pts, Streak: streak} + row := models.CheckIn{UserID: userID, Day: day, Points: pts, Streak: streak} if err := tx.Create(&row).Error; err != nil { return err } - if _, err := AdjustPointsTx(tx, userID, pts, model.PointReasonCheckIn, "check_in", row.ID, fmt.Sprintf("连续签到 %d 天", streak)); err != nil { + if _, err := AdjustPointsTx(tx, userID, pts, models.PointReasonCheckIn, "check_in", row.ID, fmt.Sprintf("连续签到 %d 天", streak)); err != nil { return err } out = CheckInStatus{CheckedIn: true, Streak: streak, TodayPoints: pts, Day: day} @@ -191,8 +191,8 @@ type LotteryStatus struct { func (s *PointsService) GetLotteryStatus(userID uint) (LotteryStatus, error) { day := todayLocal() st := LotteryStatus{Day: day, Pool: defaultLotteryPool, Cost: 0} - var row model.LotteryDraw - if err := model.DB.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&row).Error; err != nil { + var row models.LotteryDraw + if err := models.DB.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&row).Error; err != nil { return st, err } if row.ID > 0 { @@ -228,8 +228,8 @@ func pickLottery(pool []LotteryPrize) (int, error) { func (s *PointsService) DrawLottery(userID uint) (LotteryStatus, error) { day := todayLocal() var out LotteryStatus - err := model.DB.Transaction(func(tx *gorm.DB) error { - var existing model.LotteryDraw + err := models.DB.Transaction(func(tx *gorm.DB) error { + var existing models.LotteryDraw if err := tx.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&existing).Error; err != nil { return err } @@ -240,12 +240,12 @@ func (s *PointsService) DrawLottery(userID uint) (LotteryStatus, error) { if err != nil { return err } - row := model.LotteryDraw{UserID: userID, Day: day, Points: pts} + row := models.LotteryDraw{UserID: userID, Day: day, Points: pts} if err := tx.Create(&row).Error; err != nil { return err } if pts > 0 { - if _, err := AdjustPointsTx(tx, userID, pts, model.PointReasonLottery, "lottery", row.ID, "每日抽奖"); err != nil { + if _, err := AdjustPointsTx(tx, userID, pts, models.PointReasonLottery, "lottery", row.ID, "每日抽奖"); err != nil { return err } } @@ -256,7 +256,7 @@ func (s *PointsService) DrawLottery(userID uint) (LotteryStatus, error) { } // ListLedger 积分流水 -func (s *PointsService) ListLedger(userID uint, page, size int) ([]model.PointLedger, int64, error) { +func (s *PointsService) ListLedger(userID uint, page, size int) ([]models.PointLedger, int64, error) { if page < 1 { page = 1 } @@ -264,9 +264,9 @@ func (s *PointsService) ListLedger(userID uint, page, size int) ([]model.PointLe size = 20 } var total int64 - model.DB.Model(&model.PointLedger{}).Where("user_id = ?", userID).Count(&total) - var rows []model.PointLedger - err := model.DB.Where("user_id = ?", userID).Order("id desc"). + models.DB.Model(&models.PointLedger{}).Where("user_id = ?", userID).Count(&total) + var rows []models.PointLedger + err := models.DB.Where("user_id = ?", userID).Order("id desc"). Offset((page - 1) * size).Limit(size).Find(&rows).Error return rows, total, err } diff --git a/services/poll.go b/services/poll.go index c798dd3..fce813f 100644 --- a/services/poll.go +++ b/services/poll.go @@ -1,4 +1,4 @@ -package service +package services import ( "encoding/json" @@ -6,7 +6,7 @@ import ( "strings" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -54,7 +54,7 @@ func CreatePollForPost(tx *gorm.DB, postID uint, multi bool, maxChoices int, end } else if maxChoices < 1 || maxChoices > len(options) { maxChoices = len(options) } - poll := model.Poll{PostID: postID, Multi: multi, MaxChoices: maxChoices, EndsAt: endsAt} + poll := models.Poll{PostID: postID, Multi: multi, MaxChoices: maxChoices, EndsAt: endsAt} if err := tx.Create(&poll).Error; err != nil { return err } @@ -66,7 +66,7 @@ func CreatePollForPost(tx *gorm.DB, postID uint, multi bool, maxChoices int, end if len([]rune(text)) > 64 { return errors.New("投票选项最多 64 字") } - row := model.PollOption{PostID: postID, Text: text, SortOrder: i} + row := models.PollOption{PostID: postID, Text: text, SortOrder: i} if err := tx.Create(&row).Error; err != nil { return err } @@ -131,8 +131,8 @@ func parsePollEndsAt(raw string) (*time.Time, error) { // closePollIfExpired 若已过截止时间则自动关闭投票 func closePollIfExpired(postID uint) error { - var poll model.Poll - if err := model.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil { + var poll models.Poll + if err := models.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil { return err } if poll.Closed || poll.EndsAt == nil { @@ -141,7 +141,7 @@ func closePollIfExpired(postID uint) error { if time.Now().Before(*poll.EndsAt) { return nil } - res := model.DB.Model(&poll).Where("post_id = ? AND closed = ?", postID, false).Update("closed", true) + res := models.DB.Model(&poll).Where("post_id = ? AND closed = ?", postID, false).Update("closed", true) return res.Error } @@ -150,12 +150,12 @@ func GetPollView(postID, viewerID uint) (*PollView, error) { if err := closePollIfExpired(postID); err != nil { return nil, err } - var poll model.Poll - if err := model.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil { + var poll models.Poll + if err := models.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil { return nil, err } - var opts []model.PollOption - if err := model.DB.Where("post_id = ?", postID).Order("sort_order ASC, id ASC").Find(&opts).Error; err != nil { + var opts []models.PollOption + if err := models.DB.Where("post_id = ?", postID).Order("sort_order ASC, id ASC").Find(&opts).Error; err != nil { return nil, err } total := 0 @@ -165,8 +165,8 @@ func GetPollView(postID, viewerID uint) (*PollView, error) { showResults := poll.Closed var myIDs []uint if viewerID > 0 { - var votes []model.PollVote - model.DB.Where("post_id = ? AND user_id = ?", postID, viewerID).Find(&votes) + var votes []models.PollVote + models.DB.Where("post_id = ? AND user_id = ?", postID, viewerID).Find(&votes) for _, v := range votes { myIDs = append(myIDs, v.OptionID) } @@ -196,15 +196,15 @@ func VotePoll(postID, userID uint, optionIDs []uint) error { if err := closePollIfExpired(postID); err != nil { return err } - var poll model.Poll - if err := model.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil { + var poll models.Poll + if err := models.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil { return err } if poll.Closed { return ErrPollClosed } var existing int64 - model.DB.Model(&model.PollVote{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&existing) + models.DB.Model(&models.PollVote{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&existing) if existing > 0 { return ErrPollAlreadyVoted } @@ -223,18 +223,18 @@ func VotePoll(postID, userID uint, optionIDs []uint) error { return ErrPollInvalidVote } seen[oid] = true - var opt model.PollOption - if err := model.DB.Where("id = ? AND post_id = ?", oid, postID).First(&opt).Error; err != nil { + var opt models.PollOption + if err := models.DB.Where("id = ? AND post_id = ?", oid, postID).First(&opt).Error; err != nil { return ErrPollInvalidVote } } - return model.DB.Transaction(func(tx *gorm.DB) error { + return models.DB.Transaction(func(tx *gorm.DB) error { for _, oid := range optionIDs { - v := model.PollVote{PostID: postID, OptionID: oid, UserID: userID} + v := models.PollVote{PostID: postID, OptionID: oid, UserID: userID} if err := tx.Create(&v).Error; err != nil { return err } - if err := tx.Model(&model.PollOption{}).Where("id = ?", oid). + if err := tx.Model(&models.PollOption{}).Where("id = ?", oid). UpdateColumn("vote_count", gorm.Expr("vote_count + 1")).Error; err != nil { return err } @@ -248,7 +248,7 @@ func ClosePoll(postID, userID uint, isAdmin bool, postAuthorID uint) error { if !isAdmin && userID != postAuthorID { return ErrPermissionDenied } - res := model.DB.Model(&model.Poll{}).Where("post_id = ?", postID).Update("closed", true) + res := models.DB.Model(&models.Poll{}).Where("post_id = ?", postID).Update("closed", true) if res.Error != nil { return res.Error } @@ -261,20 +261,20 @@ func ClosePoll(postID, userID uint, isAdmin bool, postAuthorID uint) error { // LockPollOptions 编辑时锁定选项(已发布帖不允许改选项文案) func LockPollOptions(postID uint) bool { var n int64 - model.DB.Model(&model.PollVote{}).Where("post_id = ?", postID).Count(&n) + models.DB.Model(&models.PollVote{}).Where("post_id = ?", postID).Count(&n) return n > 0 } // EnsurePollExists 检查投票帖是否有 poll 记录 func EnsurePollExists(postID uint) bool { var n int64 - model.DB.Model(&model.Poll{}).Where("post_id = ?", postID).Count(&n) + models.DB.Model(&models.Poll{}).Where("post_id = ?", postID).Count(&n) return n > 0 } // DeletePollData 删帖时清理投票数据 func DeletePollData(tx *gorm.DB, postID uint) { - tx.Where("post_id = ?", postID).Delete(&model.PollVote{}) - tx.Where("post_id = ?", postID).Delete(&model.PollOption{}) - tx.Where("post_id = ?", postID).Delete(&model.Poll{}) + tx.Where("post_id = ?", postID).Delete(&models.PollVote{}) + tx.Where("post_id = ?", postID).Delete(&models.PollOption{}) + tx.Where("post_id = ?", postID).Delete(&models.Poll{}) } diff --git a/services/post.go b/services/post.go index 2caf45d..e10d998 100644 --- a/services/post.go +++ b/services/post.go @@ -1,4 +1,4 @@ -package service +package services import ( "errors" @@ -6,7 +6,7 @@ import ( "strings" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -21,21 +21,21 @@ func NewPostService(filter *SensitiveFilter, settings *ForumSettingsService) *Po func normalizePostType(raw string) string { switch strings.TrimSpace(raw) { - case model.PostTypeQuestion: - return model.PostTypeQuestion - case model.PostTypePoll: - return model.PostTypePoll - case model.PostTypeBounty: - return model.PostTypeBounty - case model.PostTypeLottery: - return model.PostTypeLottery + case models.PostTypeQuestion: + return models.PostTypeQuestion + case models.PostTypePoll: + return models.PostTypePoll + case models.PostTypeBounty: + return models.PostTypeBounty + case models.PostTypeLottery: + return models.PostTypeLottery default: - return model.PostTypeNormal + return models.PostTypeNormal } } func isSpecialPostType(t string) bool { - return t == model.PostTypePoll || t == model.PostTypeBounty || t == model.PostTypeLottery + return t == models.PostTypePoll || t == models.PostTypeBounty || t == models.PostTypeLottery } type PostListQuery struct { @@ -55,16 +55,16 @@ type PostListQuery struct { // PostListItem 帖子列表项(含评论数等扩展字段) type PostListItem struct { - model.Post + models.Post CommentCount int `json:"comment_count"` LastReplyAt *time.Time `json:"last_reply_at,omitempty"` - LastReplyUser *model.User `json:"last_reply_user,omitempty"` + LastReplyUser *models.User `json:"last_reply_user,omitempty"` LastReplyGuestNick string `json:"last_reply_guest_nick,omitempty"` } type lastReplyInfo struct { At *time.Time - User *model.User + User *models.User GuestNick string } @@ -102,8 +102,8 @@ func (s *PostService) commentCountMap(postIDs []uint) map[uint]int { Count int } var rows []row - model.DB.Model(&model.Comment{}).Select("post_id, count(*) as count"). - Where("post_id IN ? AND status = ?", postIDs, model.ContentStatusPublished). + models.DB.Model(&models.Comment{}).Select("post_id, count(*) as count"). + Where("post_id IN ? AND status = ?", postIDs, models.ContentStatusPublished). Group("post_id").Scan(&rows) m := make(map[uint]int) for _, r := range rows { @@ -122,9 +122,9 @@ func (s *PostService) lastReplyInfoMap(postIDs []uint) map[uint]lastReplyInfo { MaxID uint } var idRows []idRow - model.DB.Model(&model.Comment{}). + models.DB.Model(&models.Comment{}). Select("post_id, MAX(id) as max_id"). - Where("post_id IN ? AND status = ?", postIDs, model.ContentStatusPublished). + Where("post_id IN ? AND status = ?", postIDs, models.ContentStatusPublished). Group("post_id"). Scan(&idRows) if len(idRows) == 0 { @@ -134,8 +134,8 @@ func (s *PostService) lastReplyInfoMap(postIDs []uint) map[uint]lastReplyInfo { for i, r := range idRows { commentIDs[i] = r.MaxID } - var comments []model.Comment - if err := model.DB.Preload("User").Where("id IN ?", commentIDs).Find(&comments).Error; err != nil { + var comments []models.Comment + if err := models.DB.Preload("User").Where("id IN ?", commentIDs).Find(&comments).Error; err != nil { return m } for i := range comments { @@ -162,16 +162,16 @@ func (s *PostService) HotPosts(limit int) ([]PostListItem, error) { limit = 10 } since := time.Now().Add(-7 * 24 * time.Hour) - var posts []model.Post - err := model.DB.Preload("User").Preload("Board"). - Where("status = ?", model.ContentStatusPublished). + var posts []models.Post + err := models.DB.Preload("User").Preload("Board"). + Where("status = ?", models.ContentStatusPublished). Where(`EXISTS ( SELECT 1 FROM comments WHERE comments.post_id = posts.id AND comments.deleted_at IS NULL AND comments.status = ? AND comments.created_at >= ? - )`, model.ContentStatusPublished, since). + )`, models.ContentStatusPublished, since). Order(`( SELECT MAX(created_at) FROM comments WHERE comments.post_id = posts.id @@ -214,9 +214,9 @@ func (s *PostService) PopularTags(limit int) ([]TagCount, error) { limit = 40 } var rows []struct{ Tags string } - if err := model.DB.Model(&model.Post{}). + if err := models.DB.Model(&models.Post{}). Select("tags"). - Where("status = ? AND tags <> '' AND tags IS NOT NULL", model.ContentStatusPublished). + Where("status = ? AND tags <> '' AND tags IS NOT NULL", models.ContentStatusPublished). Find(&rows).Error; err != nil { return nil, err } @@ -258,21 +258,21 @@ func (s *PostService) PopularTags(limit int) ([]TagCount, error) { func (s *PostService) CommentCount(postID uint) int { var count int64 - model.DB.Model(&model.Comment{}). - Where("post_id = ? AND status = ?", postID, model.ContentStatusPublished). + models.DB.Model(&models.Comment{}). + Where("post_id = ? AND status = ?", postID, models.ContentStatusPublished). Count(&count) return int(count) } // CanViewPost 是否可查看该帖(pending/rejected 仅作者与管理员) -func CanViewPost(post *model.Post, viewerID uint, isAdmin bool) bool { +func CanViewPost(post *models.Post, viewerID uint, isAdmin bool) bool { if post == nil { return false } - if isAdmin || post.Status == model.ContentStatusPublished || post.Status == "" { + if isAdmin || post.Status == models.ContentStatusPublished || post.Status == "" { return true } - if post.Status == model.ContentStatusPending || post.Status == model.ContentStatusRejected { + if post.Status == models.ContentStatusPending || post.Status == models.ContentStatusRejected { return viewerID > 0 && post.UserID == viewerID } return false @@ -281,7 +281,7 @@ func CanViewPost(post *model.Post, viewerID uint, isAdmin bool) bool { func applyPostVisibility(db *gorm.DB, q PostListQuery) *gorm.DB { if q.ViewerIsAdmin { switch q.Status { - case model.ContentStatusPending, model.ContentStatusPublished, model.ContentStatusRejected: + case models.ContentStatusPending, models.ContentStatusPublished, models.ContentStatusRejected: return db.Where("status = ?", q.Status) case "all", "": return db @@ -292,15 +292,15 @@ func applyPostVisibility(db *gorm.DB, q PostListQuery) *gorm.DB { if q.ViewerID > 0 { return db.Where( "status = ? OR (status IN ? AND user_id = ?)", - model.ContentStatusPublished, - []string{model.ContentStatusPending, model.ContentStatusRejected}, + models.ContentStatusPublished, + []string{models.ContentStatusPending, models.ContentStatusRejected}, q.ViewerID, ) } - return db.Where("status = ?", model.ContentStatusPublished) + return db.Where("status = ?", models.ContentStatusPublished) } -func (s *PostService) List(q PostListQuery) ([]model.Post, int64, error) { +func (s *PostService) List(q PostListQuery) ([]models.Post, int64, error) { if q.Page < 1 { q.Page = 1 } @@ -317,11 +317,11 @@ func (s *PostService) List(q PostListQuery) ([]model.Post, int64, error) { if uid, ok := resolveAuthorUserID(author); ok { q.UserID = uid } else { - return []model.Post{}, 0, nil + return []models.Post{}, 0, nil } } } - db := model.DB.Model(&model.Post{}).Preload("User").Preload("Board") + db := models.DB.Model(&models.Post{}).Preload("User").Preload("Board") db = applyPostVisibility(db, q) if q.BoardID > 0 { db = db.Where("board_id = ?", q.BoardID) @@ -345,7 +345,7 @@ func (s *PostService) List(q PostListQuery) ([]model.Post, int64, error) { } var total int64 db.Count(&total) - var posts []model.Post + var posts []models.Post db = db.Order("pinned desc") if q.BoardID > 0 { db = db.Order("board_pinned desc") @@ -396,19 +396,19 @@ func resolveAuthorUserID(author string) (uint, bool) { if author == "" { return 0, false } - var u model.User - if err := model.DB.Select("id").Where("username = ?", author).First(&u).Error; err == nil { + var u models.User + if err := models.DB.Select("id").Where("username = ?", author).First(&u).Error; err == nil { return u.ID, true } - if err := model.DB.Select("id").Where("nickname = ?", author).First(&u).Error; err == nil { + if err := models.DB.Select("id").Where("nickname = ?", author).First(&u).Error; err == nil { return u.ID, true } return 0, false } -func (s *PostService) FindByID(id uint) (*model.Post, error) { - var post model.Post - err := model.DB.Preload("User").Preload("Board").First(&post, id).Error +func (s *PostService) FindByID(id uint) (*models.Post, error) { + var post models.Post + err := models.DB.Preload("User").Preload("Board").First(&post, id).Error if err != nil { return nil, ErrPostNotFound } @@ -416,11 +416,11 @@ func (s *PostService) FindByID(id uint) (*model.Post, error) { } func (s *PostService) RecordView(id uint) { - model.DB.Model(&model.Post{}).Where("id = ?", id). + models.DB.Model(&models.Post{}).Where("id = ?", id). UpdateColumn("view_count", gorm.Expr("view_count + 1")) } -func (s *PostService) GetByID(id uint) (*model.Post, error) { +func (s *PostService) GetByID(id uint) (*models.Post, error) { post, err := s.FindByID(id) if err != nil { return nil, err @@ -429,7 +429,7 @@ func (s *PostService) GetByID(id uint) (*model.Post, error) { return post, nil } -func (s *PostService) Create(userID, boardID uint, title, content, tags, postType string, skipModeration bool) (*model.Post, error) { +func (s *PostService) Create(userID, boardID uint, title, content, tags, postType string, skipModeration bool) (*models.Post, error) { title = s.filter.Filter(strings.TrimSpace(title)) content = s.filter.Filter(SanitizePostHTML(content)) tags = s.filter.Filter(strings.TrimSpace(tags)) @@ -449,11 +449,11 @@ func (s *PostService) Create(userID, boardID uint, title, content, tags, postTyp if _, err := NewBoardService().GetByID(boardID); err != nil { return nil, err } - status := model.ContentStatusPending + status := models.ContentStatusPending if skipModeration { - status = model.ContentStatusPublished + status = models.ContentStatusPublished } - post := &model.Post{ + post := &models.Post{ BoardID: boardID, UserID: userID, Title: title, @@ -464,10 +464,10 @@ func (s *PostService) Create(userID, boardID uint, title, content, tags, postTyp QuestionResolved: false, Status: status, } - if err := model.DB.Create(post).Error; err != nil { + if err := models.DB.Create(post).Error; err != nil { return nil, err } - if status == model.ContentStatusPublished { + if status == models.ContentStatusPublished { AddExp(userID, 10) } return post, nil @@ -476,8 +476,8 @@ func (s *PostService) Create(userID, boardID uint, title, content, tags, postTyp // Update 更新帖子。boardID>0 时可改板块;为 0 时保持原板块。 // postType 为空时保持原类型;改为非 question 时清除已解决标记。 func (s *PostService) Update(userID, postID uint, isAdmin, skipModeration bool, title, content, tags, postType string, boardID uint) error { - var post model.Post - if err := model.DB.First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.First(&post, postID).Error; err != nil { return ErrPostNotFound } if !isAdmin && post.UserID != userID { @@ -517,11 +517,11 @@ func (s *PostService) Update(userID, postID uint, isAdmin, skipModeration bool, return errors.New("不能改为特殊帖子类型") } nextResolved := post.QuestionResolved - if nextType != model.PostTypeQuestion { + if nextType != models.PostTypeQuestion { nextResolved = false } - return model.DB.Transaction(func(tx *gorm.DB) error { - rev := model.PostRevision{ + return models.DB.Transaction(func(tx *gorm.DB) error { + rev := models.PostRevision{ PostID: postID, EditorID: userID, Title: post.Title, Content: post.Content, Tags: post.Tags, } @@ -539,7 +539,7 @@ func (s *PostService) Update(userID, postID uint, isAdmin, skipModeration bool, } // 非免审用户修改后重新进入审核 if !skipModeration { - updates["status"] = model.ContentStatusPending + updates["status"] = models.ContentStatusPending } return tx.Model(&post).Updates(updates).Error }) @@ -548,16 +548,16 @@ func (s *PostService) Update(userID, postID uint, isAdmin, skipModeration bool, // SetStatus 设置帖子审核状态 func (s *PostService) SetStatus(postID uint, status string) error { switch status { - case model.ContentStatusPending, model.ContentStatusPublished, model.ContentStatusRejected: + case models.ContentStatusPending, models.ContentStatusPublished, models.ContentStatusRejected: default: return errors.New("无效的审核状态") } - var post model.Post - if err := model.DB.Select("id", "user_id", "status").First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.Select("id", "user_id", "status").First(&post, postID).Error; err != nil { return ErrPostNotFound } prev := post.Status - res := model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("status", status) + res := models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("status", status) if res.Error != nil { return res.Error } @@ -565,7 +565,7 @@ func (s *PostService) SetStatus(postID uint, status string) error { return ErrPostNotFound } // 首次变为已发布时加经验 - if status == model.ContentStatusPublished && prev != model.ContentStatusPublished { + if status == models.ContentStatusPublished && prev != models.ContentStatusPublished { AddExp(post.UserID, 10) } return nil @@ -574,24 +574,24 @@ func (s *PostService) SetStatus(postID uint, status string) error { // PendingPostCount 待审帖数量 func (s *PostService) PendingPostCount() (int64, error) { var n int64 - err := model.DB.Model(&model.Post{}).Where("status = ?", model.ContentStatusPending).Count(&n).Error + err := models.DB.Model(&models.Post{}).Where("status = ?", models.ContentStatusPending).Count(&n).Error return n, err } // CanEdit 判断当前用户是否可编辑帖子 -func (s *PostService) CanEdit(post *model.Post, isAdmin bool) bool { +func (s *PostService) CanEdit(post *models.Post, isAdmin bool) bool { return s.checkEditable(post, isAdmin) == nil } // EditBlockReason 返回不可编辑的原因(可编辑时返回空字符串) -func (s *PostService) EditBlockReason(post *model.Post, isAdmin bool) string { +func (s *PostService) EditBlockReason(post *models.Post, isAdmin bool) string { if err := s.checkEditable(post, isAdmin); err != nil { return err.Error() } return "" } -func (s *PostService) checkEditable(post *model.Post, isAdmin bool) error { +func (s *PostService) checkEditable(post *models.Post, isAdmin bool) error { if isAdmin { return nil } @@ -606,7 +606,7 @@ func (s *PostService) checkEditable(post *model.Post, isAdmin bool) error { } // CanUserEdit 判断指定用户是否可编辑帖子 -func (s *PostService) CanUserEdit(post *model.Post, userID uint, isAdmin bool) bool { +func (s *PostService) CanUserEdit(post *models.Post, userID uint, isAdmin bool) bool { if userID == 0 { return false } @@ -617,7 +617,7 @@ func (s *PostService) CanUserEdit(post *model.Post, userID uint, isAdmin bool) b } // UserEditBlockReason 返回用户不可编辑的原因 -func (s *PostService) UserEditBlockReason(post *model.Post, userID uint, isAdmin bool) string { +func (s *PostService) UserEditBlockReason(post *models.Post, userID uint, isAdmin bool) string { if userID == 0 { return "请先登录" } @@ -628,7 +628,7 @@ func (s *PostService) UserEditBlockReason(post *model.Post, userID uint, isAdmin } func (s *PostService) SetEditLocked(postID uint, locked bool) error { - res := model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("edit_locked", locked) + res := models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("edit_locked", locked) if res.Error != nil { return res.Error } @@ -640,7 +640,7 @@ func (s *PostService) SetEditLocked(postID uint, locked bool) error { // SetCommentsLocked 锁定/解锁讨论(禁止新评论) func (s *PostService) SetCommentsLocked(postID uint, locked bool) error { - res := model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("comments_locked", locked) + res := models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("comments_locked", locked) if res.Error != nil { return res.Error } @@ -650,22 +650,22 @@ func (s *PostService) SetCommentsLocked(postID uint, locked bool) error { return nil } -func (s *PostService) ListRevisions(postID uint) ([]model.PostRevision, error) { - var revs []model.PostRevision - err := model.DB.Preload("Editor").Where("post_id = ?", postID). +func (s *PostService) ListRevisions(postID uint) ([]models.PostRevision, error) { + var revs []models.PostRevision + err := models.DB.Preload("Editor").Where("post_id = ?", postID). Order("id desc").Find(&revs).Error if err != nil { return nil, err } if revs == nil { - revs = []model.PostRevision{} + revs = []models.PostRevision{} } return revs, nil } -func (s *PostService) GetRevision(postID, revID uint) (*model.PostRevision, error) { - var rev model.PostRevision - err := model.DB.Preload("Editor"). +func (s *PostService) GetRevision(postID, revID uint) (*models.PostRevision, error) { + var rev models.PostRevision + err := models.DB.Preload("Editor"). Where("id = ? AND post_id = ?", revID, postID).First(&rev).Error if err != nil { return nil, ErrRevisionNotFound @@ -678,17 +678,17 @@ func (s *PostService) Delete(userID, postID uint, isAdmin bool) error { if !isAdmin { return ErrPermissionDenied } - var post model.Post - if err := model.DB.First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.First(&post, postID).Error; err != nil { return ErrPostNotFound } - return model.DB.Transaction(func(tx *gorm.DB) error { + return models.DB.Transaction(func(tx *gorm.DB) error { if err := RefundBountyIfOpen(tx, &post); err != nil { return err } DeletePollData(tx, postID) DeleteLotteryData(tx, postID) - if err := tx.Where("post_id = ?", postID).Delete(&model.Comment{}).Error; err != nil { + if err := tx.Where("post_id = ?", postID).Delete(&models.Comment{}).Error; err != nil { return err } return tx.Delete(&post).Error @@ -707,7 +707,7 @@ func (s *PostService) ListTrash(page, size int, keyword string) ([]TrashPostItem page = 1 } size = s.settings.NormalizePageSize(size) - db := model.DB.Unscoped().Model(&model.Post{}). + db := models.DB.Unscoped().Model(&models.Post{}). Where("deleted_at IS NOT NULL"). Preload("User").Preload("Board") if keyword != "" { @@ -722,7 +722,7 @@ func (s *PostService) ListTrash(page, size int, keyword string) ([]TrashPostItem if err := db.Count(&total).Error; err != nil { return nil, 0, err } - var posts []model.Post + var posts []models.Post if err := db.Order("deleted_at DESC").Offset((page - 1) * size).Limit(size).Find(&posts).Error; err != nil { return nil, 0, err } @@ -739,7 +739,7 @@ func (s *PostService) ListTrash(page, size int, keyword string) ([]TrashPostItem Cnt int } var rows []row - _ = model.DB.Unscoped().Model(&model.Comment{}). + _ = models.DB.Unscoped().Model(&models.Comment{}). Select("post_id, COUNT(*) as cnt"). Where("post_id IN ?", ids). Group("post_id").Scan(&rows) @@ -760,15 +760,15 @@ func (s *PostService) ListTrash(page, size int, keyword string) ([]TrashPostItem // Restore 从回收站恢复帖子及评论 func (s *PostService) Restore(postID uint) error { - var post model.Post - if err := model.DB.Unscoped().First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.Unscoped().First(&post, postID).Error; err != nil { return ErrPostNotFound } if !post.DeletedAt.Valid { return errors.New("帖子未被删除") } - return model.DB.Transaction(func(tx *gorm.DB) error { - if err := tx.Unscoped().Model(&model.Comment{}). + return models.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Unscoped().Model(&models.Comment{}). Where("post_id = ? AND deleted_at IS NOT NULL", postID). Update("deleted_at", nil).Error; err != nil { return err @@ -779,33 +779,33 @@ func (s *PostService) Restore(postID uint) error { // Purge 永久删除回收站中的帖子(含评论、点赞、收藏、修订) func (s *PostService) Purge(postID uint) error { - var post model.Post - if err := model.DB.Unscoped().First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.Unscoped().First(&post, postID).Error; err != nil { return ErrPostNotFound } if !post.DeletedAt.Valid { return errors.New("仅可彻底删除回收站中的帖子,请先删除帖子") } - return model.DB.Transaction(func(tx *gorm.DB) error { + return models.DB.Transaction(func(tx *gorm.DB) error { var commentIDs []uint - if err := tx.Unscoped().Model(&model.Comment{}).Where("post_id = ?", postID).Pluck("id", &commentIDs).Error; err != nil { + if err := tx.Unscoped().Model(&models.Comment{}).Where("post_id = ?", postID).Pluck("id", &commentIDs).Error; err != nil { return err } if len(commentIDs) > 0 { - if err := tx.Where("comment_id IN ?", commentIDs).Delete(&model.CommentRevision{}).Error; err != nil { + if err := tx.Where("comment_id IN ?", commentIDs).Delete(&models.CommentRevision{}).Error; err != nil { return err } } - if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&model.Comment{}).Error; err != nil { + if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&models.Comment{}).Error; err != nil { return err } - if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&model.PostLike{}).Error; err != nil { + if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&models.PostLike{}).Error; err != nil { return err } - if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&model.PostFavorite{}).Error; err != nil { + if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&models.PostFavorite{}).Error; err != nil { return err } - if err := tx.Where("post_id = ?", postID).Delete(&model.PostRevision{}).Error; err != nil { + if err := tx.Where("post_id = ?", postID).Delete(&models.PostRevision{}).Error; err != nil { return err } return tx.Unscoped().Delete(&post).Error @@ -813,52 +813,52 @@ func (s *PostService) Purge(postID uint) error { } func (s *PostService) SetPinned(postID uint, pinned bool) error { - return model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("pinned", pinned).Error + return models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("pinned", pinned).Error } func (s *PostService) SetBoardPinned(postID uint, boardPinned bool) error { - return model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("board_pinned", boardPinned).Error + return models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("board_pinned", boardPinned).Error } func (s *PostService) SetFeatured(postID uint, featured bool) error { - return model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("featured", featured).Error + return models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("featured", featured).Error } // SetQuestionResolved 标记问答帖已解决 / 未解决(作者或管理员) func (s *PostService) SetQuestionResolved(userID, postID uint, isAdmin bool, resolved bool) error { - var post model.Post - if err := model.DB.First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.First(&post, postID).Error; err != nil { return ErrPostNotFound } if !isAdmin && post.UserID != userID { return ErrPermissionDenied } - if post.PostType != model.PostTypeQuestion { + if post.PostType != models.PostTypeQuestion { return errors.New("仅问答帖可标记解决状态") } - return model.DB.Model(&post).Update("question_resolved", resolved).Error + return models.DB.Model(&post).Update("question_resolved", resolved).Error } func (s *PostService) ToggleLike(userID, postID uint) (liked bool, err error) { - var post model.Post - if err := model.DB.Select("id", "user_id").First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.Select("id", "user_id").First(&post, postID).Error; err != nil { return false, ErrPostNotFound } - var like model.PostLike - result := model.DB.Where("post_id = ? AND user_id = ?", postID, userID).Limit(1).Find(&like) + var like models.PostLike + result := models.DB.Where("post_id = ? AND user_id = ?", postID, userID).Limit(1).Find(&like) if result.Error != nil { return false, result.Error } if result.RowsAffected > 0 { - model.DB.Delete(&like) - model.DB.Model(&model.Post{}).Where("id = ?", postID).UpdateColumn("like_count", gorm.Expr("like_count - 1")) + models.DB.Delete(&like) + models.DB.Model(&models.Post{}).Where("id = ?", postID).UpdateColumn("like_count", gorm.Expr("like_count - 1")) return false, nil } - like = model.PostLike{PostID: postID, UserID: userID} - if err := model.DB.Create(&like).Error; err != nil { + like = models.PostLike{PostID: postID, UserID: userID} + if err := models.DB.Create(&like).Error; err != nil { return false, err } - model.DB.Model(&model.Post{}).Where("id = ?", postID).UpdateColumn("like_count", gorm.Expr("like_count + 1")) + models.DB.Model(&models.Post{}).Where("id = ?", postID).UpdateColumn("like_count", gorm.Expr("like_count + 1")) // 他人点赞给作者加经验;自赞不计 if userID != post.UserID { AddExp(post.UserID, 1) @@ -871,24 +871,24 @@ func (s *PostService) ToggleLike(userID, postID uint) (liked bool, err error) { func (s *PostService) IsLiked(userID, postID uint) bool { var count int64 - model.DB.Model(&model.PostLike{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count) + models.DB.Model(&models.PostLike{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count) return count > 0 } func (s *PostService) ToggleFavorite(userID, postID uint) (faved bool, err error) { - var fav model.PostFavorite - result := model.DB.Where("post_id = ? AND user_id = ?", postID, userID).Limit(1).Find(&fav) + var fav models.PostFavorite + result := models.DB.Where("post_id = ? AND user_id = ?", postID, userID).Limit(1).Find(&fav) if result.Error != nil { return false, result.Error } if result.RowsAffected > 0 { - if err := model.DB.Delete(&fav).Error; err != nil { + if err := models.DB.Delete(&fav).Error; err != nil { return false, err } return false, nil } - fav = model.PostFavorite{PostID: postID, UserID: userID} - if err := model.DB.Create(&fav).Error; err != nil { + fav = models.PostFavorite{PostID: postID, UserID: userID} + if err := models.DB.Create(&fav).Error; err != nil { return false, err } return true, nil @@ -896,11 +896,11 @@ func (s *PostService) ToggleFavorite(userID, postID uint) (faved bool, err error func (s *PostService) IsFavorited(userID, postID uint) bool { var count int64 - model.DB.Model(&model.PostFavorite{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count) + models.DB.Model(&models.PostFavorite{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count) return count > 0 } -func (s *PostService) ListFavorites(userID uint, page, size int) ([]model.PostFavorite, int64, error) { +func (s *PostService) ListFavorites(userID uint, page, size int) ([]models.PostFavorite, int64, error) { if page < 1 { page = 1 } @@ -908,17 +908,17 @@ func (s *PostService) ListFavorites(userID uint, page, size int) ([]model.PostFa size = 20 } // 仅统计可查看的收藏(已公开,或本人未公开帖) - base := model.DB.Model(&model.PostFavorite{}). + base := models.DB.Model(&models.PostFavorite{}). Joins("JOIN posts ON posts.id = post_favorites.post_id AND posts.deleted_at IS NULL"). Where("post_favorites.user_id = ?", userID). - Where("posts.status = ? OR posts.user_id = ?", model.ContentStatusPublished, userID) + Where("posts.status = ? OR posts.user_id = ?", models.ContentStatusPublished, userID) var total int64 base.Count(&total) - var favs []model.PostFavorite - err := model.DB.Preload("Post.User").Preload("Post.Board"). + var favs []models.PostFavorite + err := models.DB.Preload("Post.User").Preload("Post.Board"). Joins("JOIN posts ON posts.id = post_favorites.post_id AND posts.deleted_at IS NULL"). Where("post_favorites.user_id = ?", userID). - Where("posts.status = ? OR posts.user_id = ?", model.ContentStatusPublished, userID). + Where("posts.status = ? OR posts.user_id = ?", models.ContentStatusPublished, userID). Order("post_favorites.id desc"). Offset((page - 1) * size).Limit(size).Find(&favs).Error return favs, total, err @@ -937,9 +937,9 @@ func (s *PostService) ListSitemap(limit int) ([]SitemapPost, error) { limit = 5000 } var rows []SitemapPost - err := model.DB.Model(&model.Post{}). + err := models.DB.Model(&models.Post{}). Select("id, created_at, updated_at"). - Where("status = ?", model.ContentStatusPublished). + Where("status = ?", models.ContentStatusPublished). Order("updated_at desc, id desc"). Limit(limit). Find(&rows).Error diff --git a/services/post_special.go b/services/post_special.go index 1e91bd1..55bde5e 100644 --- a/services/post_special.go +++ b/services/post_special.go @@ -1,10 +1,10 @@ -package service +package services import ( "errors" "strconv" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -16,30 +16,30 @@ type PostCreateExtras struct { } // FinalizeSpecialPostCreate 创建帖后初始化投票/悬赏/抽奖 -func FinalizeSpecialPostCreate(post *model.Post, userID uint, extras PostCreateExtras) error { +func FinalizeSpecialPostCreate(post *models.Post, userID uint, extras PostCreateExtras) error { if post == nil { return errors.New("帖子不存在") } - return model.DB.Transaction(func(tx *gorm.DB) error { + return models.DB.Transaction(func(tx *gorm.DB) error { switch post.PostType { - case model.PostTypePoll: + case models.PostTypePoll: opts, multi, maxChoices, endsAt, err := ParsePollOptionsJSON(extras.PollOptionsJSON) if err != nil { return err } return CreatePollForPost(tx, post.ID, multi, maxChoices, endsAt, opts) - case model.PostTypeBounty: + case models.PostTypeBounty: if extras.BountyPoints < 1 { return ErrBountyInvalidPoint } if err := tx.Model(post).Updates(map[string]interface{}{ "bounty_points": extras.BountyPoints, - "bounty_status": model.BountyStatusOpen, + "bounty_status": models.BountyStatusOpen, }).Error; err != nil { return err } return EscrowBounty(tx, userID, post.ID, extras.BountyPoints) - case model.PostTypeLottery: + case models.PostTypeLottery: count := extras.LotteryWinnerCount if count < 1 { count = 1 @@ -49,7 +49,7 @@ func FinalizeSpecialPostCreate(post *model.Post, userID uint, extras PostCreateE } return tx.Model(post).Updates(map[string]interface{}{ "lottery_winner_count": count, - "lottery_status": model.PostLotteryStatusOpen, + "lottery_status": models.PostLotteryStatusOpen, }).Error default: return nil diff --git a/services/ratelimit.go b/services/ratelimit.go index c78826b..5a79d17 100644 --- a/services/ratelimit.go +++ b/services/ratelimit.go @@ -1,4 +1,4 @@ -package service +package services import ( "sync" diff --git a/services/report.go b/services/report.go index 32ec791..9f964f6 100644 --- a/services/report.go +++ b/services/report.go @@ -1,4 +1,4 @@ -package service +package services import ( "errors" @@ -7,7 +7,7 @@ import ( "time" "unicode/utf8" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -38,11 +38,11 @@ func NewReportService( func normalizeReportReason(reason string) (string, error) { switch strings.TrimSpace(reason) { - case model.ReportReasonSpam, - model.ReportReasonAbuse, - model.ReportReasonIllegal, - model.ReportReasonIrrelevant, - model.ReportReasonOther: + case models.ReportReasonSpam, + models.ReportReasonAbuse, + models.ReportReasonIllegal, + models.ReportReasonIrrelevant, + models.ReportReasonOther: return reason, nil default: return "", errors.New("请选择有效的举报原因") @@ -51,15 +51,15 @@ func normalizeReportReason(reason string) (string, error) { func ReportReasonLabel(reason string) string { switch reason { - case model.ReportReasonSpam: + case models.ReportReasonSpam: return "垃圾广告" - case model.ReportReasonAbuse: + case models.ReportReasonAbuse: return "人身攻击 / 辱骂" - case model.ReportReasonIllegal: + case models.ReportReasonIllegal: return "违法违规" - case model.ReportReasonIrrelevant: + case models.ReportReasonIrrelevant: return "内容无关 / 灌水" - case model.ReportReasonOther: + case models.ReportReasonOther: return "其他" default: return reason @@ -67,7 +67,7 @@ func ReportReasonLabel(reason string) string { } // Create 用户举报帖子 -func (s *ReportService) Create(reporterID, postID uint, reason, detail string) (*model.PostReport, error) { +func (s *ReportService) Create(reporterID, postID uint, reason, detail string) (*models.PostReport, error) { reason, err := normalizeReportReason(reason) if err != nil { return nil, err @@ -80,8 +80,8 @@ func (s *ReportService) Create(reporterID, postID uint, reason, detail string) ( detail = s.filter.Filter(detail) } - var post model.Post - if err := model.DB.Select("id", "user_id", "title").First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.Select("id", "user_id", "title").First(&post, postID).Error; err != nil { return nil, ErrPostNotFound } if post.UserID == reporterID { @@ -89,29 +89,29 @@ func (s *ReportService) Create(reporterID, postID uint, reason, detail string) ( } var existing int64 - model.DB.Model(&model.PostReport{}). - Where("post_id = ? AND reporter_id = ? AND status = ?", postID, reporterID, model.ReportStatusPending). + models.DB.Model(&models.PostReport{}). + Where("post_id = ? AND reporter_id = ? AND status = ?", postID, reporterID, models.ReportStatusPending). Count(&existing) if existing > 0 { return nil, ErrReportAlreadyExists } - rep := &model.PostReport{ + rep := &models.PostReport{ PostID: postID, ReporterID: reporterID, Reason: reason, Detail: detail, - Status: model.ReportStatusPending, + Status: models.ReportStatusPending, } - if err := model.DB.Create(rep).Error; err != nil { + if err := models.DB.Create(rep).Error; err != nil { return nil, err } - _ = model.DB.Preload("Post").Preload("Reporter").First(rep, rep.ID).Error + _ = models.DB.Preload("Post").Preload("Reporter").First(rep, rep.ID).Error return rep, nil } // CreateCommentReport 用户举报评论 -func (s *ReportService) CreateCommentReport(reporterID, commentID uint, reason, detail string) (*model.PostReport, error) { +func (s *ReportService) CreateCommentReport(reporterID, commentID uint, reason, detail string) (*models.PostReport, error) { reason, err := normalizeReportReason(reason) if err != nil { return nil, err @@ -133,26 +133,26 @@ func (s *ReportService) CreateCommentReport(reporterID, commentID uint, reason, } var existing int64 - model.DB.Model(&model.PostReport{}). - Where("comment_id = ? AND reporter_id = ? AND status = ?", commentID, reporterID, model.ReportStatusPending). + models.DB.Model(&models.PostReport{}). + Where("comment_id = ? AND reporter_id = ? AND status = ?", commentID, reporterID, models.ReportStatusPending). Count(&existing) if existing > 0 { return nil, ErrReportAlreadyExists } cid := commentID - rep := &model.PostReport{ + rep := &models.PostReport{ PostID: comment.PostID, CommentID: &cid, ReporterID: reporterID, Reason: reason, Detail: detail, - Status: model.ReportStatusPending, + Status: models.ReportStatusPending, } - if err := model.DB.Create(rep).Error; err != nil { + if err := models.DB.Create(rep).Error; err != nil { return nil, err } - _ = model.DB.Preload("Post").Preload("Comment").Preload("Reporter").First(rep, rep.ID).Error + _ = models.DB.Preload("Post").Preload("Comment").Preload("Reporter").First(rep, rep.ID).Error return rep, nil } @@ -163,13 +163,13 @@ type ReportListQuery struct { } // ListAdmin 管理员举报列表 -func (s *ReportService) ListAdmin(q ReportListQuery) ([]model.PostReport, int64, error) { +func (s *ReportService) ListAdmin(q ReportListQuery) ([]models.PostReport, int64, error) { if q.Page < 1 { q.Page = 1 } q.Size = s.settings.NormalizePageSize(q.Size) - db := model.DB.Model(&model.PostReport{}) + db := models.DB.Model(&models.PostReport{}) if q.Status != "" && q.Status != "all" { db = db.Where("status = ?", q.Status) } @@ -179,7 +179,7 @@ func (s *ReportService) ListAdmin(q ReportListQuery) ([]model.PostReport, int64, return nil, 0, err } - var list []model.PostReport + var list []models.PostReport err := db.Preload("Post", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).Preload("Post.User").Preload("Comment", func(tx *gorm.DB) *gorm.DB { @@ -195,8 +195,8 @@ func (s *ReportService) ListAdmin(q ReportListQuery) ([]model.PostReport, int64, // PendingCount 待处理举报数 func (s *ReportService) PendingCount() (int64, error) { var n int64 - err := model.DB.Model(&model.PostReport{}). - Where("status = ?", model.ReportStatusPending). + err := models.DB.Model(&models.PostReport{}). + Where("status = ?", models.ReportStatusPending). Count(&n).Error return n, err } @@ -210,16 +210,16 @@ type HandleReportInput struct { } // Handle 处理举报 -func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) { - var rep model.PostReport - if err := model.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB { +func (s *ReportService) Handle(in HandleReportInput) (*models.PostReport, error) { + var rep models.PostReport + if err := models.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).Preload("Comment", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).First(&rep, in.ReportID).Error; err != nil { return nil, ErrReportNotFound } - if rep.Status != model.ReportStatusPending { + if rep.Status != models.ReportStatusPending { return nil, errors.New("该举报已处理") } @@ -251,9 +251,9 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) switch in.Action { case "dismiss": - rep.Status = model.ReportStatusDismissed + rep.Status = models.ReportStatusDismissed case "resolve": - rep.Status = model.ReportStatusResolved + rep.Status = models.ReportStatusResolved case "reject_post": if isCommentReport { return nil, errors.New("评论举报请使用「拒绝该评论」") @@ -265,10 +265,10 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) if utf8.RuneCountInString(reason) > 1000 { return nil, errors.New("拒绝原因过长") } - if err := s.posts.SetStatus(postID, model.ContentStatusRejected); err != nil { + if err := s.posts.SetStatus(postID, models.ContentStatusRejected); err != nil { return nil, err } - rep.Status = model.ReportStatusResolved + rep.Status = models.ReportStatusResolved if note == "" { rep.HandleNote = "已拒绝该帖并通知作者" } @@ -279,7 +279,7 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) authorID, fmt.Sprintf("帖子《%s》未通过审核", postTitle), FormatRejectContent(postTitle, postID, reason), - model.MessageKindReject, + models.MessageKindReject, &pid, &rid, ) @@ -295,10 +295,10 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) if utf8.RuneCountInString(reason) > 1000 { return nil, errors.New("拒绝原因过长") } - if err := s.comments.SetStatus(*rep.CommentID, model.ContentStatusRejected); err != nil { + if err := s.comments.SetStatus(*rep.CommentID, models.ContentStatusRejected); err != nil { return nil, err } - rep.Status = model.ReportStatusResolved + rep.Status = models.ReportStatusResolved if note == "" { rep.HandleNote = "已拒绝该评论并通知作者" } @@ -310,7 +310,7 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) commentAuthorID, fmt.Sprintf("评论未通过审核 · 《%s》", postTitle), body, - model.MessageKindReject, + models.MessageKindReject, &pid, &rid, ) @@ -319,13 +319,13 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) return nil, errors.New("无效的处理操作") } - if err := model.DB.Save(&rep).Error; err != nil { + if err := models.DB.Save(&rep).Error; err != nil { return nil, err } // 通知举报人处理结果 resultText := "已忽略" - if rep.Status == model.ReportStatusResolved { + if rep.Status == models.ReportStatusResolved { switch in.Action { case "reject_post": resultText = "已核实并下架该帖" @@ -349,12 +349,12 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) rep.ReporterID, "举报处理结果通知", content, - model.MessageKindReportResult, + models.MessageKindReportResult, &pid, &rid, ) - _ = model.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB { + _ = models.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() }).Preload("Comment", func(tx *gorm.DB) *gorm.DB { return tx.Unscoped() diff --git a/services/sanitize_html.go b/services/sanitize_html.go index d4de16b..d72cc8d 100644 --- a/services/sanitize_html.go +++ b/services/sanitize_html.go @@ -1,4 +1,4 @@ -package service +package services import ( "sync" diff --git a/services/sanitize_html_test.go b/services/sanitize_html_test.go index b1f4fab..41609ff 100644 --- a/services/sanitize_html_test.go +++ b/services/sanitize_html_test.go @@ -1,4 +1,4 @@ -package service +package services import ( "strings" diff --git a/services/seo.go b/services/seo.go index d9b137c..952cffc 100644 --- a/services/seo.go +++ b/services/seo.go @@ -1,4 +1,4 @@ -package service +package services import ( "net/url" @@ -7,7 +7,7 @@ import ( "time" "unicode/utf8" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) var ( @@ -89,7 +89,7 @@ func FirstImageURL(htmlContent string) string { } // DisplayName 用户展示名 -func DisplayName(u *model.User) string { +func DisplayName(u *models.User) string { if u == nil { return "" } diff --git a/services/settings.go b/services/settings.go index 42b2d51..7229f48 100644 --- a/services/settings.go +++ b/services/settings.go @@ -1,4 +1,4 @@ -package service +package services import ( "encoding/json" @@ -8,7 +8,7 @@ import ( "strings" "sync" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) // 论坛设置键名 @@ -403,72 +403,72 @@ func NewForumSettingsService() *ForumSettingsService { func (s *ForumSettingsService) ensureDefaults() { for _, def := range forumSettingDefs { var count int64 - model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", def.key).Count(&count) + models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", def.key).Count(&count) if count == 0 { - model.DB.Create(&model.ForumSetting{Key: def.key, Value: def.defaultVal}) + models.DB.Create(&models.ForumSetting{Key: def.key, Value: def.defaultVal}) } } for key, val := range feedSettingDefaults { var count int64 - model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count) + models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count) if count == 0 { - model.DB.Create(&model.ForumSetting{Key: key, Value: val}) + models.DB.Create(&models.ForumSetting{Key: key, Value: val}) } } for key, val := range asideSettingDefaults { var count int64 - model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count) + models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count) if count == 0 { - model.DB.Create(&model.ForumSetting{Key: key, Value: val}) + models.DB.Create(&models.ForumSetting{Key: key, Value: val}) } } for key, val := range mailSettingDefaults { var count int64 - model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count) + models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count) if count == 0 { - model.DB.Create(&model.ForumSetting{Key: key, Value: val}) + models.DB.Create(&models.ForumSetting{Key: key, Value: val}) } } for key, val := range oidcSettingDefaults { var count int64 - model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count) + models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count) if count == 0 { - model.DB.Create(&model.ForumSetting{Key: key, Value: val}) + models.DB.Create(&models.ForumSetting{Key: key, Value: val}) } } for key, val := range giteaSettingDefaults { var count int64 - model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count) + models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count) if count == 0 { - model.DB.Create(&model.ForumSetting{Key: key, Value: val}) + models.DB.Create(&models.ForumSetting{Key: key, Value: val}) } } for key, val := range storageSettingDefaults { var count int64 - model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count) + models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count) if count == 0 { - model.DB.Create(&model.ForumSetting{Key: key, Value: val}) + models.DB.Create(&models.ForumSetting{Key: key, Value: val}) } } for key, val := range siteBrandingDefaults { var count int64 - model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count) + models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count) if count == 0 { - model.DB.Create(&model.ForumSetting{Key: key, Value: val}) + models.DB.Create(&models.ForumSetting{Key: key, Value: val}) } } for key, val := range friendLinkSettingDefaults { var count int64 - model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count) + models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count) if count == 0 { - model.DB.Create(&model.ForumSetting{Key: key, Value: val}) + models.DB.Create(&models.ForumSetting{Key: key, Value: val}) } } } func (s *ForumSettingsService) getString(key, fallback string) string { - var setting model.ForumSetting - if err := model.DB.First(&setting, "`key` = ?", key).Error; err != nil { + var setting models.ForumSetting + if err := models.DB.First(&setting, "`key` = ?", key).Error; err != nil { return fallback } return setting.Value @@ -477,12 +477,12 @@ func (s *ForumSettingsService) getString(key, fallback string) string { func (s *ForumSettingsService) setString(key, value string) error { s.mu.Lock() defer s.mu.Unlock() - return model.DB.Save(&model.ForumSetting{Key: key, Value: value}).Error + return models.DB.Save(&models.ForumSetting{Key: key, Value: value}).Error } func (s *ForumSettingsService) getInt(key string, fallback int) int { - var setting model.ForumSetting - if err := model.DB.First(&setting, "`key` = ?", key).Error; err != nil { + var setting models.ForumSetting + if err := models.DB.First(&setting, "`key` = ?", key).Error; err != nil { return fallback } v, err := strconv.Atoi(setting.Value) @@ -505,7 +505,7 @@ func (s *ForumSettingsService) setInt(key string, value int) error { } s.mu.Lock() defer s.mu.Unlock() - return model.DB.Save(&model.ForumSetting{Key: key, Value: strconv.Itoa(value)}).Error + return models.DB.Save(&models.ForumSetting{Key: key, Value: strconv.Itoa(value)}).Error } return ErrInvalidSetting } @@ -1042,7 +1042,7 @@ func (s *ForumSettingsService) GiteaSyncConfig() GiteaSyncConfig { } cfg.Ready = cfg.Enabled && base != "" && cfg.HasToken var n int64 - model.DB.Model(&model.GiteaRepo{}).Where("private = ?", false).Count(&n) + models.DB.Model(&models.GiteaRepo{}).Where("private = ?", false).Count(&n) cfg.RepoCount = n return cfg } diff --git a/services/settings_util.go b/services/settings_util.go index 6e8f64c..4327574 100644 --- a/services/settings_util.go +++ b/services/settings_util.go @@ -1,4 +1,4 @@ -package service +package services import ( "strings" diff --git a/services/site_page.go b/services/site_page.go index b5777a5..ece4f7b 100644 --- a/services/site_page.go +++ b/services/site_page.go @@ -1,10 +1,10 @@ -package service +package services import ( "errors" "strings" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) var ( @@ -32,8 +32,8 @@ type SitePageSummary struct { } func (s *SitePageService) ListPublished() ([]SitePageSummary, error) { - var rows []model.SitePage - err := model.DB.Where("published = ?", true). + var rows []models.SitePage + err := models.DB.Where("published = ?", true). Order("sort_order ASC, id ASC"). Find(&rows).Error if err != nil { @@ -49,22 +49,22 @@ func (s *SitePageService) ListPublished() ([]SitePageSummary, error) { return out, nil } -func (s *SitePageService) ListAll() ([]model.SitePage, error) { - var rows []model.SitePage - err := model.DB.Order("sort_order ASC, id ASC").Find(&rows).Error +func (s *SitePageService) ListAll() ([]models.SitePage, error) { + var rows []models.SitePage + err := models.DB.Order("sort_order ASC, id ASC").Find(&rows).Error if err != nil { return nil, err } return rows, nil } -func (s *SitePageService) GetBySlug(slug string, allowUnpublished bool) (*model.SitePage, error) { +func (s *SitePageService) GetBySlug(slug string, allowUnpublished bool) (*models.SitePage, error) { slug, ok := NormalizePageSlug(slug) if !ok { return nil, ErrSitePageNotFound } - var page model.SitePage - q := model.DB.Where("slug = ?", slug) + var page models.SitePage + q := models.DB.Where("slug = ?", slug) if !allowUnpublished { q = q.Where("published = ?", true) } @@ -75,9 +75,9 @@ func (s *SitePageService) GetBySlug(slug string, allowUnpublished bool) (*model. return &page, nil } -func (s *SitePageService) GetByID(id uint) (*model.SitePage, error) { - var page model.SitePage - if err := model.DB.First(&page, id).Error; err != nil { +func (s *SitePageService) GetByID(id uint) (*models.SitePage, error) { + var page models.SitePage + if err := models.DB.First(&page, id).Error; err != nil { return nil, ErrSitePageNotFound } page.Content = SanitizePostHTML(UnwrapContentGateTags(page.Content)) @@ -94,17 +94,17 @@ type SitePageInput struct { ShowInNav bool `json:"show_in_nav"` } -func (s *SitePageService) Create(in SitePageInput) (*model.SitePage, error) { +func (s *SitePageService) Create(in SitePageInput) (*models.SitePage, error) { page, err := s.normalizeInput(in) if err != nil { return nil, err } var exists int64 - model.DB.Model(&model.SitePage{}).Where("slug = ?", page.Slug).Count(&exists) + models.DB.Model(&models.SitePage{}).Where("slug = ?", page.Slug).Count(&exists) if exists > 0 { return nil, ErrSitePageSlugUsed } - if err := model.DB.Create(page).Error; err != nil { + if err := models.DB.Create(page).Error; err != nil { return nil, err } return page, nil @@ -120,11 +120,11 @@ func (s *SitePageService) Update(id uint, in SitePageInput) error { return err } var exists int64 - model.DB.Model(&model.SitePage{}).Where("slug = ? AND id <> ?", next.Slug, id).Count(&exists) + models.DB.Model(&models.SitePage{}).Where("slug = ? AND id <> ?", next.Slug, id).Count(&exists) if exists > 0 { return ErrSitePageSlugUsed } - return model.DB.Model(page).Updates(map[string]interface{}{ + return models.DB.Model(page).Updates(map[string]interface{}{ "title": next.Title, "slug": next.Slug, "content": next.Content, @@ -136,7 +136,7 @@ func (s *SitePageService) Update(id uint, in SitePageInput) error { } func (s *SitePageService) Delete(id uint) error { - res := model.DB.Delete(&model.SitePage{}, id) + res := models.DB.Delete(&models.SitePage{}, id) if res.Error != nil { return res.Error } @@ -152,20 +152,20 @@ func (s *SitePageService) SetPublished(id uint, published bool) error { if err != nil { return err } - return model.DB.Model(page).Update("published", published).Error + return models.DB.Model(page).Update("published", published).Error } -func (s *SitePageService) ListSitemap(limit int) ([]model.SitePage, error) { +func (s *SitePageService) ListSitemap(limit int) ([]models.SitePage, error) { if limit <= 0 { limit = 500 } - var rows []model.SitePage - err := model.DB.Where("published = ?", true). + var rows []models.SitePage + err := models.DB.Where("published = ?", true). Order("updated_at DESC").Limit(limit).Find(&rows).Error return rows, err } -func (s *SitePageService) normalizeInput(in SitePageInput) (*model.SitePage, error) { +func (s *SitePageService) normalizeInput(in SitePageInput) (*models.SitePage, error) { title := s.filter.Filter(strings.TrimSpace(in.Title)) slug, ok := NormalizePageSlug(in.Slug) if !ok { @@ -179,7 +179,7 @@ func (s *SitePageService) normalizeInput(in SitePageInput) (*model.SitePage, err if content == "" { return nil, errors.New("正文不能为空") } - return &model.SitePage{ + return &models.SitePage{ Title: title, Slug: slug, Content: content, Published: in.Published, SortOrder: in.SortOrder, ShowInFooter: in.ShowInFooter, ShowInNav: in.ShowInNav, diff --git a/services/storage.go b/services/storage.go index 17dced5..a1dc273 100644 --- a/services/storage.go +++ b/services/storage.go @@ -1,4 +1,4 @@ -package service +package services import ( "bytes" diff --git a/services/thumb.go b/services/thumb.go index ba9e277..ac47b0c 100644 --- a/services/thumb.go +++ b/services/thumb.go @@ -1,4 +1,4 @@ -package service +package services import ( "errors" diff --git a/services/unlock.go b/services/unlock.go index fe8fdc0..95ddd44 100644 --- a/services/unlock.go +++ b/services/unlock.go @@ -1,4 +1,4 @@ -package service +package services import ( "crypto/sha256" @@ -10,7 +10,7 @@ import ( "strings" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" "gorm.io/gorm" ) @@ -110,8 +110,8 @@ func ListUnlockedKeys(userID, postID uint) (map[string]bool, error) { if userID == 0 || postID == 0 { return out, nil } - var rows []model.PostContentUnlock - if err := model.DB.Select("block_key").Where("user_id = ? AND post_id = ?", userID, postID).Find(&rows).Error; err != nil { + var rows []models.PostContentUnlock + if err := models.DB.Select("block_key").Where("user_id = ? AND post_id = ?", userID, postID).Find(&rows).Error; err != nil { return out, err } for _, r := range rows { @@ -131,8 +131,8 @@ type UnlockResult struct { // UnlockPointsBlock 积分解锁付费块 func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, error) { - var post model.Post - if err := model.DB.First(&post, postID).Error; err != nil { + var post models.Post + if err := models.DB.First(&post, postID).Error; err != nil { return nil, errors.New("帖子不存在") } block, ok := FindPointsBlock(post.Content, blockKey) @@ -143,26 +143,26 @@ func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, e // 作者自己免费解锁记录(无分成) if readerID == post.UserID { var n int64 - model.DB.Model(&model.PostContentUnlock{}).Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Count(&n) + models.DB.Model(&models.PostContentUnlock{}).Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Count(&n) if n == 0 { - _ = model.DB.Create(&model.PostContentUnlock{ + _ = models.DB.Create(&models.PostContentUnlock{ UserID: readerID, PostID: postID, BlockKey: blockKey, Cost: 0, }).Error } return &UnlockResult{BlockKey: blockKey, Cost: 0, AuthorShare: 0, InnerHTML: block.Inner}, nil } - var existing model.PostContentUnlock - model.DB.Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Limit(1).Find(&existing) + var existing models.PostContentUnlock + models.DB.Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Limit(1).Find(&existing) if existing.ID > 0 { return nil, ErrAlreadyUnlocked } - var reader, author model.User - if err := model.DB.First(&reader, readerID).Error; err != nil { + var reader, author models.User + if err := models.DB.First(&reader, readerID).Error; err != nil { return nil, err } - if err := model.DB.First(&author, post.UserID).Error; err != nil { + if err := models.DB.First(&author, post.UserID).Error; err != nil { return nil, err } @@ -174,8 +174,8 @@ func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, e cost := block.Cost authorShare := cost * CreatorSharePercent / 100 var bal int - err := model.DB.Transaction(func(tx *gorm.DB) error { - var again model.PostContentUnlock + err := models.DB.Transaction(func(tx *gorm.DB) error { + var again models.PostContentUnlock if err := tx.Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Limit(1).Find(&again).Error; err != nil { return err } @@ -183,20 +183,20 @@ func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, e return ErrAlreadyUnlocked } var e error - bal, e = AdjustPointsTx(tx, readerID, -cost, model.PointReasonUnlockSpend, "post_unlock", postID, "解锁付费内容") + bal, e = AdjustPointsTx(tx, readerID, -cost, models.PointReasonUnlockSpend, "post_unlock", postID, "解锁付费内容") if e != nil { return e } if authorShare > 0 { - if _, e = AdjustPointsTx(tx, author.ID, authorShare, model.PointReasonCreatorIncome, "post_unlock", postID, "创作分成"); e != nil { + if _, e = AdjustPointsTx(tx, author.ID, authorShare, models.PointReasonCreatorIncome, "post_unlock", postID, "创作分成"); e != nil { return e } - if e = tx.Model(&model.User{}).Where("id = ?", author.ID). + if e = tx.Model(&models.User{}).Where("id = ?", author.ID). UpdateColumn("creator_income_total", gorm.Expr("creator_income_total + ?", authorShare)).Error; e != nil { return e } } - return tx.Create(&model.PostContentUnlock{ + return tx.Create(&models.PostContentUnlock{ UserID: readerID, PostID: postID, BlockKey: blockKey, Cost: cost, }).Error }) @@ -213,7 +213,7 @@ func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, e }, nil } -func suspiciousUnlockPair(reader, author *model.User) bool { +func suspiciousUnlockPair(reader, author *models.User) bool { if reader == nil || author == nil { return false } diff --git a/services/upload.go b/services/upload.go index b1d88d8..0c4954a 100644 --- a/services/upload.go +++ b/services/upload.go @@ -1,4 +1,4 @@ -package service +package services import ( "mime/multipart" diff --git a/services/user.go b/services/user.go index 30226ef..3ec8ffb 100644 --- a/services/user.go +++ b/services/user.go @@ -1,4 +1,4 @@ -package service +package services import ( "errors" @@ -8,7 +8,7 @@ import ( "strings" "time" - "git.iioio.com/freefire/jiang13-forum/model" + "git.iioio.com/freefire/jiang13-forum/models" ) type UserService struct { @@ -21,9 +21,9 @@ func NewUserService(filter *SensitiveFilter, settings *ForumSettingsService) *Us } // GetByID 获取用户信息 -func (s *UserService) GetByID(id uint) (*model.User, error) { - var user model.User - if err := model.DB.First(&user, id).Error; err != nil { +func (s *UserService) GetByID(id uint) (*models.User, error) { + var user models.User + if err := models.DB.First(&user, id).Error; err != nil { return nil, err } return &user, nil @@ -43,17 +43,17 @@ func (s *UserService) ActivityStats(userID uint) (UserActivityStats, error) { if userID == 0 { return st, errors.New("无效用户") } - if err := model.DB.Model(&model.Post{}).Where("user_id = ?", userID).Count(&st.PostCount).Error; err != nil { + if err := models.DB.Model(&models.Post{}).Where("user_id = ?", userID).Count(&st.PostCount).Error; err != nil { return st, err } - if err := model.DB.Model(&model.Comment{}).Where("user_id = ?", userID).Count(&st.CommentCount).Error; err != nil { + if err := models.DB.Model(&models.Comment{}).Where("user_id = ?", userID).Count(&st.CommentCount).Error; err != nil { return st, err } - if err := model.DB.Model(&model.PostFavorite{}).Where("user_id = ?", userID).Count(&st.FavoriteCount).Error; err != nil { + if err := models.DB.Model(&models.PostFavorite{}).Where("user_id = ?", userID).Count(&st.FavoriteCount).Error; err != nil { return st, err } var likeSum int64 - if err := model.DB.Model(&model.Post{}). + if err := models.DB.Model(&models.Post{}). Select("COALESCE(SUM(like_count), 0)"). Where("user_id = ?", userID). Scan(&likeSum).Error; err != nil { @@ -64,19 +64,19 @@ func (s *UserService) ActivityStats(userID uint) (UserActivityStats, error) { } // GetByUsername 按用户名查询 -func (s *UserService) GetByUsername(username string) (*model.User, error) { - var user model.User - if err := model.DB.Where("username = ?", username).First(&user).Error; err != nil { +func (s *UserService) GetByUsername(username string) (*models.User, error) { + var user models.User + if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil { return nil, err } return &user, nil } // GetByEmail 按邮箱查询 -func (s *UserService) GetByEmail(email string) (*model.User, error) { +func (s *UserService) GetByEmail(email string) (*models.User, error) { email = NormalizeEmail(email) - var user model.User - if err := model.DB.Where("email = ?", email).First(&user).Error; err != nil { + var user models.User + if err := models.DB.Where("email = ?", email).First(&user).Error; err != nil { return nil, err } return &user, nil @@ -95,21 +95,21 @@ func (s *UserService) ResetPasswordByEmail(email, newPass string) error { if err != nil { return err } - return model.DB.Model(&model.User{}).Where("id = ?", user.ID).Update("password", hash).Error + return models.DB.Model(&models.User{}).Where("id = ?", user.ID).Update("password", hash).Error } // SearchUsersBrief 公开用户搜索(@补全):匹配用户名/昵称,不含邮箱 -func (s *UserService) SearchUsersBrief(keyword string, limit int) ([]model.User, error) { +func (s *UserService) SearchUsersBrief(keyword string, limit int) ([]models.User, error) { keyword = strings.TrimSpace(keyword) if keyword == "" { - return []model.User{}, nil + return []models.User{}, nil } if limit <= 0 || limit > 20 { limit = 8 } like := "%" + keyword + "%" - var users []model.User - err := model.DB.Select("id", "username", "nickname", "avatar", "role", "verified"). + var users []models.User + err := models.DB.Select("id", "username", "nickname", "avatar", "role", "verified"). Where("username LIKE ? OR nickname LIKE ?", like, like). Order("username ASC"). Limit(limit). @@ -118,7 +118,7 @@ func (s *UserService) SearchUsersBrief(keyword string, limit int) ([]model.User, return nil, err } if users == nil { - users = []model.User{} + users = []models.User{} } return users, nil } @@ -136,8 +136,8 @@ func (s *UserService) ListRecentRegistered(limit int) ([]RecentUserItem, error) if limit < 1 { limit = 8 } - var users []model.User - err := model.DB.Select("id", "username", "nickname", "avatar", "created_at"). + var users []models.User + err := models.DB.Select("id", "username", "nickname", "avatar", "created_at"). Where("banned = ?", false). Order("created_at DESC, id DESC"). Limit(limit). @@ -168,7 +168,7 @@ func (s *UserService) UpdateNickname(userID uint, nickname string) error { return errors.New("昵称不能为空") } nickname = s.filter.Filter(nickname) - return model.DB.Model(&model.User{}).Where("id = ?", userID).Update("nickname", nickname).Error + return models.DB.Model(&models.User{}).Where("id = ?", userID).Update("nickname", nickname).Error } // UpdateSignature 修改个人签名 @@ -184,7 +184,7 @@ func (s *UserService) UpdateSignature(userID uint, signature string) error { if signature != "" { signature = s.filter.Filter(signature) } - return model.DB.Model(&model.User{}).Where("id = ?", userID).Update("signature", signature).Error + return models.DB.Model(&models.User{}).Where("id = ?", userID).Update("signature", signature).Error } // UpdatePassword 修改密码 @@ -192,8 +192,8 @@ func (s *UserService) UpdatePassword(userID uint, oldPass, newPass string) error if err := ValidatePassword(newPass, s.settings.PasswordMinLen()); err != nil { return err } - var user model.User - if err := model.DB.First(&user, userID).Error; err != nil { + var user models.User + if err := models.DB.First(&user, userID).Error; err != nil { return err } if !CheckPassword(user.Password, oldPass) { @@ -203,20 +203,20 @@ func (s *UserService) UpdatePassword(userID uint, oldPass, newPass string) error if err != nil { return err } - return model.DB.Model(&user).Update("password", hash).Error + return models.DB.Model(&user).Update("password", hash).Error } // UploadAvatar 上传头像;成功后删除用户旧头像文件,避免磁盘/对象存储堆积 func (s *UserService) UploadAvatar(userID uint, file *multipart.FileHeader, store *UploadStore) (string, error) { - var user model.User - if err := model.DB.Select("id", "avatar").First(&user, userID).Error; err != nil { + var user models.User + if err := models.DB.Select("id", "avatar").First(&user, userID).Error; err != nil { return "", err } url, err := SaveUploadedImage(store, file, UploadCategoryAvatars, fmt.Sprintf("%d", userID)) if err != nil { return "", err } - if err := model.DB.Model(&model.User{}).Where("id = ?", userID).Update("avatar", url).Error; err != nil { + if err := models.DB.Model(&models.User{}).Where("id = ?", userID).Update("avatar", url).Error; err != nil { return "", err } if old := strings.TrimSpace(user.Avatar); old != "" && old != url { @@ -234,7 +234,7 @@ type UserListQuery struct { Filter string // all | verified | banned | admin } -func (s *UserService) ListUsers(q UserListQuery) ([]model.User, int64, error) { +func (s *UserService) ListUsers(q UserListQuery) ([]models.User, int64, error) { if q.Page < 1 { q.Page = 1 } @@ -245,7 +245,7 @@ func (s *UserService) ListUsers(q UserListQuery) ([]model.User, int64, error) { q.Size = 100 } - db := model.DB.Model(&model.User{}) + db := models.DB.Model(&models.User{}) kw := strings.TrimSpace(q.Keyword) if kw != "" { like := "%" + kw + "%" @@ -257,18 +257,18 @@ func (s *UserService) ListUsers(q UserListQuery) ([]model.User, int64, error) { } switch strings.TrimSpace(q.Filter) { case "verified": - db = db.Where("verified = ? AND role <> ?", true, model.RoleAdmin) + db = db.Where("verified = ? AND role <> ?", true, models.RoleAdmin) case "banned": db = db.Where("banned = ?", true) case "admin": - db = db.Where("role = ?", model.RoleAdmin) + db = db.Where("role = ?", models.RoleAdmin) } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } - var users []model.User + var users []models.User offset := (q.Page - 1) * q.Size err := db.Order("id desc").Offset(offset).Limit(q.Size).Find(&users).Error return users, total, err @@ -276,11 +276,11 @@ func (s *UserService) ListUsers(q UserListQuery) ([]model.User, int64, error) { // BanUser 禁言用户 func (s *UserService) BanUser(userID uint, banned bool) error { - var user model.User - if err := model.DB.First(&user, userID).Error; err != nil { + var user models.User + if err := models.DB.First(&user, userID).Error; err != nil { return errors.New("用户不存在") } - if user.Role == model.RoleAdmin { + if user.Role == models.RoleAdmin { return errors.New("不能禁言管理员账号") } now := time.Now() @@ -288,7 +288,7 @@ func (s *UserService) BanUser(userID uint, banned bool) error { if banned { updates["banned_at"] = &now } - return model.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error + return models.DB.Model(&models.User{}).Where("id = ?", userID).Updates(updates).Error } // SitemapUser 站点地图用的轻量用户字段 @@ -303,7 +303,7 @@ func (s *UserService) ListSitemap(limit int) ([]SitemapUser, error) { limit = 5000 } var rows []SitemapUser - err := model.DB.Model(&model.User{}). + err := models.DB.Model(&models.User{}). Select("id, updated_at"). Where("banned = ?", false). Order("updated_at desc, id desc").