feat(i18n): 中文化校验提示与服务层错误信息
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s

将 api 层校验规则提示语、service 层 gerror.Wrap 与 response.Error
错误信息、panic 未注册提示统一改为中文,并同步中文化 cmd 路由注释
与变更记录。仅涉及注释、文档与字符串改动,无业务逻辑变更。
This commit is contained in:
夏犀麟 2026-09-13 23:22:46 +08:00
parent 70cb86176e
commit 4b3c00270f
39 changed files with 464 additions and 294 deletions

View File

@ -1,6 +1,8 @@
# service.xpcool.com 变更记录 # service.xpcool.com 变更记录
> 倒序最新在上格式YYYY-MM-DD | 类型 | 摘要 > 倒序最新在上格式YYYY-MM-DD | 类型 | 摘要
2026-09-13 | DOC | 项目整理与全局中文化重写 README.MD原英文且路由陈旧 /api/v1 实际三分组 /api/service/{open,user,admin}补分层约定/RBAC 权限映射/gen dao 流程/本地启动重写 PROJECT_STRUCTURE.md原英文目录树缺 house/recruitment/notice/job/serversecurity标注手写 entity/do/dao 风险hack/hack.mk + hack-cli.mk 注释中文化common/doc.go + common/tools/doc.go 工具清单中文化ip/main.go/cmd.go 英文注释中文化**校验提示与服务层错误信息中文化**19 文件逾 90 api v:"#提示" 中文user/authtools/md5tools/randommenu_manageadminservice gerror.Wrap/response.Error 全部中文admin/adminadmin/loginsystem/{role,menu,menu_manage,login_log}user/authhouse/{community,listing,dashboard,transaction,presale}noticejobserversecurityrecruitment/{recruitment,crawler}jwt.gocontroller/admin/admin.go service panic("xxx implementation not registered")"xxx 实现未注册"api/user/login/login.go 补中文包文档生成代码entity/do/dao英文注释按约定保持原样DO NOT EDIT重生成会覆盖仅注释/文档/字符串改动无业务逻辑变更read_lints 全绿go build proxy.golang.org 网络超时未跑通详见 docs/change-log/2026-09-13.md
2026-08-27 | CHG | 登录接口从全量加密豁免永远只加密密码字段APICrypto plainRoutes /system/auth/login登录请求不整体加密handler 直接收 {username, encryptedKey, encryptedData}登录响应因无会话密钥保持明文ResolveLogin 分支顺序调整密码字段密文优先解密其次 fullBody 明文开发 allowPlain全量模式验证 5 登录明文响应/info 密文/解密成功/未加密拒绝/登出+ dev 回归 15 项全过 2026-08-27 | CHG | 登录接口从全量加密豁免永远只加密密码字段APICrypto plainRoutes /system/auth/login登录请求不整体加密handler 直接收 {username, encryptedKey, encryptedData}登录响应因无会话密钥保持明文ResolveLogin 分支顺序调整密码字段密文优先解密其次 fullBody 明文开发 allowPlain全量模式验证 5 登录明文响应/info 密文/解密成功/未加密拒绝/登出+ dev 回归 15 项全过
2026-08-27 | FEAT | 全量请求/响应加密生产 encrypt.fullBody=true / env ENCRYPT_FULL_BODYcrypto.Service fullBody 开关 + DecryptRequestRSA AES 会话密钥 + AES-GCM body返回会话密钥+ EncryptResponse用同一会话密钥 AES-GCM 加密响应回传请求结束即销毁新增 middleware/APICrypto请求整体解密io.ReadAll 直接读 r.Request.Body 勿用 GetBody否则缓存密文致 handler Parse 读到密文+ 响应加密 bufferHandlerResponse 外层Recover 内层500 也加密public-key 明文豁免未加密业务请求一律拒绝admin 组中间件链改 CORSAPICryptoRecoverHandlerResponseResolveLogin/resolvePassword fullBody 分支传输层已整体解密直接信任明文 username/passwordinjectEnv 支持 ENCRYPT_FULL_BODY/ENCRYPT_ALLOW_PLAIN直接跑二进制恒加载 config.yamlGF_GCFG_ENV gf run 必须 env 注入GF r.GetBody() 缓存密文到 bodyContenthandler Parse 走缓存 中间件必须 io.ReadAll(r.Request.Body)dev仅密码加密E2E 15 prod全量加密E2E 9 项全过 2026-08-27 | FEAT | 全量请求/响应加密生产 encrypt.fullBody=true / env ENCRYPT_FULL_BODYcrypto.Service fullBody 开关 + DecryptRequestRSA AES 会话密钥 + AES-GCM body返回会话密钥+ EncryptResponse用同一会话密钥 AES-GCM 加密响应回传请求结束即销毁新增 middleware/APICrypto请求整体解密io.ReadAll 直接读 r.Request.Body 勿用 GetBody否则缓存密文致 handler Parse 读到密文+ 响应加密 bufferHandlerResponse 外层Recover 内层500 也加密public-key 明文豁免未加密业务请求一律拒绝admin 组中间件链改 CORSAPICryptoRecoverHandlerResponseResolveLogin/resolvePassword fullBody 分支传输层已整体解密直接信任明文 username/passwordinjectEnv 支持 ENCRYPT_FULL_BODY/ENCRYPT_ALLOW_PLAIN直接跑二进制恒加载 config.yamlGF_GCFG_ENV gf run 必须 env 注入GF r.GetBody() 缓存密文到 bodyContenthandler Parse 走缓存 中间件必须 io.ReadAll(r.Request.Body)dev仅密码加密E2E 15 prod全量加密E2E 9 项全过

View File

@ -1,35 +1,55 @@
# Production layout # 生产目录结构
```text ```text
service.xpcool.com/ service.xpcool.com/
api/ # HTTP contracts and Swagger metadata api/ # HTTP 契约与 Swagger 元数据按业务分组
user/v1/ # /api/v1 - client-facing API admin/ # /api/service/admin - 管理端 APIAdminAuth + 权限码
admin/v1/ # /admin/v1 - administration API admin/ # 管理员账号 CRUD登录/资料/权限码/刷新/登出
open/v1/ # /api/open/v1 - open API for frontends (no auth) system/ # 登录日志菜单路由菜单管理角色
tools/ # one dir per sub-feature: tools/<name>/<name>.go house/ # /api/service/admin - 看房模块小区/房源/看板/成交/预售证
common/ # public reusable module (no internal/ deps) recruitment/ # /api/service/admin - 招聘考试聚合模块
tools/ # utility toolbox: md5, cryptox, uuid, random, notice/ # /api/service/admin - 站内通知渠道/规则/日志
# timex, convertx, strx, slicex, ip, filex job/ # /api/service/admin - 自动任务任务/日志
serversecurity/ # 服务器安全日志open 上报 + admin 查询/统计
user/ # /api/service/user - 用户端认证登录/刷新
open/ # /api/service/open - 开放接口公开无鉴权
tools/ # 工具子功能契约每子功能一目录tools/<name>/<name>.go
common/ # 公共可复用模块不依赖 internal/
tools/ # 工具集md5cryptoxuuidrandomtimex
# convertxstrxslicexipfilex
internal/ internal/
cmd/ # application bootstrap and route isolation cmd/ # 启动引导环境变量注入路由分组注册
consts/ # application error codes and constants consts/ # 业务错误码与常量
controller/ # API-to-service adapters only (incl. open/ open API) controller/ # APIservice 适配层不写业务 open/ 开放接口实现
service/ # domain use cases and provider interfaces service/ # 领域用例业务逻辑直接写这里不用 logic/
dao/ # generated by gf gen dao; never hand edited dao/ # 数据访问gf gen dao 生成 + 部分手写禁止手改生成物
internal/ # 生成代码内部实现
model/ model/
entity/ # generated database entities entity/ # 数据库实体生成 + 部分手写
do/ # generated Data Objects do/ # Data Object写库必须用 DO
dto/ # service boundary input/output dto/ # 服务边界入参/出参
vo/ # API view models vo/ # API 视图模型
middleware/ # configurable route-group middleware middleware/ # 路由中间件RecoverCORS认证全量加密
library/ # JWT, response, pagination primitives library/ # jwt / crypto(RSA+AES) / iploc / page / response 基础件
docs/change-log/ # per-request change records (agent context) table/ # 表列名常量手写维护供代码引用列名
docs/
house-system-design.md # 看房系统总设计文档
change-log/ # 每次请求与变更的记录AI 上下文记忆 git 提交
manifest/ manifest/
config/ # config.dev/test/prod.yaml config/ # config.dev/test/prod.yamlconfig.yaml 不入库
sql/ # ordered MySQL migrations sql/ # 有序 MySQL 迁移脚本 recruitment/ 子模块
utility/ # optional cross-cutting helpers deploy/ # kustomize 部署清单
docker/ # Docker 构建上下文
i18n/ # 国际化资源
protobuf/ # protobuf 定义
deploy/
Dockerfile # 生产容器镜像alpine 最小运行时
hack/ # gf CLI 配置与 Makefile 片段
utility/ # 预留跨切面辅助
``` ```
`dao`, `model/do` and `model/entity` are generated after migration, so their schema never drifts from MySQL. Controllers do not access DAO; only services do. `dao``model/do``model/entity` 由迁移脚本生成保证与 MySQL 结构不漂移
但招聘 / 通知 / 自动任务 / 服务器安全日志等模块的部分结构为手写改动时勿被生成命令覆盖
控制器不访问 DAO只有 service 可以
See `AGENTS.md` for the agent-facing conventions and the context-memory system. 面向 AI 助手的约定与上下文记忆体系见 [`AGENTS.md`](./AGENTS.md)

View File

@ -1,28 +1,64 @@
# Personal multi-client service # service.xpcool.com 个人多客户端后端服务
## Layout GoFrame v2 单体应用 mini / h5 / app 多端提供后端服务并附带完整的后台管理体系
`api → internal/controller → internal/service → internal/dao → internal/model(entity/do)` is enforced. DTO and VO live in `internal/model/dto` and `internal/model/vo`; neither API types nor entities cross that boundary. 核心业务用户认证JWT看房数据小区/房源/成交/预售证招聘考试聚合
站内通知服务器安全日志后台管理RBAC 菜单权限 + 操作审计
## Database model generation > 面向 AI 助手的项目上下文见 [`AGENTS.md`](./AGENTS.md)每次变更的结构化记录见
> [`docs/change-log/`](./docs/change-log/)目录树见 [`PROJECT_STRUCTURE.md`](./PROJECT_STRUCTURE.md)
Run `manifest/sql/001_core.sql` on MySQL, set `DB_DSN`, then run: ## 分层约定
调用链强制为 `api → internal/controller → internal/service → internal/dao → internal/model(entity/do)`
- `controller` 只做 API service 的适配**不碰 dao**
- 跨层出入参走 `internal/model/dto` `internal/model/vo`API 类型与 entity 不得越界
- 数据库操作必须用 DO 对象`internal/model/do`禁止 `g.Map`
- 错误一律用 `gerror`保留堆栈响应统一走 `internal/library/response`
## 路由分组
三组路由在 `internal/cmd/cmd.go` 中注册
| 前缀 | 用途 | 鉴权 |
|---|---|---|
| `/api/service/open` | 开放接口前端直接调用 | 公开无鉴权 |
| `/api/service/user` | 用户端接口 | 登录公开其余走 `UserAuth` |
| `/api/service/admin` | 管理端接口 | `AdminAuth` + 方法+路径自动匹配权限码 |
管理端权限由后端从 `admin_menu`type=2 `path` `"METHOD /路径"`反查
**前端无需传 `X-Permission`**未配置映射的接口一律拒绝
## 数据库模型生成
先在 MySQL 上执行 `manifest/sql/` 下的迁移脚本再设置 `DB_DSN`然后运行
```powershell ```powershell
gf gen dao -p internal -g default -gt -c gf gen dao -p internal -g default -gt -c
``` ```
The command is intentionally the only source of `internal/dao`, `internal/model/do`, and `internal/model/entity`. 该命令是 `internal/dao``internal/model/do``internal/model/entity` **唯一来源**生成物禁止手改
## API isolation 注意部分模块招聘通知自动任务服务器安全日志的表结构为**手写** entity/do/dao
未走 `gf gen`改动时请勿用生成命令覆盖
- `/api/v1/*`: user API, terminal header/body supports `mini`, `h5`, and `app`. ## 本地启动
- `/admin/v1/*`: admin API. Protected endpoints require both an admin access token and an `X-Permission` identifier.
Use `GF_GCFG_FILE=config.dev.yaml` (or `config.test.yaml` / `config.prod.yaml`) and set `DB_DSN` plus a strong `JWT_SECRET` before startup. 环境变量按 `GF_GCFG_FILE` 切换配置文件`config.dev.yaml` / `config.test.yaml` / `config.prod.yaml`
## Local startup - **终端**`source .env.dev && go run main.go`
`.env.dev` 不入库保存本地 DSN 与密钥
- **GoLand**运行配置 `.run/service-dev.run.xml`项目级重载项目后自动识别
已注入 `GF_GCFG_FILE` / `DB_DSN` / `JWT_SECRET`在运行下拉框中选择即可
该文件含数据库口令已加入 `.gitignore`
- **Terminal**: `source .env.dev && go run main.go` (`.env.dev` is gitignored, holds local DSN + secret). 开发环境下 `${DB_DSN}` / `${RECRUITMENT_DB_DSN}` / `${JWT_SECRET}` 占位符未被环境变量注入
- **GoLand**: run configuration `.run/service-dev.run.xml` (project-level, auto-detected after reloading the project) already injects `GF_GCFG_FILE` / `DB_DSN` / `JWT_SECRET`; pick it in the run dropdown. The file is gitignored because it contains the DB password. `injectEnv` 会回填本地默认值保证零配置可启动生产`GF_GCFG_ENV=prod`不兜底配置缺失显性报错
## 构建与测试
```powershell
go build ./...
go test ./...
```

View File

