feat(i18n): 中文化校验提示与服务层错误信息
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s
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:
parent
70cb86176e
commit
4b3c00270f
@ -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/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 | 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 项全过
|
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 项全过
|
||||||
|
|||||||
@ -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 - 管理端 API(AdminAuth + 权限码)
|
||||||
│ ├── 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/ # 工具集:md5、cryptox、uuid、random、timex、
|
||||||
|
│ # convertx、strx、slicex、ip、filex
|
||||||
├── 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/ # API→service 适配层(不写业务;含 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/ # 路由中间件:Recover、CORS、认证、全量加密
|
||||||
│ └── 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.yaml(config.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)。
|
||||||
|
|||||||
62
README.MD
62
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
|
```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 ./...
|
||||||
|
```
|
||||||
|
|||||||
@ -5,15 +5,15 @@ import "github.com/gogf/gf/v2/frame/g"
|
|||||||
|
|
||||||
// AdminItem 管理员列表中的一行。
|
// AdminItem 管理员列表中的一行。
|
||||||
type AdminItem struct {
|
type AdminItem struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
BarkDeviceId string `json:"barkDeviceId"`
|
BarkDeviceId string `json:"barkDeviceId"`
|
||||||
PushplusToken string `json:"pushplusToken"`
|
PushplusToken string `json:"pushplusToken"`
|
||||||
RoleIds []uint64 `json:"roleIds"`
|
RoleIds []uint64 `json:"roleIds"`
|
||||||
RoleNames []string `json:"roleNames"`
|
RoleNames []string `json:"roleNames"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminListReq 分页查询管理员。
|
// AdminListReq 分页查询管理员。
|
||||||
@ -34,15 +34,15 @@ type AdminListRes struct {
|
|||||||
// 密码采用「RSA + AES-GCM」混合加密传输:encryptedKey/encryptedData 必填(生产强制),
|
// 密码采用「RSA + AES-GCM」混合加密传输:encryptedKey/encryptedData 必填(生产强制),
|
||||||
// password 明文字段仅当服务端 encrypt.allowPlain=true(本地联调)时可用。
|
// password 明文字段仅当服务端 encrypt.allowPlain=true(本地联调)时可用。
|
||||||
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"`
|
||||||
BarkDeviceId string `json:"barkDeviceId"`
|
BarkDeviceId string `json:"barkDeviceId"`
|
||||||
PushplusToken string `json:"pushplusToken"`
|
PushplusToken string `json:"pushplusToken"`
|
||||||
RoleIds []uint64 `json:"roleIds"`
|
RoleIds []uint64 `json:"roleIds"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminCreateRes 是 AdminCreateReq 的响应。
|
// AdminCreateRes 是 AdminCreateReq 的响应。
|
||||||
@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"`
|
||||||
|
|||||||
@ -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 的响应。
|
||||||
|
|||||||
@ -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 的响应。
|
||||||
|
|||||||
@ -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 用户登录响应。
|
||||||
|
|||||||
@ -1 +1,6 @@
|
|||||||
|
// Package user_login 用户端登录契约。
|
||||||
|
//
|
||||||
|
// 说明:当前用户端登录已统一收敛到 `api/user/auth`(支持微信 / 手机号 / 密码三种方式),
|
||||||
|
// user 路由组直接绑定 Auth 控制器即可,因此本包暂无端点定义。
|
||||||
|
// 保留该包是为了维持目录结构一致性,后续如需拆分「扫码登录」「短信登录」等独立契约,可在此扩展。
|
||||||
package user_login
|
package user_login
|
||||||
|
|||||||
@ -6,5 +6,5 @@
|
|||||||
//
|
//
|
||||||
// 当前结构:
|
// 当前结构:
|
||||||
//
|
//
|
||||||
// common/tools shared utility toolbox (md5, cryptox, uuid, ...)
|
// common/tools 公共工具集(md5、cryptox、uuid 等)
|
||||||
package common
|
package common
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
69
docs/change-log/2026-09-13.md
Normal file
69
docs/change-log/2026-09-13.md
Normal 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
8
go.mod
@ -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
2
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/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=
|
||||||
|
|||||||
@ -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;
|
||||||
22
hack/hack.mk
22
hack/hack.mk
@ -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
|
||||||
@ -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=true):CORS → APICrypto → Recover → HandlerResponse。
|
// 全量请求/响应加密(生产 encrypt.fullBody=true):CORS → 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))
|
||||||
|
|||||||
@ -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 分页查询管理员。
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ func New() INotice { return ¬ice{} }
|
|||||||
// 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 // 未配置规则 = 静默
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
@ -245,8 +245,8 @@ func looksLikeHtml(s string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type anchor struct {
|
type anchor struct {
|
||||||
URL string
|
URL string
|
||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
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 {
|
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
|
||||||
|
|||||||
@ -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(®ionRows); err != nil {
|
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 {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
39
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
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user