diff --git a/.workbuddy/memory/CHANGELOG.md b/.workbuddy/memory/CHANGELOG.md index b4f605a..70ca41e 100644 --- a/.workbuddy/memory/CHANGELOG.md +++ b/.workbuddy/memory/CHANGELOG.md @@ -1,6 +1,8 @@ # service.xpcool.com 变更记录 > 倒序:最新在上。格式: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/auth、tools/md5、tools/random、menu_manage、admin)、service 层 gerror.Wrap/response.Error 全部中文(admin/admin、admin/login、system/{role,menu,menu_manage,login_log}、user/auth、house/{community,listing,dashboard,transaction,presale}、notice、job、serversecurity、recruitment/{recruitment,crawler})、jwt.go、controller/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 | FEAT | 全量请求/响应加密(生产 encrypt.fullBody=true / env ENCRYPT_FULL_BODY):①crypto.Service 增 fullBody 开关 + DecryptRequest(RSA 解 AES 会话密钥 + AES-GCM 解 body,返回会话密钥)+ EncryptResponse(用同一会话密钥 AES-GCM 加密响应回传,请求结束即销毁);②新增 middleware/APICrypto——请求整体解密(io.ReadAll 直接读 r.Request.Body 勿用 GetBody,否则缓存密文致 handler Parse 读到密文)+ 响应加密 buffer(HandlerResponse 外层、Recover 内层,500 也加密),public-key 明文豁免,未加密业务请求一律拒绝;③admin 组中间件链改 CORS→APICrypto→Recover→HandlerResponse;④ResolveLogin/resolvePassword 增 fullBody 分支(传输层已整体解密,直接信任明文 username/password);⑤injectEnv 支持 ENCRYPT_FULL_BODY/ENCRYPT_ALLOW_PLAIN(直接跑二进制恒加载 config.yaml,GF_GCFG_ENV 仅 gf run 认,必须 env 注入)。⚠️GF 坑:r.GetBody() 缓存密文到 bodyContent,handler Parse 走缓存 → 中间件必须 io.ReadAll(r.Request.Body)。dev(仅密码加密)E2E 15 项、prod(全量加密)E2E 9 项全过 diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 4ee1f41..56baa28 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -1,35 +1,55 @@ -# Production layout +# 生产目录结构 ```text service.xpcool.com/ -├── api/ # HTTP contracts and Swagger metadata -│ ├── user/v1/ # /api/v1 - client-facing API -│ ├── admin/v1/ # /admin/v1 - administration API -│ └── open/v1/ # /api/open/v1 - open API for frontends (no auth) -│ └── tools/ # one dir per sub-feature: tools//.go -├── common/ # public reusable module (no internal/ deps) -│ └── tools/ # utility toolbox: md5, cryptox, uuid, random, -│ # timex, convertx, strx, slicex, ip, filex +├── api/ # HTTP 契约与 Swagger 元数据(按业务分组) +│ ├── admin/ # /api/service/admin - 管理端 API(AdminAuth + 权限码) +│ │ ├── admin/ # 管理员账号 CRUD、登录/资料/权限码/刷新/登出 +│ │ └── system/ # 登录日志、菜单路由、菜单管理、角色 +│ ├── house/ # /api/service/admin - 看房模块(小区/房源/看板/成交/预售证) +│ ├── recruitment/ # /api/service/admin - 招聘考试聚合模块 +│ ├── notice/ # /api/service/admin - 站内通知(渠道/规则/日志) +│ ├── job/ # /api/service/admin - 自动任务(任务/日志) +│ ├── serversecurity/ # 服务器安全日志(open 上报 + admin 查询/统计) +│ ├── user/ # /api/service/user - 用户端认证(登录/刷新) +│ └── open/ # /api/service/open - 开放接口(公开、无鉴权) +│ └── tools/ # 工具子功能契约,每子功能一目录:tools//.go +├── common/ # 公共可复用模块(不依赖 internal/) +│ └── tools/ # 工具集:md5、cryptox、uuid、random、timex、 +│ # convertx、strx、slicex、ip、filex ├── internal/ -│ ├── cmd/ # application bootstrap and route isolation -│ ├── consts/ # application error codes and constants -│ ├── controller/ # API-to-service adapters only (incl. open/ open API) -│ ├── service/ # domain use cases and provider interfaces -│ ├── dao/ # generated by gf gen dao; never hand edited +│ ├── cmd/ # 启动引导、环境变量注入、路由分组注册 +│ ├── consts/ # 业务错误码与常量 +│ ├── controller/ # API→service 适配层(不写业务;含 open/ 开放接口实现) +│ ├── service/ # 领域用例(业务逻辑直接写这里,不用 logic/) +│ ├── dao/ # 数据访问(gf gen dao 生成 + 部分手写,禁止手改生成物) +│ │ └── internal/ # 生成代码内部实现 │ ├── model/ -│ │ ├── entity/ # generated database entities -│ │ ├── do/ # generated Data Objects -│ │ ├── dto/ # service boundary input/output -│ │ └── vo/ # API view models -│ ├── middleware/ # configurable route-group middleware -│ └── library/ # JWT, response, pagination primitives -├── docs/change-log/ # per-request change records (agent context) +│ │ ├── entity/ # 数据库实体(生成 + 部分手写) +│ │ ├── do/ # Data Object(写库必须用 DO) +│ │ ├── dto/ # 服务边界入参/出参 +│ │ └── vo/ # API 视图模型 +│ ├── middleware/ # 路由中间件:Recover、CORS、认证、全量加密 +│ ├── library/ # jwt / crypto(RSA+AES) / iploc / page / response 基础件 +│ └── table/ # 表列名常量(手写维护,供代码引用列名) +├── docs/ +│ ├── house-system-design.md # 看房系统总设计文档 +│ └── change-log/ # 每次请求与变更的记录(AI 上下文记忆,随 git 提交) ├── manifest/ -│ ├── config/ # config.dev/test/prod.yaml -│ └── sql/ # ordered MySQL migrations -└── utility/ # optional cross-cutting helpers +│ ├── config/ # config.dev/test/prod.yaml(config.yaml 不入库) +│ ├── sql/ # 有序 MySQL 迁移脚本(含 recruitment/ 子模块) +│ ├── 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)。 diff --git a/README.MD b/README.MD index aa89557..98b3581 100644 --- a/README.MD +++ b/README.MD @@ -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 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). -- **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. +开发环境下,若 `${DB_DSN}` / `${RECRUITMENT_DB_DSN}` / `${JWT_SECRET}` 占位符未被环境变量注入, +`injectEnv` 会回填本地默认值,保证「零配置可启动」;生产(`GF_GCFG_ENV=prod`)不兜底,配置缺失显性报错。 +## 构建与测试 + +```powershell +go build ./... +go test ./... +``` diff --git a/api/admin/admin/admin/admin.go b/api/admin/admin/admin/admin.go index e0d3404..aa1aa78 100644 --- a/api/admin/admin/admin/admin.go +++ b/api/admin/admin/admin/admin.go @@ -5,15 +5,15 @@ import "github.com/gogf/gf/v2/frame/g" // AdminItem 管理员列表中的一行。 type AdminItem struct { - Id uint64 `json:"id"` - Username string `json:"username"` - Nickname string `json:"nickname"` - Status int `json:"status"` - BarkDeviceId string `json:"barkDeviceId"` - PushplusToken string `json:"pushplusToken"` - RoleIds []uint64 `json:"roleIds"` - RoleNames []string `json:"roleNames"` - CreatedAt string `json:"createdAt"` + Id uint64 `json:"id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Status int `json:"status"` + BarkDeviceId string `json:"barkDeviceId"` + PushplusToken string `json:"pushplusToken"` + RoleIds []uint64 `json:"roleIds"` + RoleNames []string `json:"roleNames"` + CreatedAt string `json:"createdAt"` } // AdminListReq 分页查询管理员。 @@ -34,15 +34,15 @@ type AdminListRes struct { // 密码采用「RSA + AES-GCM」混合加密传输:encryptedKey/encryptedData 必填(生产强制), // password 明文字段仅当服务端 encrypt.allowPlain=true(本地联调)时可用。 type AdminCreateReq struct { - g.Meta `path:"/admin/create" method:"post" tags:"Admin/Admin" summary:"创建管理员"` - Username string `json:"username" v:"required"` - Password string `json:"password" v:"min-length:6#password too short"` - EncryptedKey string `json:"encryptedKey"` - EncryptedData string `json:"encryptedData"` - Nickname string `json:"nickname"` - BarkDeviceId string `json:"barkDeviceId"` - PushplusToken string `json:"pushplusToken"` - RoleIds []uint64 `json:"roleIds"` + g.Meta `path:"/admin/create" method:"post" tags:"Admin/Admin" summary:"创建管理员"` + Username string `json:"username" v:"required"` + Password string `json:"password" v:"min-length:6#密码长度不能少于 6 位"` + EncryptedKey string `json:"encryptedKey"` + EncryptedData string `json:"encryptedData"` + Nickname string `json:"nickname"` + BarkDeviceId string `json:"barkDeviceId"` + PushplusToken string `json:"pushplusToken"` + RoleIds []uint64 `json:"roleIds"` } // AdminCreateRes 是 AdminCreateReq 的响应。 @@ -70,7 +70,7 @@ type AdminUpdateRes struct{} type AdminResetPwdReq struct { g.Meta `path:"/admin/resetPwd/{id}" method:"post" tags:"Admin/Admin" summary:"重置管理员密码"` 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"` EncryptedData string `json:"encryptedData"` } diff --git a/api/admin/system/menu_manage/menu_manage.go b/api/admin/system/menu_manage/menu_manage.go index 56c2ba3..3e88836 100644 --- a/api/admin/system/menu_manage/menu_manage.go +++ b/api/admin/system/menu_manage/menu_manage.go @@ -35,7 +35,7 @@ type MenuCreateReq struct { ParentId uint64 `json:"parentId"` Name string `json:"name" v:"required"` 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"` Component string `json:"component"` Permission string `json:"permission" v:"required"` @@ -56,7 +56,7 @@ type MenuUpdateReq struct { ParentId uint64 `json:"parentId"` Name string `json:"name" v:"required"` 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"` Component string `json:"component"` Permission string `json:"permission" v:"required"` diff --git a/api/open/tools/md5/md5.go b/api/open/tools/md5/md5.go index 62b5c1a..6faed79 100644 --- a/api/open/tools/md5/md5.go +++ b/api/open/tools/md5/md5.go @@ -6,7 +6,7 @@ import "github.com/gogf/gf/v2/frame/g" // MD5Req 计算文本的 MD5 摘要。 type MD5Req struct { 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 的响应。 diff --git a/api/open/tools/random/random.go b/api/open/tools/random/random.go index 6845645..677c95d 100644 --- a/api/open/tools/random/random.go +++ b/api/open/tools/random/random.go @@ -6,8 +6,8 @@ import "github.com/gogf/gf/v2/frame/g" // RandomReq 生成随机字符串。 type RandomReq struct { 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"` - Type string `json:"type" d:"alnum" v:"in:alnum,digits,letters#unsupported type"` + Length int `json:"length" d:"16" v:"min:1|max:128#长度必须在 1-128 之间"` + Type string `json:"type" d:"alnum" v:"in:alnum,digits,letters#不支持的随机类型"` } // RandomRes 是 RandomReq 的响应。 diff --git a/api/user/auth/auth.go b/api/user/auth/auth.go index 53001f4..b223df1 100644 --- a/api/user/auth/auth.go +++ b/api/user/auth/auth.go @@ -6,13 +6,13 @@ import "github.com/gogf/gf/v2/frame/g" // LoginReq 用户登录请求(支持微信/手机验证码/账号密码三种方式)。 type LoginReq struct { 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"` // 微信授权码 Mobile string `json:"mobile"` // 手机号(mobile 登录方式) VerifyCode string `json:"verifyCode"` // 短信验证码 Account string `json:"account"` // 账号(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 用户登录响应。 diff --git a/api/user/login/login.go b/api/user/login/login.go index 607adca..a295944 100644 --- a/api/user/login/login.go +++ b/api/user/login/login.go @@ -1 +1,6 @@ +// Package user_login 用户端登录契约。 +// +// 说明:当前用户端登录已统一收敛到 `api/user/auth`(支持微信 / 手机号 / 密码三种方式), +// user 路由组直接绑定 Auth 控制器即可,因此本包暂无端点定义。 +// 保留该包是为了维持目录结构一致性,后续如需拆分「扫码登录」「短信登录」等独立契约,可在此扩展。 package user_login diff --git a/common/doc.go b/common/doc.go index 9d82f3a..31e5625 100644 --- a/common/doc.go +++ b/common/doc.go @@ -6,5 +6,5 @@ // // 当前结构: // -// common/tools shared utility toolbox (md5, cryptox, uuid, ...) +// common/tools 公共工具集(md5、cryptox、uuid 等) package common diff --git a/common/tools/doc.go b/common/tools/doc.go index 5fc5ea0..ac100c9 100644 --- a/common/tools/doc.go +++ b/common/tools/doc.go @@ -6,19 +6,19 @@ // // 结构: // -// common/tools/md5 MD5 digest helpers -// common/tools/cryptox AES/DES encryption with base64 output -// common/tools/uuid unique ID generation -// common/tools/random random numbers and strings -// common/tools/timex time formatting and computation -// common/tools/convertx type conversion with default fallback -// common/tools/strx string / naming / masking helpers -// common/tools/slicex generic slice utilities -// common/tools/ip IP address helpers -// common/tools/filex file system helpers +// common/tools/md5 MD5 摘要工具 +// common/tools/cryptox AES/DES 加解密(base64 输出) +// common/tools/uuid 唯一 ID 生成 +// common/tools/random 随机数与随机字符串 +// common/tools/timex 时间格式化与计算 +// common/tools/convertx 类型转换(带默认值兜底) +// common/tools/strx 字符串 / 命名 / 脱敏工具 +// common/tools/slicex 泛型切片工具 +// common/tools/ip IP 地址工具 +// common/tools/filex 文件系统工具 // // 规则: -// - Never depend on internal/ — this module must stay self-contained. -// - Prefer reusing GoFrame built-in components over re-implementing. -// - Keep each helper small and add a Chinese doc comment. +// - 禁止依赖 internal/ —— 本模块必须保持自包含。 +// - 优先复用 GoFrame 内置组件,避免重复造轮子。 +// - 每个工具保持小巧,并补充中文文档注释。 package tools diff --git a/common/tools/ip/ip.go b/common/tools/ip/ip.go index abebf37..5719ef5 100644 --- a/common/tools/ip/ip.go +++ b/common/tools/ip/ip.go @@ -31,7 +31,7 @@ func LocalIP() (string, error) { } // IsInternal 判断 s 是否为私有/内网 IPv4 地址 -// (private ranges, loopback or link-local). +// (含私有网段、回环地址、链路本地地址)。 func IsInternal(s string) bool { parsed := net.ParseIP(s) if parsed == nil || parsed.To4() == nil { @@ -41,7 +41,7 @@ func IsInternal(s string) bool { } // ToLong 将 IPv4 字符串转为 uint32 数值表示 -// (big-endian, same as inet_aton). +// (大端序,与 inet_aton 一致)。 func ToLong(s string) (uint32, error) { ipv4 := net.ParseIP(s).To4() if ipv4 == nil { diff --git a/docs/change-log/2026-09-13.md b/docs/change-log/2026-09-13.md new file mode 100644 index 0000000..7cde999 --- /dev/null +++ b/docs/change-log/2026-09-13.md @@ -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 的英文注释属预期现象,无需处理。 diff --git a/go.mod b/go.mod index b3d4128..6bbc3a2 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,13 @@ module service.xpcool.com go 1.23.0 -require github.com/gogf/gf/v2 v2.10.2 - require ( - github.com/go-sql-driver/mysql v1.7.1 // indirect - github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2 // indirect + github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2 + github.com/gogf/gf/v2 v2.10.2 ) +require github.com/go-sql-driver/mysql v1.7.1 // indirect + require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect diff --git a/go.sum b/go.sum index 1dcf7ac..dba8ac4 100644 --- a/go.sum +++ b/go.sum @@ -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/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= diff --git a/hack/hack-cli.mk b/hack/hack-cli.mk index 526124e..61f036c 100644 --- a/hack/hack-cli.mk +++ b/hack/hack-cli.mk @@ -1,18 +1,18 @@ -# Install/Update to the latest CLI tool. +# 安装/更新到最新的 CLI 工具。 .PHONY: cli cli: @set -e; \ echo "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 cli.install: @set -e; \ 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; \ - fi; \ No newline at end of file + fi; diff --git a/hack/hack.mk b/hack/hack.mk index 2f68179..4299481 100644 --- a/hack/hack.mk +++ b/hack/hack.mk @@ -1,37 +1,37 @@ .DEFAULT_GOAL := build -# Update GoFrame and its CLI to latest stable version. +# 更新 GoFrame 及 CLI 到最新稳定版。 .PHONY: up up: cli.install @gf up -a -# Build binary using configuration from hack/config.yaml. +# 使用 hack/config.yaml 中的配置构建二进制。 .PHONY: build build: cli.install @gf build -ew -# Parse api and generate controller/sdk. +# 解析 api 目录并生成 controller/sdk。 .PHONY: ctrl ctrl: cli.install @gf gen ctrl -# Generate Go files for DAO/DO/Entity. +# 生成 DAO/DO/Entity 的 Go 代码。 .PHONY: dao dao: cli.install @gf gen dao -# Parse current project go files and generate enums go file. +# 解析当前项目 Go 文件并生成枚举文件。 .PHONY: enums enums: cli.install @gf gen enums -# Generate Go files for Service. +# 生成 Service 接口与实现代码。 .PHONY: service service: cli.install @gf gen service -# Build docker image. +# 构建 Docker 镜像。 .PHONY: image image: cli.install $(eval _TAG = $(shell git rev-parse --short HEAD)) @@ -43,13 +43,13 @@ endif @gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG}; -# Build docker image and automatically push to docker repo. +# 构建 Docker 镜像并自动推送到镜像仓库。 .PHONY: image.push image.push: cli.install @make image PUSH=-p; -# Deploy image and yaml to current kubectl environment. +# 部署镜像与 yaml 到当前 kubectl 环境。 .PHONY: deploy deploy: cli.install $(eval _TAG = $(if ${TAG}, ${TAG}, develop)) @@ -64,12 +64,12 @@ deploy: cli.install fi; -# Parsing protobuf files and generating go files. +# 解析 protobuf 文件并生成 Go 代码。 .PHONY: pb pb: cli.install @gf gen pb -# Generate protobuf files for database tables. +# 根据数据库表生成 protobuf 文件。 .PHONY: pbentity pbentity: cli.install - @gf gen pbentity \ No newline at end of file + @gf gen pbentity diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 62ca2d3..8f28bdb 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -4,19 +4,19 @@ import ( "context" "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/os/gcfg" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/genv" "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" adminctl "service.xpcool.com/internal/controller/admin" 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" 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" userctl "service.xpcool.com/internal/controller/user" "service.xpcool.com/internal/library/crypto" @@ -30,24 +30,27 @@ import ( adminmenu "service.xpcool.com/internal/service/admin/system/menu" menu_manage "service.xpcool.com/internal/service/admin/system/menu_manage" 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" housedashboard "service.xpcool.com/internal/service/house/dashboard" houselisting "service.xpcool.com/internal/service/house/listing" - housetransaction "service.xpcool.com/internal/service/house/transaction" housepresale "service.xpcool.com/internal/service/house/presale" - recruitmentsvc "service.xpcool.com/internal/service/recruitment" - noticesvc "service.xpcool.com/internal/service/notice" + housetransaction "service.xpcool.com/internal/service/house/transaction" 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" + userauth "service.xpcool.com/internal/service/user/auth" ) // 开发环境默认值:任何启动方式(GoLand / 命令行 / go run)漏注入环境变量时兜底, // 避免 ${DB_DSN} 占位符原样传入 gdb 导致全站接口 500。 // 生产(GF_GCFG_ENV=prod)不启用兜底,配置缺失应显性报错。 +// +// 注意:库名以本机实际建库为准(本项目为 service / recruitment)。 +// 若你的本地库名或账号口令不同,请通过 DB_DSN / RECRUITMENT_DB_DSN 覆盖。 const ( - devDefaultDBDSN = "mysql:root:root123@tcp(127.0.0.1:3306)/service_xpcool_com?loc=Local" - devDefaultRecruitmentDSN = "mysql:root:root123@tcp(127.0.0.1:3306)/recruitment?loc=Local" + devDefaultDBDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/service?loc=Local" + devDefaultRecruitmentDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/recruitment?loc=Local" devDefaultJWTSecret = "dev-only-secret-not-for-production" ) @@ -130,19 +133,19 @@ var ( serversecuritysvc.RegisterServerSecurity(serversecuritysvc.New()) s.Group("/api/service/open", func(group *ghttp.RouterGroup) { group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) - group.Bind(openctl.New()) // Open tools API for frontends, no auth required. - group.Bind(serversecurityctl.NewReport()) // 安全日志上报(宿主机脚本,内部令牌校验)。 + group.Bind(openctl.New()) // 开放工具接口(前端调用,免鉴权)。 + group.Bind(serversecurityctl.NewReport()) // 安全日志上报(宿主机脚本,内部令牌校验)。 }) s.Group("/api/service/user", func(group *ghttp.RouterGroup) { 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)) }) }) s.Group("/api/service/admin", func(group *ghttp.RouterGroup) { // 全量请求/响应加密(生产 encrypt.fullBody=true):CORS → APICrypto → Recover → HandlerResponse。 // Recover 置于 APICrypto 内层,使 500 异常响应同样被加密;public-key 端点明文豁免。 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) { // 仅登录端点:个人资料 / 权限码 / 菜单路由。 profile.Middleware(middleware.AdminAuthOnly(tokens)) diff --git a/internal/controller/admin/admin.go b/internal/controller/admin/admin.go index ab05e2f..3a35357 100644 --- a/internal/controller/admin/admin.go +++ b/internal/controller/admin/admin.go @@ -28,7 +28,7 @@ func resolvePassword(ctx context.Context, encryptedKey, encryptedData, password if cryptolib.Get().AllowPlain() { return password, nil } - return "", response.Error(consts.CodeInvalidParam, "encrypted payload required") + return "", response.Error(consts.CodeInvalidParam, "必须提交加密凭据") } // AdminList 分页查询管理员。 diff --git a/internal/library/crypto/crypto.go b/internal/library/crypto/crypto.go index b7d1b16..6931468 100644 --- a/internal/library/crypto/crypto.go +++ b/internal/library/crypto/crypto.go @@ -74,7 +74,7 @@ func SetDefault(s *Service) { defaultSvc = s } // Get 返回进程级默认加密服务实例,未初始化时 panic(启动必经 New 注册)。 func Get() *Service { if defaultSvc == nil { - panic("crypto service not registered") + panic("加密服务未注册") } return defaultSvc } diff --git a/internal/library/jwt/jwt.go b/internal/library/jwt/jwt.go index c931ef7..5a5c109 100644 --- a/internal/library/jwt/jwt.go +++ b/internal/library/jwt/jwt.go @@ -52,18 +52,18 @@ func (s *Service) Parse(token, typ, scope string) (*Claims, error) { // 先使用常量时间比较校验签名,再解析声明,避免伪造令牌进入业务层。 parts := strings.Split(token, ".") 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]) if err != nil { - return nil, gerror.Wrap(err, "decode token") + return nil, gerror.Wrap(err, "解码令牌失败") } var c Claims 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 { - return nil, gerror.New("token expired or scope mismatch") + return nil, gerror.New("令牌已过期或作用域不匹配") } return &c, nil } @@ -80,7 +80,7 @@ func (s *Service) sign(c Claims) (string, error) { h := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) b, err := json.Marshal(c) if err != nil { - return "", gerror.Wrap(err, "encode claims") + return "", gerror.Wrap(err, "编码令牌声明失败") } p := h + "." + base64.RawURLEncoding.EncodeToString(b) return p + "." + s.signature(p), nil diff --git a/internal/service/admin/admin/admin/admin.go b/internal/service/admin/admin/admin/admin.go index 4330c2a..3959f24 100644 --- a/internal/service/admin/admin/admin/admin.go +++ b/internal/service/admin/admin/admin/admin.go @@ -31,7 +31,7 @@ func NewAdminManage() IAdminManage { return &adminManage{} } func AdminManage() IAdminManage { if localAdminManage == nil { - panic("AdminManage implementation not registered") + panic("AdminManage 实现未注册") } 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) { total, err := dao.AdminUser.Ctx(ctx).Count() if err != nil { - return nil, 0, gerror.Wrap(err, "count admins") + return nil, 0, gerror.Wrap(err, "统计管理员总数失败") } if total == 0 { return nil, 0, nil } var list []entity.AdminUser 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)) 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) { var rels []entity.AdminUserRole 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)) for _, r := range rels { @@ -82,7 +82,7 @@ func (s *adminManage) roles(ctx context.Context, adminID uint64) ([]uint64, []st } var roles []entity.AdminRole 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)) 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) { count, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Username: in.Username}).Count() if err != nil { - return 0, gerror.Wrap(err, "check username") + return 0, gerror.Wrap(err, "校验用户名是否存在失败") } 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) if err != nil { - return 0, gerror.Wrap(err, "hash password") + return 0, gerror.Wrap(err, "生成密码哈希失败") } id, err := dao.AdminUser.Ctx(ctx).Data(do.AdminUser{ Username: in.Username, PasswordHash: string(hash), Nickname: in.Nickname, Status: 1, BarkDeviceId: in.BarkDeviceId, PushplusToken: in.PushplusToken, }).InsertAndGetId() 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 { 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, BarkDeviceId: in.BarkDeviceId, PushplusToken: in.PushplusToken} 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) } @@ -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 { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 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 { - return gerror.Wrap(err, "reset password") + return gerror.Wrap(err, "重置管理员密码失败") } return nil } func (s *adminManage) Delete(ctx context.Context, id uint64) error { 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 { - return gerror.Wrap(err, "delete admin role bindings") + return gerror.Wrap(err, "删除管理员角色关联失败") } return nil } @@ -150,11 +150,11 @@ func (s *adminManage) Delete(ctx context.Context, id uint64) error { // bindAdminRoles 全量重绑管理员-角色关系。 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 { - return gerror.Wrap(err, "clear admin roles") + return gerror.Wrap(err, "清除管理员角色关联失败") } for _, rid := range roleIds { 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 diff --git a/internal/service/admin/admin/login/login.go b/internal/service/admin/admin/login/login.go index 55ddc35..04fa831 100644 --- a/internal/service/admin/admin/login/login.go +++ b/internal/service/admin/admin/login/login.go @@ -40,7 +40,7 @@ var localAdminAuth IAdminAuth func AdminAuth() IAdminAuth { if localAdminAuth == nil { - panic("AdminAuth implementation not registered") + panic("AdminAuth 实现未注册") } return localAdminAuth } @@ -58,13 +58,13 @@ func (s *adminAuth) Login(ctx context.Context, in dto.AdminLoginInput) (*dto.Tok // 管理端只允许账号密码登录,状态异常或密码错误均返回统一错误,避免枚举账号。 var a entity.AdminUser 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 { - 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 { - return nil, 0, response.Error(consts.CodeAdminPasswordWrong, "username or password incorrect") + return nil, 0, response.Error(consts.CodeAdminPasswordWrong, "用户名或密码错误") } // 通过 issue 签发令牌对并把刷新令牌 JTI 落库,保证后续可刷新、可撤销。 return s.issue(ctx, a.Id, "") @@ -85,14 +85,14 @@ func (s *adminAuth) ResolveLogin(ctx context.Context, encryptedKey, encryptedDat } if s.crypto.FullBody() { if username == "" || password == "" { - return "", "", response.Error(consts.CodeInvalidParam, "username or password required") + return "", "", response.Error(consts.CodeInvalidParam, "用户名和密码不能为空") } return username, password, nil } if s.crypto.AllowPlain() { return username, password, nil } - return "", "", response.Error(consts.CodeInvalidParam, "encrypted payload required") + return "", "", response.Error(consts.CodeInvalidParam, "必须提交加密凭据") } // 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) { c, err := s.tokens.Parse(refresh, "refresh", "admin") 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() @@ -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) { access, refresh, exp, err := s.tokens.Issue(id, "admin", terminal) 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") if err != nil { @@ -160,17 +160,17 @@ func (s *adminAuth) HasPermission(ctx context.Context, adminID uint64, permissio // 必须过滤角色启用状态 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() if err != nil { - return false, gerror.Wrap(err, "check permission") + return false, gerror.Wrap(err, "校验权限失败") } return count > 0, nil } func (s *adminAuth) Info(ctx context.Context, adminID uint64) (*dto.AdminInfo, error) { var a entity.AdminUser 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 { - return nil, response.Error(consts.CodeAdminNotFound, "administrator not found") + return nil, response.Error(consts.CodeAdminNotFound, "管理员不存在") } roles, err := s.roleCodes(ctx, adminID) if err != nil { @@ -183,7 +183,7 @@ func (s *adminAuth) Codes(ctx context.Context, adminID uint64) ([]string, error) // 同时用作 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() if err != nil { - return nil, gerror.Wrap(err, "query access codes") + return nil, gerror.Wrap(err, "查询权限码失败") } codes := make([]string, 0, len(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) { 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 { - return nil, gerror.Wrap(err, "query admin roles") + return nil, gerror.Wrap(err, "查询管理员角色失败") } codes := make([]string, 0, len(list)) for _, v := range list { @@ -207,7 +207,7 @@ func (s *adminAuth) PermissionForPath(ctx context.Context, method, path string) // 未配置映射的接口一律拒绝访问(返回空则中间件拦截)。 var list []entity.AdminMenu 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 for _, m := range list { diff --git a/internal/service/admin/system/login_log/login_log.go b/internal/service/admin/system/login_log/login_log.go index 22448aa..2bf4dde 100644 --- a/internal/service/admin/system/login_log/login_log.go +++ b/internal/service/admin/system/login_log/login_log.go @@ -33,7 +33,7 @@ type adminLoginLog struct{} func AdminLoginLog() IAdminLoginLog { if localAdminLoginLog == nil { - panic("AdminLoginLog implementation not registered") + panic("AdminLoginLog 实现未注册") } 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, Status: e.Status, FailReason: e.FailReason, }).Insert() - return gerror.Wrap(err, "insert login log") + return gerror.Wrap(err, "写入登录日志失败") } // List 分页查询管理员登录日志,支持账号与结果过滤(Status 为 nil 时不过滤)。 @@ -61,14 +61,14 @@ func (s *adminLoginLog) List(ctx context.Context, q dto.LoginLogQuery) ([]*dto.L } total, err := m.Count() if err != nil { - return nil, 0, gerror.Wrap(err, "count login logs") + return nil, 0, gerror.Wrap(err, "统计登录日志总数失败") } if total == 0 { return nil, 0, nil } var list []entity.AdminLoginLog 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)) for i := range list { diff --git a/internal/service/admin/system/menu/menu.go b/internal/service/admin/system/menu/menu.go index ed9a0e6..326cfd1 100644 --- a/internal/service/admin/system/menu/menu.go +++ b/internal/service/admin/system/menu/menu.go @@ -26,7 +26,7 @@ var localAdminMenu IAdminMenu func AdminMenu() IAdminMenu { if localAdminMenu == nil { - panic("AdminMenu implementation not registered") + panic("AdminMenu 实现未注册") } return localAdminMenu } @@ -55,7 +55,7 @@ func (s *adminMenu) Routes(ctx context.Context, adminID uint64) ([]*dto.RouteIte Where("m.deleted_at IS NULL"). Fields("m.*"). 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)) for i := range list { diff --git a/internal/service/admin/system/menu_manage/menu_manage.go b/internal/service/admin/system/menu_manage/menu_manage.go index 8e30b6e..79f2fc1 100644 --- a/internal/service/admin/system/menu_manage/menu_manage.go +++ b/internal/service/admin/system/menu_manage/menu_manage.go @@ -30,7 +30,7 @@ func NewMenuManage() IMenuManage { return &menuManage{} } func MenuManage() IMenuManage { if localMenuManage == nil { - panic("MenuManage implementation not registered") + panic("MenuManage 实现未注册") } return localMenuManage } @@ -41,7 +41,7 @@ func RegisterMenuManage(i IMenuManage) { localMenuManage = i } func (s *menuManage) Tree(ctx context.Context) ([]*dto.MenuNode, error) { var list []entity.AdminMenu 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)) 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) { count, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Permission: in.Permission}).Count() if err != nil { - return 0, gerror.Wrap(err, "check menu permission") + return 0, gerror.Wrap(err, "校验菜单权限码是否存在失败") } 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{ 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, }).InsertAndGetId() if err != nil { - return 0, gerror.Wrap(err, "insert menu") + return 0, gerror.Wrap(err, "新增菜单失败") } 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 { count, err := dao.AdminMenu.Ctx(ctx).Where("permission = ? AND id != ?", in.Permission, in.Id).Count() if err != nil { - return gerror.Wrap(err, "check menu permission") + return gerror.Wrap(err, "校验菜单权限码是否存在失败") } 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{ 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, }).Update() if err != nil { - return gerror.Wrap(err, "update menu") + return gerror.Wrap(err, "更新菜单失败") } 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() if err != nil { - return gerror.Wrap(err, "check menu children") + return gerror.Wrap(err, "校验菜单子节点失败") } 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 { - 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 { - return gerror.Wrap(err, "delete role menu bindings") + return gerror.Wrap(err, "删除角色菜单关联失败") } return nil } diff --git a/internal/service/admin/system/role/role.go b/internal/service/admin/system/role/role.go index 4dc4307..dc85a97 100644 --- a/internal/service/admin/system/role/role.go +++ b/internal/service/admin/system/role/role.go @@ -29,7 +29,7 @@ func NewRoleManage() IRoleManage { return &roleManage{} } func RoleManage() IRoleManage { if localRoleManage == nil { - panic("RoleManage implementation not registered") + panic("RoleManage 实现未注册") } 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) { total, err := dao.AdminRole.Ctx(ctx).Count() if err != nil { - return nil, 0, gerror.Wrap(err, "count roles") + return nil, 0, gerror.Wrap(err, "统计角色总数失败") } if total == 0 { return nil, 0, nil } var list []entity.AdminRole 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)) 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) { count, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Code: in.Code}).Count() if err != nil { - return 0, gerror.Wrap(err, "check role code") + return 0, gerror.Wrap(err, "校验角色编码是否存在失败") } 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() 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 { 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 { 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) } func (s *roleManage) Delete(ctx context.Context, id uint64) error { 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 { - 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 { - return gerror.Wrap(err, "delete user role bindings") + return gerror.Wrap(err, "删除用户角色关联失败") } 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) { var rels []entity.AdminRoleMenu 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)) for _, r := range rels { @@ -117,11 +117,11 @@ func roleMenuIDs(ctx context.Context, roleID uint64) ([]uint64, error) { // bindRoleMenus 全量重绑角色-菜单关系。 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 { - return gerror.Wrap(err, "clear role menus") + return gerror.Wrap(err, "清除角色菜单关联失败") } for _, mid := range menuIds { 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 diff --git a/internal/service/house/community/community.go b/internal/service/house/community/community.go index cfea671..3856715 100644 --- a/internal/service/house/community/community.go +++ b/internal/service/house/community/community.go @@ -29,7 +29,7 @@ func NewCommunity() ICommunity { return &community{} } // Community 返回已注册的小区服务实现。 func Community() ICommunity { if localCommunity == nil { - panic("Community implementation not registered") + panic("Community 实现未注册") } return localCommunity } @@ -48,11 +48,11 @@ func (s *community) List(ctx context.Context, page, size int, keyword, region st } total, err := m.Clone().Count() if err != nil { - return nil, 0, gerror.Wrap(err, "count community") + return nil, 0, gerror.Wrap(err, "统计小区总数失败") } var list []entity.HouseCommunity 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 } @@ -76,7 +76,7 @@ func (s *community) Create(ctx context.Context, in dto.HouseCommunityInput) (uin Source: in.Source, }).InsertAndGetId() if err != nil { - return 0, gerror.Wrap(err, "insert community") + return 0, gerror.Wrap(err, "新增小区失败") } return uint64(id), nil } @@ -99,7 +99,7 @@ func (s *community) Update(ctx context.Context, in dto.HouseCommunityInput) erro Developer: in.Developer, }).Update() if err != nil { - return gerror.Wrap(err, "update community") + return gerror.Wrap(err, "更新小区失败") } return nil } @@ -107,7 +107,7 @@ func (s *community) Update(ctx context.Context, in dto.HouseCommunityInput) erro // Delete 软删除小区。 func (s *community) Delete(ctx context.Context, id uint64) error { 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 } diff --git a/internal/service/house/dashboard/dashboard.go b/internal/service/house/dashboard/dashboard.go index 8a1ffbb..dcd034d 100644 --- a/internal/service/house/dashboard/dashboard.go +++ b/internal/service/house/dashboard/dashboard.go @@ -27,7 +27,7 @@ func NewDashboard() IDashboard { return &dashboard{} } // Dashboard 返回已注册的看板服务实现。 func Dashboard() IDashboard { if localDashboard == nil { - panic("Dashboard implementation not registered") + panic("Dashboard 实现未注册") } return localDashboard } @@ -44,7 +44,7 @@ func (s *dashboard) Overview(ctx context.Context, region string) (dto.DashboardO } cnt, err := cm.Count() if err != nil { - return out, gerror.Wrap(err, "count community") + return out, gerror.Wrap(err, "统计小区总数失败") } 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( "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 { - return out, gerror.Wrap(err, "aggregate overview") + return out, gerror.Wrap(err, "聚合概览指标失败") } out.AvgUnitPrice = agg.AvgUnitPrice out.AvgTotalPrice = agg.AvgTotalPrice @@ -93,7 +93,7 @@ func (s *dashboard) MapPoints(ctx context.Context, region string, priceMin, pric } var list []dto.DashboardMapPoint if err := m.Scan(&list); err != nil { - return nil, gerror.Wrap(err, "query map points") + return nil, gerror.Wrap(err, "查询地图点位失败") } return list, nil } @@ -115,7 +115,7 @@ func (s *dashboard) PriceTrend(ctx context.Context, communityId uint64, region s } var list []dto.DashboardTrendPoint 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 { @@ -135,7 +135,7 @@ func (s *dashboard) AggregateRegion(ctx context.Context) ([]dto.DashboardRegionA OrderAsc("c.region") var list []dto.DashboardRegionAgg if err := m.Scan(&list); err != nil { - return nil, gerror.Wrap(err, "query region aggregate") + return nil, gerror.Wrap(err, "查询区域聚合失败") } return list, nil } diff --git a/internal/service/house/listing/listing.go b/internal/service/house/listing/listing.go index 8c1211d..dcafc1b 100644 --- a/internal/service/house/listing/listing.go +++ b/internal/service/house/listing/listing.go @@ -31,7 +31,7 @@ func NewListing() IListing { return &listing{} } // Listing 返回已注册的房源服务实现。 func Listing() IListing { if localListing == nil { - panic("Listing implementation not registered") + panic("Listing 实现未注册") } return localListing } @@ -80,11 +80,11 @@ func (s *listing) List(ctx context.Context, f dto.HouseListingFilter) ([]dto.Hou } total, err := m.Clone().Count() if err != nil { - return nil, 0, gerror.Wrap(err, "count listing") + return nil, 0, gerror.Wrap(err, "统计房源总数失败") } 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 { - return nil, 0, gerror.Wrap(err, "query listing list") + return nil, 0, gerror.Wrap(err, "查询房源列表失败") } return list, total, nil } @@ -116,7 +116,7 @@ func (s *listing) Create(ctx context.Context, in dto.HouseListingInput) (uint64, IsBargain: in.IsBargain, }).InsertAndGetId() if err != nil { - return 0, gerror.Wrap(err, "insert listing") + return 0, gerror.Wrap(err, "新增房源失败") } return uint64(id), nil } @@ -143,7 +143,7 @@ func (s *listing) Update(ctx context.Context, in dto.HouseListingInput) error { IsBargain: in.IsBargain, }).Update() if err != nil { - return gerror.Wrap(err, "update listing") + return gerror.Wrap(err, "更新房源失败") } return nil } @@ -151,7 +151,7 @@ func (s *listing) Update(ctx context.Context, in dto.HouseListingInput) error { // Delete 软删除房源。 func (s *listing) Delete(ctx context.Context, id uint64) error { 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 } @@ -167,11 +167,11 @@ func (s *listing) BatchMark(ctx context.Context, ids []uint64, field string, val case "status": data = do.HouseListing{Status: value} 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() if err != nil { - return 0, gerror.Wrap(err, "batch mark listing") + return 0, gerror.Wrap(err, "批量标记房源失败") } affected, _ := res.RowsAffected() return affected, nil diff --git a/internal/service/house/presale/presale.go b/internal/service/house/presale/presale.go index 8dceb5c..3e7b46d 100644 --- a/internal/service/house/presale/presale.go +++ b/internal/service/house/presale/presale.go @@ -24,7 +24,7 @@ func NewPresale() IPresale { return &presale{} } // Presale 返回已注册的预售证服务实现。 func Presale() IPresale { if localPresale == nil { - panic("Presale implementation not registered") + panic("Presale 实现未注册") } return localPresale } @@ -47,11 +47,11 @@ func (s *presale) List(ctx context.Context, page, size int, region, keyword, pur } total, err := m.Clone().Count() if err != nil { - return nil, 0, gerror.Wrap(err, "count presale") + return nil, 0, gerror.Wrap(err, "统计预售证总数失败") } var list []dto.HousePresaleVO 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 } diff --git a/internal/service/house/transaction/transaction.go b/internal/service/house/transaction/transaction.go index 6a2a29f..804d8af 100644 --- a/internal/service/house/transaction/transaction.go +++ b/internal/service/house/transaction/transaction.go @@ -24,7 +24,7 @@ func NewTransaction() ITransaction { return &transaction{} } // Transaction 返回已注册的成交服务实现。 func Transaction() ITransaction { if localTransaction == nil { - panic("Transaction implementation not registered") + panic("Transaction 实现未注册") } return localTransaction } @@ -44,11 +44,11 @@ func (s *transaction) List(ctx context.Context, page, size int, region, keyword } total, err := m.Clone().Count() if err != nil { - return nil, 0, gerror.Wrap(err, "count transaction") + return nil, 0, gerror.Wrap(err, "统计成交记录总数失败") } 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 { - return nil, 0, gerror.Wrap(err, "query transaction list") + return nil, 0, gerror.Wrap(err, "查询成交记录列表失败") } return list, total, nil } diff --git a/internal/service/job/job.go b/internal/service/job/job.go index 76f66bd..e10a936 100644 --- a/internal/service/job/job.go +++ b/internal/service/job/job.go @@ -48,7 +48,7 @@ func New() IJob { // Job 返回已注册的自动任务服务实现。 func Job() IJob { if localJob == nil { - panic("Job implementation not registered") + panic("Job 实现未注册") } return localJob } @@ -68,7 +68,7 @@ func (s *job) Register(code string, fn TaskFunc) { func (s *job) List(ctx context.Context) ([]dto.JobVO, error) { var list []entity.AutoJob 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)) for _, v := range list { @@ -94,7 +94,7 @@ func (s *job) Save(ctx context.Context, in dto.JobInput) error { data["enabled"] = in.Enabled } 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,按新配置重新注册 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() if err != nil { - return nil, 0, gerror.Wrap(err, "count job log") + return nil, 0, gerror.Wrap(err, "统计任务日志总数失败") } var list []entity.AutoJobLog 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)) 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) { var jobs []entity.AutoJob 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 } for _, j := range jobs { 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(不存在的函数跳过)。 @@ -160,13 +160,13 @@ func (s *job) addCron(ctx context.Context, j entity.AutoJob) { _, ok := s.registry[j.Code] s.mu.Unlock() if !ok { - g.Log().Warningf(ctx, "job code %s has no handler, skip cron", j.Code) + g.Log().Warningf(ctx, "任务 %s 未注册执行函数,跳过定时注册", j.Code) return } if _, err := gcron.Add(ctx, j.CronExpr, func(c context.Context) { _, _ = s.run(c, j) }, 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] s.mu.Unlock() 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) 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_result": result, "last_error": errMsg, }).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{}{ "job_id": j.Id, "result": result, "error": errMsg, "summary": summary, "duration_ms": duration, }).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} diff --git a/internal/service/notice/notice.go b/internal/service/notice/notice.go index f52223a..da0cd81 100644 --- a/internal/service/notice/notice.go +++ b/internal/service/notice/notice.go @@ -41,7 +41,7 @@ func New() INotice { return ¬ice{} } // Notice 返回已注册的通知服务实现。 func Notice() INotice { if localNotice == nil { - panic("Notice implementation not registered") + panic("Notice 实现未注册") } return localNotice } @@ -54,7 +54,7 @@ func RegisterNotice(i INotice) { localNotice = i } func (s *notice) ChannelList(ctx context.Context) ([]dto.NoticeChannelVO, error) { var list []entity.NoticeChannel 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)) for _, v := range list { @@ -70,13 +70,13 @@ func (s *notice) ChannelSave(ctx context.Context, in dto.NoticeChannelInput) (ui } if in.Id > 0 { 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 } id, err := dao.NoticeChannel.Ctx(ctx).Data(data).InsertAndGetId() if err != nil { - return 0, gerror.Wrap(err, "insert notice channel") + return 0, gerror.Wrap(err, "新增通知渠道失败") } return uint64(id), nil } @@ -90,7 +90,7 @@ func (s *notice) RuleList(ctx context.Context, eventType string) ([]dto.NoticeRu } var list []entity.NoticeRule 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)) for _, v := range list { @@ -111,20 +111,20 @@ func (s *notice) RuleSave(ctx context.Context, in dto.NoticeRuleInput) (uint64, } if in.Id > 0 { 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 } id, err := dao.NoticeRule.Ctx(ctx).Data(data).InsertAndGetId() if err != nil { - return 0, gerror.Wrap(err, "insert notice rule") + return 0, gerror.Wrap(err, "新增通知规则失败") } return uint64(id), nil } func (s *notice) RuleDelete(ctx context.Context, id uint64) error { 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 } @@ -150,11 +150,11 @@ func (s *notice) LogList(ctx context.Context, f dto.NoticeLogFilter) ([]dto.Noti } total, err := m.Clone().Count() if err != nil { - return nil, 0, gerror.Wrap(err, "count notice log") + return nil, 0, gerror.Wrap(err, "统计通知日志总数失败") } var list []entity.NoticeLog 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)) 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 { var rules []entity.NoticeRule 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 { return nil // 未配置规则 = 静默 diff --git a/internal/service/recruitment/crawler.go b/internal/service/recruitment/crawler.go index 87f103c..4632142 100644 --- a/internal/service/recruitment/crawler.go +++ b/internal/service/recruitment/crawler.go @@ -27,7 +27,7 @@ func crawlerRun(ctx context.Context, src entity.CrawlSource, force bool) dto.Cra defer func() { // 防止单源解析panic拖垮整体调度。 if r := recover(); r != nil { - res.Err = gerror.Newf("crawler panic: %v", r) + res.Err = gerror.Newf("抓取发生 panic: %v", r) } }() var ( @@ -207,16 +207,16 @@ func fetchHTML(ctx context.Context, u string) (string, error) { client.SetHeader("Accept-Language", "zh-CN,zh;q=0.9") resp, err := client.Get(ctx, u) if err != nil { - return "", gerror.Wrapf(err, "fetch %s", u) + return "", gerror.Wrapf(err, "抓取页面失败 %s", u) } defer resp.Close() 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() if !looksLikeHtml(body) || len(body) < 3000 { 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) return body, nil @@ -233,7 +233,7 @@ func fetchWithCurl(u string) (string, error) { } out, err := exec.Command("curl", args...).Output() if err != nil { - return "", gerror.Wrapf(err, "curl %s", u) + return "", gerror.Wrapf(err, "curl 抓取失败 %s", u) } return string(out), nil } @@ -245,8 +245,8 @@ func looksLikeHtml(s string) bool { } type anchor struct { - URL string - Text string + URL string + Text string } var ( @@ -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 { // gf 的 Scan 在查无记录时返回 "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 { @@ -490,7 +490,7 @@ func upsertInfo(ctx context.Context, in dto.RecruitmentInput) (bool, error) { ExamDate: in.ExamDate, UpdatedAt: gtime.Now(), }).Update() if err != nil { - return false, gerror.Wrap(err, "update info") + return false, gerror.Wrap(err, "更新公告失败") } return false, nil } @@ -500,7 +500,7 @@ func upsertInfo(ctx context.Context, in dto.RecruitmentInput) (bool, error) { } in.OrgId = orgId 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 } @@ -513,7 +513,7 @@ func upsertOrg(ctx context.Context, name string, category int, region string) (u var org entity.Organization if err := dao.Organization.Ctx(ctx).Where("name", name).Scan(&org); err != nil { if !strings.Contains(err.Error(), "no rows") { - return 0, gerror.Wrap(err, "query org") + return 0, gerror.Wrap(err, "查询发布机构失败") } } 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(), }).InsertAndGetId() if err != nil { - return 0, gerror.Wrap(err, "insert org") + return 0, gerror.Wrap(err, "新增发布机构失败") } return uint64(id), nil } @@ -571,14 +571,14 @@ func recordCrawlLog(ctx context.Context, res dto.CrawlRunResult) { errMsg = res.Err.Error() const maxErr = 900 if len(errMsg) > maxErr { - errMsg = errMsg[:maxErr] + "...(truncated)" + errMsg = errMsg[:maxErr] + "...(已截断)" } } if _, err := dao.CrawlLog.Ctx(ctx).Data(do.CrawlLog{ SourceId: res.SourceId, RunAt: gtime.Now(), Fetched: res.Fetched, NewCount: res.NewCount, UpdatedCount: res.UpdatedCount, Error: errMsg, }).Insert(); err != nil { - g.Log().Errorf(ctx, "write crawl log failed: %v", err) + g.Log().Errorf(ctx, "写入抓取日志失败: %v", err) } if res.Err != nil { var src entity.CrawlSource @@ -603,7 +603,7 @@ func loadSubscription(ctx context.Context, id uint64) (*entity.PushSubscription, m = m.Where("enabled", 1) } 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 { return nil, nil diff --git a/internal/service/recruitment/recruitment.go b/internal/service/recruitment/recruitment.go index 545b87c..69d4885 100644 --- a/internal/service/recruitment/recruitment.go +++ b/internal/service/recruitment/recruitment.go @@ -41,7 +41,7 @@ func New() IRecruitment { return &recruitment{} } // Recruitment 返回已注册的招聘服务实现。 func Recruitment() IRecruitment { if localRecruitment == nil { - panic("Recruitment implementation not registered") + panic("Recruitment 实现未注册") } return localRecruitment } @@ -86,11 +86,11 @@ func (s *recruitment) List(ctx context.Context, f dto.RecruitmentFilter) ([]dto. } total, err := m.Clone().Count() if err != nil { - return nil, 0, gerror.Wrap(err, "count recruitment") + return nil, 0, gerror.Wrap(err, "统计招聘公告总数失败") } var list []dto.RecruitmentInfoVO 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 { 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"). Where("r.id", id).Scan(&v) if err != nil { - return nil, gerror.Wrap(err, "query recruitment detail") + return nil, gerror.Wrap(err, "查询招聘公告详情失败") } if v.Id == 0 { 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() 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() 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() if err != nil { - return nil, gerror.Wrap(err, "count week") + return nil, gerror.Wrap(err, "统计近 7 天新增失败") } var catRows []struct { Category int `json:"category"` Cnt int `json:"cnt"` } 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 { Region string `json:"region"` Cnt int `json:"cnt"` } if err = base.Clone().Fields("region, COUNT(*) AS cnt").Group("region").Scan(®ionRows); err != nil { - return nil, gerror.Wrap(err, "agg region") + return nil, gerror.Wrap(err, "按地区聚合失败") } var trendRows []struct { 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"). Where("publish_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)"). 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)} for _, r := range catRows { @@ -187,7 +187,7 @@ func (s *recruitment) Trend(ctx context.Context, region string, category, days i Cnt int `json:"cnt"` } 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)) 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) { var srcs []entity.CrawlSource 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 @@ -243,7 +243,7 @@ func (s *recruitment) Trigger(ctx context.Context, sourceId uint64, force bool) } var srcs []entity.CrawlSource 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 n := 0 @@ -267,7 +267,7 @@ func (s *recruitment) PushTest(ctx context.Context, subId uint64, title, body st sub = defaultSubscription(ctx) } if sub == nil { - return false, "无启用的推送订阅且未配置默认设备密钥", gerror.New("no enabled subscription") + return false, "无启用的推送订阅且未配置默认设备密钥", gerror.New("没有启用的推送订阅") } if 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) { var subs []entity.PushSubscription 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)) for _, sub := range subs { @@ -301,13 +301,13 @@ func (s *recruitment) SubscriptionSave(ctx context.Context, in dto.SubscriptionI if in.Id > 0 { _, err := dao.PushSubscription.Ctx(ctx).Where("id", in.Id).Data(data).Update() if err != nil { - return 0, gerror.Wrap(err, "update subscription") + return 0, gerror.Wrap(err, "更新推送订阅失败") } return in.Id, nil } id, err := dao.PushSubscription.Ctx(ctx).Data(data).InsertAndGetId() if err != nil { - return 0, gerror.Wrap(err, "insert subscription") + return 0, gerror.Wrap(err, "新增推送订阅失败") } return uint64(id), nil } @@ -315,7 +315,7 @@ func (s *recruitment) SubscriptionSave(ctx context.Context, in dto.SubscriptionI // SubscriptionDelete 删除推送订阅。 func (s *recruitment) SubscriptionDelete(ctx context.Context, id uint64) error { if _, err := dao.PushSubscription.Ctx(ctx).Where("id", id).Delete(); err != nil { - return gerror.Wrap(err, "delete subscription") + return gerror.Wrap(err, "删除推送订阅失败") } return nil } diff --git a/internal/service/serversecurity/security.go b/internal/service/serversecurity/security.go index 14df1b5..7a1d8a5 100644 --- a/internal/service/serversecurity/security.go +++ b/internal/service/serversecurity/security.go @@ -32,7 +32,7 @@ func New() ISecurity { return &security{} } // Security 返回已注册的安全日志服务实现。 func Security() ISecurity { if localSecurity == nil { - panic("Security implementation not registered") + panic("Security 实现未注册") } 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 { - return 0, gerror.Wrap(err, "insert server security log") + return 0, gerror.Wrap(err, "写入服务器安全日志失败") } 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() if err != nil { - return nil, 0, gerror.Wrap(err, "count security log") + return nil, 0, gerror.Wrap(err, "统计安全日志总数失败") } var list []dto.SecurityLogVO 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 { 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{} var err error 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 { - 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 { - 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 { - return nil, gerror.Wrap(err, "count accepted") + return nil, gerror.Wrap(err, "统计成功登录次数失败") } // TOP 攻击源(爆破+封禁优先)。 if err = base.Clone().WhereIn("event_type", g.Slice{"failed_ssh", "banned"}). Fields("src_ip, COUNT(*) AS cnt"). 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"). 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 { st.ByType[i].EventName = dto.SecurityEventName(st.ByType[i].EventType) diff --git a/internal/service/user/auth/auth.go b/internal/service/user/auth/auth.go index fe420dc..bac8110 100644 --- a/internal/service/user/auth/auth.go +++ b/internal/service/user/auth/auth.go @@ -24,7 +24,7 @@ var localUserAuth IUserAuth func UserAuth() IUserAuth { if localUserAuth == nil { - panic("UserAuth implementation not registered") + panic("UserAuth 实现未注册") } return localUserAuth } @@ -55,39 +55,39 @@ func (s *userAuth) Login(ctx context.Context, in dto.UserLoginInput) (*dto.Token case "wechat": // 微信首次授权成功后按 openid 自动创建用户。 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) 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) if err != nil { - return nil, 0, gerror.Wrap(err, "query user") + return nil, 0, gerror.Wrap(err, "查询用户失败") } if u.Id == 0 { id, e := dao.User.Ctx(ctx).Data(do.User{OpenId: openID, Nickname: "微信用户", Status: 1}).InsertAndGetId() if e != nil { - return nil, 0, gerror.Wrap(e, "create user") + return nil, 0, gerror.Wrap(e, "创建用户失败") } u.Id = uint64(id) } case "mobile": // 验证码由注入的服务校验,校验成功后按手机号自动注册。 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 { - 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) if err != nil { - return nil, 0, gerror.Wrap(err, "query user") + return nil, 0, gerror.Wrap(err, "查询用户失败") } if u.Id == 0 { id, e := dao.User.Ctx(ctx).Data(do.User{Mobile: in.Mobile, Nickname: "用户" + in.Mobile[7:], Status: 1}).InsertAndGetId() if e != nil { - return nil, 0, gerror.Wrap(e, "create user") + return nil, 0, gerror.Wrap(e, "创建用户失败") } u.Id = uint64(id) } @@ -95,20 +95,20 @@ func (s *userAuth) Login(ctx context.Context, in dto.UserLoginInput) (*dto.Token // 密码只使用 bcrypt 比对哈希值,任何场景都不回传或记录明文。 err = dao.User.Ctx(ctx).Where(do.User{Account: in.Account}).Scan(&u) 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 { - return nil, 0, response.Error(consts.CodeUserPasswordWrong, "account or password incorrect") + return nil, 0, response.Error(consts.CodeUserPasswordWrong, "账号或密码错误") } default: - return nil, 0, response.Error(consts.CodeUserLoginType, "unsupported login type") + return nil, 0, response.Error(consts.CodeUserLoginType, "不支持的登录方式") } 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() 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) } @@ -116,7 +116,7 @@ func (s *userAuth) Refresh(ctx context.Context, refresh string) (*dto.TokenPair, // 刷新时先校验 JWT,再以 JTI 原子撤销旧会话,实现单次使用的令牌轮换。 c, err := s.tokens.Parse(refresh, "refresh", "user") 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() 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) { a, r, exp, err := s.tokens.Issue(id, "user", terminal) 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") if err != nil { diff --git a/main.go b/main.go index 066e453..1f9db4f 100644 --- a/main.go +++ b/main.go @@ -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 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/genv" "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() { + injectDefaultConfigFile() cmd.Main.Run(gctx.GetInitCtx()) } + +// injectDefaultConfigFile 在配置系统初始化前,兜底指定默认配置文件。 +// +// 仅在「调用方未显式指定配置文件或目录」时才写入默认值, +// 以免覆盖使用者通过环境变量或命令行做出的选择。 +func injectDefaultConfigFile() { + if !genv.Get(envKeyForFile).IsEmpty() || !genv.Get(envKeyForPath).IsEmpty() { + return + } + _ = genv.Set(envKeyForFile, defaultConfigFile) +}