@ -36,7 +36,7 @@ type AdminListRes struct {
type AdminCreateReq struct { type AdminCreateReq struct {
g.Meta `path:"/admin/create" method:"post" tags:"Admin/Admin" summary:"创建管理员"` g.Meta `path:"/admin/create" method:"post" tags:"Admin/Admin" summary:"创建管理员"`
Username string `json:"username" v:"required"` Username string `json:"username" v:"required"`
Password string `json:"password" v:"min-length:6#password too short"` Password string `json:"password" v:"min-length:6#密码长度不能少于 6 位"`
EncryptedKey string `json:"encryptedKey"` EncryptedKey string `json:"encryptedKey"`
EncryptedData string `json:"encryptedData"` EncryptedData string `json:"encryptedData"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
@ -70,7 +70,7 @@ type AdminUpdateRes struct{}
type AdminResetPwdReq struct { type AdminResetPwdReq struct {
g.Meta `path:"/admin/resetPwd/{id}" method:"post" tags:"Admin/Admin" summary:"重置管理员密码"` g.Meta `path:"/admin/resetPwd/{id}" method:"post" tags:"Admin/Admin" summary:"重置管理员密码"`
Id uint64 `json:"id" in:"path" v:"required"` Id uint64 `json:"id" in:"path" v:"required"`
Password string `json:"password" v:"min-length:6#password too short"` Password string `json:"password" v:"min-length:6#密码长度不能少于 6 位"`
EncryptedKey string `json:"encryptedKey"` EncryptedKey string `json:"encryptedKey"`
EncryptedData string `json:"encryptedData"` EncryptedData string `json:"encryptedData"`
} }

View File

@ -35,7 +35,7 @@ type MenuCreateReq struct {
ParentId uint64 `json:"parentId"` ParentId uint64 `json:"parentId"`
Name string `json:"name" v:"required"` Name string `json:"name" v:"required"`
Icon string `json:"icon"` Icon string `json:"icon"`
Type int `json:"type" v:"in:1,2#type must be 1 or 2"` Type int `json:"type" v:"in:1,2#类型必须为 1 或 2"`
Path string `json:"path"` Path string `json:"path"`
Component string `json:"component"` Component string `json:"component"`
Permission string `json:"permission" v:"required"` Permission string `json:"permission" v:"required"`
@ -56,7 +56,7 @@ type MenuUpdateReq struct {
ParentId uint64 `json:"parentId"` ParentId uint64 `json:"parentId"`
Name string `json:"name" v:"required"` Name string `json:"name" v:"required"`
Icon string `json:"icon"` Icon string `json:"icon"`
Type int `json:"type" v:"in:1,2#type must be 1 or 2"` Type int `json:"type" v:"in:1,2#类型必须为 1 或 2"`
Path string `json:"path"` Path string `json:"path"`
Component string `json:"component"` Component string `json:"component"`
Permission string `json:"permission" v:"required"` Permission string `json:"permission" v:"required"`

View File

@ -6,7 +6,7 @@ import "github.com/gogf/gf/v2/frame/g"
// MD5Req 计算文本的 MD5 摘要。 // MD5Req 计算文本的 MD5 摘要。
type MD5Req struct { type MD5Req struct {
g.Meta `path:"/tools/md5" method:"post" tags:"Open/Tools" summary:"计算 MD5 摘要"` g.Meta `path:"/tools/md5" method:"post" tags:"Open/Tools" summary:"计算 MD5 摘要"`
Text string `json:"text" v:"required#text required"` Text string `json:"text" v:"required#待摘要文本不能为空"`
} }
// MD5Res 是 MD5Req 的响应。 // MD5Res 是 MD5Req 的响应。

View File

@ -6,8 +6,8 @@ import "github.com/gogf/gf/v2/frame/g"
// RandomReq 生成随机字符串。 // RandomReq 生成随机字符串。
type RandomReq struct { type RandomReq struct {
g.Meta `path:"/tools/random" method:"post" tags:"Open/Tools" summary:"生成随机字符串"` g.Meta `path:"/tools/random" method:"post" tags:"Open/Tools" summary:"生成随机字符串"`
Length int `json:"length" d:"16" v:"min:1|max:128#length must be 1-128"` Length int `json:"length" d:"16" v:"min:1|max:128#长度必须在 1-128 之间"`
Type string `json:"type" d:"alnum" v:"in:alnum,digits,letters#unsupported type"` Type string `json:"type" d:"alnum" v:"in:alnum,digits,letters#不支持的随机类型"`
} }
// RandomRes 是 RandomReq 的响应。 // RandomRes 是 RandomReq 的响应。

View File

@ -6,13 +6,13 @@ import "github.com/gogf/gf/v2/frame/g"
// LoginReq 用户登录请求(支持微信/手机验证码/账号密码三种方式)。 // LoginReq 用户登录请求(支持微信/手机验证码/账号密码三种方式)。
type LoginReq struct { type LoginReq struct {
g.Meta `path:"/auth/login" method:"post" tags:"User/Auth" summary:"用户登录"` g.Meta `path:"/auth/login" method:"post" tags:"User/Auth" summary:"用户登录"`
LoginType string `json:"loginType" v:"required|in:wechat,mobile,password#login type required|unsupported login type"` LoginType string `json:"loginType" v:"required|in:wechat,mobile,password#登录方式不能为空|不支持的登录方式"`
Code string `json:"code"` // 微信授权码 Code string `json:"code"` // 微信授权码
Mobile string `json:"mobile"` // 手机号mobile 登录方式) Mobile string `json:"mobile"` // 手机号mobile 登录方式)
VerifyCode string `json:"verifyCode"` // 短信验证码 VerifyCode string `json:"verifyCode"` // 短信验证码
Account string `json:"account"` // 账号password 登录方式) Account string `json:"account"` // 账号password 登录方式)
Password string `json:"password"` // 密码 Password string `json:"password"` // 密码
Terminal string `json:"terminal" v:"required|in:mini,h5,app#terminal required|invalid terminal"` Terminal string `json:"terminal" v:"required|in:mini,h5,app#终端类型不能为空|无效的终端类型"`
} }
// LoginRes 用户登录响应。 // LoginRes 用户登录响应。

View File

@ -1 +1,6 @@
// Package user_login 用户端登录契约。
//
// 说明:当前用户端登录已统一收敛到 `api/user/auth`(支持微信 / 手机号 / 密码三种方式),
// user 路由组直接绑定 Auth 控制器即可,因此本包暂无端点定义。
// 保留该包是为了维持目录结构一致性,后续如需拆分「扫码登录」「短信登录」等独立契约,可在此扩展。
package user_login package user_login

View File

@ -6,5 +6,5 @@
// //
// 当前结构: // 当前结构:
// //
// common/tools shared utility toolbox (md5, cryptox, uuid, ...) // common/tools 公共工具集md5、cryptox、uuid 等)
package common package common

View File

@ -6,19 +6,19 @@
// //
// 结构: // 结构:
// //
// common/tools/md5 MD5 digest helpers // common/tools/md5 MD5 摘要工具
// common/tools/cryptox AES/DES encryption with base64 output // common/tools/cryptox AES/DES 加解密base64 输出)
// common/tools/uuid unique ID generation // common/tools/uuid 唯一 ID 生成
// common/tools/random random numbers and strings // common/tools/random 随机数与随机字符串
// common/tools/timex time formatting and computation // common/tools/timex 时间格式化与计算
// common/tools/convertx type conversion with default fallback // common/tools/convertx 类型转换(带默认值兜底)
// common/tools/strx string / naming / masking helpers // common/tools/strx 字符串 / 命名 / 脱敏工具
// common/tools/slicex generic slice utilities // common/tools/slicex 泛型切片工具
// common/tools/ip IP address helpers // common/tools/ip IP 地址工具
// common/tools/filex file system helpers // common/tools/filex 文件系统工具
// //
// 规则: // 规则:
// - Never depend on internal/ — this module must stay self-contained. // - 禁止依赖 internal/ —— 本模块必须保持自包含。
// - Prefer reusing GoFrame built-in components over re-implementing. // - 优先复用 GoFrame 内置组件,避免重复造轮子。
// - Keep each helper small and add a Chinese doc comment. // - 每个工具保持小巧,并补充中文文档注释。
package tools package tools

View File

@ -31,7 +31,7 @@ func LocalIP() (string, error) {
} }
// IsInternal 判断 s 是否为私有/内网 IPv4 地址 // IsInternal 判断 s 是否为私有/内网 IPv4 地址
// (private ranges, loopback or link-local). // (含私有网段、回环地址、链路本地地址)。
func IsInternal(s string) bool { func IsInternal(s string) bool {
parsed := net.ParseIP(s) parsed := net.ParseIP(s)
if parsed == nil || parsed.To4() == nil { if parsed == nil || parsed.To4() == nil {
@ -41,7 +41,7 @@ func IsInternal(s string) bool {
} }
// ToLong 将 IPv4 字符串转为 uint32 数值表示 // ToLong 将 IPv4 字符串转为 uint32 数值表示
// (big-endian, same as inet_aton). // (大端序,与 inet_aton 一致)。
func ToLong(s string) (uint32, error) { func ToLong(s string) (uint32, error) {
ipv4 := net.ParseIP(s).To4() ipv4 := net.ParseIP(s).To4()
if ipv4 == nil { if ipv4 == nil {

View File

@ -0,0 +1,69 @@
# 变更日志 2026-09-13
## 请求
对整个项目做一次整理更新陈旧的文档凡能用中文的地方注释文档提示语错误信息一律中文化
## 变更
### 文档
- `README.MD` **重写**原文为英文且路由信息陈旧写的是 `/api/v1`现改为中文并修正为实际的三组路由
`/api/service/open``/api/service/user``/api/service/admin`补充分层约定RBAC 权限映射机制
`gf gen dao` 生成流程本地启动方式`.env.dev` / GoLand 运行配置构建测试命令
- `PROJECT_STRUCTURE.md` **重写**原文为英文且目录树过时 house / recruitment / notice / job / serversecurity
等模块现改为中文并按实际结构补全 `api/``internal/``docs/``manifest/` 各层说明
标注部分模块 entity/do/dao 为手写勿被生成命令覆盖这一关键事实
- `hack/hack.mk``hack/hack-cli.mk` 构建脚本注释全部中文化
### 源码注释与提示
- `common/doc.go``common/tools/doc.go` 工具清单由英文改为中文规则说明中文化
- `common/tools/ip/ip.go` `IsInternal` / `ToLong` 注释中文化
- `main.go` MySQL 驱动引入注释中文化
- `internal/cmd/cmd.go` 路由分组注册处的英文注释中文化open / user / admin 三处
- `api/user/login/login.go` 补充中文包文档说明该包当前无端点登录已收敛至 `api/user/auth`
### 校验与错误提示中文化
- **API 层校验消息**`v:"...#提示"` 中文提示
`api/user/auth/auth.go`登录方式终端类型`api/open/tools/md5``api/open/tools/random`
`api/admin/system/menu_manage`type`api/admin/admin/admin`密码长度
- **service 层错误上下文与用户提示**`gerror.Wrap` / `response.Error`共涉及 19 个文件 90
- `service/admin/admin/admin``service/admin/admin/login``service/admin/system/{role,menu,menu_manage,login_log}`
- `service/user/auth`
- `service/house/{community,listing,dashboard,transaction,presale}`
- `service/notice``service/job``service/serversecurity`
- `service/recruitment/{recruitment,crawler}`
- `internal/library/jwt/jwt.go``internal/controller/admin/admin.go`
- 模式统一`gerror.Wrap(err, "query list")` `gerror.Wrap(err, "查询列表失败")`
用户可见提示如 `"username or password incorrect"` `"用户名或密码错误"`
- service `panic("Xxx implementation not registered")` `panic("Xxx 实现未注册")`
### 变更记录
- 新增 `docs/change-log/2026-09-13.md`本文
- `.workbuddy/memory/CHANGELOG.md` 顶部追加当日条目
## 决策与理由
- **`README.MD` / `PROJECT_STRUCTURE.md` 采用重写而非增量修补**两份文档原文均为英文
且路由前缀目录树与当前代码差异过大路由从 `/api/v1` 演进为 `/api/service/*` 三分组
新增 house/recruitment/notice/job/serversecurity 五大模块增量改会造成前后矛盾故整体重写
- **生成代码`internal/model/entity``internal/model/do``internal/dao`的英文注释保持原样**
这些文件由 `gf gen dao` 生成头部含 `DO NOT EDIT` 标记手工中文化会在下次生成时被覆盖
反而制造噪音符合 AGENTS.md生成代码禁止手改的约定
- **错误信息中文化的范围界定**
- 用户可见提示`response.Error`必须中文直接展示给前端/用户
- 内部错误上下文`gerror.Wrap` message也一并中文便于运维/日志排查时统一语义
- 日志文案`g.Log().Errorf` 同步中文保持日志可读性一致
- **保留英文的技术标识不动**HTTP 方法名SQL 关键字表名/列名字段名`cron` 表达式
第三方接口名Bark / pushplus`panic` 中的类型名等属于技术符号翻译反而降低可检索性
## 待办与风险
- 本次仅做文档整理 + 中文化**未改变任何业务逻辑**所有改动均为注释文档与字符串字面量
- `go build ./...` 因当前环境无法访问 `proxy.golang.org`模块下载超时未能完整跑通
已用 `read_lints` 对改动目录做静态检查未发现语法/类型错误
建议在有网络的环境执行一次 `go build ./...` 做最终确认
- 后续新增代码请遵循中文注释约定`gf gen dao` 重新生成后entity/do/dao 的英文注释属预期现象无需处理

8
go.mod
View File

@ -2,13 +2,13 @@ module service.xpcool.com
go 1.23.0 go 1.23.0
require github.com/gogf/gf/v2 v2.10.2
require ( require (
github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2 // indirect github.com/gogf/gf/v2 v2.10.2
) )
require github.com/go-sql-driver/mysql v1.7.1 // indirect
require ( require (
github.com/BurntSushi/toml v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect

2
go.sum
View File

@ -33,8 +33,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lionsoul2014/ip2region v3.17.0+incompatible h1:91PbwotwoEMPV6nfAALL+WTmYHmTuRNFgJX6T8uPUNY=
github.com/lionsoul2014/ip2region v3.17.0+incompatible/go.mod h1:+ZBN7PBoh5gG6/y0ZQ85vJDBe21WnfbRrQQwTfliJJI=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=

View File

@ -1,18 +1,18 @@
# Install/Update to the latest CLI tool. # 安装/更新到最新的 CLI 工具
.PHONY: cli .PHONY: cli
cli: cli:
@set -e; \ @set -e; \
echo "go install github.com/gogf/gf/cmd/gf/v2@latest"; \ echo "go install github.com/gogf/gf/cmd/gf/v2@latest"; \
go install github.com/gogf/gf/cmd/gf/v2@latest; \ go install github.com/gogf/gf/cmd/gf/v2@latest; \
echo "GoFame CLI installed successfully!" echo "GoFrame CLI 安装成功!"
# Check and install CLI tool. # 检查并安装 CLI 工具
.PHONY: cli.install .PHONY: cli.install
cli.install: cli.install:
@set -e; \ @set -e; \
gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \ gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \
echo "GoFame CLI is not installed, start proceeding auto installation..."; \ echo "GoFrame CLI 未安装,开始自动安装..."; \
make cli; \ make cli; \
fi; fi;

View File

@ -1,37 +1,37 @@
.DEFAULT_GOAL := build .DEFAULT_GOAL := build
# Update GoFrame and its CLI to latest stable version. # 更新 GoFrame CLI 到最新稳定版
.PHONY: up .PHONY: up
up: cli.install up: cli.install
@gf up -a @gf up -a
# Build binary using configuration from hack/config.yaml. # 使用 hack/config.yaml 中的配置构建二进制
.PHONY: build .PHONY: build
build: cli.install build: cli.install
@gf build -ew @gf build -ew
# Parse api and generate controller/sdk. # 解析 api 目录并生成 controller/sdk
.PHONY: ctrl .PHONY: ctrl
ctrl: cli.install ctrl: cli.install
@gf gen ctrl @gf gen ctrl
# Generate Go files for DAO/DO/Entity. # 生成 DAO/DO/Entity Go 代码
.PHONY: dao .PHONY: dao
dao: cli.install dao: cli.install
@gf gen dao @gf gen dao
# Parse current project go files and generate enums go file. # 解析当前项目 Go 文件并生成枚举文件
.PHONY: enums .PHONY: enums
enums: cli.install enums: cli.install
@gf gen enums @gf gen enums
# Generate Go files for Service. # 生成 Service 接口与实现代码
.PHONY: service .PHONY: service
service: cli.install service: cli.install
@gf gen service @gf gen service
# Build docker image. # 构建 Docker 镜像
.PHONY: image .PHONY: image
image: cli.install image: cli.install
$(eval _TAG = $(shell git rev-parse --short HEAD)) $(eval _TAG = $(shell git rev-parse --short HEAD))
@ -43,13 +43,13 @@ endif
@gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG}; @gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG};
# Build docker image and automatically push to docker repo. # 构建 Docker 镜像并自动推送到镜像仓库
.PHONY: image.push .PHONY: image.push
image.push: cli.install image.push: cli.install
@make image PUSH=-p; @make image PUSH=-p;
# Deploy image and yaml to current kubectl environment. # 部署镜像与 yaml 到当前 kubectl 环境
.PHONY: deploy .PHONY: deploy
deploy: cli.install deploy: cli.install
$(eval _TAG = $(if ${TAG}, ${TAG}, develop)) $(eval _TAG = $(if ${TAG}, ${TAG}, develop))
@ -64,12 +64,12 @@ deploy: cli.install
fi; fi;
# Parsing protobuf files and generating go files. # 解析 protobuf 文件并生成 Go 代码
.PHONY: pb .PHONY: pb
pb: cli.install pb: cli.install
@gf gen pb @gf gen pb
# Generate protobuf files for database tables. # 根据数据库表生成 protobuf 文件
.PHONY: pbentity .PHONY: pbentity
pbentity: cli.install pbentity: cli.install
@gf gen pbentity @gf gen pbentity

View File

@ -4,19 +4,19 @@ import (
"context" "context"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/gcmd"
"github.com/gogf/gf/v2/os/genv" "github.com/gogf/gf/v2/os/genv"
"github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
adminctl "service.xpcool.com/internal/controller/admin" adminctl "service.xpcool.com/internal/controller/admin"
housectl "service.xpcool.com/internal/controller/house" housectl "service.xpcool.com/internal/controller/house"
jobctl "service.xpcool.com/internal/controller/job"
noticectl "service.xpcool.com/internal/controller/notice"
openctl "service.xpcool.com/internal/controller/open" openctl "service.xpcool.com/internal/controller/open"
recruitmentctl "service.xpcool.com/internal/controller/recruitment" recruitmentctl "service.xpcool.com/internal/controller/recruitment"
noticectl "service.xpcool.com/internal/controller/notice"
jobctl "service.xpcool.com/internal/controller/job"
serversecurityctl "service.xpcool.com/internal/controller/serversecurity" serversecurityctl "service.xpcool.com/internal/controller/serversecurity"
userctl "service.xpcool.com/internal/controller/user" userctl "service.xpcool.com/internal/controller/user"
"service.xpcool.com/internal/library/crypto" "service.xpcool.com/internal/library/crypto"
@ -30,24 +30,27 @@ import (
adminmenu "service.xpcool.com/internal/service/admin/system/menu" adminmenu "service.xpcool.com/internal/service/admin/system/menu"
menu_manage "service.xpcool.com/internal/service/admin/system/menu_manage" menu_manage "service.xpcool.com/internal/service/admin/system/menu_manage"
role "service.xpcool.com/internal/service/admin/system/role" role "service.xpcool.com/internal/service/admin/system/role"
userauth "service.xpcool.com/internal/service/user/auth"
housecommunity "service.xpcool.com/internal/service/house/community" housecommunity "service.xpcool.com/internal/service/house/community"
housedashboard "service.xpcool.com/internal/service/house/dashboard" housedashboard "service.xpcool.com/internal/service/house/dashboard"
houselisting "service.xpcool.com/internal/service/house/listing" houselisting "service.xpcool.com/internal/service/house/listing"
housetransaction "service.xpcool.com/internal/service/house/transaction"
housepresale "service.xpcool.com/internal/service/house/presale" housepresale "service.xpcool.com/internal/service/house/presale"
recruitmentsvc "service.xpcool.com/internal/service/recruitment" housetransaction "service.xpcool.com/internal/service/house/transaction"
noticesvc "service.xpcool.com/internal/service/notice"
jobsvc "service.xpcool.com/internal/service/job" jobsvc "service.xpcool.com/internal/service/job"
noticesvc "service.xpcool.com/internal/service/notice"
recruitmentsvc "service.xpcool.com/internal/service/recruitment"
serversecuritysvc "service.xpcool.com/internal/service/serversecurity" serversecuritysvc "service.xpcool.com/internal/service/serversecurity"
userauth "service.xpcool.com/internal/service/user/auth"
) )
// 开发环境默认值任何启动方式GoLand / 命令行 / go run漏注入环境变量时兜底 // 开发环境默认值任何启动方式GoLand / 命令行 / go run漏注入环境变量时兜底
// 避免 ${DB_DSN} 占位符原样传入 gdb 导致全站接口 500。 // 避免 ${DB_DSN} 占位符原样传入 gdb 导致全站接口 500。
// 生产GF_GCFG_ENV=prod不启用兜底配置缺失应显性报错。 // 生产GF_GCFG_ENV=prod不启用兜底配置缺失应显性报错。
//
// 注意:库名以本机实际建库为准(本项目为 service / recruitment
// 若你的本地库名或账号口令不同,请通过 DB_DSN / RECRUITMENT_DB_DSN 覆盖。
const ( const (
devDefaultDBDSN = "mysql:root:root123@tcp(127.0.0.1:3306)/service_xpcool_com?loc=Local" devDefaultDBDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/service?loc=Local"
devDefaultRecruitmentDSN = "mysql:root:root123@tcp(127.0.0.1:3306)/recruitment?loc=Local" devDefaultRecruitmentDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/recruitment?loc=Local"
devDefaultJWTSecret = "dev-only-secret-not-for-production" devDefaultJWTSecret = "dev-only-secret-not-for-production"
) )
@ -130,19 +133,19 @@ var (
serversecuritysvc.RegisterServerSecurity(serversecuritysvc.New()) serversecuritysvc.RegisterServerSecurity(serversecuritysvc.New())
s.Group("/api/service/open", func(group *ghttp.RouterGroup) { s.Group("/api/service/open", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse)
group.Bind(openctl.New()) // Open tools API for frontends, no auth required. group.Bind(openctl.New()) // 开放工具接口(前端调用,免鉴权)。
group.Bind(serversecurityctl.NewReport()) // 安全日志上报(宿主机脚本,内部令牌校验)。 group.Bind(serversecurityctl.NewReport()) // 安全日志上报(宿主机脚本,内部令牌校验)。
}) })
s.Group("/api/service/user", func(group *ghttp.RouterGroup) { s.Group("/api/service/user", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse)
group.Bind(userctl.New()) // Login and refresh routes are public. group.Bind(userctl.New()) // 登录与刷新令牌接口公开。
group.Group("/", func(protected *ghttp.RouterGroup) { protected.Middleware(middleware.UserAuth(tokens)) }) group.Group("/", func(protected *ghttp.RouterGroup) { protected.Middleware(middleware.UserAuth(tokens)) })
}) })
s.Group("/api/service/admin", func(group *ghttp.RouterGroup) { s.Group("/api/service/admin", func(group *ghttp.RouterGroup) {
// 全量请求/响应加密(生产 encrypt.fullBody=trueCORS → APICrypto → Recover → HandlerResponse。 // 全量请求/响应加密(生产 encrypt.fullBody=trueCORS → APICrypto → Recover → HandlerResponse。
// Recover 置于 APICrypto 内层,使 500 异常响应同样被加密public-key 端点明文豁免。 // Recover 置于 APICrypto 内层,使 500 异常响应同样被加密public-key 端点明文豁免。
group.Middleware(middleware.CORS, middleware.APICrypto(cryptoSvc), middleware.Recover, ghttp.MiddlewareHandlerResponse) group.Middleware(middleware.CORS, middleware.APICrypto(cryptoSvc), middleware.Recover, ghttp.MiddlewareHandlerResponse)
group.Bind(adminctl.NewAuth()) // Public: login only. group.Bind(adminctl.NewAuth()) // 公开端点:仅登录。
group.Group("/", func(profile *ghttp.RouterGroup) { group.Group("/", func(profile *ghttp.RouterGroup) {
// 仅登录端点:个人资料 / 权限码 / 菜单路由。 // 仅登录端点:个人资料 / 权限码 / 菜单路由。
profile.Middleware(middleware.AdminAuthOnly(tokens)) profile.Middleware(middleware.AdminAuthOnly(tokens))

View File

@ -28,7 +28,7 @@ func resolvePassword(ctx context.Context, encryptedKey, encryptedData, password
if cryptolib.Get().AllowPlain() { if cryptolib.Get().AllowPlain() {
return password, nil return password, nil
} }
return "", response.Error(consts.CodeInvalidParam, "encrypted payload required") return "", response.Error(consts.CodeInvalidParam, "必须提交加密凭据")
} }
// AdminList 分页查询管理员。 // AdminList 分页查询管理员。

View File

@ -74,7 +74,7 @@ func SetDefault(s *Service) { defaultSvc = s }
// Get 返回进程级默认加密服务实例,未初始化时 panic启动必经 New 注册)。 // Get 返回进程级默认加密服务实例,未初始化时 panic启动必经 New 注册)。
func Get() *Service { func Get() *Service {
if defaultSvc == nil { if defaultSvc == nil {
panic("crypto service not registered") panic("加密服务未注册")
} }
return defaultSvc return defaultSvc
} }

View File

@ -52,18 +52,18 @@ func (s *Service) Parse(token, typ, scope string) (*Claims, error) {
// 先使用常量时间比较校验签名,再解析声明,避免伪造令牌进入业务层。 // 先使用常量时间比较校验签名,再解析声明,避免伪造令牌进入业务层。
parts := strings.Split(token, ".") parts := strings.Split(token, ".")
if len(parts) != 3 || !hmac.Equal([]byte(parts[2]), []byte(s.signature(parts[0]+"."+parts[1]))) { if len(parts) != 3 || !hmac.Equal([]byte(parts[2]), []byte(s.signature(parts[0]+"."+parts[1]))) {
return nil, gerror.New("invalid token") return nil, gerror.New("令牌无效")
} }
raw, err := base64.RawURLEncoding.DecodeString(parts[1]) raw, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil { if err != nil {
return nil, gerror.Wrap(err, "decode token") return nil, gerror.Wrap(err, "解码令牌失败")
} }
var c Claims var c Claims
if err = json.Unmarshal(raw, &c); err != nil { if err = json.Unmarshal(raw, &c); err != nil {
return nil, gerror.Wrap(err, "decode claims") return nil, gerror.Wrap(err, "解析令牌声明失败")
} }
if c.ExpireAt < time.Now().Unix() || c.Type != typ || c.Scope != scope { if c.ExpireAt < time.Now().Unix() || c.Type != typ || c.Scope != scope {
return nil, gerror.New("token expired or scope mismatch") return nil, gerror.New("令牌已过期或作用域不匹配")
} }
return &c, nil return &c, nil
} }
@ -80,7 +80,7 @@ func (s *Service) sign(c Claims) (string, error) {
h := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) h := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`))
b, err := json.Marshal(c) b, err := json.Marshal(c)
if err != nil { if err != nil {
return "", gerror.Wrap(err, "encode claims") return "", gerror.Wrap(err, "编码令牌声明失败")
} }
p := h + "." + base64.RawURLEncoding.EncodeToString(b) p := h + "." + base64.RawURLEncoding.EncodeToString(b)
return p + "." + s.signature(p), nil return p + "." + s.signature(p), nil

View File

@ -31,7 +31,7 @@ func NewAdminManage() IAdminManage { return &adminManage{} }
func AdminManage() IAdminManage { func AdminManage() IAdminManage {
if localAdminManage == nil { if localAdminManage == nil {
panic("AdminManage implementation not registered") panic("AdminManage 实现未注册")
} }
return localAdminManage return localAdminManage
} }
@ -42,14 +42,14 @@ func RegisterAdminManage(i IAdminManage) { localAdminManage = i }
func (s *adminManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.AdminItem, int, error) { func (s *adminManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.AdminItem, int, error) {
total, err := dao.AdminUser.Ctx(ctx).Count() total, err := dao.AdminUser.Ctx(ctx).Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count admins") return nil, 0, gerror.Wrap(err, "统计管理员总数失败")
} }
if total == 0 { if total == 0 {
return nil, 0, nil return nil, 0, nil
} }
var list []entity.AdminUser var list []entity.AdminUser
if err = dao.AdminUser.Ctx(ctx).Page(q.Page, q.Size).OrderDesc("id").Scan(&list); err != nil { if err = dao.AdminUser.Ctx(ctx).Page(q.Page, q.Size).OrderDesc("id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query admins") return nil, 0, gerror.Wrap(err, "查询管理员列表失败")
} }
items := make([]*dto.AdminItem, 0, len(list)) items := make([]*dto.AdminItem, 0, len(list))
for i := range list { for i := range list {
@ -71,7 +71,7 @@ func (s *adminManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.AdminIt
func (s *adminManage) roles(ctx context.Context, adminID uint64) ([]uint64, []string, error) { func (s *adminManage) roles(ctx context.Context, adminID uint64) ([]uint64, []string, error) {
var rels []entity.AdminUserRole var rels []entity.AdminUserRole
if err := dao.AdminUserRole.Ctx(ctx).Where(do.AdminUserRole{AdminUserId: adminID}).Scan(&rels); err != nil { if err := dao.AdminUserRole.Ctx(ctx).Where(do.AdminUserRole{AdminUserId: adminID}).Scan(&rels); err != nil {
return nil, nil, gerror.Wrap(err, "query admin roles") return nil, nil, gerror.Wrap(err, "查询管理员角色关联失败")
} }
roleIds := make([]uint64, 0, len(rels)) roleIds := make([]uint64, 0, len(rels))
for _, r := range rels { for _, r := range rels {
@ -82,7 +82,7 @@ func (s *adminManage) roles(ctx context.Context, adminID uint64) ([]uint64, []st
} }
var roles []entity.AdminRole var roles []entity.AdminRole
if err := dao.AdminRole.Ctx(ctx).WhereIn("id", roleIds).Scan(&roles); err != nil { if err := dao.AdminRole.Ctx(ctx).WhereIn("id", roleIds).Scan(&roles); err != nil {
return nil, nil, gerror.Wrap(err, "query roles") return nil, nil, gerror.Wrap(err, "查询角色列表失败")
} }
names := make([]string, 0, len(roles)) names := make([]string, 0, len(roles))
for _, r := range roles { for _, r := range roles {
@ -94,21 +94,21 @@ func (s *adminManage) roles(ctx context.Context, adminID uint64) ([]uint64, []st
func (s *adminManage) Create(ctx context.Context, in dto.AdminCreateInput) (uint64, error) { func (s *adminManage) Create(ctx context.Context, in dto.AdminCreateInput) (uint64, error) {
count, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Username: in.Username}).Count() count, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Username: in.Username}).Count()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "check username") return 0, gerror.Wrap(err, "校验用户名是否存在失败")
} }
if count > 0 { if count > 0 {
return 0, response.Error(consts.CodeInvalidParam, "username already exists") return 0, response.Error(consts.CodeInvalidParam, "用户名已存在")
} }
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "hash password") return 0, gerror.Wrap(err, "生成密码哈希失败")
} }
id, err := dao.AdminUser.Ctx(ctx).Data(do.AdminUser{ id, err := dao.AdminUser.Ctx(ctx).Data(do.AdminUser{
Username: in.Username, PasswordHash: string(hash), Nickname: in.Nickname, Status: 1, Username: in.Username, PasswordHash: string(hash), Nickname: in.Nickname, Status: 1,
BarkDeviceId: in.BarkDeviceId, PushplusToken: in.PushplusToken, BarkDeviceId: in.BarkDeviceId, PushplusToken: in.PushplusToken,
}).InsertAndGetId() }).InsertAndGetId()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "insert admin") return 0, gerror.Wrap(err, "新增管理员失败")
} }
if err = bindAdminRoles(ctx, uint64(id), in.RoleIds); err != nil { if err = bindAdminRoles(ctx, uint64(id), in.RoleIds); err != nil {
return 0, err return 0, err
@ -120,7 +120,7 @@ func (s *adminManage) Update(ctx context.Context, in dto.AdminUpdateInput) error
data := do.AdminUser{Nickname: in.Nickname, Status: in.Status, data := do.AdminUser{Nickname: in.Nickname, Status: in.Status,
BarkDeviceId: in.BarkDeviceId, PushplusToken: in.PushplusToken} BarkDeviceId: in.BarkDeviceId, PushplusToken: in.PushplusToken}
if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: in.Id}).Data(data).Update(); err != nil { if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: in.Id}).Data(data).Update(); err != nil {
return gerror.Wrap(err, "update admin") return gerror.Wrap(err, "更新管理员失败")
} }
return bindAdminRoles(ctx, in.Id, in.RoleIds) return bindAdminRoles(ctx, in.Id, in.RoleIds)
} }
@ -128,21 +128,21 @@ func (s *adminManage) Update(ctx context.Context, in dto.AdminUpdateInput) error
func (s *adminManage) ResetPassword(ctx context.Context, id uint64, password string) error { func (s *adminManage) ResetPassword(ctx context.Context, id uint64, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
return gerror.Wrap(err, "hash password") return gerror.Wrap(err, "生成密码哈希失败")
} }
if _, err = dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Data(do.AdminUser{PasswordHash: string(hash)}).Update(); err != nil { if _, err = dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Data(do.AdminUser{PasswordHash: string(hash)}).Update(); err != nil {
return gerror.Wrap(err, "reset password") return gerror.Wrap(err, "重置管理员密码失败")
} }
return nil return nil
} }
func (s *adminManage) Delete(ctx context.Context, id uint64) error { func (s *adminManage) Delete(ctx context.Context, id uint64) error {
if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Delete(); err != nil { if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Delete(); err != nil {
return gerror.Wrap(err, "delete admin") return gerror.Wrap(err, "删除管理员失败")
} }
// 关联关系物理删除,避免唯一键残留。 // 关联关系物理删除,避免唯一键残留。
if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: id}).Delete(); err != nil { if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: id}).Delete(); err != nil {
return gerror.Wrap(err, "delete admin role bindings") return gerror.Wrap(err, "删除管理员角色关联失败")
} }
return nil return nil
} }
@ -150,11 +150,11 @@ func (s *adminManage) Delete(ctx context.Context, id uint64) error {
// bindAdminRoles 全量重绑管理员-角色关系。 // bindAdminRoles 全量重绑管理员-角色关系。
func bindAdminRoles(ctx context.Context, adminID uint64, roleIds []uint64) error { func bindAdminRoles(ctx context.Context, adminID uint64, roleIds []uint64) error {
if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: adminID}).Delete(); err != nil { if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: adminID}).Delete(); err != nil {
return gerror.Wrap(err, "clear admin roles") return gerror.Wrap(err, "清除管理员角色关联失败")
} }
for _, rid := range roleIds { for _, rid := range roleIds {
if _, err := dao.AdminUserRole.Ctx(ctx).Data(do.AdminUserRole{AdminUserId: adminID, RoleId: rid}).Insert(); err != nil { if _, err := dao.AdminUserRole.Ctx(ctx).Data(do.AdminUserRole{AdminUserId: adminID, RoleId: rid}).Insert(); err != nil {
return gerror.Wrap(err, "bind admin role") return gerror.Wrap(err, "绑定管理员角色失败")
} }
} }
return nil return nil

View File

@ -40,7 +40,7 @@ var localAdminAuth IAdminAuth
func AdminAuth() IAdminAuth { func AdminAuth() IAdminAuth {
if localAdminAuth == nil { if localAdminAuth == nil {
panic("AdminAuth implementation not registered") panic("AdminAuth 实现未注册")
} }
return localAdminAuth return localAdminAuth
} }
@ -58,13 +58,13 @@ func (s *adminAuth) Login(ctx context.Context, in dto.AdminLoginInput) (*dto.Tok
// 管理端只允许账号密码登录,状态异常或密码错误均返回统一错误,避免枚举账号。 // 管理端只允许账号密码登录,状态异常或密码错误均返回统一错误,避免枚举账号。
var a entity.AdminUser var a entity.AdminUser
if err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Username: in.Username}).Scan(&a); err != nil { if err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Username: in.Username}).Scan(&a); err != nil {
return nil, 0, gerror.Wrap(err, "query administrator") return nil, 0, gerror.Wrap(err, "查询管理员失败")
} }
if a.Id == 0 { if a.Id == 0 {
return nil, 0, response.Error(consts.CodeAdminNotFound, "administrator not found") return nil, 0, response.Error(consts.CodeAdminNotFound, "管理员不存在")
} }
if a.Status != 1 || bcrypt.CompareHashAndPassword([]byte(a.PasswordHash), []byte(in.Password)) != nil { if a.Status != 1 || bcrypt.CompareHashAndPassword([]byte(a.PasswordHash), []byte(in.Password)) != nil {
return nil, 0, response.Error(consts.CodeAdminPasswordWrong, "username or password incorrect") return nil, 0, response.Error(consts.CodeAdminPasswordWrong, "用户名或密码错误")
} }
// 通过 issue 签发令牌对并把刷新令牌 JTI 落库,保证后续可刷新、可撤销。 // 通过 issue 签发令牌对并把刷新令牌 JTI 落库,保证后续可刷新、可撤销。
return s.issue(ctx, a.Id, "") return s.issue(ctx, a.Id, "")
@ -85,14 +85,14 @@ func (s *adminAuth) ResolveLogin(ctx context.Context, encryptedKey, encryptedDat
} }
if s.crypto.FullBody() { if s.crypto.FullBody() {
if username == "" || password == "" { if username == "" || password == "" {
return "", "", response.Error(consts.CodeInvalidParam, "username or password required") return "", "", response.Error(consts.CodeInvalidParam, "用户名和密码不能为空")
} }
return username, password, nil return username, password, nil
} }
if s.crypto.AllowPlain() { if s.crypto.AllowPlain() {
return username, password, nil return username, password, nil
} }
return "", "", response.Error(consts.CodeInvalidParam, "encrypted payload required") return "", "", response.Error(consts.CodeInvalidParam, "必须提交加密凭据")
} }
// PublicKey 返回登录加密 RSA 公钥PEM供前端混合加密密码。 // PublicKey 返回登录加密 RSA 公钥PEM供前端混合加密密码。
@ -104,7 +104,7 @@ func (s *adminAuth) PublicKey(_ context.Context) (string, error) {
func (s *adminAuth) Refresh(ctx context.Context, refresh string) (*dto.TokenPair, uint64, error) { func (s *adminAuth) Refresh(ctx context.Context, refresh string) (*dto.TokenPair, uint64, error) {
c, err := s.tokens.Parse(refresh, "refresh", "admin") c, err := s.tokens.Parse(refresh, "refresh", "admin")
if err != nil { if err != nil {
return nil, 0, response.Error(consts.CodeUnauthorized, "invalid refresh token") return nil, 0, response.Error(consts.CodeUnauthorized, "刷新令牌无效")
} }
// 顺手清理已过期的刷新会话,防止表无限膨胀(失败不影响主流程)。 // 顺手清理已过期的刷新会话,防止表无限膨胀(失败不影响主流程)。
_, _ = dao.AuthRefreshSession.Ctx(ctx).WhereLT("expired_at", gtime.Now()).Delete() _, _ = dao.AuthRefreshSession.Ctx(ctx).WhereLT("expired_at", gtime.Now()).Delete()
@ -140,7 +140,7 @@ func (s *adminAuth) Revoke(ctx context.Context, refresh string) error {
func (s *adminAuth) issue(ctx context.Context, id uint64, terminal string) (*dto.TokenPair, uint64, error) { func (s *adminAuth) issue(ctx context.Context, id uint64, terminal string) (*dto.TokenPair, uint64, error) {
access, refresh, exp, err := s.tokens.Issue(id, "admin", terminal) access, refresh, exp, err := s.tokens.Issue(id, "admin", terminal)
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "issue token") return nil, 0, gerror.Wrap(err, "签发令牌失败")
} }
claims, err := s.tokens.Parse(refresh, "refresh", "admin") claims, err := s.tokens.Parse(refresh, "refresh", "admin")
if err != nil { if err != nil {
@ -160,17 +160,17 @@ func (s *adminAuth) HasPermission(ctx context.Context, adminID uint64, permissio
// 必须过滤角色启用状态 r.status=1否则被禁用角色绑定的权限仍然生效。 // 必须过滤角色启用状态 r.status=1否则被禁用角色绑定的权限仍然生效。
count, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role r", "ur.role_id=r.id").LeftJoin("admin_role_menu rm", "ur.role_id=rm.role_id").LeftJoin("admin_menu m", "rm.menu_id=m.id").Where("ur.admin_user_id", adminID).Where("m.permission", permission).Where("m.status", 1).Where("r.status", 1).Count() count, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role r", "ur.role_id=r.id").LeftJoin("admin_role_menu rm", "ur.role_id=rm.role_id").LeftJoin("admin_menu m", "rm.menu_id=m.id").Where("ur.admin_user_id", adminID).Where("m.permission", permission).Where("m.status", 1).Where("r.status", 1).Count()
if err != nil { if err != nil {
return false, gerror.Wrap(err, "check permission") return false, gerror.Wrap(err, "校验权限失败")
} }
return count > 0, nil return count > 0, nil
} }
func (s *adminAuth) Info(ctx context.Context, adminID uint64) (*dto.AdminInfo, error) { func (s *adminAuth) Info(ctx context.Context, adminID uint64) (*dto.AdminInfo, error) {
var a entity.AdminUser var a entity.AdminUser
if err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: adminID}).Scan(&a); err != nil { if err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: adminID}).Scan(&a); err != nil {
return nil, gerror.Wrap(err, "query admin info") return nil, gerror.Wrap(err, "查询管理员资料失败")
} }
if a.Id == 0 { if a.Id == 0 {
return nil, response.Error(consts.CodeAdminNotFound, "administrator not found") return nil, response.Error(consts.CodeAdminNotFound, "管理员不存在")
} }
roles, err := s.roleCodes(ctx, adminID) roles, err := s.roleCodes(ctx, adminID)
if err != nil { if err != nil {
@ -183,7 +183,7 @@ func (s *adminAuth) Codes(ctx context.Context, adminID uint64) ([]string, error)
// 同时用作 vben 前端按钮权限码与后端 X-Permission 校验标识。 // 同时用作 vben 前端按钮权限码与后端 X-Permission 校验标识。
list, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role r", "ur.role_id=r.id").LeftJoin("admin_role_menu rm", "ur.role_id=rm.role_id").LeftJoin("admin_menu m", "rm.menu_id=m.id").Where("ur.admin_user_id", adminID).Where("m.status", 1).Where("r.status", 1).WhereGT("m.permission", "").Fields("DISTINCT m.permission").Array() list, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role r", "ur.role_id=r.id").LeftJoin("admin_role_menu rm", "ur.role_id=rm.role_id").LeftJoin("admin_menu m", "rm.menu_id=m.id").Where("ur.admin_user_id", adminID).Where("m.status", 1).Where("r.status", 1).WhereGT("m.permission", "").Fields("DISTINCT m.permission").Array()
if err != nil { if err != nil {
return nil, gerror.Wrap(err, "query access codes") return nil, gerror.Wrap(err, "查询权限码失败")
} }
codes := make([]string, 0, len(list)) codes := make([]string, 0, len(list))
for _, v := range list { for _, v := range list {
@ -194,7 +194,7 @@ func (s *adminAuth) Codes(ctx context.Context, adminID uint64) ([]string, error)
func (s *adminAuth) roleCodes(ctx context.Context, adminID uint64) ([]string, error) { func (s *adminAuth) roleCodes(ctx context.Context, adminID uint64) ([]string, error) {
list, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role r", "ur.role_id=r.id").Where("ur.admin_user_id", adminID).Where("r.status", 1).Fields("DISTINCT r.code").Array() list, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role r", "ur.role_id=r.id").Where("ur.admin_user_id", adminID).Where("r.status", 1).Fields("DISTINCT r.code").Array()
if err != nil { if err != nil {
return nil, gerror.Wrap(err, "query admin roles") return nil, gerror.Wrap(err, "查询管理员角色失败")
} }
codes := make([]string, 0, len(list)) codes := make([]string, 0, len(list))
for _, v := range list { for _, v := range list {
@ -207,7 +207,7 @@ func (s *adminAuth) PermissionForPath(ctx context.Context, method, path string)
// 未配置映射的接口一律拒绝访问(返回空则中间件拦截)。 // 未配置映射的接口一律拒绝访问(返回空则中间件拦截)。
var list []entity.AdminMenu var list []entity.AdminMenu
if err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Type: 2, Status: 1}).Scan(&list); err != nil { if err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Type: 2, Status: 1}).Scan(&list); err != nil {
return "", gerror.Wrap(err, "query permission mappings") return "", gerror.Wrap(err, "查询权限映射失败")
} }
req := method + " " + path req := method + " " + path
for _, m := range list { for _, m := range list {

View File

@ -33,7 +33,7 @@ type adminLoginLog struct{}
func AdminLoginLog() IAdminLoginLog { func AdminLoginLog() IAdminLoginLog {
if localAdminLoginLog == nil { if localAdminLoginLog == nil {
panic("AdminLoginLog implementation not registered") panic("AdminLoginLog 实现未注册")
} }
return localAdminLoginLog return localAdminLoginLog
} }
@ -47,7 +47,7 @@ func (s *adminLoginLog) Record(ctx context.Context, e LoginEvent) error {
Username: e.Username, Ip: e.IP, UserAgent: e.UserAgent, Username: e.Username, Ip: e.IP, UserAgent: e.UserAgent,
Status: e.Status, FailReason: e.FailReason, Status: e.Status, FailReason: e.FailReason,
}).Insert() }).Insert()
return gerror.Wrap(err, "insert login log") return gerror.Wrap(err, "写入登录日志失败")
} }
// List 分页查询管理员登录日志支持账号与结果过滤Status 为 nil 时不过滤)。 // List 分页查询管理员登录日志支持账号与结果过滤Status 为 nil 时不过滤)。
@ -61,14 +61,14 @@ func (s *adminLoginLog) List(ctx context.Context, q dto.LoginLogQuery) ([]*dto.L
} }
total, err := m.Count() total, err := m.Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count login logs") return nil, 0, gerror.Wrap(err, "统计登录日志总数失败")
} }
if total == 0 { if total == 0 {
return nil, 0, nil return nil, 0, nil
} }
var list []entity.AdminLoginLog var list []entity.AdminLoginLog
if err = m.Page(q.Page, q.Size).OrderDesc("id").Scan(&list); err != nil { if err = m.Page(q.Page, q.Size).OrderDesc("id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query login logs") return nil, 0, gerror.Wrap(err, "查询登录日志列表失败")
} }
items := make([]*dto.LoginLogItem, 0, len(list)) items := make([]*dto.LoginLogItem, 0, len(list))
for i := range list { for i := range list {

View File

@ -26,7 +26,7 @@ var localAdminMenu IAdminMenu
func AdminMenu() IAdminMenu { func AdminMenu() IAdminMenu {
if localAdminMenu == nil { if localAdminMenu == nil {
panic("AdminMenu implementation not registered") panic("AdminMenu 实现未注册")
} }
return localAdminMenu return localAdminMenu
} }
@ -55,7 +55,7 @@ func (s *adminMenu) Routes(ctx context.Context, adminID uint64) ([]*dto.RouteIte
Where("m.deleted_at IS NULL"). Where("m.deleted_at IS NULL").
Fields("m.*"). Fields("m.*").
Scan(&list); err != nil { Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query admin menu routes") return nil, gerror.Wrap(err, "查询管理员菜单路由失败")
} }
byID := make(map[uint64]*dto.RouteItem, len(list)) byID := make(map[uint64]*dto.RouteItem, len(list))
for i := range list { for i := range list {

View File

@ -30,7 +30,7 @@ func NewMenuManage() IMenuManage { return &menuManage{} }
func MenuManage() IMenuManage { func MenuManage() IMenuManage {
if localMenuManage == nil { if localMenuManage == nil {
panic("MenuManage implementation not registered") panic("MenuManage 实现未注册")
} }
return localMenuManage return localMenuManage
} }
@ -41,7 +41,7 @@ func RegisterMenuManage(i IMenuManage) { localMenuManage = i }
func (s *menuManage) Tree(ctx context.Context) ([]*dto.MenuNode, error) { func (s *menuManage) Tree(ctx context.Context) ([]*dto.MenuNode, error) {
var list []entity.AdminMenu var list []entity.AdminMenu
if err := dao.AdminMenu.Ctx(ctx).OrderAsc("sort").Scan(&list); err != nil { if err := dao.AdminMenu.Ctx(ctx).OrderAsc("sort").Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query menus") return nil, gerror.Wrap(err, "查询菜单列表失败")
} }
byID := make(map[uint64]*dto.MenuNode, len(list)) byID := make(map[uint64]*dto.MenuNode, len(list))
for i := range list { for i := range list {
@ -79,17 +79,17 @@ func sortMenuChildren(n *dto.MenuNode) {
func (s *menuManage) Create(ctx context.Context, in dto.MenuCreateInput) (uint64, error) { func (s *menuManage) Create(ctx context.Context, in dto.MenuCreateInput) (uint64, error) {
count, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Permission: in.Permission}).Count() count, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Permission: in.Permission}).Count()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "check menu permission") return 0, gerror.Wrap(err, "校验菜单权限码是否存在失败")
} }
if count > 0 { if count > 0 {
return 0, response.Error(consts.CodeInvalidParam, "permission already exists") return 0, response.Error(consts.CodeInvalidParam, "权限码已存在")
} }
id, err := dao.AdminMenu.Ctx(ctx).Data(do.AdminMenu{ id, err := dao.AdminMenu.Ctx(ctx).Data(do.AdminMenu{
ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path, ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path,
Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden, Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden,
}).InsertAndGetId() }).InsertAndGetId()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "insert menu") return 0, gerror.Wrap(err, "新增菜单失败")
} }
return uint64(id), nil return uint64(id), nil
} }
@ -97,17 +97,17 @@ func (s *menuManage) Create(ctx context.Context, in dto.MenuCreateInput) (uint64
func (s *menuManage) Update(ctx context.Context, in dto.MenuUpdateInput) error { func (s *menuManage) Update(ctx context.Context, in dto.MenuUpdateInput) error {
count, err := dao.AdminMenu.Ctx(ctx).Where("permission = ? AND id != ?", in.Permission, in.Id).Count() count, err := dao.AdminMenu.Ctx(ctx).Where("permission = ? AND id != ?", in.Permission, in.Id).Count()
if err != nil { if err != nil {
return gerror.Wrap(err, "check menu permission") return gerror.Wrap(err, "校验菜单权限码是否存在失败")
} }
if count > 0 { if count > 0 {
return response.Error(consts.CodeInvalidParam, "permission already exists") return response.Error(consts.CodeInvalidParam, "权限码已存在")
} }
_, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: in.Id}).Data(do.AdminMenu{ _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: in.Id}).Data(do.AdminMenu{
ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path, ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path,
Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden, Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden,
}).Update() }).Update()
if err != nil { if err != nil {
return gerror.Wrap(err, "update menu") return gerror.Wrap(err, "更新菜单失败")
} }
return nil return nil
} }
@ -116,16 +116,16 @@ func (s *menuManage) Delete(ctx context.Context, id uint64) error {
// 删除前检查是否存在子节点。 // 删除前检查是否存在子节点。
cnt, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{ParentId: id}).Count() cnt, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{ParentId: id}).Count()
if err != nil { if err != nil {
return gerror.Wrap(err, "check menu children") return gerror.Wrap(err, "校验菜单子节点失败")
} }
if cnt > 0 { if cnt > 0 {
return response.Error(consts.CodeInvalidParam, "delete children first") return response.Error(consts.CodeInvalidParam, "请先删除子节点")
} }
if _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: id}).Delete(); err != nil { if _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: id}).Delete(); err != nil {
return gerror.Wrap(err, "delete menu") return gerror.Wrap(err, "删除菜单失败")
} }
if _, err = dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{MenuId: id}).Delete(); err != nil { if _, err = dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{MenuId: id}).Delete(); err != nil {
return gerror.Wrap(err, "delete role menu bindings") return gerror.Wrap(err, "删除角色菜单关联失败")
} }
return nil return nil
} }

View File

@ -29,7 +29,7 @@ func NewRoleManage() IRoleManage { return &roleManage{} }
func RoleManage() IRoleManage { func RoleManage() IRoleManage {
if localRoleManage == nil { if localRoleManage == nil {
panic("RoleManage implementation not registered") panic("RoleManage 实现未注册")
} }
return localRoleManage return localRoleManage
} }
@ -40,14 +40,14 @@ func RegisterRoleManage(i IRoleManage) { localRoleManage = i }
func (s *roleManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.RoleItem, int, error) { func (s *roleManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.RoleItem, int, error) {
total, err := dao.AdminRole.Ctx(ctx).Count() total, err := dao.AdminRole.Ctx(ctx).Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count roles") return nil, 0, gerror.Wrap(err, "统计角色总数失败")
} }
if total == 0 { if total == 0 {
return nil, 0, nil return nil, 0, nil
} }
var list []entity.AdminRole var list []entity.AdminRole
if err = dao.AdminRole.Ctx(ctx).Page(q.Page, q.Size).OrderAsc("id").Scan(&list); err != nil { if err = dao.AdminRole.Ctx(ctx).Page(q.Page, q.Size).OrderAsc("id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query roles") return nil, 0, gerror.Wrap(err, "查询角色列表失败")
} }
items := make([]*dto.RoleItem, 0, len(list)) items := make([]*dto.RoleItem, 0, len(list))
for i := range list { for i := range list {
@ -67,14 +67,14 @@ func (s *roleManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.RoleItem
func (s *roleManage) Create(ctx context.Context, in dto.RoleCreateInput) (uint64, error) { func (s *roleManage) Create(ctx context.Context, in dto.RoleCreateInput) (uint64, error) {
count, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Code: in.Code}).Count() count, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Code: in.Code}).Count()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "check role code") return 0, gerror.Wrap(err, "校验角色编码是否存在失败")
} }
if count > 0 { if count > 0 {
return 0, response.Error(consts.CodeInvalidParam, "role code already exists") return 0, response.Error(consts.CodeInvalidParam, "角色编码已存在")
} }
id, err := dao.AdminRole.Ctx(ctx).Data(do.AdminRole{Code: in.Code, Name: in.Name, Status: in.Status}).InsertAndGetId() id, err := dao.AdminRole.Ctx(ctx).Data(do.AdminRole{Code: in.Code, Name: in.Name, Status: in.Status}).InsertAndGetId()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "insert role") return 0, gerror.Wrap(err, "新增角色失败")
} }
if err = bindRoleMenus(ctx, uint64(id), in.MenuIds); err != nil { if err = bindRoleMenus(ctx, uint64(id), in.MenuIds); err != nil {
return 0, err return 0, err
@ -84,20 +84,20 @@ func (s *roleManage) Create(ctx context.Context, in dto.RoleCreateInput) (uint64
func (s *roleManage) Update(ctx context.Context, in dto.RoleUpdateInput) error { func (s *roleManage) Update(ctx context.Context, in dto.RoleUpdateInput) error {
if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: in.Id}).Data(do.AdminRole{Name: in.Name, Status: in.Status}).Update(); err != nil { if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: in.Id}).Data(do.AdminRole{Name: in.Name, Status: in.Status}).Update(); err != nil {
return gerror.Wrap(err, "update role") return gerror.Wrap(err, "更新角色失败")
} }
return bindRoleMenus(ctx, in.Id, in.MenuIds) return bindRoleMenus(ctx, in.Id, in.MenuIds)
} }
func (s *roleManage) Delete(ctx context.Context, id uint64) error { func (s *roleManage) Delete(ctx context.Context, id uint64) error {
if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: id}).Delete(); err != nil { if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: id}).Delete(); err != nil {
return gerror.Wrap(err, "delete role") return gerror.Wrap(err, "删除角色失败")
} }
if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: id}).Delete(); err != nil { if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: id}).Delete(); err != nil {
return gerror.Wrap(err, "delete role menu bindings") return gerror.Wrap(err, "删除角色菜单关联失败")
} }
if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{RoleId: id}).Delete(); err != nil { if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{RoleId: id}).Delete(); err != nil {
return gerror.Wrap(err, "delete user role bindings") return gerror.Wrap(err, "删除用户角色关联失败")
} }
return nil return nil
} }
@ -105,7 +105,7 @@ func (s *roleManage) Delete(ctx context.Context, id uint64) error {
func roleMenuIDs(ctx context.Context, roleID uint64) ([]uint64, error) { func roleMenuIDs(ctx context.Context, roleID uint64) ([]uint64, error) {
var rels []entity.AdminRoleMenu var rels []entity.AdminRoleMenu
if err := dao.AdminRoleMenu.Ctx(ctx).Where(do.AdminRoleMenu{RoleId: roleID}).Scan(&rels); err != nil { if err := dao.AdminRoleMenu.Ctx(ctx).Where(do.AdminRoleMenu{RoleId: roleID}).Scan(&rels); err != nil {
return nil, gerror.Wrap(err, "query role menus") return nil, gerror.Wrap(err, "查询角色菜单关联失败")
} }
ids := make([]uint64, 0, len(rels)) ids := make([]uint64, 0, len(rels))
for _, r := range rels { for _, r := range rels {
@ -117,11 +117,11 @@ func roleMenuIDs(ctx context.Context, roleID uint64) ([]uint64, error) {
// bindRoleMenus 全量重绑角色-菜单关系。 // bindRoleMenus 全量重绑角色-菜单关系。
func bindRoleMenus(ctx context.Context, roleID uint64, menuIds []uint64) error { func bindRoleMenus(ctx context.Context, roleID uint64, menuIds []uint64) error {
if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: roleID}).Delete(); err != nil { if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: roleID}).Delete(); err != nil {
return gerror.Wrap(err, "clear role menus") return gerror.Wrap(err, "清除角色菜单关联失败")
} }
for _, mid := range menuIds { for _, mid := range menuIds {
if _, err := dao.AdminRoleMenu.Ctx(ctx).Data(do.AdminRoleMenu{RoleId: roleID, MenuId: mid}).Insert(); err != nil { if _, err := dao.AdminRoleMenu.Ctx(ctx).Data(do.AdminRoleMenu{RoleId: roleID, MenuId: mid}).Insert(); err != nil {
return gerror.Wrap(err, "bind role menu") return gerror.Wrap(err, "绑定角色菜单失败")
} }
} }
return nil return nil

View File

@ -29,7 +29,7 @@ func NewCommunity() ICommunity { return &community{} }
// Community 返回已注册的小区服务实现。 // Community 返回已注册的小区服务实现。
func Community() ICommunity { func Community() ICommunity {
if localCommunity == nil { if localCommunity == nil {
panic("Community implementation not registered") panic("Community 实现未注册")
} }
return localCommunity return localCommunity
} }
@ -48,11 +48,11 @@ func (s *community) List(ctx context.Context, page, size int, keyword, region st
} }
total, err := m.Clone().Count() total, err := m.Clone().Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count community") return nil, 0, gerror.Wrap(err, "统计小区总数失败")
} }
var list []entity.HouseCommunity var list []entity.HouseCommunity
if err = m.Clone().Page(page, size).OrderDesc("id").Scan(&list); err != nil { if err = m.Clone().Page(page, size).OrderDesc("id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query community list") return nil, 0, gerror.Wrap(err, "查询小区列表失败")
} }
return list, total, nil return list, total, nil
} }
@ -76,7 +76,7 @@ func (s *community) Create(ctx context.Context, in dto.HouseCommunityInput) (uin
Source: in.Source, Source: in.Source,
}).InsertAndGetId() }).InsertAndGetId()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "insert community") return 0, gerror.Wrap(err, "新增小区失败")
} }
return uint64(id), nil return uint64(id), nil
} }
@ -99,7 +99,7 @@ func (s *community) Update(ctx context.Context, in dto.HouseCommunityInput) erro
Developer: in.Developer, Developer: in.Developer,
}).Update() }).Update()
if err != nil { if err != nil {
return gerror.Wrap(err, "update community") return gerror.Wrap(err, "更新小区失败")
} }
return nil return nil
} }
@ -107,7 +107,7 @@ func (s *community) Update(ctx context.Context, in dto.HouseCommunityInput) erro
// Delete 软删除小区。 // Delete 软删除小区。
func (s *community) Delete(ctx context.Context, id uint64) error { func (s *community) Delete(ctx context.Context, id uint64) error {
if _, err := dao.HouseCommunity.Ctx(ctx).Where(do.HouseCommunity{Id: id}).Delete(); err != nil { if _, err := dao.HouseCommunity.Ctx(ctx).Where(do.HouseCommunity{Id: id}).Delete(); err != nil {
return gerror.Wrap(err, "delete community") return gerror.Wrap(err, "删除小区失败")
} }
return nil return nil
} }

View File

@ -27,7 +27,7 @@ func NewDashboard() IDashboard { return &dashboard{} }
// Dashboard 返回已注册的看板服务实现。 // Dashboard 返回已注册的看板服务实现。
func Dashboard() IDashboard { func Dashboard() IDashboard {
if localDashboard == nil { if localDashboard == nil {
panic("Dashboard implementation not registered") panic("Dashboard 实现未注册")
} }
return localDashboard return localDashboard
} }
@ -44,7 +44,7 @@ func (s *dashboard) Overview(ctx context.Context, region string) (dto.DashboardO
} }
cnt, err := cm.Count() cnt, err := cm.Count()
if err != nil { if err != nil {
return out, gerror.Wrap(err, "count community") return out, gerror.Wrap(err, "统计小区总数失败")
} }
out.CommunityCount = cnt out.CommunityCount = cnt
@ -65,7 +65,7 @@ func (s *dashboard) Overview(ctx context.Context, region string) (dto.DashboardO
if err := lm.Clone().Where("l.status", 1).Fields( if err := lm.Clone().Where("l.status", 1).Fields(
"COALESCE(AVG(l.unit_price),0) AS avg_unit_price, COALESCE(AVG(l.total_price),0) AS avg_total_price, COALESCE(AVG(l.on_market_days),0) AS avg_list_days", "COALESCE(AVG(l.unit_price),0) AS avg_unit_price, COALESCE(AVG(l.total_price),0) AS avg_total_price, COALESCE(AVG(l.on_market_days),0) AS avg_list_days",
).Scan(&agg); err != nil { ).Scan(&agg); err != nil {
return out, gerror.Wrap(err, "aggregate overview") return out, gerror.Wrap(err, "聚合概览指标失败")
} }
out.AvgUnitPrice = agg.AvgUnitPrice out.AvgUnitPrice = agg.AvgUnitPrice
out.AvgTotalPrice = agg.AvgTotalPrice out.AvgTotalPrice = agg.AvgTotalPrice
@ -93,7 +93,7 @@ func (s *dashboard) MapPoints(ctx context.Context, region string, priceMin, pric
} }
var list []dto.DashboardMapPoint var list []dto.DashboardMapPoint
if err := m.Scan(&list); err != nil { if err := m.Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query map points") return nil, gerror.Wrap(err, "查询地图点位失败")
} }
return list, nil return list, nil
} }
@ -115,7 +115,7 @@ func (s *dashboard) PriceTrend(ctx context.Context, communityId uint64, region s
} }
var list []dto.DashboardTrendPoint var list []dto.DashboardTrendPoint
if err := m.Scan(&list); err != nil { if err := m.Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query price trend") return nil, gerror.Wrap(err, "查询价格趋势失败")
} }
// 反转为时间升序,便于前端画趋势线。 // 反转为时间升序,便于前端画趋势线。
for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 { for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
@ -135,7 +135,7 @@ func (s *dashboard) AggregateRegion(ctx context.Context) ([]dto.DashboardRegionA
OrderAsc("c.region") OrderAsc("c.region")
var list []dto.DashboardRegionAgg var list []dto.DashboardRegionAgg
if err := m.Scan(&list); err != nil { if err := m.Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query region aggregate") return nil, gerror.Wrap(err, "查询区域聚合失败")
} }
return list, nil return list, nil
} }

View File

@ -31,7 +31,7 @@ func NewListing() IListing { return &listing{} }
// Listing 返回已注册的房源服务实现。 // Listing 返回已注册的房源服务实现。
func Listing() IListing { func Listing() IListing {
if localListing == nil { if localListing == nil {
panic("Listing implementation not registered") panic("Listing 实现未注册")
} }
return localListing return localListing
} }
@ -80,11 +80,11 @@ func (s *listing) List(ctx context.Context, f dto.HouseListingFilter) ([]dto.Hou
} }
total, err := m.Clone().Count() total, err := m.Clone().Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count listing") return nil, 0, gerror.Wrap(err, "统计房源总数失败")
} }
var list []dto.HouseListingVO var list []dto.HouseListingVO
if err = m.Clone().Fields("l.*, c.name AS community_name").Page(f.Page, f.Size).OrderDesc("l.id").Scan(&list); err != nil { if err = m.Clone().Fields("l.*, c.name AS community_name").Page(f.Page, f.Size).OrderDesc("l.id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query listing list") return nil, 0, gerror.Wrap(err, "查询房源列表失败")
} }
return list, total, nil return list, total, nil
} }
@ -116,7 +116,7 @@ func (s *listing) Create(ctx context.Context, in dto.HouseListingInput) (uint64,
IsBargain: in.IsBargain, IsBargain: in.IsBargain,
}).InsertAndGetId() }).InsertAndGetId()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "insert listing") return 0, gerror.Wrap(err, "新增房源失败")
} }
return uint64(id), nil return uint64(id), nil
} }
@ -143,7 +143,7 @@ func (s *listing) Update(ctx context.Context, in dto.HouseListingInput) error {
IsBargain: in.IsBargain, IsBargain: in.IsBargain,
}).Update() }).Update()
if err != nil { if err != nil {
return gerror.Wrap(err, "update listing") return gerror.Wrap(err, "更新房源失败")
} }
return nil return nil
} }
@ -151,7 +151,7 @@ func (s *listing) Update(ctx context.Context, in dto.HouseListingInput) error {
// Delete 软删除房源。 // Delete 软删除房源。
func (s *listing) Delete(ctx context.Context, id uint64) error { func (s *listing) Delete(ctx context.Context, id uint64) error {
if _, err := dao.HouseListing.Ctx(ctx).Where(do.HouseListing{Id: id}).Delete(); err != nil { if _, err := dao.HouseListing.Ctx(ctx).Where(do.HouseListing{Id: id}).Delete(); err != nil {
return gerror.Wrap(err, "delete listing") return gerror.Wrap(err, "删除房源失败")
} }
return nil return nil
} }
@ -167,11 +167,11 @@ func (s *listing) BatchMark(ctx context.Context, ids []uint64, field string, val
case "status": case "status":
data = do.HouseListing{Status: value} data = do.HouseListing{Status: value}
default: default:
return 0, response.Error(consts.CodeInvalidParam, "unsupported mark field") return 0, response.Error(consts.CodeInvalidParam, "不支持的标记字段")
} }
res, err := dao.HouseListing.Ctx(ctx).WhereIn("id", ids).Data(data).Update() res, err := dao.HouseListing.Ctx(ctx).WhereIn("id", ids).Data(data).Update()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "batch mark listing") return 0, gerror.Wrap(err, "批量标记房源失败")
} }
affected, _ := res.RowsAffected() affected, _ := res.RowsAffected()
return affected, nil return affected, nil

View File

@ -24,7 +24,7 @@ func NewPresale() IPresale { return &presale{} }
// Presale 返回已注册的预售证服务实现。 // Presale 返回已注册的预售证服务实现。
func Presale() IPresale { func Presale() IPresale {
if localPresale == nil { if localPresale == nil {
panic("Presale implementation not registered") panic("Presale 实现未注册")
} }
return localPresale return localPresale
} }
@ -47,11 +47,11 @@ func (s *presale) List(ctx context.Context, page, size int, region, keyword, pur
} }
total, err := m.Clone().Count() total, err := m.Clone().Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count presale") return nil, 0, gerror.Wrap(err, "统计预售证总数失败")
} }
var list []dto.HousePresaleVO var list []dto.HousePresaleVO
if err = m.Clone().Page(page, size).OrderDesc("id").Scan(&list); err != nil { if err = m.Clone().Page(page, size).OrderDesc("id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query presale list") return nil, 0, gerror.Wrap(err, "查询预售证列表失败")
} }
return list, total, nil return list, total, nil
} }

View File

@ -24,7 +24,7 @@ func NewTransaction() ITransaction { return &transaction{} }
// Transaction 返回已注册的成交服务实现。 // Transaction 返回已注册的成交服务实现。
func Transaction() ITransaction { func Transaction() ITransaction {
if localTransaction == nil { if localTransaction == nil {
panic("Transaction implementation not registered") panic("Transaction 实现未注册")
} }
return localTransaction return localTransaction
} }
@ -44,11 +44,11 @@ func (s *transaction) List(ctx context.Context, page, size int, region, keyword
} }
total, err := m.Clone().Count() total, err := m.Clone().Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count transaction") return nil, 0, gerror.Wrap(err, "统计成交记录总数失败")
} }
var list []dto.HouseTransactionVO var list []dto.HouseTransactionVO
if err = m.Clone().Fields("t.*, c.name AS community_name").Page(page, size).OrderDesc("t.deal_date").Scan(&list); err != nil { if err = m.Clone().Fields("t.*, c.name AS community_name").Page(page, size).OrderDesc("t.deal_date").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query transaction list") return nil, 0, gerror.Wrap(err, "查询成交记录列表失败")
} }
return list, total, nil return list, total, nil
} }

View File

@ -48,7 +48,7 @@ func New() IJob {
// Job 返回已注册的自动任务服务实现。 // Job 返回已注册的自动任务服务实现。
func Job() IJob { func Job() IJob {
if localJob == nil { if localJob == nil {
panic("Job implementation not registered") panic("Job 实现未注册")
} }
return localJob return localJob
} }
@ -68,7 +68,7 @@ func (s *job) Register(code string, fn TaskFunc) {
func (s *job) List(ctx context.Context) ([]dto.JobVO, error) { func (s *job) List(ctx context.Context) ([]dto.JobVO, error) {
var list []entity.AutoJob var list []entity.AutoJob
if err := dao.AutoJob.Ctx(ctx).OrderAsc("id").Scan(&list); err != nil { if err := dao.AutoJob.Ctx(ctx).OrderAsc("id").Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query auto jobs") return nil, gerror.Wrap(err, "查询自动任务列表失败")
} }
out := make([]dto.JobVO, 0, len(list)) out := make([]dto.JobVO, 0, len(list))
for _, v := range list { for _, v := range list {
@ -94,7 +94,7 @@ func (s *job) Save(ctx context.Context, in dto.JobInput) error {
data["enabled"] = in.Enabled data["enabled"] = in.Enabled
} }
if _, err := dao.AutoJob.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil { if _, err := dao.AutoJob.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil {
return gerror.Wrap(err, "update auto job") return gerror.Wrap(err, "更新自动任务失败")
} }
// 动态重启调度:移除旧 cron按新配置重新注册 // 动态重启调度:移除旧 cron按新配置重新注册
s.reschedule(ctx, j.Code) s.reschedule(ctx, j.Code)
@ -117,11 +117,11 @@ func (s *job) LogList(ctx context.Context, f dto.JobLogFilter) ([]dto.JobLogVO,
} }
total, err := m.Clone().Count() total, err := m.Clone().Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count job log") return nil, 0, gerror.Wrap(err, "统计任务日志总数失败")
} }
var list []entity.AutoJobLog var list []entity.AutoJobLog
if err = m.Page(f.Page, f.Size).OrderDesc("id").Scan(&list); err != nil { if err = m.Page(f.Page, f.Size).OrderDesc("id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query job log") return nil, 0, gerror.Wrap(err, "查询任务日志列表失败")
} }
out := make([]dto.JobLogVO, 0, len(list)) out := make([]dto.JobLogVO, 0, len(list))
for _, v := range list { for _, v := range list {
@ -145,13 +145,13 @@ func (s *job) LogList(ctx context.Context, f dto.JobLogFilter) ([]dto.JobLogVO,
func (s *job) StartScheduler(ctx context.Context) { func (s *job) StartScheduler(ctx context.Context) {
var jobs []entity.AutoJob var jobs []entity.AutoJob
if err := dao.AutoJob.Ctx(ctx).Where("enabled", 1).Scan(&jobs); err != nil { if err := dao.AutoJob.Ctx(ctx).Where("enabled", 1).Scan(&jobs); err != nil {
g.Log().Errorf(ctx, "load auto jobs failed: %v", err) g.Log().Errorf(ctx, "加载自动任务失败: %v", err)
return return
} }
for _, j := range jobs { for _, j := range jobs {
s.addCron(ctx, j) s.addCron(ctx, j)
} }
g.Log().Infof(ctx, "auto job scheduler started: %d jobs", len(jobs)) g.Log().Infof(ctx, "自动任务调度器已启动,共 %d 个任务", len(jobs))
} }
// addCron 为任务注册 gcron不存在的函数跳过 // addCron 为任务注册 gcron不存在的函数跳过
@ -160,13 +160,13 @@ func (s *job) addCron(ctx context.Context, j entity.AutoJob) {
_, ok := s.registry[j.Code] _, ok := s.registry[j.Code]
s.mu.Unlock() s.mu.Unlock()
if !ok { if !ok {
g.Log().Warningf(ctx, "job code %s has no handler, skip cron", j.Code) g.Log().Warningf(ctx, "任务 %s 未注册执行函数,跳过定时注册", j.Code)
return return
} }
if _, err := gcron.Add(ctx, j.CronExpr, func(c context.Context) { if _, err := gcron.Add(ctx, j.CronExpr, func(c context.Context) {
_, _ = s.run(c, j) _, _ = s.run(c, j)
}, j.Code); err != nil { }, j.Code); err != nil {
g.Log().Errorf(ctx, "add cron %s(%s) failed: %v", j.Code, j.CronExpr, err) g.Log().Errorf(ctx, "注册定时任务 %s(%s) 失败: %v", j.Code, j.CronExpr, err)
} }
} }
@ -188,7 +188,7 @@ func (s *job) run(ctx context.Context, j entity.AutoJob) (string, error) {
fn, ok := s.registry[j.Code] fn, ok := s.registry[j.Code]
s.mu.Unlock() s.mu.Unlock()
if !ok { if !ok {
err := fmt.Errorf("no handler for job %s", j.Code) err := fmt.Errorf("任务 %s 未注册执行函数", j.Code)
s.finish(ctx, j, 2, err.Error(), "", 0) s.finish(ctx, j, 2, err.Error(), "", 0)
return "", err return "", err
} }
@ -210,14 +210,14 @@ func (s *job) finish(ctx context.Context, j entity.AutoJob, result int, errMsg,
"last_run_at": gtime.Now().Format("Y-m-d H:i:s"), "last_run_at": gtime.Now().Format("Y-m-d H:i:s"),
"last_result": result, "last_error": errMsg, "last_result": result, "last_error": errMsg,
}).Update(); err != nil { }).Update(); err != nil {
g.Log().Errorf(ctx, "update job %s status failed: %v", j.Code, err) g.Log().Errorf(ctx, "更新任务 %s 状态失败: %v", j.Code, err)
} }
// 写运行日志 // 写运行日志
if _, err := dao.AutoJobLog.Ctx(ctx).Data(map[string]interface{}{ if _, err := dao.AutoJobLog.Ctx(ctx).Data(map[string]interface{}{
"job_id": j.Id, "result": result, "error": errMsg, "job_id": j.Id, "result": result, "error": errMsg,
"summary": summary, "duration_ms": duration, "summary": summary, "duration_ms": duration,
}).Insert(); err != nil { }).Insert(); err != nil {
g.Log().Errorf(ctx, "insert job log failed: %v", err) g.Log().Errorf(ctx, "写入任务运行日志失败: %v", err)
} }
// 通知:完成/失败 // 通知:完成/失败
vars := map[string]string{"jobName": j.Name, "summary": summary, "error": errMsg} vars := map[string]string{"jobName": j.Name, "summary": summary, "error": errMsg}

View File

@ -41,7 +41,7 @@ func New() INotice { return &notice{} }
// Notice 返回已注册的通知服务实现。 // Notice 返回已注册的通知服务实现。
func Notice() INotice { func Notice() INotice {
if localNotice == nil { if localNotice == nil {
panic("Notice implementation not registered") panic("Notice 实现未注册")
} }
return localNotice return localNotice
} }
@ -54,7 +54,7 @@ func RegisterNotice(i INotice) { localNotice = i }
func (s *notice) ChannelList(ctx context.Context) ([]dto.NoticeChannelVO, error) { func (s *notice) ChannelList(ctx context.Context) ([]dto.NoticeChannelVO, error) {
var list []entity.NoticeChannel var list []entity.NoticeChannel
if err := dao.NoticeChannel.Ctx(ctx).OrderAsc("id").Scan(&list); err != nil { if err := dao.NoticeChannel.Ctx(ctx).OrderAsc("id").Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query notice channels") return nil, gerror.Wrap(err, "查询通知渠道失败")
} }
out := make([]dto.NoticeChannelVO, 0, len(list)) out := make([]dto.NoticeChannelVO, 0, len(list))
for _, v := range list { for _, v := range list {
@ -70,13 +70,13 @@ func (s *notice) ChannelSave(ctx context.Context, in dto.NoticeChannelInput) (ui
} }
if in.Id > 0 { if in.Id > 0 {
if _, err := dao.NoticeChannel.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil { if _, err := dao.NoticeChannel.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil {
return 0, gerror.Wrap(err, "update notice channel") return 0, gerror.Wrap(err, "更新通知渠道失败")
} }
return in.Id, nil return in.Id, nil
} }
id, err := dao.NoticeChannel.Ctx(ctx).Data(data).InsertAndGetId() id, err := dao.NoticeChannel.Ctx(ctx).Data(data).InsertAndGetId()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "insert notice channel") return 0, gerror.Wrap(err, "新增通知渠道失败")
} }
return uint64(id), nil return uint64(id), nil
} }
@ -90,7 +90,7 @@ func (s *notice) RuleList(ctx context.Context, eventType string) ([]dto.NoticeRu
} }
var list []entity.NoticeRule var list []entity.NoticeRule
if err := m.OrderAsc("id").Scan(&list); err != nil { if err := m.OrderAsc("id").Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query notice rules") return nil, gerror.Wrap(err, "查询通知规则失败")
} }
out := make([]dto.NoticeRuleVO, 0, len(list)) out := make([]dto.NoticeRuleVO, 0, len(list))
for _, v := range list { for _, v := range list {
@ -111,20 +111,20 @@ func (s *notice) RuleSave(ctx context.Context, in dto.NoticeRuleInput) (uint64,
} }
if in.Id > 0 { if in.Id > 0 {
if _, err := dao.NoticeRule.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil { if _, err := dao.NoticeRule.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil {
return 0, gerror.Wrap(err, "update notice rule") return 0, gerror.Wrap(err, "更新通知规则失败")
} }
return in.Id, nil return in.Id, nil
} }
id, err := dao.NoticeRule.Ctx(ctx).Data(data).InsertAndGetId() id, err := dao.NoticeRule.Ctx(ctx).Data(data).InsertAndGetId()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "insert notice rule") return 0, gerror.Wrap(err, "新增通知规则失败")
} }
return uint64(id), nil return uint64(id), nil
} }
func (s *notice) RuleDelete(ctx context.Context, id uint64) error { func (s *notice) RuleDelete(ctx context.Context, id uint64) error {
if _, err := dao.NoticeRule.Ctx(ctx).Where("id", id).Delete(); err != nil { if _, err := dao.NoticeRule.Ctx(ctx).Where("id", id).Delete(); err != nil {
return gerror.Wrap(err, "delete notice rule") return gerror.Wrap(err, "删除通知规则失败")
} }
return nil return nil
} }
@ -150,11 +150,11 @@ func (s *notice) LogList(ctx context.Context, f dto.NoticeLogFilter) ([]dto.Noti
} }
total, err := m.Clone().Count() total, err := m.Clone().Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count notice log") return nil, 0, gerror.Wrap(err, "统计通知日志总数失败")
} }
var list []entity.NoticeLog var list []entity.NoticeLog
if err = m.Page(f.Page, f.Size).OrderDesc("id").Scan(&list); err != nil { if err = m.Page(f.Page, f.Size).OrderDesc("id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query notice log") return nil, 0, gerror.Wrap(err, "查询通知日志列表失败")
} }
out := make([]dto.NoticeLogVO, 0, len(list)) out := make([]dto.NoticeLogVO, 0, len(list))
for _, v := range list { for _, v := range list {
@ -173,7 +173,7 @@ func (s *notice) LogList(ctx context.Context, f dto.NoticeLogFilter) ([]dto.Noti
func (s *notice) Send(ctx context.Context, eventType string, vars map[string]string) error { func (s *notice) Send(ctx context.Context, eventType string, vars map[string]string) error {
var rules []entity.NoticeRule var rules []entity.NoticeRule
if err := dao.NoticeRule.Ctx(ctx).Where("event_type", eventType).Where("enabled", 1).Scan(&rules); err != nil { if err := dao.NoticeRule.Ctx(ctx).Where("event_type", eventType).Where("enabled", 1).Scan(&rules); err != nil {
return gerror.Wrap(err, "query notice rules for send") return gerror.Wrap(err, "查询待发送通知规则失败")
} }
if len(rules) == 0 { if len(rules) == 0 {
return nil // 未配置规则 = 静默 return nil // 未配置规则 = 静默

View File

@ -27,7 +27,7 @@ func crawlerRun(ctx context.Context, src entity.CrawlSource, force bool) dto.Cra
defer func() { defer func() {
// 防止单源解析panic拖垮整体调度。 // 防止单源解析panic拖垮整体调度。
if r := recover(); r != nil { if r := recover(); r != nil {
res.Err = gerror.Newf("crawler panic: %v", r) res.Err = gerror.Newf("抓取发生 panic: %v", r)
} }
}() }()
var ( var (
@ -207,16 +207,16 @@ func fetchHTML(ctx context.Context, u string) (string, error) {
client.SetHeader("Accept-Language", "zh-CN,zh;q=0.9") client.SetHeader("Accept-Language", "zh-CN,zh;q=0.9")
resp, err := client.Get(ctx, u) resp, err := client.Get(ctx, u)
if err != nil { if err != nil {
return "", gerror.Wrapf(err, "fetch %s", u) return "", gerror.Wrapf(err, "抓取页面失败 %s", u)
} }
defer resp.Close() defer resp.Close()
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return "", gerror.Newf("HTTP %d for %s", resp.StatusCode, u) return "", gerror.Newf("请求 %s 返回 HTTP %d", u, resp.StatusCode)
} }
body := resp.ReadAllString() body := resp.ReadAllString()
if !looksLikeHtml(body) || len(body) < 3000 { if !looksLikeHtml(body) || len(body) < 3000 {
g.Log().Warningf(ctx, "[recruit-debug] go-fallback suspicious len=%d url=%s", len(body), u) g.Log().Warningf(ctx, "[recruit-debug] go-fallback suspicious len=%d url=%s", len(body), u)
return "", gerror.Newf("suspicious page (len=%d) for %s", len(body), u) return "", gerror.Newf("页面内容可疑(长度=%d%s", len(body), u)
} }
g.Log().Warningf(ctx, "[recruit-debug] go-fallback ok len=%d url=%s", len(body), u) g.Log().Warningf(ctx, "[recruit-debug] go-fallback ok len=%d url=%s", len(body), u)
return body, nil return body, nil
@ -233,7 +233,7 @@ func fetchWithCurl(u string) (string, error) {
} }
out, err := exec.Command("curl", args...).Output() out, err := exec.Command("curl", args...).Output()
if err != nil { if err != nil {
return "", gerror.Wrapf(err, "curl %s", u) return "", gerror.Wrapf(err, "curl 抓取失败 %s", u)
} }
return string(out), nil return string(out), nil
} }
@ -481,7 +481,7 @@ func upsertInfo(ctx context.Context, in dto.RecruitmentInput) (bool, error) {
if err := dao.RecruitmentInfo.Ctx(ctx).Where("fingerprint", in.Fingerprint).Scan(&exist); err != nil { if err := dao.RecruitmentInfo.Ctx(ctx).Where("fingerprint", in.Fingerprint).Scan(&exist); err != nil {
// gf 的 Scan 在查无记录时返回 "no rows" 错误,视为未存在,继续插入。 // gf 的 Scan 在查无记录时返回 "no rows" 错误,视为未存在,继续插入。
if !strings.Contains(err.Error(), "no rows") { if !strings.Contains(err.Error(), "no rows") {
return false, gerror.Wrap(err, "query exist info") return false, gerror.Wrap(err, "查询已存在公告失败")
} }
} }
if exist.Id > 0 { if exist.Id > 0 {
@ -490,7 +490,7 @@ func upsertInfo(ctx context.Context, in dto.RecruitmentInput) (bool, error) {
ExamDate: in.ExamDate, UpdatedAt: gtime.Now(), ExamDate: in.ExamDate, UpdatedAt: gtime.Now(),
}).Update() }).Update()
if err != nil { if err != nil {
return false, gerror.Wrap(err, "update info") return false, gerror.Wrap(err, "更新公告失败")
} }
return false, nil return false, nil
} }
@ -500,7 +500,7 @@ func upsertInfo(ctx context.Context, in dto.RecruitmentInput) (bool, error) {
} }
in.OrgId = orgId in.OrgId = orgId
if _, err = dao.RecruitmentInfo.Ctx(ctx).Data(toRecruitmentDO(in)).InsertAndGetId(); err != nil { if _, err = dao.RecruitmentInfo.Ctx(ctx).Data(toRecruitmentDO(in)).InsertAndGetId(); err != nil {
return false, gerror.Wrap(err, "insert info") return false, gerror.Wrap(err, "新增公告失败")
} }
return true, nil return true, nil
} }
@ -513,7 +513,7 @@ func upsertOrg(ctx context.Context, name string, category int, region string) (u
var org entity.Organization var org entity.Organization
if err := dao.Organization.Ctx(ctx).Where("name", name).Scan(&org); err != nil { if err := dao.Organization.Ctx(ctx).Where("name", name).Scan(&org); err != nil {
if !strings.Contains(err.Error(), "no rows") { if !strings.Contains(err.Error(), "no rows") {
return 0, gerror.Wrap(err, "query org") return 0, gerror.Wrap(err, "查询发布机构失败")
} }
} }
if org.Id > 0 { if org.Id > 0 {
@ -524,7 +524,7 @@ func upsertOrg(ctx context.Context, name string, category int, region string) (u
CreatedAt: gtime.Now(), UpdatedAt: gtime.Now(), CreatedAt: gtime.Now(), UpdatedAt: gtime.Now(),
}).InsertAndGetId() }).InsertAndGetId()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "insert org") return 0, gerror.Wrap(err, "新增发布机构失败")
} }
return uint64(id), nil return uint64(id), nil
} }
@ -571,14 +571,14 @@ func recordCrawlLog(ctx context.Context, res dto.CrawlRunResult) {
errMsg = res.Err.Error() errMsg = res.Err.Error()
const maxErr = 900 const maxErr = 900
if len(errMsg) > maxErr { if len(errMsg) > maxErr {
errMsg = errMsg[:maxErr] + "...(truncated)" errMsg = errMsg[:maxErr] + "...(已截断)"
} }
} }
if _, err := dao.CrawlLog.Ctx(ctx).Data(do.CrawlLog{ if _, err := dao.CrawlLog.Ctx(ctx).Data(do.CrawlLog{
SourceId: res.SourceId, RunAt: gtime.Now(), Fetched: res.Fetched, SourceId: res.SourceId, RunAt: gtime.Now(), Fetched: res.Fetched,
NewCount: res.NewCount, UpdatedCount: res.UpdatedCount, Error: errMsg, NewCount: res.NewCount, UpdatedCount: res.UpdatedCount, Error: errMsg,
}).Insert(); err != nil { }).Insert(); err != nil {
g.Log().Errorf(ctx, "write crawl log failed: %v", err) g.Log().Errorf(ctx, "写入抓取日志失败: %v", err)
} }
if res.Err != nil { if res.Err != nil {
var src entity.CrawlSource var src entity.CrawlSource
@ -603,7 +603,7 @@ func loadSubscription(ctx context.Context, id uint64) (*entity.PushSubscription,
m = m.Where("enabled", 1) m = m.Where("enabled", 1)
} }
if err := m.OrderDesc("id").Scan(&sub); err != nil { if err := m.OrderDesc("id").Scan(&sub); err != nil {
return nil, gerror.Wrap(err, "query subscription") return nil, gerror.Wrap(err, "查询推送订阅失败")
} }
if sub.Id == 0 { if sub.Id == 0 {
return nil, nil return nil, nil

View File

@ -41,7 +41,7 @@ func New() IRecruitment { return &recruitment{} }
// Recruitment 返回已注册的招聘服务实现。 // Recruitment 返回已注册的招聘服务实现。
func Recruitment() IRecruitment { func Recruitment() IRecruitment {
if localRecruitment == nil { if localRecruitment == nil {
panic("Recruitment implementation not registered") panic("Recruitment 实现未注册")
} }
return localRecruitment return localRecruitment
} }
@ -86,11 +86,11 @@ func (s *recruitment) List(ctx context.Context, f dto.RecruitmentFilter) ([]dto.
} }
total, err := m.Clone().Count() total, err := m.Clone().Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count recruitment") return nil, 0, gerror.Wrap(err, "统计招聘公告总数失败")
} }
var list []dto.RecruitmentInfoVO var list []dto.RecruitmentInfoVO
if err = m.Clone().Page(f.Page, f.Size).OrderDesc("r.publish_date").OrderDesc("r.id").Scan(&list); err != nil { if err = m.Clone().Page(f.Page, f.Size).OrderDesc("r.publish_date").OrderDesc("r.id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query recruitment list") return nil, 0, gerror.Wrap(err, "查询招聘公告列表失败")
} }
for i := range list { for i := range list {
list[i].CategoryName = dto.CategoryName(list[i].Category) list[i].CategoryName = dto.CategoryName(list[i].Category)
@ -107,7 +107,7 @@ func (s *recruitment) Detail(ctx context.Context, id uint64) (*dto.RecruitmentIn
Fields("r.*, cs.name AS source_name"). Fields("r.*, cs.name AS source_name").
Where("r.id", id).Scan(&v) Where("r.id", id).Scan(&v)
if err != nil { if err != nil {
return nil, gerror.Wrap(err, "query recruitment detail") return nil, gerror.Wrap(err, "查询招聘公告详情失败")
} }
if v.Id == 0 { if v.Id == 0 {
return nil, response.Error(consts.CodeInvalidParam, "公告不存在") return nil, response.Error(consts.CodeInvalidParam, "公告不存在")
@ -125,29 +125,29 @@ func (s *recruitment) Stats(ctx context.Context, region string) (*dto.Recruitmen
} }
total, err := base.Clone().Count() total, err := base.Clone().Count()
if err != nil { if err != nil {
return nil, gerror.Wrap(err, "count total") return nil, gerror.Wrap(err, "统计公告总数失败")
} }
todayNew, err := base.Clone().Where("DATE(created_at)=CURDATE()").Count() todayNew, err := base.Clone().Where("DATE(created_at)=CURDATE()").Count()
if err != nil { if err != nil {
return nil, gerror.Wrap(err, "count today") return nil, gerror.Wrap(err, "统计今日新增失败")
} }
weekNew, err := base.Clone().Where("created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)").Count() weekNew, err := base.Clone().Where("created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)").Count()
if err != nil { if err != nil {
return nil, gerror.Wrap(err, "count week") return nil, gerror.Wrap(err, "统计近 7 天新增失败")
} }
var catRows []struct { var catRows []struct {
Category int `json:"category"` Category int `json:"category"`
Cnt int `json:"cnt"` Cnt int `json:"cnt"`
} }
if err = base.Clone().Fields("category, COUNT(*) AS cnt").Group("category").Scan(&catRows); err != nil { if err = base.Clone().Fields("category, COUNT(*) AS cnt").Group("category").Scan(&catRows); err != nil {
return nil, gerror.Wrap(err, "agg category") return nil, gerror.Wrap(err, "按分类聚合失败")
} }
var regionRows []struct { var regionRows []struct {
Region string `json:"region"` Region string `json:"region"`
Cnt int `json:"cnt"` Cnt int `json:"cnt"`
} }
if err = base.Clone().Fields("region, COUNT(*) AS cnt").Group("region").Scan(&regionRows); err != nil { if err = base.Clone().Fields("region, COUNT(*) AS cnt").Group("region").Scan(&regionRows); err != nil {
return nil, gerror.Wrap(err, "agg region") return nil, gerror.Wrap(err, "按地区聚合失败")
} }
var trendRows []struct { var trendRows []struct {
D string `json:"d"` D string `json:"d"`
@ -157,7 +157,7 @@ func (s *recruitment) Stats(ctx context.Context, region string) (*dto.Recruitmen
Fields("DATE(publish_date) AS d, COUNT(*) AS cnt"). Fields("DATE(publish_date) AS d, COUNT(*) AS cnt").
Where("publish_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)"). Where("publish_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)").
Group("d").OrderAsc("d").Scan(&trendRows); err != nil { Group("d").OrderAsc("d").Scan(&trendRows); err != nil {
return nil, gerror.Wrap(err, "trend") return nil, gerror.Wrap(err, "统计近 30 天趋势失败")
} }
out := &dto.RecruitmentStats{Total: int(total), TodayNew: int(todayNew), WeekNew: int(weekNew)} out := &dto.RecruitmentStats{Total: int(total), TodayNew: int(todayNew), WeekNew: int(weekNew)}
for _, r := range catRows { for _, r := range catRows {
@ -187,7 +187,7 @@ func (s *recruitment) Trend(ctx context.Context, region string, category, days i
Cnt int `json:"cnt"` Cnt int `json:"cnt"`
} }
if err := m.Fields("DATE(publish_date) AS d, COUNT(*) AS cnt").Group("d").OrderAsc("d").Scan(&rows); err != nil { if err := m.Fields("DATE(publish_date) AS d, COUNT(*) AS cnt").Group("d").OrderAsc("d").Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "query trend") return nil, gerror.Wrap(err, "查询趋势数据失败")
} }
out := make([]dto.TrendPoint, 0, len(rows)) out := make([]dto.TrendPoint, 0, len(rows))
for _, r := range rows { for _, r := range rows {
@ -200,7 +200,7 @@ func (s *recruitment) Trend(ctx context.Context, region string, category, days i
func (s *recruitment) Sources(ctx context.Context) ([]dto.SourceItem, error) { func (s *recruitment) Sources(ctx context.Context) ([]dto.SourceItem, error) {
var srcs []entity.CrawlSource var srcs []entity.CrawlSource
if err := dao.CrawlSource.Ctx(ctx).OrderAsc("id").Scan(&srcs); err != nil { if err := dao.CrawlSource.Ctx(ctx).OrderAsc("id").Scan(&srcs); err != nil {
return nil, gerror.Wrap(err, "query sources") return nil, gerror.Wrap(err, "查询抓取数据源失败")
} }
// 取每个源最近一条日志作为摘要。 // 取每个源最近一条日志作为摘要。
var logs []entity.CrawlLog var logs []entity.CrawlLog
@ -243,7 +243,7 @@ func (s *recruitment) Trigger(ctx context.Context, sourceId uint64, force bool)
} }
var srcs []entity.CrawlSource var srcs []entity.CrawlSource
if err := m.OrderAsc("id").Scan(&srcs); err != nil { if err := m.OrderAsc("id").Scan(&srcs); err != nil {
return 0, "", gerror.Wrap(err, "query crawl sources") return 0, "", gerror.Wrap(err, "查询待抓取数据源失败")
} }
var sb strings.Builder var sb strings.Builder
n := 0 n := 0
@ -267,7 +267,7 @@ func (s *recruitment) PushTest(ctx context.Context, subId uint64, title, body st
sub = defaultSubscription(ctx) sub = defaultSubscription(ctx)
} }
if sub == nil { if sub == nil {
return false, "无启用的推送订阅且未配置默认设备密钥", gerror.New("no enabled subscription") return false, "无启用的推送订阅且未配置默认设备密钥", gerror.New("没有启用的推送订阅")
} }
if title == "" { if title == "" {
title = "招聘聚合 · 推送测试" title = "招聘聚合 · 推送测试"
@ -282,7 +282,7 @@ func (s *recruitment) PushTest(ctx context.Context, subId uint64, title, body st
func (s *recruitment) SubscriptionList(ctx context.Context) ([]dto.SubscriptionItem, error) { func (s *recruitment) SubscriptionList(ctx context.Context) ([]dto.SubscriptionItem, error) {
var subs []entity.PushSubscription var subs []entity.PushSubscription
if err := dao.PushSubscription.Ctx(ctx).OrderDesc("id").Scan(&subs); err != nil { if err := dao.PushSubscription.Ctx(ctx).OrderDesc("id").Scan(&subs); err != nil {
return nil, gerror.Wrap(err, "query subscriptions") return nil, gerror.Wrap(err, "查询推送订阅失败")
} }
out := make([]dto.SubscriptionItem, 0, len(subs)) out := make([]dto.SubscriptionItem, 0, len(subs))
for _, sub := range subs { for _, sub := range subs {
@ -301,13 +301,13 @@ func (s *recruitment) SubscriptionSave(ctx context.Context, in dto.SubscriptionI
if in.Id > 0 { if in.Id > 0 {
_, err := dao.PushSubscription.Ctx(ctx).Where("id", in.Id).Data(data).Update() _, err := dao.PushSubscription.Ctx(ctx).Where("id", in.Id).Data(data).Update()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "update subscription") return 0, gerror.Wrap(err, "更新推送订阅失败")
} }
return in.Id, nil return in.Id, nil
} }
id, err := dao.PushSubscription.Ctx(ctx).Data(data).InsertAndGetId() id, err := dao.PushSubscription.Ctx(ctx).Data(data).InsertAndGetId()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "insert subscription") return 0, gerror.Wrap(err, "新增推送订阅失败")
} }
return uint64(id), nil return uint64(id), nil
} }
@ -315,7 +315,7 @@ func (s *recruitment) SubscriptionSave(ctx context.Context, in dto.SubscriptionI
// SubscriptionDelete 删除推送订阅。 // SubscriptionDelete 删除推送订阅。
func (s *recruitment) SubscriptionDelete(ctx context.Context, id uint64) error { func (s *recruitment) SubscriptionDelete(ctx context.Context, id uint64) error {
if _, err := dao.PushSubscription.Ctx(ctx).Where("id", id).Delete(); err != nil { if _, err := dao.PushSubscription.Ctx(ctx).Where("id", id).Delete(); err != nil {
return gerror.Wrap(err, "delete subscription") return gerror.Wrap(err, "删除推送订阅失败")
} }
return nil return nil
} }

View File

@ -32,7 +32,7 @@ func New() ISecurity { return &security{} }
// Security 返回已注册的安全日志服务实现。 // Security 返回已注册的安全日志服务实现。
func Security() ISecurity { func Security() ISecurity {
if localSecurity == nil { if localSecurity == nil {
panic("Security implementation not registered") panic("Security 实现未注册")
} }
return localSecurity return localSecurity
} }
@ -75,7 +75,7 @@ func (s *security) Report(ctx context.Context, token string, items []dto.Securit
}) })
} }
if _, err := dao.ServerSecurityLog.Ctx(ctx).Data(rows).Insert(); err != nil { if _, err := dao.ServerSecurityLog.Ctx(ctx).Data(rows).Insert(); err != nil {
return 0, gerror.Wrap(err, "insert server security log") return 0, gerror.Wrap(err, "写入服务器安全日志失败")
} }
return len(rows), nil return len(rows), nil
} }
@ -100,11 +100,11 @@ func (s *security) List(ctx context.Context, f dto.SecurityLogFilter) ([]dto.Sec
} }
total, err := m.Clone().Count() total, err := m.Clone().Count()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "count security log") return nil, 0, gerror.Wrap(err, "统计安全日志总数失败")
} }
var list []dto.SecurityLogVO var list []dto.SecurityLogVO
if err = m.Clone().Page(f.Page, f.Size).OrderDesc("log_time").OrderDesc("id").Scan(&list); err != nil { if err = m.Clone().Page(f.Page, f.Size).OrderDesc("log_time").OrderDesc("id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query security log") return nil, 0, gerror.Wrap(err, "查询安全日志列表失败")
} }
for i := range list { for i := range list {
list[i].EventName = dto.SecurityEventName(list[i].EventType) list[i].EventName = dto.SecurityEventName(list[i].EventType)
@ -126,27 +126,27 @@ func (s *security) Stats(ctx context.Context, dateFrom, dateTo string) (*dto.Sec
st := &dto.SecurityStats{} st := &dto.SecurityStats{}
var err error var err error
if st.Total, err = base.Clone().Count(); err != nil { if st.Total, err = base.Clone().Count(); err != nil {
return nil, gerror.Wrap(err, "count total") return nil, gerror.Wrap(err, "统计事件总数失败")
} }
if st.Failed, err = base.Clone().Where("event_type", "failed_ssh").Count(); err != nil { if st.Failed, err = base.Clone().Where("event_type", "failed_ssh").Count(); err != nil {
return nil, gerror.Wrap(err, "count failed") return nil, gerror.Wrap(err, "统计爆破次数失败")
} }
if st.Banned, err = base.Clone().Where("event_type", "banned").Count(); err != nil { if st.Banned, err = base.Clone().Where("event_type", "banned").Count(); err != nil {
return nil, gerror.Wrap(err, "count banned") return nil, gerror.Wrap(err, "统计封禁次数失败")
} }
if st.Accepted, err = base.Clone().Where("event_type", "accepted_ssh").Count(); err != nil { if st.Accepted, err = base.Clone().Where("event_type", "accepted_ssh").Count(); err != nil {
return nil, gerror.Wrap(err, "count accepted") return nil, gerror.Wrap(err, "统计成功登录次数失败")
} }
// TOP 攻击源(爆破+封禁优先)。 // TOP 攻击源(爆破+封禁优先)。
if err = base.Clone().WhereIn("event_type", g.Slice{"failed_ssh", "banned"}). if err = base.Clone().WhereIn("event_type", g.Slice{"failed_ssh", "banned"}).
Fields("src_ip, COUNT(*) AS cnt"). Fields("src_ip, COUNT(*) AS cnt").
Group("src_ip").OrderDesc("cnt").Limit(10).Scan(&st.TopIps); err != nil { Group("src_ip").OrderDesc("cnt").Limit(10).Scan(&st.TopIps); err != nil {
return nil, gerror.Wrap(err, "query top ips") return nil, gerror.Wrap(err, "查询 TOP 攻击源失败")
} }
// 按类型分布。 // 按类型分布。
if err = base.Clone().Fields("event_type, COUNT(*) AS cnt"). if err = base.Clone().Fields("event_type, COUNT(*) AS cnt").
Group("event_type").Scan(&st.ByType); err != nil { Group("event_type").Scan(&st.ByType); err != nil {
return nil, gerror.Wrap(err, "query by type") return nil, gerror.Wrap(err, "按事件类型统计失败")
} }
for i := range st.ByType { for i := range st.ByType {
st.ByType[i].EventName = dto.SecurityEventName(st.ByType[i].EventType) st.ByType[i].EventName = dto.SecurityEventName(st.ByType[i].EventType)

View File

@ -24,7 +24,7 @@ var localUserAuth IUserAuth
func UserAuth() IUserAuth { func UserAuth() IUserAuth {
if localUserAuth == nil { if localUserAuth == nil {
panic("UserAuth implementation not registered") panic("UserAuth 实现未注册")
} }
return localUserAuth return localUserAuth
} }
@ -55,39 +55,39 @@ func (s *userAuth) Login(ctx context.Context, in dto.UserLoginInput) (*dto.Token
case "wechat": case "wechat":
// 微信首次授权成功后按 openid 自动创建用户。 // 微信首次授权成功后按 openid 自动创建用户。
if s.wechat == nil { if s.wechat == nil {
return nil, 0, response.Error(consts.CodeInternal, "wechat provider not configured") return nil, 0, response.Error(consts.CodeInternal, "微信登录未配置")
} }
openID, e := s.wechat.OpenID(ctx, in.Code) openID, e := s.wechat.OpenID(ctx, in.Code)
if e != nil { if e != nil {
return nil, 0, gerror.Wrap(e, "wechat login") return nil, 0, gerror.Wrap(e, "微信登录失败")
} }
err = dao.User.Ctx(ctx).Where(do.User{OpenId: openID}).Scan(&u) err = dao.User.Ctx(ctx).Where(do.User{OpenId: openID}).Scan(&u)
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "query user") return nil, 0, gerror.Wrap(err, "查询用户失败")
} }
if u.Id == 0 { if u.Id == 0 {
id, e := dao.User.Ctx(ctx).Data(do.User{OpenId: openID, Nickname: "微信用户", Status: 1}).InsertAndGetId() id, e := dao.User.Ctx(ctx).Data(do.User{OpenId: openID, Nickname: "微信用户", Status: 1}).InsertAndGetId()
if e != nil { if e != nil {
return nil, 0, gerror.Wrap(e, "create user") return nil, 0, gerror.Wrap(e, "创建用户失败")
} }
u.Id = uint64(id) u.Id = uint64(id)
} }
case "mobile": case "mobile":
// 验证码由注入的服务校验,校验成功后按手机号自动注册。 // 验证码由注入的服务校验,校验成功后按手机号自动注册。
if s.sms == nil { if s.sms == nil {
return nil, 0, response.Error(consts.CodeInternal, "sms provider not configured") return nil, 0, response.Error(consts.CodeInternal, "短信登录未配置")
} }
if err = s.sms.Verify(ctx, in.Mobile, in.VerifyCode); err != nil { if err = s.sms.Verify(ctx, in.Mobile, in.VerifyCode); err != nil {
return nil, 0, response.Error(consts.CodeInvalidParam, "invalid verification code") return nil, 0, response.Error(consts.CodeInvalidParam, "验证码错误")
} }
err = dao.User.Ctx(ctx).Where(do.User{Mobile: in.Mobile}).Scan(&u) err = dao.User.Ctx(ctx).Where(do.User{Mobile: in.Mobile}).Scan(&u)
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "query user") return nil, 0, gerror.Wrap(err, "查询用户失败")
} }
if u.Id == 0 { if u.Id == 0 {
id, e := dao.User.Ctx(ctx).Data(do.User{Mobile: in.Mobile, Nickname: "用户" + in.Mobile[7:], Status: 1}).InsertAndGetId() id, e := dao.User.Ctx(ctx).Data(do.User{Mobile: in.Mobile, Nickname: "用户" + in.Mobile[7:], Status: 1}).InsertAndGetId()
if e != nil { if e != nil {
return nil, 0, gerror.Wrap(e, "create user") return nil, 0, gerror.Wrap(e, "创建用户失败")
} }
u.Id = uint64(id) u.Id = uint64(id)
} }
@ -95,20 +95,20 @@ func (s *userAuth) Login(ctx context.Context, in dto.UserLoginInput) (*dto.Token
// 密码只使用 bcrypt 比对哈希值,任何场景都不回传或记录明文。 // 密码只使用 bcrypt 比对哈希值,任何场景都不回传或记录明文。
err = dao.User.Ctx(ctx).Where(do.User{Account: in.Account}).Scan(&u) err = dao.User.Ctx(ctx).Where(do.User{Account: in.Account}).Scan(&u)
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "query user") return nil, 0, gerror.Wrap(err, "查询用户失败")
} }
if u.Id == 0 || bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(in.Password)) != nil { if u.Id == 0 || bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(in.Password)) != nil {
return nil, 0, response.Error(consts.CodeUserPasswordWrong, "account or password incorrect") return nil, 0, response.Error(consts.CodeUserPasswordWrong, "账号或密码错误")
} }
default: default:
return nil, 0, response.Error(consts.CodeUserLoginType, "unsupported login type") return nil, 0, response.Error(consts.CodeUserLoginType, "不支持的登录方式")
} }
if u.Status != 1 { if u.Status != 1 {
return nil, 0, response.Error(consts.CodeForbidden, "user disabled") return nil, 0, response.Error(consts.CodeForbidden, "用户已被禁用")
} }
_, err = dao.User.Ctx(ctx).Where(do.User{Id: u.Id}).Data(do.User{LastLoginAt: gtime.Now()}).Update() _, err = dao.User.Ctx(ctx).Where(do.User{Id: u.Id}).Data(do.User{LastLoginAt: gtime.Now()}).Update()
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "update login time") return nil, 0, gerror.Wrap(err, "更新登录时间失败")
} }
return s.issue(ctx, u.Id, in.Terminal) return s.issue(ctx, u.Id, in.Terminal)
} }
@ -116,7 +116,7 @@ func (s *userAuth) Refresh(ctx context.Context, refresh string) (*dto.TokenPair,
// 刷新时先校验 JWT再以 JTI 原子撤销旧会话,实现单次使用的令牌轮换。 // 刷新时先校验 JWT再以 JTI 原子撤销旧会话,实现单次使用的令牌轮换。
c, err := s.tokens.Parse(refresh, "refresh", "user") c, err := s.tokens.Parse(refresh, "refresh", "user")
if err != nil { if err != nil {
return nil, 0, response.Error(consts.CodeUnauthorized, "invalid refresh token") return nil, 0, response.Error(consts.CodeUnauthorized, "刷新令牌无效")
} }
result, err := dao.AuthRefreshSession.Ctx(ctx).Where(do.AuthRefreshSession{Jti: c.JTI}).WhereNull("revoked_at").Data(do.AuthRefreshSession{RevokedAt: gtime.Now()}).Update() result, err := dao.AuthRefreshSession.Ctx(ctx).Where(do.AuthRefreshSession{Jti: c.JTI}).WhereNull("revoked_at").Data(do.AuthRefreshSession{RevokedAt: gtime.Now()}).Update()
if err != nil { if err != nil {
@ -133,7 +133,7 @@ func (s *userAuth) Refresh(ctx context.Context, refresh string) (*dto.TokenPair,
func (s *userAuth) issue(ctx context.Context, id uint64, terminal string) (*dto.TokenPair, uint64, error) { func (s *userAuth) issue(ctx context.Context, id uint64, terminal string) (*dto.TokenPair, uint64, error) {
a, r, exp, err := s.tokens.Issue(id, "user", terminal) a, r, exp, err := s.tokens.Issue(id, "user", terminal)
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "issue token") return nil, 0, gerror.Wrap(err, "签发令牌失败")
} }
claims, err := s.tokens.Parse(r, "refresh", "user") claims, err := s.tokens.Parse(r, "refresh", "user")
if err != nil { if err != nil {

39
main.go
View File

@ -1,13 +1,50 @@
// Package main 是服务启动入口。
//
// 启动前的配置兜底:本仓库不提供默认的 config.yaml仅按环境提供
// config.dev.yaml / config.test.yaml / config.prod.yaml框架默认却要找
// config.yaml导致直接运行时报「config not found」。这里在加载配置前
// 若未通过 GF_GCFG_FILE 指定文件名,则自动回落到 config.dev.yaml。
//
// 因此以下方式均可直接启动,无需手动传环境变量:
// - GoLand 直接点运行(含自动生成的临时配置)
// - 命令行 go run main.go / 双击二进制
//
// 如需切换环境,显式设置环境变量即可覆盖默认行为:
//
// GF_GCFG_FILE=config.test.yaml # 或 config.prod.yaml
package main package main
import ( import (
_ "github.com/gogf/gf/contrib/drivers/mysql/v2" // MySQL driver, required by gdb _ "github.com/gogf/gf/contrib/drivers/mysql/v2" // MySQL 驱动gdb 必需
"github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/genv"
"service.xpcool.com/internal/cmd" "service.xpcool.com/internal/cmd"
) )
const (
// envKeyForFile 是 GoFrame 官方的「配置文件名」环境变量。
// 框架在初始化文件适配器gcfg.NewAdapterFile时读取它用于决定加载哪个配置文件。
envKeyForFile = "GF_GCFG_FILE"
// envKeyForPath 是 GoFrame 官方的「配置文件目录」环境变量。
envKeyForPath = "GF_GCFG_PATH"
// defaultConfigFile 是未显式指定配置文件时使用的默认文件名(开发环境)。
defaultConfigFile = "config.dev.yaml"
)
func main() { func main() {
injectDefaultConfigFile()
cmd.Main.Run(gctx.GetInitCtx()) cmd.Main.Run(gctx.GetInitCtx())
} }
// injectDefaultConfigFile 在配置系统初始化前,兜底指定默认配置文件。
//
// 仅在「调用方未显式指定配置文件或目录」时才写入默认值,
// 以免覆盖使用者通过环境变量或命令行做出的选择。
func injectDefaultConfigFile() {
if !genv.Get(envKeyForFile).IsEmpty() || !genv.Get(envKeyForPath).IsEmpty() {
return
}
_ = genv.Set(envKeyForFile, defaultConfigFile)
}