Compare commits

..

40 Commits
dev ... main

Author SHA1 Message Date
夏犀麟
4b3c00270f feat(i18n): 中文化校验提示与服务层错误信息
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s
将 api 层校验规则提示语、service 层 gerror.Wrap 与 response.Error
错误信息、panic 未注册提示统一改为中文,并同步中文化 cmd 路由注释
与变更记录。仅涉及注释、文档与字符串改动,无业务逻辑变更。
2026-09-13 23:22:46 +08:00
夏犀麟
70cb86176e chore(auth): 登录接口豁免全量加密,仅加密密码字段
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 37s
- APICrypto plainRoutes 增 /system/auth/login(登录请求不整体加密)
- ResolveLogin 密码字段密文优先解密,兼容全量模式
2026-08-28 00:28:58 +08:00
夏犀麟
ebc507ee6c feat(auth): 生产全量请求/响应加密中间件
- crypto.Service 增 fullBody 开关 + DecryptRequest/EncryptResponse(会话密钥双向加密)
- 新增 middleware/APICrypto:请求整体解密 + 响应加密(public-key 豁免),未加密请求拒绝
- ResolveLogin/resolvePassword 增 fullBody 分支;admin 组中间件链调整
- injectEnv 支持 ENCRYPT_FULL_BODY/ENCRYPT_ALLOW_PLAIN(直接跑二进制需 env 注入)
2026-08-28 00:17:47 +08:00
夏犀麟
06484774ad feat(auth): 登录密码 RSA+AES 混合加密传输与加密字段支持
- 新增 internal/library/crypto:RSA-OAEP(SHA-256)+AES-256-GCM 混合解密,
  私钥按 配置PEM > data/crypto/rsa_private.pem > 自动生成 三级来源,公钥 SPKI 输出
- 公开接口 POST /system/auth/public-key;LoginReq 支持 encryptedKey/encryptedData
- AdminCreate/AdminResetPwd 密码字段支持加密传输(allowPlain 仅开发联调)
- 配置 encrypt.privateKey/allowPlain;data/ 加入 gitignore 防止私钥入库
2026-08-27 23:43:41 +08:00
夏犀麟
5e517a2d95 chore: 移除误提交的本地测试产物并忽略
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 33s
2026-08-27 23:20:53 +08:00
夏犀麟
abd81486ad refactor(api): 去除 api 层 /v1 目录,统一为 api/<模块>/<子模块> 层级规则(包名=路径段下划线连接) 2026-08-27 23:20:32 +08:00
夏犀麟
3d24dbd8cb feat(auth): 认证链路联调修复——401 语义化 + 登出撤销会话 + 零配置兜底
- 未授权响应改为 HTTP 401(原 200+code10002 vben 拦截器不识别,
  token 过期永远不触发静默刷新/重登)
- 新增 response.JSONWithStatus;三处 401(UserAuth/AdminAuthOnly/AdminAuth)切换
- LogoutReq 支持 refreshToken:登出时撤销刷新会话(幂等),阻止设备续期;
  Refresh 时顺带清理已过期会话行
- IAdminAuth 新增 Revoke;dao/internal/admin_user.go 补 bark_device_id/
  pushplus_token 列映射(与 016 SQL/0569c70 对齐,恢复 int/gtime 类型映射)
- injectEnv 开发模式兜底: 等占位符未注入 env 时回填本地默认 DSN,
  任何启动方式(GoLand/命令行/go run)零配置可跑;prod 不兜底
2026-08-27 23:00:53 +08:00
夏犀麟
e868c1ef9c docs: 通知/任务模块变更记录 2026-08-27 22:59:21 +08:00
夏犀麟
0569c70690 feat(notice+job): 通知模块(渠道/规则/日志/测试) + 自动任务管理(DB驱动gcron) + 用户表加Bark/pushplus字段 2026-08-27 22:58:33 +08:00
夏犀麟
e18b25f32a fix(recruitment): 招聘中心菜单 96→97,为 015 安全日志菜单(96)让位
015_server_security.sql 已在生产部署使用 id=96(安全日志,挂系统监控),
招聘中心不能再占 96。95=新房预售、96=安全日志、97=招聘中心。
2026-08-27 12:27:44 +08:00
夏犀麟
743683efdd fix(recruitment): 菜单种子主键冲突,招聘中心 95→96 避免覆盖新房预售
003_menu.sql 的招聘中心一级菜单写死 id=95,与 014_house_presale_menu.sql 的
新房预售菜单(id=95,挂看房中心 90)ON DUPLICATE 互相覆盖,后导入者赢。
本地已实际发生覆盖。改为 id=96,951-954 父级同步改 96,超管角色绑定列表同步。
2026-08-27 12:22:50 +08:00
夏犀麟
1d451499b9 feat(security): 服务器安全日志模块——上报/查询/统计接口 + 015建表菜单 2026-08-27 11:30:00 +08:00
夏犀麟
a85a520af7 chore: gitignore 忽略本地验证用二进制 service_test.exe 2026-08-27 10:50:33 +08:00
夏犀麟
f81ce64fdd fix(recruitment): gcron 表达式改 6 段式,修复每日抓取/推送任务从未注册
gcron 不支持 5 段标准 cron,'0 3 * * *'/'0 8 * * *' 注册时报 invalid pattern
被 ERRO 日志吞掉,导致线上每日 03:00 增量抓取与 08:00 招聘早报推送从未执行。
改为 '0 0 3 * * *' / '0 0 8 * * *'。
2026-08-27 10:50:23 +08:00
夏犀麟
8721d01028 fix(recruitment): fetchHTML curl 优先 + https 归一化 + 调试日志改 warning 级(logger.level=warning 过滤了 Infof)
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 32s
2026-08-27 02:26:53 +08:00
夏犀麟
8b11893d35 feat(recruitment): 爬虫修复与部署打通 - Scan no-rows 入库修复 / title 属性兜底 / 双条件过滤 / 空日期插 NULL / crawl_log 截断 / fetchHTML curl 兜底 / 人社局改 https / 新增 config.yaml 2026-08-27 02:12:03 +08:00
夏犀麟
1e28e02dc1 feat(house): 新增新房预售证模块并扩展相关功能
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s
- 添加 api/house/presale 接口模块实现新房预售证查询功能
- 新增 POST /api/service/admin/house/presale/list 接口支持 region/purpose/keyword 筛选
- 在 HousePresaleVO 中增加 PublishDate 字段用于存储公示发布日期
- 在 house_presale 表中添加 publish_date 字段并完成数据库同步
- 更新权限种子配置新增菜单 95 新房预售及 950 查询权限
- 完成相关 controller 和 service 层代码实现并验证接口返回正常数据
2026-08-27 02:01:04 +08:00
夏犀麟
ea50261cb8 feat(house): 扩充看房数据并增强服务器日志功能
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 35s
- 新增新房预售证表 house_presale 存储预售许可信息
- 为 house_community 表添加 avg_price 字段存储小区参考均价
- 增强服务器操作审计日志功能,新增管理员账号、IP归属地、错误信息、UA等字段
- 实现纯Go版IP归属地离线解析器,无外部依赖,支持二分查找
- 优化看房列表查询逻辑,修复Fields设置位置导致的SQL语法错误
- 集成招聘模块,添加独立数据库配置和Bark推送服务支持
- 重构日志查询接口,支持多维度筛选和综合分页列表展示
- 更新DAO实体结构同步数据库表结构调整
2026-08-27 01:47:32 +08:00
夏犀麟
856d62f9c7 FIX: workflow Go 下载源改 aliyun 镜像(go.dev 偶发 SSL 错误 exitcode 35)+ 3 次重试
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 20s
2026-08-27 00:54:33 +08:00
夏犀麟
4445b8e8bf CHG: house transaction service 层 + recruitment 招聘模块 + 012 菜单 SQL(补提交未推送代码)
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 14s
2026-08-27 00:41:59 +08:00
夏犀麟
a2f60c3410 CHG: house transaction controller
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 25s
2026-08-27 00:34:22 +08:00
夏犀麟
69137f366c CHG: house 成交记录(transaction)模块:api/service/controller 全链路 + cmd 注册 + dto 扩展
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 25s
2026-08-27 00:33:52 +08:00
夏犀麟
b439c4d89c FIX: workflow apt 换腾讯云源(deb.debian.org 国内直连超时导致卡死)
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 2m37s
2026-08-27 00:18:50 +08:00
夏犀麟
8950dc83e9 FIX: deploy workflow Go 下载源改 go.dev 官方(npmmirror/腾讯云无归档),go1.24.5
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Has been cancelled
2026-08-27 00:06:48 +08:00
夏犀麟
361e063b80 CFG: Gitea act_runner 自动部署(Go 交叉编译 + Docker 重建容器)+ 生产镜像 Dockerfile
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 4m6s
- .gitea/workflows/deploy.yml:push main/dev → Go1.23 交叉编译(linux/amd64) → 组装 main+manifest/config+resource → docker build → 重建容器 127.0.0.1:10100(xpcool-net, GF_GCFG_ENV=prod, secrets: DB_DSN/JWT_SECRET)→ /api.json 冒烟
- deploy/Dockerfile:alpine 最小运行时(main + manifest/config + resource,EXPOSE 10100)
- 新增 009_admin_account_v2.sql(超管账号替换 xxcool)已含在本仓
2026-08-26 23:55:46 +08:00
夏犀麟
4aca0c7f6f feat(house): 实现看房模块后端功能
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 5m7s
- 新增 9 张房屋相关数据表(社区/楼宇/房源/价格快照/交易/设施/社区设施/学区/偏好)
- 添加菜单权限种子数据并绑定超级管理员角色
- 生成 DAO 层代码和实体对象
- 实现房屋模块 API 接口(社区/房源/看板)和控制器服务层
- 支持多平台软关联匹配、笋盘标记和低可信度标记功能
- 更新超级管理员账号为 xxcool/xxCool@2026
- 调整 RBAC 菜单结构,移除管理员管理功能,新增日志管理菜单
- 修复 RBAC 安全漏洞,确保禁用角色权限失效
- 重构认证模块,将登录相关接口迁移到统一包结构下
- 移除废弃的管理模块和工具类接口定义
- 为通用工具包添加中文注释和文档说明
2026-08-26 23:42:38 +08:00
夏犀麟
ded75e1bed DOC: 记录菜单调整(移除管理员管理/新增日志管理)与早前 RBAC 隔离修复 2026-08-26 22:24:45 +08:00
夏犀麟
78756a849f CHG: 移除管理员管理菜单、新增日志管理菜单(008_menu_rbac_v4.sql) 2026-08-26 22:23:21 +08:00
夏犀麟
2ed31f012e FIX: RBAC 禁用角色权限隔离(Codes/HasPermission/Routes 过滤 r.status=1)
- 三处联表查询统一 LeftJoin admin_role 并加启用状态过滤
- 修复前 opuser(禁用角色) 仍能拿到权限码并访问受保护接口
2026-08-26 21:43:15 +08:00
夏犀麟
45f9098e89 chore: CHANGELOG 纳入 git 管理;AGENTS.md 中央工作空间路径改相对引用 ../workbuddy.xpcool.com 2026-08-26 19:07:23 +08:00
夏犀麟
6e3ef67709 refactor(service): 重构服务层结构并更新导入路径
- 将 admin 相关服务移动到 internal/service/admin 目录下
- 更新控制器中的服务导入路径引用
- 移除已合并的服务文件
- 添加统一工作约定文档
- 更新 API 接口定义的包路径
2026-08-26 15:40:13 +08:00
夏犀麟
061dbde8ce docs: 记录前端 tdesign 迁移与清理 2026-08-24 23:46:08 +08:00
夏犀麟
43fa79686d refactor(api): 后台管理 API 按 base/system/admin 分组重构
- api/admin/v1 拆 base/system/admin 三子包,路由前缀 /admin/v1/{base,system,admin}
- system: auth/menu/role;admin: 管理员;base: log
- controller 改 import 子包;006_menu_paths_v2.sql 更新接口路径映射
- 权限码与路由分离,permission 标识不变
- 冒烟测试新路径 8 接口全通过
2026-08-24 23:40:39 +08:00
夏犀麟
1d632065fe docs: 记录前端 vben 对接进度与本地环境待办 2026-08-24 23:15:47 +08:00
夏犀麟
75e56dae43 docs: 后台管理 API 说明与变更记录(AGENTS.md / change-log) 2026-08-24 22:36:25 +08:00
夏犀麟
8fdb25e6bd feat(admin): 后台登录/菜单/按钮级权限/RBAC管理与服务器日志监控
后端:
- 种子数据:超级管理员(admin/admin123)、super_admin 角色、22 条菜单(含按钮权限码)、角色绑定
- admin_menu 扩展 icon/component/hidden 字段(003_schema_ext.sql),手动补齐 entity/do/table
- auth 扩展:/auth/info、/auth/codes(仅登录),路由拆分公开/Profile/受保护三组
- 菜单路由:/menu/routes 返回 vben backend 动态路由树(按角色过滤、排序)
- RBAC 管理:/admins、/roles、/menus CRUD(含角色绑定、重置密码、菜单授权)
- 日志监控:/log/files、/log/tail(尾部读取+关键词过滤+路径穿越防护)
- 安全加固:接口鉴权改为「方法+路径→权限码」自动映射,杜绝任意权限码越权
- 修复:gf v2.10.2  不自动替换→cmd 注入;MySQL driver 需显式引入 contrib
- 全链路冒烟测试通过(含越权拒绝 30003)
2026-08-24 22:35:54 +08:00
夏犀麟
5cd8f0f551 refactor(api): 契约文件 index.go 重命名为 <功能名>.go
- tools/{uuid,md5,random,time,ip}/index.go → 对应 <功能名>.go(git mv 保留历史)
- 目录名=包名=文件名三一致,扩展时目录内按功能加文件(ocr.go → + idcard.go + invoice.go)
- doc.go / AGENTS.md / PROJECT_STRUCTURE.md 同步规则说明
- 冒烟测试 5 端点全部通过
2026-08-24 18:07:33 +08:00
夏犀麟
82b1ad2b5c refactor(api): 公共接口改名 open 并重构契约层为 tools/<子功能>/index.go
- api/common/v1 → api/open/v1,路由前缀 /api/common/v1 → /api/open/v1
- 契约层按子功能拆目录:api/open/v1/tools/{uuid,md5,random,time,ip}/index.go
- 控制器同步拆文件:internal/controller/open/{uuid,md5,random,time,ip}.go
- 命名规则(open 优于 common)与 tools 目录规则记入 AGENTS.md
- 冒烟测试:旧路由 404,新路由 5 端点全部通过
2026-08-24 18:03:30 +08:00
夏犀麟
1486bb903f feat(api): 新增公共接口 api/common/v1(tools 工具端点)并修复 gtime 格式化
- api/common/v1/tools.go 契约 + internal/controller/common 实现:uuid/md5/random/time/ip
- cmd.go 注册 /api/common/v1 公开分组(无鉴权),复用 common/tools Go 包
- 修复 gtime v2.10.2 坑:Format 为 PHP 风格,Go layout 须用 Layout(),timex 封装为 Layout 语义
- 冒烟测试 5 端点全部通过;AGENTS.md / PROJECT_STRUCTURE.md / change-log 同步更新
2026-08-24 17:29:30 +08:00
夏犀麟
8d5f2c65ae feat(common): 新增公共模块 common/tools 及项目上下文记忆体系
- common/tools 工具模块,10 个子功能:md5/cryptox/uuid/random/timex/convertx/strx/slicex/ip/filex(薄封装 GoFrame 内置组件)
- AGENTS.md 项目智能体说明书(架构、规范、命令、记忆体系索引)
- docs/change-log/2026-08-24.md 本次请求与变更记录
- .gitignore 排除 .workbuddy/;PROJECT_STRUCTURE.md 补充目录树
2026-08-24 17:18:10 +08:00
264 changed files with 12949 additions and 791 deletions

View File

@ -1,4 +1,9 @@
# deployed by gitea act_runner - trigger on push v5 # service.xpcool.com 自动部署Gitea act_runnerpush main/dev 触发
# 注意点与前端差异
# 1. 后端不是拷贝静态文件而是 Go 交叉编译 + Docker 镜像重建容器runner 已挂载 docker.sock
# 2. job 基础镜像 node:20-bullseye Go需下载 Go 1.24 工具链aliyun 镜像GOPROXY goproxy.cn
# 3. 容器 env 通过 Gitea Actions secrets 注入DB_DSN 必须带 mysql: 类型前缀勿写死在仓库
# 4. 数据库结构变更请手动执行 manifest/sql/ 下脚本003004004b007workflow 不做自动 DDL
name: Build and Deploy (service.xpcool.com) name: Build and Deploy (service.xpcool.com)
on: on:
@ -10,54 +15,71 @@ jobs:
build-and-deploy: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Install git & docker CLI - name: Install git & download Go toolchain
run: | run: |
set -e
# apt 换腾讯云源deb.debian.org 国内直连超时
sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g; s|security.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list 2>/dev/null || true sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g; s|security.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list 2>/dev/null || true
sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g; s|security.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g; s|security.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true
apt-get update apt-get update -qq
apt-get install -y git docker.io apt-get install -y -qq git >/dev/null
# aliyun golang 镜像实测 200 go.dev 稳定go.dev 偶发 SSL 错误 exitcode 35
for i in 1 2 3; do
curl -fsSL https://mirrors.aliyun.com/golang/go1.24.5.linux-amd64.tar.gz -o /tmp/go.tar.gz && break
echo "aliyun 下载失败,第 $i 次重试..."; sleep 5
done
mkdir -p /usr/local && tar -C /usr/local -xzf /tmp/go.tar.gz
export PATH="/usr/local/go/bin:$PATH"
echo "/usr/local/go/bin" >> "$GITHUB_ENV"
go version
- name: Checkout (local Gitea) - name: Checkout (local Gitea)
run: | run: |
git clone --depth 1 --branch "${{ github.ref_name }}" https://oauth2:${{ github.token }}@git.xpcool.com/${{ github.repository }}.git . git clone --depth 1 --branch "${{ github.ref_name }}" https://oauth2:${{ github.token }}@git.xpcool.com/${{ github.repository }}.git .
git checkout ${{ github.sha }} git checkout ${{ github.sha }}
- name: Install Go 1.23 - name: Cross compile (linux/amd64, CGO disabled)
run: |
curl -fsSL https://mirrors.aliyun.com/golang/go1.23.0.linux-amd64.tar.gz -o /tmp/go.tgz
rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tgz
/usr/local/go/bin/go version
- name: Build (linux/amd64)
env: env:
GOPROXY: https://goproxy.cn,direct GOPROXY: https://goproxy.cn,direct
GOFLAGS: -mod=mod
run: | run: |
export PATH=$PATH:/usr/local/go/bin export PATH="/usr/local/go/bin:$PATH"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o temp/linux_amd64/main . CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tmp/deploy/main main.go
ls -lh /tmp/deploy/main
- name: Docker build & deploy - name: Assemble deploy dir (binary + config + resource + Dockerfile)
run: | run: |
docker build -f manifest/docker/Dockerfile -t service.xpcool.com:latest . set -e
docker rm -f service.xpcool.com 2>/dev/null || true mkdir -p /tmp/deploy/manifest/config /tmp/deploy/resource
docker run -d \ # 配置基础 config.yaml + 环境化 config.prod.yamlGF_GCFG_ENV=prod 时生效
--name service.xpcool.com \ cp manifest/config/config.prod.yaml /tmp/deploy/manifest/config/config.prod.yaml
--restart unless-stopped \ cp manifest/config/config.prod.yaml /tmp/deploy/manifest/config/config.yaml
# 静态资源GoFrame public/template
cp -r resource/public /tmp/deploy/resource/public
mkdir -p /tmp/deploy/resource/template
# 容器 Dockerfile仓库内维护 deploy/Dockerfile避免 workflow heredoc 缩进问题
cp deploy/Dockerfile /tmp/deploy/Dockerfile
echo "assemble done"
- name: Build image & restart container
env:
DB_DSN: ${{ secrets.DB_DSN }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
run: |
set -e
cd /tmp/deploy
docker build -t service.xpcool.com:latest .
docker rm -f service.xpcool.com >/dev/null 2>&1 || true
docker run -d --name service.xpcool.com \
--network xpcool-net \ --network xpcool-net \
-p 127.0.0.1:8000:8000 \ -p 127.0.0.1:10100:10100 \
-e DB_DSN="${{ secrets.DB_DSN }}" \ -e GF_GCFG_ENV=prod \
-e JWT_SECRET="${{ secrets.JWT_SECRET }}" \ -e "DB_DSN=$DB_DSN" \
-e "JWT_SECRET=$JWT_SECRET" \
--restart unless-stopped \
service.xpcool.com:latest service.xpcool.com:latest
# 冒烟等待启动并验证 OpenAPI
- name: Notify success (PushPlus) sleep 5
if: success() curl -fsS -m 10 http://127.0.0.1:10100/api.json | head -c 120 || { echo "SMOKE TEST FAILED"; exit 1; }
run: | echo
curl -s -X POST "https://www.pushplus.plus/send" \ echo "Deployed service.xpcool.com (container restarted, 127.0.0.1:10100)"
-H "Content-Type: application/json" \
-d '{"token":"${{ secrets.PUSHPLUS_TOKEN }}","title":"✅ 部署成功 service.xpcool.com","content":"仓库: ${{ github.repository }}<br>分支: ${{ github.ref_name }}<br>提交: ${{ github.sha }}<br>触发人: ${{ github.actor }}<br>状态: 编译打包部署成功<br>访问: https://service.xpcool.com","template":"html"}'
- name: Notify failure (PushPlus)
if: failure()
run: |
curl -s -X POST "https://www.pushplus.plus/send" \
-H "Content-Type: application/json" \
-d '{"token":"${{ secrets.PUSHPLUS_TOKEN }}","title":"❌ 部署失败 service.xpcool.com","content":"仓库: ${{ github.repository }}<br>分支: ${{ github.ref_name }}<br>提交: ${{ github.sha }}<br>触发人: ${{ github.actor }}<br>状态: 构建或部署失败<br>日志: https://git.xpcool.com/${{ github.repository }}/actions","template":"html"}'

19
.gitignore vendored
View File

@ -17,3 +17,22 @@ temp/
temp.yaml temp.yaml
bin bin
**/config/config.yaml **/config/config.yaml
# WorkBuddy 本地记忆仅放行变更记录CHANGELOG.md其余本地数据不入库
.workbuddy/*
!.workbuddy/memory/
.workbuddy/memory/*
!.workbuddy/memory/CHANGELOG.md
# server runtime logs
log/
# 本地环境变量含数据库口令禁止提交
.env*
# 运行时数据目录含自动生成的 RSA 登录加密私钥禁止提交
data/
# GoLand 项目级运行配置含数据库口令禁止提交
.run/
service_test.exe
service_test2*

View File

@ -0,0 +1,57 @@
# service.xpcool.com 变更记录
> 倒序最新在上格式YYYY-MM-DD | 类型 | 摘要
2026-09-13 | DOC | 项目整理与全局中文化重写 README.MD原英文且路由陈旧 /api/v1 实际三分组 /api/service/{open,user,admin}补分层约定/RBAC 权限映射/gen dao 流程/本地启动重写 PROJECT_STRUCTURE.md原英文目录树缺 house/recruitment/notice/job/serversecurity标注手写 entity/do/dao 风险hack/hack.mk + hack-cli.mk 注释中文化common/doc.go + common/tools/doc.go 工具清单中文化ip/main.go/cmd.go 英文注释中文化**校验提示与服务层错误信息中文化**19 文件逾 90 api v:"#提示" 中文user/authtools/md5tools/randommenu_manageadminservice gerror.Wrap/response.Error 全部中文admin/adminadmin/loginsystem/{role,menu,menu_manage,login_log}user/authhouse/{community,listing,dashboard,transaction,presale}noticejobserversecurityrecruitment/{recruitment,crawler}jwt.gocontroller/admin/admin.go service panic("xxx implementation not registered")"xxx 实现未注册"api/user/login/login.go 补中文包文档生成代码entity/do/dao英文注释按约定保持原样DO NOT EDIT重生成会覆盖仅注释/文档/字符串改动无业务逻辑变更read_lints 全绿go build proxy.golang.org 网络超时未跑通详见 docs/change-log/2026-09-13.md
2026-08-27 | CHG | 登录接口从全量加密豁免永远只加密密码字段APICrypto plainRoutes /system/auth/login登录请求不整体加密handler 直接收 {username, encryptedKey, encryptedData}登录响应因无会话密钥保持明文ResolveLogin 分支顺序调整密码字段密文优先解密其次 fullBody 明文开发 allowPlain全量模式验证 5 登录明文响应/info 密文/解密成功/未加密拒绝/登出+ dev 回归 15 项全过
2026-08-27 | FEAT | 全量请求/响应加密生产 encrypt.fullBody=true / env ENCRYPT_FULL_BODYcrypto.Service fullBody 开关 + DecryptRequestRSA AES 会话密钥 + AES-GCM body返回会话密钥+ EncryptResponse用同一会话密钥 AES-GCM 加密响应回传请求结束即销毁新增 middleware/APICrypto请求整体解密io.ReadAll 直接读 r.Request.Body 勿用 GetBody否则缓存密文致 handler Parse 读到密文+ 响应加密 bufferHandlerResponse 外层Recover 内层500 也加密public-key 明文豁免未加密业务请求一律拒绝admin 组中间件链改 CORSAPICryptoRecoverHandlerResponseResolveLogin/resolvePassword fullBody 分支传输层已整体解密直接信任明文 username/passwordinjectEnv 支持 ENCRYPT_FULL_BODY/ENCRYPT_ALLOW_PLAIN直接跑二进制恒加载 config.yamlGF_GCFG_ENV gf run 必须 env 注入GF r.GetBody() 缓存密文到 bodyContenthandler Parse 走缓存 中间件必须 io.ReadAll(r.Request.Body)dev仅密码加密E2E 15 prod全量加密E2E 9 项全过
2026-08-27 | FEAT | 登录密码RSA + AES-GCM混合加密传输对称+非对称结合+ 创建/重置密码加密字段新建 internal/library/crypto密钥优先级 配置 encrypt.privateKey(PEM) > data/crypto/rsa_private.pem > 自动生成落盘data/ gitignore公钥输出 SPKIx509.MarshalPKIXPublicKey前端 WebCrypto importKey('spki')PKCS#1 ASN.1 wrong tagDecryptLogin 解密 {username,password,ts}ts 5 分钟窗口防重放DecryptField 解密 {value,ts}创建/重置密码复用进程级 SetDefault/Get 单例公开接口 POST /system/auth/public-key 返回 publicKeyLoginReq encryptedKey+encryptedData明文字段仅 encrypt.allowPlain=true 时可用config.yaml/prod 默认 falsedev truecontroller.ResolveLogin 统一解析凭据AdminCreate/AdminResetPwd 同样支持加密 passwordresolvePassword 复用 DecryptField密文结构encryptedData=base64(nonce(12B)||ciphertext||tag)encryptedKey=base64(RSA-OAEP(SHA-256) 加密 AES-256 密钥)端到端 Node 模拟 WebCrypto 15 项全过公钥/加密登录/错密码30002/篡改密文/过期载荷/受保护接口/加密创建/加密重置/旧密码失效/登出撤销/登出后刷新拒绝数据库密码本就是 bcrypt 哈希保存bcrypt.GenerateFromPassword存储安全已达标
2026-08-27 | FEAT | 认证链路联调登录/登出/超时3d24dbd未授权响应 HTTP 200+code10002 401 语义化response.JSONWithStatus三处中间件切换vben authenticateResponseInterceptor 只认 HTTP 401此前 token 过期永不触发静默刷新LogoutReq 支持 refreshToken登出撤销刷新会话IAdminAuth.Revoke 幂等Refresh 顺带清理过期会话行injectEnv 开发兜底${DB_DSN}/${RECRUITMENT_DB_DSN}/${JWT_SECRET} 占位符未注入 env 时回填本地默认 DSN任何启动方式零配置可跑当日两起登录 500 均为启动漏 envprod 不兜底端到端 8 curl 全过错密码 30002/登录/伪造 token HTTP401/轮换/旧令牌重放拒绝/登出撤销/登出后拒绝/有效访问 200并行 notice/job 会话提交的 admin_user entity bool/string 映射公司 gen 模板已按旧风格手工恢复 int/gtime 并补两列
2026-08-27 | CHG | 通知模块+自动任务+用户渠道字段016 SQLadmin_user bark_device_id/pushplus_token notice_channel/rule/log + auto_job/auto_job_log 5 菜单 98 通知中心(980规则/981渠道/982日志)/99 自动任务(990任务/991日志) + type=2 权限渠道/规则/任务种子人员管理菜单改名用户管理api/notice + api/job11 接口全 POST URL 参数service/noticeSend 统一发送Bark 路径式 POST /{key} + pushplus 官方接口渠道/规则/日志/测试service/jobDB 驱动 gcronList/Save/Trigger/LogList + Register 业务函数注册 + 完成/失败自动通知 job_done/job_failrecruitment scheduler RegisterTasks 纳管 StartScheduler 废弃用户 admin 接口支持渠道字段已部署容器重建 api.json 20011 路由全注册调度器无 handler 缺失告警种子数据验证 OK
2026-08-27 | FIX | 菜单种子主键冲突修复743683erecruitment/003_menu.sql 招聘中心一级菜单 id=95 014_house_presale_menu.sql 新房预售 id=95挂看房中心 90ON DUPLICATE 互相覆盖后导者赢本地实际发生新房预售菜单被顶掉致 /house/presale 404生产库同样风险改为 id=96 后发现 015_server_security.sql并行会话生产已部署也用 96=安全日志挂系统监控5再改 **97**95=新房预售(挂90)96=安全日志(挂5)97=招聘中心三模块错开本地按 014015003 重放routes 接口 22 节点验证全部正确e18b25f 生产库需按 014015003 顺序重放修复当前生产 95=招聘中心新房预售菜单丢失
2026-08-27 | CHG | 服务器安全日志模块015 SQL server_security_log + 菜单(96安全日志/960查询/961统计)api/serversecurity/v1 三接口全 POSTopen 上报 /api/service/open/security/log/reportbody token+list校验 internalToken=INTERNAL_TOKEN env+ admin 查询 /server-security/log/list统计 /server-security/log/stats时间/IP/类型/端口筛选+TOP攻击源entity/do/dao(主库)/dto/service/controllerReportController+ManageController 双控制器cmd.go 注入 INTERNAL_TOKENinternalTokenopen 组绑 NewReportprotected 组绑 NewManagego build 通过交叉编译上传 + 服务器 docker build + 容器重建复用 env 追加 INTERNAL_TOKEN路由/上报验证 OK错误 token 返回 10002
2026-08-27 | FIX | 本地登录 500 修复根因=config.dev.yaml ${RECRUITMENT_DB_DSN} 占位符在本地无对应 envinjectEnv 只回填已有变量gdb 配置解析失败致 g.DB() 全局报 invalid link configuration本地补建 recruitment 导入 001_init+002_seed_sources6 5 +.env.dev .run/service-dev.run.xml RECRUITMENT_DB_DSN两文件 gitignore 不入库附带修复 gcron 5 段式表达式注册失败invalid pattern "0 3 * * *" 6 段式 '0 0 3 * * *'/'0 0 8 * * *'此前线上每日抓取/推送 cron 实际从未执行f81ce64验证重启后登录返回业务码 30002非500 cron 报错
2026-08-27 | CHG | 看房新增新房预售证模块api/house/presale + controller/servicePOST /api/service/admin/house/presale/list筛选 region/purpose/keywordid 倒序dto HousePresaleVOhouse_presale publish_date 字段013 SQL 同步 + hack/config.yaml tables house_presale + gen dao 更新权限种子 014菜单 95 新房预售 + 950 查询权限go build 通过接口返回 487 条干净数据presaleNo/publishDate 正确
2026-08-27 | CHG | 看房数据扩充013_house_presale.sql 建预售证表presale_no 唯一house_community avg_price 字段010 SQL 同步 + gen dao 更新 entity/do AvgPricego build 通过
2026-08-27 | CHG | 服务器日志操作审计 admin_operation_log全面增强为综合分页列表 新增字段 admin_username(冗余账号)ip_location(IP归属地)error_message(失败原因)user_agent 自建纯 Go xdb v4 离线解析器 internal/library/iplocgo:embed 嵌入 ip2region.xdb无外部依赖单次内存二分审计写入时解析账号+归属地记录 UA 与错误摘要 查询筛选覆盖时间范围/账号/IP/归属地/HTTP方法/结果(成功2xx·失败非2xx)/关键字(权限码·路径·IP)/耗时上下限/排序(时间·耗时) 接口 POST /api/service/admin/system/logbody 入参遵守全 POST 无参约定dto.LogQuery/LogItem 同步 SQL001_core.sql 建表补列 + 新增 010_server_log_enhance.sql 增量迁移须手动导库DB 结构变更不自动 DDLgo build 通过iploc 单元冒烟通过
2026-08-27 | FIX | 看房中心接口异常修复根因=后端进程是旧代码recruitment 模块中途编译错误致项目编译不过后端停在旧进程未加载 house 路由重启后端加载新代码附带修复 house listing/transaction List 方法 Fields Count 前设置导致 COUNT(t.*,c.name) SQL 语法错误Fields 移到 Scan 7 个看房接口全部 code 0 验证通过
2026-08-27 | CFG | 新增 013_workbench_menu.sql工作台顶层菜单id=9, parent_id=0, path=/workbench, component=dashboard/workbench/index, permission=workbench, sort=0 置顶+ 超管 role_id=1 绑定前端工作台页面复用既有列表接口聚合无新增 Go 接口
2026-08-27 | CHG | 看房成交模块api/house/transaction + service/house/transaction + controller/house/transaction 实现成交记录 list 接口/api/service/admin/house/transaction/list POST 分页关联小区名按成交日期倒序dto HouseTransactionVOcmd.go 注册012_house_transaction_menu.sql 权限种子house:data:transaction id=941 挂数据明细 94go build 通过
2026-08-26 | CFG | 新增 Gitea act_runner 自动部署.gitea/workflows/deploy.ymlpush main/dev 下载 Go1.23 工具链 npmmirror + GOPROXY goproxy.cn CGO=0 linux/amd64 交叉编译 组装 main+manifest/config(config.yaml+config.prod.yaml)+resource+deploy/Dockerfile docker build 重建容器 127.0.0.1:10100xpcool-netGF_GCFG_ENV=prodDB_DSN/JWT_SECRET Gitea Actions secrets curl /api.json 冒烟deploy/Dockerfile 仓库内维护DB 结构变更不自动 DDL需手动导 manifest/sql
2026-08-26 | FIX | 本地登录 no rows 修复根因=本地库 service_xpcool_com admin_user 空表本地/服务器库两套此前仅改服务器 super_admin 角色 + 导入 009_admin_account_v2.sql --default-character-set=utf8mb4 否则中文 Data too long 本地 xxcool/xxCool@2026 登录成功subject=3同时服务器废弃 webhook 方案已删除deploy-webhook.service/webhook.py/secrets.env/deploy.sh自动部署改走 Gitea act_runner + .gitea/workflows仓库内尚未配置待补
2026-08-26 | CHG | 看房 house 模块后端落地010_house_tables.sql 9 community/building/listing/price_snapshot/transaction/facility/community_facility/school_district/preference+ 011_house_menu.sql 菜单/权限种子超管 role_id=1 绑定gf gen dao 生成 dao/entity/do实现 api/house/{community,listing,dashboard} + controller/house + service/house管理 CRUD + 看板聚合 POST 动作式前缀 /api/service/admin/house含多平台软关联 match_group_id笋盘/低可信标记cmd.go 注册 housectl + 3 RegisterServicego build 通过gf CLI com.lib.gf.v2 分支gen dao 产物 import sed github.com/gogf/gf/v218 文件mysql 客户端须 --default-character-set=utf8mb4 否则中文 COMMENT/INSERT 乱码TINYINT 字段 comment 0/1 gf 映射 boolstatus 三值改 INThack/config.yaml link+tables 已指向本地库
2026-08-26 | CHG | 生产超级管理员账号替换删除 admin/admin123用户/绑定/refresh 会话全清新增 xxcool/xxCool@2026bcrypt $2b$10$绑定 super_admin脚本 manifest/sql/009_admin_account_v2.sql008 编号已被 008_menu_rbac_v4.sql 占用004_seed.sql 种子账号同步改 xxcool登录验证xxcool 成功admin no rows预期 线上前端仍是旧版默认 admin/admin123需部署新版 dist xxcool 才可正常登录
2026-08-26 | CHG | RBAC 菜单调整移除管理员管理id=2 及按钮 21-25 软删+解绑新增日志管理菜单id=7, system:log, component=system/log/index原按钮 63system:log:list挂到 id=7 超管绑定新菜单幂等脚本 manifest/sql/008_menu_rbac_v4.sql
2026-08-26 | FIX | RBAC 安全漏洞修复禁用角色status=0绑定的权限仍生效根因为 Codes/HasPermission/Routes 三处联表查询未过滤 admin_role 启用状态 roleCodes 过滤修复三处统一 LeftJoin admin_role 并加 r.status=1opuser 联调验证 codes /routes /接口 403admin 全链路回归通过
2026-08-26 | REQ | 看房方案补充后台管理 + 可视化看板两层数据管理用 vben 标准列表 CRUD查询表单 + vxe-table + 分页 + 批量页面含小区/房源/快照/成交/配套学区/采集任务可视化看板用全局筛选器驱动图表联动筛选器图表图表交叉过滤列表地图双向状态放 PiniaGo 接口分管理 CRUD 与看板聚合两类统一 FilterDto 入参供管理与看板共用echarts/vxe-table/RBAC 均复用现有依赖
2026-08-26 | REQ | 看房方案迭代推送改 Bark苹果自建 Server 到腾讯云/或官方免费版 level/group/url/badge 参数可视化加贵阳区域地图底图腾讯地图 GL JS区县边界 DataV 审图号 GeoJSON叠加地铁线/学区划片/楼栋点/价格热力楼栋坐标三级落地小区中心自动楼栋图解析重点盘人工校准多平台同房源不去重软关联对比推翻去重source+source_house_id 同平台唯一match_group_id 跨平台软关联补充笋盘识别/挂牌天数+调价历史/租金回报率/可负担性硬过滤/面积建面套内标准化/贵阳区域画像(观山湖新中心地铁1/2/3号线山地通勤)/快照长期保留1-2
2026-08-26 | REQ | 规划看房功能模块贵阳购房决策辅助仅规划设计未开发架构定为 service 新增 house 业务模块复用 gf gen dao + MySQL数据模型 9 张表 community/listing/price_snapshot/transaction/facility/community_facility/school_district/preference/scoreadmin 新增看房页面echarts@6 已内置另立独立 Python 采集分析服务采集/清洗/去重/打分只写库不对外 API决策目标城市贵阳新房+二手房都做先本地后上云核心风险为贵阳网签/成交数据公开程度不如一线
2026-08-26 | DEP | 生产部署上线193.112.118.168交叉编译 linux/amd64重建容器 service.xpcool.com:new127.0.0.1:10100GF_GCFG_ENV=prodDB_DSN mysql: 前缀沿用 JWT_SECRETnginx HTTPS443+80301证书 /data/nginx/ssl/service.xpcool.com/反代 10100DB service 库补种 003004004b007权限映射对齐 /api/service 路由admin/admin123 登录验证通过
2026-08-26 | CFG | 新增 manifest/sql/007_menu_permissions_v3.sqltype=2 权限路径映射最新 /api/service POST 路由 system:log:list id=63 004b_fill_seed.sql004 中断后的幂等补齐脚本部署要点容器内需 config.yaml基础配置否则 GoFrame 启动报找不到 config
2026-08-26 | FIX | RBAC 安全漏洞修复禁用角色status=0绑定的权限仍生效根因为 Codes/HasPermission/Routes 三处联表查询未过滤 admin_role 启用状态 roleCodes 过滤修复三处统一 LeftJoin admin_role 并加 r.status=1opuser 联调验证 codes /routes /接口 403admin 全链路回归通过
2026-08-26 | CFG | 修复 GoLand 直接 Run 找不到 config.yaml新增项目级运行配置 .run/service-dev.run.xml内置 GF_GCFG_FILE=config.dev.yaml + DB_DSN + JWT_SECRET 三环境变量.run/ 加入 .gitignore含口令README Local startup小节终端 .env.dev / GoLand 运行配置两种方式
2026-08-26 | FIX | 前端登录 502 联调根因为服务未启动10100 无监听 .env.dev 启动后 login/info/codes/menu-routes 全链路 curl 200dev server20100/20101/20102/20103代理全部恢复
2026-08-26 | CFG | CHANGELOG 纳入 git 管理.gitignore 放行 .workbuddy/memory/CHANGELOG.md中央工作空间迁移至 ../workbuddy.xpcool.com相对路径索引
2026-08-26 | CHG | 统一 API 前缀 /api/service/admin/api/service/adminuser/api/service/useropen/api/service/open服务端口统一 10100dev/prod/testDB admin_menu 权限映射前缀同步前端 vite 代理与 api 文件同步适配 POST + 动作式路径路由回归 29 条一致
2026-08-26 | CHG | 遗留 /v1 全移除URL 前缀 /admin/v1/admin/api/v1/api/api/open/v1/api/open全部接口改 POST5 CRUD 冲突路径改动作式 /list /create /update/{id} DB admin_menu 权限映射 path 同步全项目英文注释转中文 dao/entity/do/table 生成模板路由回归 29 条全 POST 一致
2026-08-26 | FIX | 修复审计落库失败admin_operation_log.request_param JSON 仅合法 JSON 入库其余存 NULL此前一直失败被尽力而为吞掉系统日志查不到数据
2026-08-26 | CHG | 区分系统日志与服务器日志审计服务由 admin/system/audit 归位为 admin/system/logpackage admin_system_log Record+List新增 GET /admin/v1/system/log 分页查询 + system:log:list 权限映射菜单 id=63服务器日志保持 admin/base/log
2026-08-26 | CHG | login 资源迁移至 admin/admin/loginapi+service api/service 包名统一为路径下划线拼接package admin_admin_login / admin_system_menu / user_auth 恢复 api/open 并规范化 v1包名 open_tools_ip controller/cmd 引用同步路由回归 28 条一致
2026-08-26 | REQ | 后台登录接口联调通过login/info/codes/menu-routes/RBAC/refresh(单次)/logout 全链路 curl 验证 OK service_xpcool_com
2026-08-26 | FIX | 联调发现并修复 admin refresh 单次使用漏洞Refresh 撤销旧会话后补 RowsAffected 校验重复使用旧 refreshToken 返回 code 10002
2026-08-26 | CFG | 新建 .env.dev 本地配置DB_DSN=mysql:root:root123@tcp(127.0.0.1:3306)/service_xpcool_com?loc=Local + JWT_SECRET.gitignore 追加 .env* 防口令入库
2026-08-26 | CHG | 清理模板删除 api/helloapi/opencommon/tools controller/hellocontroller/openAPI 目录去掉 v1 api/admin/system/auth/auth.go service 层完全镜像URL 前缀保持 /admin/v1 兼容前端
2026-08-26 | CHG | 目录重构API 层资源目录化api/admin/v1/system/auth/auth.go service 层严格镜像 API 多级结构internal/service/admin/system/auth/auth.go 接口随实现归位admin_rbac 拆为 admin/role/menu_manage 三包controller/cmd 引用同步路由回归 29 条一致
2026-08-26 | CHG | 补全管理端登录接口新增 Refresh/Logout 端点/admin/v1/system/auth/refresh|logout登录改走 issue 把刷新令牌 JTI 落库 user 端刷新撤旧机制一致
2026-08-26 | CHG | 同步三铁律中文注释/做记录/中文优先 AGENTS.md统一工作约定小节
2026-08-26 | CFG | 建立统一变更记录机制CHANGELOG接入 xpcool.com 中央索引

126
AGENTS.md Normal file
View File

@ -0,0 +1,126 @@
# AGENTS.md 项目智能体说明书
> 本文件是给所有 AI 编程助手CodeBuddy / WorkBuddy / Claude Code / Codex / Cursor 看的项目级上下文
> 任何账号 clone 本仓库后助手都应先读本文件与 `docs/change-log/` 下最近的记录即可无缝衔接
> 请保持本文件**长期稳定**只写架构规范约定不要写一次性事项
## 项目简介
`service.xpcool.com`个人多客户端mini / h5 / app后端服务GoFrame v2 单体应用
核心业务用户认证JWT内容收藏站内消息后台管理RBAC + 操作审计
## 技术栈
| | |
|---|---|
| 语言 | Go 1.23.0 |
| 框架 | github.com/gogf/gf/v2 v2.10.2 |
| 数据库 | MySQLORM gf 生成 dao/do/entity |
| 认证 | JWT`internal/library/jwt`admin 另有 `X-Permission` 校验 |
## 目录结构
```
service.xpcool.com/
api/ # HTTP 契约与 Swagger 元数据
user/v1/ # /api/v1 用户端 API登录公开其余走 UserAuth
admin/v1/ # /admin/v1 管理端 APIAdminAuth + X-Permission
open/v1/ # /api/open/v1 开放接口给前端调用公开无鉴权
tools/ # 工具子功能契约每子功能一目录tools/<name>/<name>.go
common/ # 公共可复用模块不依赖 internal可独立抽取成库
tools/ # 工具模块按子功能分包见下文
internal/
cmd/ # 启动引导路由分组注册在此
consts/ # 错误码与常量
controller/ # APIservice 适配层不写业务 open/ 开放接口实现
service/ # 领域用例业务逻辑直接写这里不用 logic/
dao/ # gf gen dao 生成禁止手改
model/ # entity/ do/ dto/ vo/entitydo 生成禁止手改
middleware/ # 路由中间件
library/ # jwt / page / response 等内部基础件
table/ # 表列名常量
manifest/ # config.dev/test/prod.yamlsql 迁移
docs/change-log/ # 每次请求与变更的记录重要上下文记忆
utility/ # 预留跨切面辅助
```
## 后台管理 APIapi/admin/v1 base/system/admin 分组
- **分组规则**`api/admin/v1/{base,system,admin}/` 三个子包路由前缀 `/admin/v1/{base,system,admin}`
- **base**基础常规`/base/log/*`服务器日志监控
- **system**系统管理`/system/auth/*`登录/信息/权限码`/system/menu/*`菜单路由+CRUD`/system/role/*`角色 CRUD
- **admin**后台管理`/admin`管理员账号 CRUD
- 三层路由隔离`internal/cmd/cmd.go`
- **公开**无鉴权`POST /system/auth/login`
- **仅登录** `AdminAuthOnly``GET /system/auth/info``GET /system/auth/codes``GET /system/menu/routes`
- **接口级鉴权** `AdminAuth`RBAC 管理日志等
- **接口鉴权机制重要**中间件按请求方法+路径 `admin_menu`type=2 path `"METHOD /路径"``{id}` 为动态段反查所需权限码再校验用户是否拥有**前端无需传 X-Permission**未配置映射的接口一律拒绝
- 权限码permission与路由分离权限码保持 `system:admin:list` 等逻辑标识路由路径按 base/system/admin 分组
- 新增受保护接口三步 `api/admin/v1/{分组}/<xxx>.go` Req/Res `internal/controller/admin/<xxx>.go` 加方法 `admin_menu` type=2 `permission` 填权限码`path` `"METHOD /路径"` 映射
- 迁移脚本`003_schema_ext.sql`admin_menu 加列`004_seed.sql`初始账号/角色/菜单`005_menu_paths.sql``006_menu_paths_v2.sql`按钮-接口路径映射006 为分组重构后
## 开放接口api/open/v1前端调用与命名规则
- **命名决策**公共接口前缀用 **open**不用 common理由`common` 语义偏"内部公共代码"`open` 是开放接口业界惯例支付宝 /open/api 更能表达"对外暴露、无鉴权"同属"公开"语义的备选还有 `public`
- 路由前缀 `/api/open/v1`**公开无鉴权**实现于 `internal/controller/open`
- **tools 子功能目录规则**`api/open/v1/tools/<子功能名>/<子功能名>.go` 定义该子功能的 Req/Res 契约目录名=包名=文件名三一致控制器 `internal/controller/open/<子功能名>.go` 放对应方法controller 统一 `package open`子功能变大后按端点/子领域在目录内**加文件** ocr 目录下 `ocr.go` `ocr.go + idcard.go + invoice.go`不要堆在一个文件里
- 当前端点`GET/POST /tools/*`uuidmd5randomtimeip底层复用 `common/tools` Go
- **新增子功能三步** `api/open/v1/tools/<name>/<name>.go` Req/Resg.Meta path/method `internal/controller/open/<name>.go` 加方法 路由自动绑定无需改 cmd.go
## 公共工具模块 common/tools
- 规则**不依赖 internal/**只薄封装 GoFrame 内置组件新工具优先复用内置gmd5/gaes/gdes/guid/grand/gtime/gconv/gstr/gfile...避免重复造轮子
- 子功能`md5``cryptox`(AES/DES)`uuid``random``timex``convertx``strx``slicex``ip``filex`
- 新增子功能 `common/tools/` 下建子包更新 `common/tools/doc.go` 的布局清单
## 分层与调用规范必须遵守
1. 调用链`controller → service → dao → model(do)`controller 不碰 dao
2. DTO/VO 边界跨层出入参走 `internal/model/dto` `internal/model/vo`API 类型与 entity 不得越界
3. **数据库操作必须用 DO 对象**`internal/model/do`禁止 `g.Map`未赋值字段保持 nil 自动忽略
```go
dao.Users.Ctx(ctx).Where(cols.Id, id).Data(do.User{Uid: uid}).Update()
```
4. **时间字段自动维护**`created_at/updated_at/deleted_at` ORM 自动处理禁止手动赋值软删除用 `Delete()`禁止手写 `WhereNull(cols.DeletedAt)`
5. **错误处理一律用 gerror**保留堆栈响应统一走 `internal/library/response`
6. 生成代码dao/do/entity**禁止手改**改表后跑 `gf gen dao` 重新生成
7. 声明 3 个相关变量时 `var (...)` 块对齐
## 常用命令
```bash
# 运行dev
GF_GCFG_FILE=config.dev.yaml DB_DSN="user:pass@tcp(127.0.0.1:3306)/db?loc=Local" JWT_SECRET=xxx go run main.go
# 数据库模型生成唯一来源
gf gen dao -p internal -g default -gt -c
# 构建 / 测试
go build ./...
go test ./...
```
## 上下文记忆重要
三层配合保证换个账号/换台机器也能无缝衔接
1. **本文件AGENTS.md**长期稳定的架构与规范
2. **`docs/change-log/YYYY-MM-DD.md`**每次对话的请求 + 变更 + 决策记录 git 提交
- 每次完成任务后 `docs/change-log/` 已有当日文件则**追加**否则新建
- 格式固定`## 请求` / `## 变更`含文件清单/ `## 决策与理由` / `## 待办与风险`
- 助手开工前先读最近 1-2 快速恢复上下文
3. **`.workbuddy/memory/`**WorkBuddy 桌面端本机记忆每日日志 + MEMORY.md**已加入 .gitignore不入库**仅本机增强
## 注意事项
- **gtime v2.10.2 格式化**`Time.Format("Y-m-d H:i:s")` PHP 风格 Go layout`2006-01-02`要用 `Time.Layout(...)`工具包 `common/tools/timex` 已统一封装
- **gf v2.10.2 包名与旧版不同**AES/DES `crypto/gaes``crypto/gdes` gcryptoUUID `util/guid` guuid gslicer用标准库 slices
- 配置文件按 `GF_GCFG_FILE` 切换`manifest/config/config.yaml` 不入库
- 生产环境强密码`JWT_SECRET`数据库口令
- 变更涉及 API 时同步更新 `api/` 下的 Swagger 元数据注释
## 统一工作约定xpcool.com 中央规则
> 与中央 `../workbuddy.xpcool.com/.workbuddy/memory/MEMORY.md` 保持一致本项目变更流水在 `.workbuddy/memory/CHANGELOG.md`
1. **中文注释**/改代码时在应有处函数复杂逻辑配置项非显然分支加中文注释
2. **做记录**每次请求/变更/修复/文档动作都在本项目 `.workbuddy/memory/CHANGELOG.md` 顶部追加一条格式 `YYYY-MM-DD | 类型 | 一句话摘要`类型REQ/CHG/FIX/DOC/CFG/DEP
3. **中文优先**与用户的思考输出交流能中文尽量中文

View File

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

View File

@ -1,22 +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`
- **终端**`source .env.dev && go run main.go`
`.env.dev` 不入库保存本地 DSN 与密钥
- **GoLand**运行配置 `.run/service-dev.run.xml`项目级重载项目后自动识别
已注入 `GF_GCFG_FILE` / `DB_DSN` / `JWT_SECRET`在运行下拉框中选择即可
该文件含数据库口令已加入 `.gitignore`
开发环境下 `${DB_DSN}` / `${RECRUITMENT_DB_DSN}` / `${JWT_SECRET}` 占位符未被环境变量注入
`injectEnv` 会回填本地默认值保证零配置可启动生产`GF_GCFG_ENV=prod`不兜底配置缺失显性报错
## 构建与测试
```powershell
go build ./...
go test ./...
```

View File

@ -0,0 +1,88 @@
// Package admin_admin_admin 定义后台管理接口(管理员账号管理),路由前缀 /admin。
package admin_admin_admin
import "github.com/gogf/gf/v2/frame/g"
// AdminItem 管理员列表中的一行。
type AdminItem struct {
Id uint64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Status int `json:"status"`
BarkDeviceId string `json:"barkDeviceId"`
PushplusToken string `json:"pushplusToken"`
RoleIds []uint64 `json:"roleIds"`
RoleNames []string `json:"roleNames"`
CreatedAt string `json:"createdAt"`
}
// AdminListReq 分页查询管理员。
type AdminListReq struct {
g.Meta `path:"/admin/list" method:"post" tags:"Admin/Admin" summary:"管理员列表"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
Keyword string `json:"keyword"` // 匹配用户名或昵称
}
// AdminListRes 是 AdminListReq 的响应。
type AdminListRes struct {
List []*AdminItem `json:"list"`
Total int `json:"total"`
}
// AdminCreateReq 创建管理员。
// 密码采用「RSA + AES-GCM」混合加密传输encryptedKey/encryptedData 必填(生产强制),
// password 明文字段仅当服务端 encrypt.allowPlain=true本地联调时可用。
type AdminCreateReq struct {
g.Meta `path:"/admin/create" method:"post" tags:"Admin/Admin" summary:"创建管理员"`
Username string `json:"username" v:"required"`
Password string `json:"password" v:"min-length:6#密码长度不能少于 6 位"`
EncryptedKey string `json:"encryptedKey"`
EncryptedData string `json:"encryptedData"`
Nickname string `json:"nickname"`
BarkDeviceId string `json:"barkDeviceId"`
PushplusToken string `json:"pushplusToken"`
RoleIds []uint64 `json:"roleIds"`
}
// AdminCreateRes 是 AdminCreateReq 的响应。
type AdminCreateRes struct {
Id uint64 `json:"id"`
}
// AdminUpdateReq 更新管理员的资料、状态与角色。
type AdminUpdateReq struct {
g.Meta `path:"/admin/update/{id}" method:"post" tags:"Admin/Admin" summary:"更新管理员"`
Id uint64 `json:"id" in:"path" v:"required"`
Nickname string `json:"nickname"`
Status int `json:"status"`
BarkDeviceId string `json:"barkDeviceId"`
PushplusToken string `json:"pushplusToken"`
RoleIds []uint64 `json:"roleIds"`
}
// AdminUpdateRes 是 AdminUpdateReq 的响应。
type AdminUpdateRes struct{}
// AdminResetPwdReq 重置管理员密码。
// 密码采用「RSA + AES-GCM」混合加密传输encryptedKey/encryptedData 必填(生产强制),
// password 明文字段仅当服务端 encrypt.allowPlain=true本地联调时可用。
type AdminResetPwdReq struct {
g.Meta `path:"/admin/resetPwd/{id}" method:"post" tags:"Admin/Admin" summary:"重置管理员密码"`
Id uint64 `json:"id" in:"path" v:"required"`
Password string `json:"password" v:"min-length:6#密码长度不能少于 6 位"`
EncryptedKey string `json:"encryptedKey"`
EncryptedData string `json:"encryptedData"`
}
// AdminResetPwdRes 是 AdminResetPwdReq 的响应。
type AdminResetPwdRes struct{}
// AdminDeleteReq 删除管理员(软删除)。
type AdminDeleteReq struct {
g.Meta `path:"/admin/delete/{id}" method:"post" tags:"Admin/Admin" summary:"删除管理员"`
Id uint64 `json:"id" in:"path" v:"required"`
}
// AdminDeleteRes 是 AdminDeleteReq 的响应。
type AdminDeleteRes struct{}

View File

@ -0,0 +1,79 @@
// Package admin_admin_login 定义管理端认证接口(登录/资料/权限码/刷新/登出),路由前缀 /admin。
package admin_admin_login
import "github.com/gogf/gf/v2/frame/g"
// LoginReq 管理员登录请求。
// 密码采用「RSA + AES-GCM」混合加密传输encryptedKey/encryptedData 必填,生产强制):
// - encryptedKeyRSA-OAEP 加密的 AES-256 会话密钥base64
// - encryptedDataAES-GCM 加密的明文载荷 {username,password,ts}base64nonce 前缀)
//
// username/password 明文字段仅当服务端 encrypt.allowPlain=true本地联调时可用。
type LoginReq struct {
g.Meta `path:"/system/auth/login" method:"post" tags:"Admin/System/Auth" summary:"管理员登录"`
EncryptedKey string `json:"encryptedKey"`
EncryptedData string `json:"encryptedData"`
Username string `json:"username"`
Password string `json:"password"`
}
// LoginRes 管理员登录响应(令牌对 + 管理员 ID
type LoginRes struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresIn int64 `json:"expiresIn"`
AdminID uint64 `json:"adminId"`
}
// PublicKeyReq 获取登录加密公钥RSA 公钥 PEM前端用于混合加密密码。公开接口免鉴权。
type PublicKeyReq struct {
g.Meta `path:"/system/auth/public-key" method:"post" tags:"Admin/System/Auth" summary:"获取登录加密公钥"`
}
// PublicKeyRes 是 PublicKeyReq 的响应。
type PublicKeyRes struct {
// PublicKey PEM 格式 RSA 公钥(-----BEGIN PUBLIC KEY-----)。
PublicKey string `json:"publicKey"`
}
// InfoReq 获取当前管理员资料(供 vben getUserInfo 使用)。
type InfoReq struct {
g.Meta `path:"/system/auth/info" method:"post" tags:"Admin/System/Auth" summary:"当前管理员资料"`
}
// InfoRes 是 InfoReq 的响应Roles 携带角色码供 vben 权限使用。
type InfoRes struct {
AdminID uint64 `json:"adminId"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Roles []string `json:"roles"`
}
// CodesReq 获取当前管理员的按钮级权限码vben getAccessCodes后端鉴权模式
type CodesReq struct {
g.Meta `path:"/system/auth/codes" method:"post" tags:"Admin/System/Auth" summary:"当前管理员权限码"`
}
// CodesRes 是 CodesReq 的响应。
type CodesRes struct {
Codes []string `json:"codes"`
}
// RefreshReq 使用刷新令牌轮换管理员令牌对。
type RefreshReq struct {
g.Meta `path:"/system/auth/refresh" method:"post" tags:"Admin/System/Auth" summary:"轮换管理员令牌"`
RefreshToken string `json:"refreshToken" v:"required"`
}
// RefreshRes 是 RefreshReq 的响应。
type RefreshRes LoginRes
// LogoutReq 结束管理员会话。refreshToken 可选携带:
// 传入时撤销对应刷新会话,阻止该设备继续续期(幂等)。
type LogoutReq struct {
g.Meta `path:"/system/auth/logout" method:"post" tags:"Admin/System/Auth" summary:"管理员登出"`
RefreshToken string `json:"refreshToken"`
}
// LogoutRes 是 LogoutReq 的响应。
type LogoutRes struct{}

View File

@ -0,0 +1,30 @@
// Package admin_system_login_log 定义管理员登录日志查询接口,路由前缀 /admin。
package admin_system_login_log
import "github.com/gogf/gf/v2/frame/g"
// LoginLogItem 一条管理员登录日志记录。
type LoginLogItem struct {
Id uint64 `json:"id"`
Username string `json:"username"`
IP string `json:"ip"`
UserAgent string `json:"userAgent"`
Status int `json:"status"` // 1 成功, 0 失败
FailReason string `json:"failReason"`
CreatedAt string `json:"createdAt"`
}
// LoginLogListReq 分页查询管理员登录日志。
type LoginLogListReq struct {
g.Meta `path:"/system/login-log" method:"post" tags:"Admin/System/LoginLog" summary:"登录日志列表"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
Username string `json:"username"` // 按账号过滤
Status *int `json:"status"` // 按结果过滤1 成功 0 失败,不传为全部
}
// LoginLogListRes 是 LoginLogListReq 的响应。
type LoginLogListRes struct {
List []*LoginLogItem `json:"list"`
Total int `json:"total"`
}

View File

@ -0,0 +1,33 @@
// Package admin_system_menu 定义菜单路由接口(当前管理员可见菜单树),路由前缀 /admin。
package admin_system_menu
import "github.com/gogf/gf/v2/frame/g"
// RouteMeta 与 vben 后台动态路由元数据对应。
type RouteMeta struct {
Title string `json:"title"`
Icon string `json:"icon"`
Order int `json:"order"`
Authority []string `json:"authority,omitempty"` // 角色码
HideInMenu bool `json:"hideInMenu,omitempty"`
KeepAlive bool `json:"keepAlive,omitempty"`
}
// RouteItem vben 路由树中的一个节点。
type RouteItem struct {
Name string `json:"name"`
Path string `json:"path"`
Component string `json:"component,omitempty"`
Meta RouteMeta `json:"meta"`
Children []*RouteItem `json:"children,omitempty"`
}
// MenuRoutesReq 获取当前管理员的菜单树(后端鉴权模式,登录后即可访问)。
type MenuRoutesReq struct {
g.Meta `path:"/system/menu/routes" method:"post" tags:"Admin/System/Menu" summary:"当前管理员菜单路由"`
}
// MenuRoutesRes 是 MenuRoutesReq 的响应。
type MenuRoutesRes struct {
Routes []*RouteItem `json:"routes"`
}

View File

@ -0,0 +1,78 @@
// Package admin_system_menu_manage 定义菜单管理接口(菜单 + 按钮权限树),路由前缀 /admin。
package admin_system_menu_manage
import "github.com/gogf/gf/v2/frame/g"
// MenuItem 完整菜单树中的一个节点type 1 菜单 / 2 按钮)。
type MenuItem struct {
Id uint64 `json:"id"`
ParentId uint64 `json:"parentId"`
Name string `json:"name"`
Icon string `json:"icon"`
Type int `json:"type"` // 1 菜单, 2 按钮/接口
Path string `json:"path"`
Component string `json:"component"`
Permission string `json:"permission"`
Sort int `json:"sort"`
Status int `json:"status"`
Hidden bool `json:"hidden"`
Children []*MenuItem `json:"children,omitempty"`
}
// MenuTreeReq 获取完整菜单树(供管理)。
type MenuTreeReq struct {
g.Meta `path:"/system/menu/tree" method:"post" tags:"Admin/System/Menu" summary:"完整菜单树"`
}
// MenuTreeRes 是 MenuTreeReq 的响应。
type MenuTreeRes struct {
Tree []*MenuItem `json:"tree"`
}
// MenuCreateReq 创建菜单或按钮节点。
type MenuCreateReq struct {
g.Meta `path:"/system/menu/create" method:"post" tags:"Admin/System/Menu" summary:"创建菜单"`
ParentId uint64 `json:"parentId"`
Name string `json:"name" v:"required"`
Icon string `json:"icon"`
Type int `json:"type" v:"in:1,2#类型必须为 1 或 2"`
Path string `json:"path"`
Component string `json:"component"`
Permission string `json:"permission" v:"required"`
Sort int `json:"sort"`
Status int `json:"status"`
Hidden bool `json:"hidden"`
}
// MenuCreateRes 是 MenuCreateReq 的响应。
type MenuCreateRes struct {
Id uint64 `json:"id"`
}
// MenuUpdateReq 更新菜单或按钮节点。
type MenuUpdateReq struct {
g.Meta `path:"/system/menu/update/{id}" method:"post" tags:"Admin/System/Menu" summary:"更新菜单"`
Id uint64 `json:"id" in:"path" v:"required"`
ParentId uint64 `json:"parentId"`
Name string `json:"name" v:"required"`
Icon string `json:"icon"`
Type int `json:"type" v:"in:1,2#类型必须为 1 或 2"`
Path string `json:"path"`
Component string `json:"component"`
Permission string `json:"permission" v:"required"`
Sort int `json:"sort"`
Status int `json:"status"`
Hidden bool `json:"hidden"`
}
// MenuUpdateRes 是 MenuUpdateReq 的响应。
type MenuUpdateRes struct{}
// MenuDeleteReq 删除菜单节点(软删除)。
type MenuDeleteReq struct {
g.Meta `path:"/system/menu/delete/{id}" method:"post" tags:"Admin/System/Menu" summary:"删除菜单"`
Id uint64 `json:"id" in:"path" v:"required"`
}
// MenuDeleteRes 是 MenuDeleteReq 的响应。
type MenuDeleteRes struct{}

View File

@ -0,0 +1,63 @@
// Package admin_system_role 定义角色管理接口(角色及其菜单绑定),路由前缀 /admin。
package admin_system_role
import "github.com/gogf/gf/v2/frame/g"
// RoleItem 角色列表中的一行。
type RoleItem struct {
Id uint64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Status int `json:"status"`
MenuIds []uint64 `json:"menuIds"` // 已绑定的菜单 id编辑用
CreatedAt string `json:"createdAt"`
}
// RoleListReq 分页查询角色。
type RoleListReq struct {
g.Meta `path:"/system/role/list" method:"post" tags:"Admin/System/Role" summary:"角色列表"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
Keyword string `json:"keyword"` // 匹配角色码或名称
}
// RoleListRes 是 RoleListReq 的响应。
type RoleListRes struct {
List []*RoleItem `json:"list"`
Total int `json:"total"`
}
// RoleCreateReq 创建角色并绑定菜单。
type RoleCreateReq struct {
g.Meta `path:"/system/role/create" method:"post" tags:"Admin/System/Role" summary:"创建角色"`
Code string `json:"code" v:"required"`
Name string `json:"name" v:"required"`
Status int `json:"status"`
MenuIds []uint64 `json:"menuIds"`
}
// RoleCreateRes 是 RoleCreateReq 的响应。
type RoleCreateRes struct {
Id uint64 `json:"id"`
}
// RoleUpdateReq 更新角色并重新绑定菜单。
type RoleUpdateReq struct {
g.Meta `path:"/system/role/update/{id}" method:"post" tags:"Admin/System/Role" summary:"更新角色"`
Id uint64 `json:"id" in:"path" v:"required"`
Name string `json:"name" v:"required"`
Status int `json:"status"`
MenuIds []uint64 `json:"menuIds"`
}
// RoleUpdateRes 是 RoleUpdateReq 的响应。
type RoleUpdateRes struct{}
// RoleDeleteReq 删除角色(软删除)。
type RoleDeleteReq struct {
g.Meta `path:"/system/role/delete/{id}" method:"post" tags:"Admin/System/Role" summary:"删除角色"`
Id uint64 `json:"id" in:"path" v:"required"`
}
// RoleDeleteRes 是 RoleDeleteReq 的响应。
type RoleDeleteRes struct{}

View File

@ -1,15 +0,0 @@
package v1
import "github.com/gogf/gf/v2/frame/g"
type LoginReq struct {
g.Meta `path:"/auth/login" method:"post" tags:"Admin/Auth" summary:"Administrator login"`
Username string `json:"username" v:"required"`
Password string `json:"password" v:"required"`
}
type LoginRes struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresIn int64 `json:"expiresIn"`
AdminID uint64 `json:"adminId"`
}

View File

@ -1,15 +0,0 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package hello
import (
"context"
"service.xpcool.com/api/hello/v1"
)
type IHelloV1 interface {
Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error)
}

View File

@ -1,12 +0,0 @@
package v1
import (
"github.com/gogf/gf/v2/frame/g"
)
type HelloReq struct {
g.Meta `path:"/hello" tags:"Hello" method:"get" summary:"You first hello api"`
}
type HelloRes struct {
g.Meta `mime:"text/html" example:"string"`
}

View File

@ -0,0 +1,93 @@
// Package house_community 定义楼盘/小区管理接口,路由前缀 /house。
package house_community
import "github.com/gogf/gf/v2/frame/g"
// CommunityItem 小区列表中的一行。
type CommunityItem struct {
Id uint64 `json:"id"`
Name string `json:"name"`
Region string `json:"region"`
BusinessDistrict string `json:"businessDistrict"`
Address string `json:"address"`
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
BuildYear int `json:"buildYear"`
Households int `json:"households"`
PlotRatio float64 `json:"plotRatio"`
GreenRate float64 `json:"greenRate"`
PropertyCompany string `json:"propertyCompany"`
PropertyFee float64 `json:"propertyFee"`
Developer string `json:"developer"`
Source string `json:"source"`
}
// CommunityListReq 分页查询小区。
type CommunityListReq struct {
g.Meta `path:"/house/community/list" method:"post" tags:"Admin/House/Community" summary:"小区列表"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
Keyword string `json:"keyword"` // 匹配名称/地址
Region string `json:"region"`
}
// CommunityListRes 是 CommunityListReq 的响应。
type CommunityListRes struct {
List []*CommunityItem `json:"list"`
Total int `json:"total"`
}
// CommunityCreateReq 新增小区。
type CommunityCreateReq struct {
g.Meta `path:"/house/community/create" method:"post" tags:"Admin/House/Community" summary:"新增小区"`
Name string `json:"name" v:"required"`
Region string `json:"region"`
BusinessDistrict string `json:"businessDistrict"`
Address string `json:"address"`
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
BuildYear int `json:"buildYear"`
Households int `json:"households"`
PlotRatio float64 `json:"plotRatio"`
GreenRate float64 `json:"greenRate"`
PropertyCompany string `json:"propertyCompany"`
PropertyFee float64 `json:"propertyFee"`
Developer string `json:"developer"`
Source string `json:"source"`
}
// CommunityCreateRes 是 CommunityCreateReq 的响应。
type CommunityCreateRes struct {
Id uint64 `json:"id"`
}
// CommunityUpdateReq 更新小区。
type CommunityUpdateReq struct {
g.Meta `path:"/house/community/update/{id}" method:"post" tags:"Admin/House/Community" summary:"更新小区"`
Id uint64 `json:"id" in:"path" v:"required"`
Name string `json:"name" v:"required"`
Region string `json:"region"`
BusinessDistrict string `json:"businessDistrict"`
Address string `json:"address"`
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
BuildYear int `json:"buildYear"`
Households int `json:"households"`
PlotRatio float64 `json:"plotRatio"`
GreenRate float64 `json:"greenRate"`
PropertyCompany string `json:"propertyCompany"`
PropertyFee float64 `json:"propertyFee"`
Developer string `json:"developer"`
}
// CommunityUpdateRes 是 CommunityUpdateReq 的响应。
type CommunityUpdateRes struct{}
// CommunityDeleteReq 删除小区(软删除)。
type CommunityDeleteReq struct {
g.Meta `path:"/house/community/delete/{id}" method:"post" tags:"Admin/House/Community" summary:"删除小区"`
Id uint64 `json:"id" in:"path" v:"required"`
}
// CommunityDeleteRes 是 CommunityDeleteReq 的响应。
type CommunityDeleteRes struct{}

View File

@ -0,0 +1,85 @@
// Package house_dashboard 定义看房数据看板聚合接口,路由前缀 /house。
package house_dashboard
import "github.com/gogf/gf/v2/frame/g"
// OverviewRes 看板统计概览。
type OverviewRes struct {
CommunityCount int `json:"communityCount"` // 小区数
ListingCount int `json:"listingCount"` // 在售房源数
BargainCount int `json:"bargainCount"` // 笋盘数
LowConfidence int `json:"lowConfidence"` // 低可信房源数
AvgUnitPrice float64 `json:"avgUnitPrice"` // 在售均价(元/平米)
AvgTotalPrice float64 `json:"avgTotalPrice"` // 在售平均总价(万元)
AvgListDays float64 `json:"avgListDays"` // 平均挂牌天数
}
// OverviewReq 看板统计概览请求。
type OverviewReq struct {
g.Meta `path:"/house/dashboard/overview" method:"post" tags:"Admin/House/Dashboard" summary:"看板概览"`
Region string `json:"region"`
}
// MapPoint 地图上的一个小区点。
type MapPoint struct {
CommunityId uint64 `json:"communityId"`
Name string `json:"name"`
Region string `json:"region"`
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
AvgUnitPrice float64 `json:"avgUnitPrice"`
ListingCount int `json:"listingCount"`
BargainCount int `json:"bargainCount"`
}
// MapPointsReq 地图点位查询(带筛选)。
type MapPointsReq struct {
g.Meta `path:"/house/dashboard/map-points" method:"post" tags:"Admin/House/Dashboard" summary:"地图点位"`
Region string `json:"region"`
PriceMin float64 `json:"priceMin"`
PriceMax float64 `json:"priceMax"`
Status int `json:"status"`
}
// MapPointsRes 是 MapPointsReq 的响应。
type MapPointsRes struct {
Points []*MapPoint `json:"points"`
}
// TrendPoint 一个日期的均价点。
type TrendPoint struct {
Date string `json:"date"`
AvgListPrice float64 `json:"avgListPrice"` // 挂牌均价(万元)
AvgDealPrice float64 `json:"avgDealPrice"` // 成交均价(万元)
}
// PriceTrendReq 价格趋势查询。
type PriceTrendReq struct {
g.Meta `path:"/house/dashboard/price-trend" method:"post" tags:"Admin/House/Dashboard" summary:"价格趋势"`
CommunityId uint64 `json:"communityId"`
Region string `json:"region"`
Limit int `json:"limit" d:"30"` // 最近 N 天
}
// PriceTrendRes 是 PriceTrendReq 的响应。
type PriceTrendRes struct {
Trend []*TrendPoint `json:"trend"`
}
// RegionAgg 一个区域的聚合指标。
type RegionAgg struct {
Region string `json:"region"`
AvgUnitPrice float64 `json:"avgUnitPrice"`
ListingCount int `json:"listingCount"`
BargainCount int `json:"bargainCount"`
}
// AggregateRegionReq 区域聚合查询。
type AggregateRegionReq struct {
g.Meta `path:"/house/dashboard/aggregate-region" method:"post" tags:"Admin/House/Dashboard" summary:"区域聚合"`
}
// AggregateRegionRes 是 AggregateRegionReq 的响应。
type AggregateRegionRes struct {
List []*RegionAgg `json:"list"`
}

View File

@ -0,0 +1,139 @@
// Package house_listing 定义房源/挂牌管理接口,路由前缀 /house。
package house_listing
import "github.com/gogf/gf/v2/frame/g"
// ListingItem 房源列表中的一行。
type ListingItem struct {
Id uint64 `json:"id"`
CommunityId uint64 `json:"communityId"`
CommunityName string `json:"communityName"`
BuildingId uint64 `json:"buildingId"`
HouseNo string `json:"houseNo"`
Layout string `json:"layout"`
Area float64 `json:"area"`
UsableArea float64 `json:"usableArea"`
Orientation string `json:"orientation"`
Floor int `json:"floor"`
TotalFloors int `json:"totalFloors"`
Decoration string `json:"decoration"`
TotalPrice float64 `json:"totalPrice"`
UnitPrice float64 `json:"unitPrice"`
ListPrice float64 `json:"listPrice"`
Source string `json:"source"`
SourceHouseId string `json:"sourceHouseId"`
SourceUrl string `json:"sourceUrl"`
MatchGroupId uint64 `json:"matchGroupId"`
OnMarketDays int `json:"onMarketDays"`
PriceChangeCount int `json:"priceChangeCount"`
Status int `json:"status"`
Confidence int `json:"confidence"`
IsBargain int `json:"isBargain"`
ListingTime string `json:"listingTime"`
CreatedAt string `json:"createdAt"`
}
// ListingListReq 分页查询房源(统一筛选入参,供管理列表与看板共用)。
type ListingListReq struct {
g.Meta `path:"/house/listing/list" method:"post" tags:"Admin/House/Listing" summary:"房源列表"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
CommunityId uint64 `json:"communityId"`
Keyword string `json:"keyword"` // 匹配房号/户型
Layout string `json:"layout"`
Region string `json:"region"`
Source string `json:"source"`
PriceMin float64 `json:"priceMin"`
PriceMax float64 `json:"priceMax"`
AreaMin float64 `json:"areaMin"`
AreaMax float64 `json:"areaMax"`
Status int `json:"status"`
IsBargain int `json:"isBargain"` // 0 全部1 仅笋盘
Confidence int `json:"confidence"` // 0 全部1 仅低可信
}
// ListingListRes 是 ListingListReq 的响应。
type ListingListRes struct {
List []*ListingItem `json:"list"`
Total int `json:"total"`
}
// ListingCreateReq 新增房源(采集器/手动录入)。
type ListingCreateReq struct {
g.Meta `path:"/house/listing/create" method:"post" tags:"Admin/House/Listing" summary:"新增房源"`
CommunityId uint64 `json:"communityId" v:"required"`
BuildingId uint64 `json:"buildingId"`
HouseNo string `json:"houseNo"`
Layout string `json:"layout"`
Area float64 `json:"area"`
UsableArea float64 `json:"usableArea"`
Orientation string `json:"orientation"`
Floor int `json:"floor"`
TotalFloors int `json:"totalFloors"`
Decoration string `json:"decoration"`
TotalPrice float64 `json:"totalPrice"`
UnitPrice float64 `json:"unitPrice"`
ListPrice float64 `json:"listPrice"`
Source string `json:"source"`
SourceHouseId string `json:"sourceHouseId"`
SourceUrl string `json:"sourceUrl"`
MatchGroupId uint64 `json:"matchGroupId"`
OnMarketDays int `json:"onMarketDays"`
PriceChangeCount int `json:"priceChangeCount"`
Status int `json:"status" d:"1"`
Confidence int `json:"confidence"`
IsBargain int `json:"isBargain"`
}
// ListingCreateRes 是 ListingCreateReq 的响应。
type ListingCreateRes struct {
Id uint64 `json:"id"`
}
// ListingUpdateReq 更新房源。
type ListingUpdateReq struct {
g.Meta `path:"/house/listing/update/{id}" method:"post" tags:"Admin/House/Listing" summary:"更新房源"`
Id uint64 `json:"id" in:"path" v:"required"`
CommunityId uint64 `json:"communityId"`
BuildingId uint64 `json:"buildingId"`
HouseNo string `json:"houseNo"`
Layout string `json:"layout"`
Area float64 `json:"area"`
UsableArea float64 `json:"usableArea"`
Orientation string `json:"orientation"`
Floor int `json:"floor"`
TotalFloors int `json:"totalFloors"`
Decoration string `json:"decoration"`
TotalPrice float64 `json:"totalPrice"`
UnitPrice float64 `json:"unitPrice"`
ListPrice float64 `json:"listPrice"`
MatchGroupId uint64 `json:"matchGroupId"`
Status int `json:"status"`
Confidence int `json:"confidence"`
IsBargain int `json:"isBargain"`
}
// ListingUpdateRes 是 ListingUpdateReq 的响应。
type ListingUpdateRes struct{}
// ListingDeleteReq 删除房源(软删除)。
type ListingDeleteReq struct {
g.Meta `path:"/house/listing/delete/{id}" method:"post" tags:"Admin/House/Listing" summary:"删除房源"`
Id uint64 `json:"id" in:"path" v:"required"`
}
// ListingDeleteRes 是 ListingDeleteReq 的响应。
type ListingDeleteRes struct{}
// ListingBatchMarkReq 批量标记房源(置信度/笋盘/状态)。
type ListingBatchMarkReq struct {
g.Meta `path:"/house/listing/batch-mark" method:"post" tags:"Admin/House/Listing" summary:"批量标记房源"`
Ids []uint64 `json:"ids" v:"required"`
Field string `json:"field" v:"required"` // confidence/isBargain/status
Value int `json:"value"`
}
// ListingBatchMarkRes 是 ListingBatchMarkReq 的响应。
type ListingBatchMarkRes struct {
Affected int64 `json:"affected"`
}

View File

@ -0,0 +1,37 @@
// Package house_presale 定义新房预售证查询接口,路由前缀 /house。
package house_presale
import "github.com/gogf/gf/v2/frame/g"
// PresaleItem 一条新房预售许可证。
type PresaleItem struct {
Id uint64 `json:"id"`
PresaleNo string `json:"presaleNo"`
CommunityName string `json:"communityName"`
Developer string `json:"developer"`
Region string `json:"region"`
Address string `json:"address"`
BuildingNo string `json:"buildingNo"`
HouseCount int `json:"houseCount"`
Area float64 `json:"area"`
Purpose string `json:"purpose"`
IssueDate string `json:"issueDate"`
PublishDate string `json:"publishDate"`
Source string `json:"source"`
}
// PresaleListReq 分页查询新房预售证。
type PresaleListReq struct {
g.Meta `path:"/house/presale/list" method:"post" tags:"Admin/House/Presale" summary:"新房预售证列表"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
Region string `json:"region"`
Purpose string `json:"purpose"` // 规划用途(住宅/商业)
Keyword string `json:"keyword"` // 匹配楼盘名/开发商/预售证号
}
// PresaleListRes 是 PresaleListReq 的响应。
type PresaleListRes struct {
List []*PresaleItem `json:"list"`
Total int `json:"total"`
}

View File

@ -0,0 +1,33 @@
// Package house_transaction 定义成交记录查询接口,路由前缀 /house。
package house_transaction
import "github.com/gogf/gf/v2/frame/g"
// TransactionItem 一条成交记录。
type TransactionItem struct {
Id uint64 `json:"id"`
CommunityId uint64 `json:"communityId"`
CommunityName string `json:"communityName"`
Layout string `json:"layout"`
Area float64 `json:"area"`
DealPrice float64 `json:"dealPrice"`
DealUnitPrice float64 `json:"dealUnitPrice"`
ListDays int `json:"listDays"`
DealDate string `json:"dealDate"`
Source string `json:"source"`
}
// TransactionListReq 分页查询成交记录。
type TransactionListReq struct {
g.Meta `path:"/house/transaction/list" method:"post" tags:"Admin/House/Transaction" summary:"成交记录列表"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
Region string `json:"region"`
Keyword string `json:"keyword"` // 匹配小区名/户型
}
// TransactionListRes 是 TransactionListReq 的响应。
type TransactionListRes struct {
List []*TransactionItem `json:"list"`
Total int `json:"total"`
}

73
api/job/job.go Normal file
View File

@ -0,0 +1,73 @@
// Package job_v1 自动任务管理模块接口契约DB 驱动的 gcron 调度 + 运行记录)。
// 规范2026-08-27全部 POSTURL 不含参数;入参一律 body。
package job
import "github.com/gogf/gf/v2/frame/g"
// ---------- 任务列表 ----------
type JobItem struct {
Id uint64 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
JobType string `json:"jobType"`
CronExpr string `json:"cronExpr"`
Enabled int `json:"enabled"`
Remark string `json:"remark"`
LastRunAt string `json:"lastRunAt"`
LastResult int `json:"lastResult"` // 0未运行 1成功 2失败
LastError string `json:"lastError"`
}
type AutoJobListReq struct {
g.Meta `path:"/auto-job/list" method:"post" tags:"Admin/AutoJob" summary:"自动任务列表"`
}
type AutoJobListRes struct {
List []*JobItem `json:"list"`
}
// ---------- 保存任务(改 cron/启用/备注) ----------
type AutoJobSaveReq struct {
g.Meta `path:"/auto-job/save" method:"post" tags:"Admin/AutoJob" summary:"保存自动任务(改cron/启用)"`
Id uint64 `json:"id" v:"required"`
CronExpr string `json:"cronExpr"`
Enabled int `json:"enabled" d:"1"`
Remark string `json:"remark"`
}
type AutoJobSaveRes struct{}
// ---------- 手动触发 ----------
type AutoJobTriggerReq struct {
g.Meta `path:"/auto-job/trigger" method:"post" tags:"Admin/AutoJob" summary:"手动触发任务"`
Id uint64 `json:"id" v:"required"`
}
type AutoJobTriggerRes struct {
Summary string `json:"summary"`
Ok bool `json:"ok"`
}
// ---------- 任务运行日志 ----------
type JobLogItem struct {
Id uint64 `json:"id"`
JobId uint64 `json:"jobId"`
JobName string `json:"jobName"`
RunAt string `json:"runAt"`
Result int `json:"result"`
Error string `json:"error"`
Summary string `json:"summary"`
DurationMs int `json:"durationMs"`
}
type AutoJobLogListReq struct {
g.Meta `path:"/auto-job/log/list" method:"post" tags:"Admin/AutoJob" summary:"任务运行日志分页"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
JobId uint64 `json:"jobId"` // 0=全部
}
type AutoJobLogListRes struct {
List []*JobLogItem `json:"list"`
Total int `json:"total"`
}

128
api/notice/notice.go Normal file
View File

@ -0,0 +1,128 @@
// Package notice_v1 通知模块接口契约(统一推送管理:渠道/规则/日志/测试)。
// 规范2026-08-27全部 POSTURL 不含参数;入参一律 body。
package notice
import "github.com/gogf/gf/v2/frame/g"
// ---------- 通知渠道 ----------
type NoticeChannelItem struct {
Id uint64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Enabled int `json:"enabled"`
Config string `json:"config"` // JSON 字符串
Remark string `json:"remark"`
}
type NoticeChannelListReq struct {
g.Meta `path:"/notice/channel/list" method:"post" tags:"Admin/Notice" summary:"通知渠道列表"`
}
type NoticeChannelListRes struct {
List []*NoticeChannelItem `json:"list"`
}
type NoticeChannelSaveReq struct {
g.Meta `path:"/notice/channel/save" method:"post" tags:"Admin/Notice" summary:"保存通知渠道(新增/更新)"`
Id uint64 `json:"id"` // 0=新增
Code string `json:"code" v:"required"`
Name string `json:"name" v:"required"`
Enabled int `json:"enabled" d:"1"`
Config string `json:"config"`
Remark string `json:"remark"`
}
type NoticeChannelSaveRes struct {
Id uint64 `json:"id"`
}
// ---------- 通知规则 ----------
type NoticeRuleItem struct {
Id uint64 `json:"id"`
Name string `json:"name"`
EventType string `json:"eventType"`
ChannelCodes []string `json:"channelCodes"`
UserIds []uint64 `json:"userIds"`
TitleTemplate string `json:"titleTemplate"`
BodyTemplate string `json:"bodyTemplate"`
Enabled int `json:"enabled"`
}
type NoticeRuleListReq struct {
g.Meta `path:"/notice/rule/list" method:"post" tags:"Admin/Notice" summary:"通知规则列表"`
EventType string `json:"eventType"` // 可选过滤
}
type NoticeRuleListRes struct {
List []*NoticeRuleItem `json:"list"`
}
type NoticeRuleSaveReq struct {
g.Meta `path:"/notice/rule/save" method:"post" tags:"Admin/Notice" summary:"保存通知规则(新增/更新)"`
Id uint64 `json:"id"` // 0=新增
Name string `json:"name" v:"required"`
EventType string `json:"eventType" v:"required"`
ChannelCodes []string `json:"channelCodes" v:"required|min-length:1"`
UserIds []uint64 `json:"userIds" v:"required|min-length:1"`
TitleTemplate string `json:"titleTemplate"`
BodyTemplate string `json:"bodyTemplate"`
Enabled int `json:"enabled" d:"1"`
}
type NoticeRuleSaveRes struct {
Id uint64 `json:"id"`
}
type NoticeRuleDeleteReq struct {
g.Meta `path:"/notice/rule/delete" method:"post" tags:"Admin/Notice" summary:"删除通知规则"`
Id uint64 `json:"id" v:"required"`
}
type NoticeRuleDeleteRes struct{}
// ---------- 通知日志 ----------
type NoticeLogItem struct {
Id uint64 `json:"id"`
RuleId uint64 `json:"ruleId"`
EventType string `json:"eventType"`
ChannelCode string `json:"channelCode"`
UserId uint64 `json:"userId"`
Target string `json:"target"`
Title string `json:"title"`
Body string `json:"body"`
Result int `json:"result"`
Error string `json:"error"`
CreatedAt string `json:"createdAt"`
}
type NoticeLogListReq struct {
g.Meta `path:"/notice/log/list" method:"post" tags:"Admin/Notice" summary:"通知发送记录分页"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
EventType string `json:"eventType"`
ChannelCode string `json:"channelCode"`
Result int `json:"result"` // 0全部 1成功 2失败
DateFrom string `json:"dateFrom"`
DateTo string `json:"dateTo"`
}
type NoticeLogListRes struct {
List []*NoticeLogItem `json:"list"`
Total int `json:"total"`
}
// ---------- 测试发送 ----------
type NoticeTestReq struct {
g.Meta `path:"/notice/test" method:"post" tags:"Admin/Notice" summary:"测试通知(按规则或直接指定)"`
RuleId uint64 `json:"ruleId"` // 按规则测试(0=手动指定)
ChannelCode string `json:"channelCode"` // 手动:渠道编码
Target string `json:"target"` // 手动设备key/token
UserIds []uint64 `json:"userIds"` // 手动:或按用户
Title string `json:"title"`
Body string `json:"body"`
}
type NoticeTestRes struct {
Result bool `json:"result"`
Message string `json:"message"`
}

15
api/open/tools/doc.go Normal file
View File

@ -0,0 +1,15 @@
// Package open_tools 承载开放工具接口GET/POST /tools/*)。
//
// 命名规则tools/ 下每个子功能一个独立目录目录名即包名Req/Res 契约放在
// 目录内以功能命名的文件中,例如:
//
// api/open/tools/uuid/uuid.go - POST /tools/uuid
// api/open/tools/md5/md5.go - POST /tools/md5
// api/open/tools/random/random.go - POST /tools/random
// api/open/tools/time/time.go - POST /tools/time
// api/open/tools/ip/ip.go - POST /tools/ip
//
// 功能增长时,在其目录内新增文件(如 ocr/ 下加 idcard.go、invoice.go
// 而不是把起始文件写大。新增功能:创建 tools/<name>/<name>.go 并在
// internal/controller/open/<name>.go 中增加对应方法,路由自动绑定。
package open_tools

15
api/open/tools/ip/ip.go Normal file
View File

@ -0,0 +1,15 @@
// Package open_tools_ip 定义 POST /tools/ip 接口契约。
package open_tools_ip
import "github.com/gogf/gf/v2/frame/g"
// IPReq 获取调用方 IP 信息。
type IPReq struct {
g.Meta `path:"/tools/ip" method:"post" tags:"Open/Tools" summary:"客户端 IP 信息"`
}
// IPRes 是 IPReq 的响应。
type IPRes struct {
IP string `json:"ip"`
Internal bool `json:"internal"` // 是否为内网/私网地址
}

15
api/open/tools/md5/md5.go Normal file
View File

@ -0,0 +1,15 @@
// Package open_tools_md5 定义 POST /tools/md5 接口契约。
package open_tools_md5
import "github.com/gogf/gf/v2/frame/g"
// MD5Req 计算文本的 MD5 摘要。
type MD5Req struct {
g.Meta `path:"/tools/md5" method:"post" tags:"Open/Tools" summary:"计算 MD5 摘要"`
Text string `json:"text" v:"required#待摘要文本不能为空"`
}
// MD5Res 是 MD5Req 的响应。
type MD5Res struct {
MD5 string `json:"md5"`
}

View File

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

View File

@ -0,0 +1,16 @@
// Package open_tools_time 定义 POST /tools/time 接口契约。
package open_tools_time
import "github.com/gogf/gf/v2/frame/g"
// TimeReq 获取当前服务器时间。
type TimeReq struct {
g.Meta `path:"/tools/time" method:"post" tags:"Open/Tools" summary:"当前服务器时间"`
}
// TimeRes 是 TimeReq 的响应。
type TimeRes struct {
Timestamp int64 `json:"timestamp"` // Unix 秒
DateTime string `json:"dateTime"` // 2006-01-02 15:04:05
Date string `json:"date"` // 2006-01-02
}

View File

@ -0,0 +1,15 @@
// Package open_tools_uuid 定义 POST /tools/uuid 接口契约。
package open_tools_uuid
import "github.com/gogf/gf/v2/frame/g"
// UUIDReq 生成唯一 ID。
type UUIDReq struct {
g.Meta `path:"/tools/uuid" method:"post" tags:"Open/Tools" summary:"生成唯一 ID"`
Short bool `json:"short"` // true: 8 位短码false: 32 位 ID
}
// UUIDRes 是 UUIDReq 的响应。
type UUIDRes struct {
UUID string `json:"uuid"`
}

View File

@ -0,0 +1,191 @@
// Package recruitment_v1 招聘考试聚合模块接口契约。
// 规范2026-08-27全部 POSTURL 不含任何参数(查询/路径参数均禁止);入参一律走 body。
package recruitment
import "github.com/gogf/gf/v2/frame/g"
// ---------- 公告列表 ----------
type RecruitmentListReq struct {
g.Meta `path:"/recruitment/info/list" method:"post" tags:"Admin/Recruitment/Info" summary:"招聘公告列表(多维筛选)"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
Region string `json:"region"` // 地区过滤(空=全部)
Category int `json:"category"` // 分类(0=全部)
Keyword string `json:"keyword"` // 标题/正文/单位关键字
Status int `json:"status"` // 状态(0=有效+已更正;传 2 只看已失效等)
DateFrom string `json:"dateFrom"` // 发布日期起点 YYYY-MM-DD
DateTo string `json:"dateTo"` // 发布日期终点 YYYY-MM-DD
OrgName string `json:"orgName"` // 发布主体名称模糊匹配
SourceId uint64 `json:"sourceId"` // 指定数据源
OnlyNewToday bool `json:"onlyNewToday"` // 仅当日新增
}
type RecruitmentItem struct {
Id uint64 `json:"id"`
Title string `json:"title"`
SourceId uint64 `json:"sourceId"`
SourceName string `json:"sourceName"`
OrgId uint64 `json:"orgId"`
OrgName string `json:"orgName"`
Category int `json:"category"`
CategoryName string `json:"categoryName"`
Region string `json:"region"`
PublishDate string `json:"publishDate"`
Deadline string `json:"deadline"`
ExamDate string `json:"examDate"`
Url string `json:"url"`
Content string `json:"content"`
Attachments string `json:"attachments"`
Status int `json:"status"`
StatusName string `json:"statusName"`
GroupKey string `json:"groupKey"`
CreatedAt string `json:"createdAt"`
}
type RecruitmentListRes struct {
List []*RecruitmentItem `json:"list"`
Total int `json:"total"`
}
// ---------- 公告详情 ----------
type RecruitmentDetailReq struct {
g.Meta `path:"/recruitment/info/detail" method:"post" tags:"Admin/Recruitment/Info" summary:"招聘公告详情"`
Id uint64 `json:"id" v:"required"`
}
type RecruitmentDetailRes struct {
Info *RecruitmentItem `json:"info"`
}
// ---------- 看板统计 ----------
type RecruitmentStatsReq struct {
g.Meta `path:"/recruitment/stats" method:"post" tags:"Admin/Recruitment/Dashboard" summary:"招聘数据看板统计"`
Region string `json:"region"` // 可选:按地区过滤统计
}
type RecruitmentStatsRes struct {
Total int `json:"total"`
TodayNew int `json:"todayNew"`
WeekNew int `json:"weekNew"`
ByCategory []CategoryAggItem `json:"byCategory"`
ByRegion []RegionAggItem `json:"byRegion"`
RecentTrend []TrendPointItem `json:"recentTrend"`
}
type CategoryAggItem struct {
Category int `json:"category"`
CategoryName string `json:"categoryName"`
Count int `json:"count"`
}
type RegionAggItem struct {
Region string `json:"region"`
Count int `json:"count"`
}
type TrendPointItem struct {
Date string `json:"date"`
Count int `json:"count"`
}
// ---------- 趋势分析 ----------
type RecruitmentTrendReq struct {
g.Meta `path:"/recruitment/trend" method:"post" tags:"Admin/Recruitment/Dashboard" summary:"招聘公告趋势(按日)"`
Region string `json:"region"`
Category int `json:"category"`
Days int `json:"days" d:"30" v:"min:1|max:365"`
}
type RecruitmentTrendRes struct {
Trend []TrendPointItem `json:"trend"`
}
// ---------- 数据源状态 ----------
type RecruitmentSourcesReq struct {
g.Meta `path:"/recruitment/source/list" method:"post" tags:"Admin/Recruitment/Crawler" summary:"数据源列表与运行状态"`
}
type SourceItem struct {
Id uint64 `json:"id"`
Name string `json:"name"`
BaseUrl string `json:"baseUrl"`
SourceType int `json:"sourceType"`
Category int `json:"category"`
Region string `json:"region"`
Enabled int `json:"enabled"`
LastSuccessAt string `json:"lastSuccessAt"`
FailCount int `json:"failCount"`
LastSummary string `json:"lastSummary"`
}
type RecruitmentSourcesRes struct {
List []*SourceItem `json:"list"`
}
// ---------- 手动触发抓取 ----------
type RecruitmentTriggerReq struct {
g.Meta `path:"/recruitment/crawl/trigger" method:"post" tags:"Admin/Recruitment/Crawler" summary:"手动触发抓取(单源或全量)"`
SourceId uint64 `json:"sourceId"` // 0=全部启用源
Force bool `json:"force"` // 是否强制全量回溯
}
type RecruitmentTriggerRes struct {
Triggered int `json:"triggered"` // 触发的源数量
Summary string `json:"summary"` // 运行摘要
}
// ---------- Bark 推送测试 ----------
type RecruitmentPushTestReq struct {
g.Meta `path:"/recruitment/push/test" method:"post" tags:"Admin/Recruitment/Push" summary:"Bark 推送测试"`
SubscriptionId uint64 `json:"subscriptionId"` // 0=使用首个启用订阅
Title string `json:"title"`
Body string `json:"body"`
}
type RecruitmentPushTestRes struct {
Result bool `json:"result"`
Message string `json:"message"`
}
// ---------- 推送订阅管理 ----------
type RecruitmentSubscriptionListReq struct {
g.Meta `path:"/recruitment/subscription/list" method:"post" tags:"Admin/Recruitment/Push" summary:"推送订阅列表"`
}
type SubscriptionItem struct {
Id uint64 `json:"id"`
Name string `json:"name"`
DeviceKey string `json:"deviceKey"`
Regions []string `json:"regions"`
Categories []int `json:"categories"`
OnlyNew int `json:"onlyNew"`
PushTime string `json:"pushTime"`
Enabled int `json:"enabled"`
}
type RecruitmentSubscriptionListRes struct {
List []*SubscriptionItem `json:"list"`
}
type RecruitmentSubscriptionSaveReq struct {
g.Meta `path:"/recruitment/subscription/save" method:"post" tags:"Admin/Recruitment/Push" summary:"保存推送订阅(新增/更新)"`
Id uint64 `json:"id"` // 0=新增
Name string `json:"name" v:"required"`
DeviceKey string `json:"deviceKey" v:"required"`
Regions []string `json:"regions"`
Categories []int `json:"categories"`
OnlyNew int `json:"onlyNew" d:"1"`
PushTime string `json:"pushTime" d:"08:00"`
Enabled int `json:"enabled" d:"1"`
}
type RecruitmentSubscriptionSaveRes struct {
Id uint64 `json:"id"`
}
type RecruitmentSubscriptionDeleteReq struct {
g.Meta `path:"/recruitment/subscription/delete" method:"post" tags:"Admin/Recruitment/Push" summary:"删除推送订阅"`
Id uint64 `json:"id" v:"required"`
}
type RecruitmentSubscriptionDeleteRes struct{}

View File

@ -0,0 +1,91 @@
// Package serversecurity_v1 服务器安全监控日志模块接口契约。
// 规范2026-08-27全部 POSTURL 不含任何参数(查询/路径参数均禁止);入参一律走 body。
package serversecurity
import "github.com/gogf/gf/v2/frame/g"
// ---------- 上报宿主机采集脚本调用open 组 + 内部 token 校验) ----------
// SecurityLogItem 单条安全日志。
type SecurityLogItem struct {
LogTime string `json:"logTime"` // 事件发生时间 YYYY-MM-DD HH:MM:SS
SrcIp string `json:"srcIp"` // 来源 IP
SrcPort int `json:"srcPort"` // 来源端口
DestPort int `json:"destPort"` // 目标端口(如 22025
EventType string `json:"eventType"` // failed_ssh / accepted_ssh / banned / unbanned
Detail string `json:"detail"` // 日志详情/原文
}
// SecurityLogReportReq 上报请求。
type SecurityLogReportReq struct {
g.Meta `path:"/security/log/report" method:"post" tags:"Open/Security" summary:"上报服务器安全日志(内部脚本调用)"`
Token string `json:"token" v:"required"` // 内部上报令牌
List []SecurityLogItem `json:"list" v:"required|min-length:1"`
}
// SecurityLogReportRes 上报结果。
type SecurityLogReportRes struct {
Accepted int `json:"accepted"` // 成功入库条数
}
// ---------- 查询admin 组,受权限保护) ----------
// SecurityLogListReq 分页查询请求。
type SecurityLogListReq struct {
g.Meta `path:"/server-security/log/list" method:"post" tags:"Admin/Security/Log" summary:"安全日志分页查询"`
Page int `json:"page" d:"1" v:"min:1"`
Size int `json:"size" d:"10" v:"min:1|max:100"`
SrcIp string `json:"srcIp"` // 来源 IP 精确/模糊
EventType string `json:"eventType"` // 事件类型过滤(空=全部)
DestPort int `json:"destPort"` // 目标端口(0=全部)
DateFrom string `json:"dateFrom"` // 起始时间 YYYY-MM-DD HH:MM:SS
DateTo string `json:"dateTo"` // 结束时间 YYYY-MM-DD HH:MM:SS
}
// SecurityLogListItem 日志列表项。
type SecurityLogListItem struct {
Id uint64 `json:"id"`
LogTime string `json:"logTime"`
SrcIp string `json:"srcIp"`
SrcPort int `json:"srcPort"`
DestPort int `json:"destPort"`
EventType string `json:"eventType"`
EventName string `json:"eventName"` // 派生:事件中文名
Detail string `json:"detail"`
CreatedAt string `json:"createdAt"`
}
// SecurityLogListRes 分页查询结果。
type SecurityLogListRes struct {
List []*SecurityLogListItem `json:"list"`
Total int `json:"total"`
}
// ---------- 统计admin 组,受权限保护) ----------
// SecurityLogStatsReq 统计请求。
type SecurityLogStatsReq struct {
g.Meta `path:"/server-security/log/stats" method:"post" tags:"Admin/Security/Log" summary:"安全日志统计(默认最近24h)"`
DateFrom string `json:"dateFrom"` // 起始时间(空=24小时前)
DateTo string `json:"dateTo"` // 结束时间(空=当前)
}
// SecurityTopIp TOP 攻击来源 IP。
type SecurityTopIp struct {
SrcIp string `json:"srcIp"`
Count int `json:"count"`
}
// SecurityByType 按事件类型分布。
type SecurityByType struct {
EventType string `json:"eventType"`
EventName string `json:"eventName"`
Count int `json:"count"`
}
// SecurityLogStatsRes 统计结果。
type SecurityLogStatsRes struct {
Total int `json:"total"` // 范围内总记录
Failed int `json:"failed"` // SSH 爆破尝试
Banned int `json:"banned"` // fail2ban 封禁
Accepted int `json:"accepted"` // 成功登录
TopIps []SecurityTopIp `json:"topIps"` // TOP 攻击源(按条数)
ByType []SecurityByType `json:"byType"` // 按类型分布
}

33
api/user/auth/auth.go Normal file
View File

@ -0,0 +1,33 @@
// Package user_auth 定义用户端认证接口(登录/刷新),路由前缀 /api。
package user_auth
import "github.com/gogf/gf/v2/frame/g"
// LoginReq 用户登录请求(支持微信/手机验证码/账号密码三种方式)。
type LoginReq struct {
g.Meta `path:"/auth/login" method:"post" tags:"User/Auth" summary:"用户登录"`
LoginType string `json:"loginType" v:"required|in:wechat,mobile,password#登录方式不能为空|不支持的登录方式"`
Code string `json:"code"` // 微信授权码
Mobile string `json:"mobile"` // 手机号mobile 登录方式)
VerifyCode string `json:"verifyCode"` // 短信验证码
Account string `json:"account"` // 账号password 登录方式)
Password string `json:"password"` // 密码
Terminal string `json:"terminal" v:"required|in:mini,h5,app#终端类型不能为空|无效的终端类型"`
}
// LoginRes 用户登录响应。
type LoginRes struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresIn int64 `json:"expiresIn"`
UserID uint64 `json:"userId"`
}
// RefreshReq 使用刷新令牌轮换用户令牌对。
type RefreshReq struct {
g.Meta `path:"/auth/refresh" method:"post" tags:"User/Auth" summary:"轮换用户令牌"`
RefreshToken string `json:"refreshToken" v:"required"`
}
// RefreshRes 是 RefreshReq 的响应。
type RefreshRes LoginRes

6
api/user/login/login.go Normal file
View File

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

View File

@ -1,25 +0,0 @@
package v1
import "github.com/gogf/gf/v2/frame/g"
type LoginReq struct {
g.Meta `path:"/auth/login" method:"post" tags:"User/Auth" summary:"User login"`
LoginType string `json:"loginType" v:"required|in:wechat,mobile,password#login type required|unsupported login type"`
Code string `json:"code"`
Mobile string `json:"mobile"`
VerifyCode string `json:"verifyCode"`
Account string `json:"account"`
Password string `json:"password"`
Terminal string `json:"terminal" v:"required|in:mini,h5,app#terminal required|invalid terminal"`
}
type LoginRes struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresIn int64 `json:"expiresIn"`
UserID uint64 `json:"userId"`
}
type RefreshReq struct {
g.Meta `path:"/auth/refresh" method:"post" tags:"User/Auth" summary:"Rotate user token"`
RefreshToken string `json:"refreshToken" v:"required"`
}
type RefreshRes LoginRes

10
common/doc.go Normal file
View File

@ -0,0 +1,10 @@
// Package common 是 service.xpcool.com 的公共可复用模块。
//
// 仅包含与 internal/ 无关的代码——此处的内容可以
// 在仓库内跨服务共享,或日后抽取为独立库,
// 均无需改动业务代码。
//
// 当前结构:
//
// common/tools 公共工具集md5、cryptox、uuid 等)
package common

View File

@ -0,0 +1,64 @@
// Package convertx 提供带默认值兜底的类型转换工具,
// 基于 gconv 构建。
//
// 注意gconv 转换失败时静默返回零值,
// 因此本工具仅对 nil / 空字符串输入回退到默认值。
// 需要严格转换时请传入已校验的数据。
package convertx
import (
"strings"
"github.com/gogf/gf/v2/util/gconv"
)
// ToInt 将 v 转为 intv 为 nil 或空字符串时返回 def。
func ToInt(v any, def int) int {
if isEmpty(v) {
return def
}
return gconv.Int(v)
}
// ToInt64 将 v 转为 int64v 为 nil 或空字符串时返回 def。
func ToInt64(v any, def int64) int64 {
if isEmpty(v) {
return def
}
return gconv.Int64(v)
}
// ToFloat64 将 v 转为 float64v 为 nil 或空字符串时返回 def。
func ToFloat64(v any, def float64) float64 {
if isEmpty(v) {
return def
}
return gconv.Float64(v)
}
// ToString 将 v 转为 stringv 为 nil 时返回 def。
func ToString(v any, def string) string {
if v == nil {
return def
}
return gconv.String(v)
}
// ToBool 将 v 转为 boolv 为 nil 或空字符串时返回 def。
func ToBool(v any, def bool) bool {
if isEmpty(v) {
return def
}
return gconv.Bool(v)
}
// isEmpty 判断 v 是否为 nil 或空字符串。
func isEmpty(v any) bool {
if v == nil {
return true
}
if s, ok := v.(string); ok {
return strings.TrimSpace(s) == ""
}
return false
}

View File

@ -0,0 +1,80 @@
// Package cryptox 提供 AES/DES 加解密工具base64 输出),
// 基于 gaes 与 gdes 构建。
//
// 密钥:接受任意长度密钥,内部会归一化为精确密钥尺寸,
// 调用方无需关心密钥长度。
package cryptox
import (
"crypto/md5"
"encoding/base64"
"github.com/gogf/gf/v2/crypto/gaes"
"github.com/gogf/gf/v2/crypto/gdes"
"github.com/gogf/gf/v2/errors/gerror"
)
const (
aesKeySize = 16 // AES-128 key size in bytes
desKeySize = 8 // DES key size in bytes
)
// normalizeKey 从任意长度密钥派生出固定尺寸密钥。
func normalizeKey(secret string, size int) []byte {
key := []byte(secret)
if len(key) == size {
return key
}
sum := md5.Sum([]byte(secret))
out := make([]byte, size)
for i := 0; i < size; i++ {
out[i] = sum[i%len(sum)]
}
return out
}
// AesEncrypt 使用由 secret 派生的密钥对 plainText 做 AES-128-CBC 加密,
// 返回 base64 编码的密文。
func AesEncrypt(plainText, secret string) (string, error) {
out, err := gaes.Encrypt([]byte(plainText), normalizeKey(secret, aesKeySize))
if err != nil {
return "", gerror.Wrap(err, `AesEncrypt failed`)
}
return base64.StdEncoding.EncodeToString(out), nil
}
// AesDecrypt 解密 AesEncrypt 产生的 base64 密文。
func AesDecrypt(cipherText, secret string) (string, error) {
data, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", gerror.Wrap(err, `base64 decode failed`)
}
out, err := gaes.Decrypt(data, normalizeKey(secret, aesKeySize))
if err != nil {
return "", gerror.Wrap(err, `AesDecrypt failed`)
}
return string(out), nil
}
// DesEncrypt 使用由 secret 派生的密钥对 plainText 做 DES-ECBPKCS5 填充)加密,
// 返回 base64 编码的密文。
func DesEncrypt(plainText, secret string) (string, error) {
out, err := gdes.EncryptECB([]byte(plainText), normalizeKey(secret, desKeySize), gdes.PKCS5PADDING)
if err != nil {
return "", gerror.Wrap(err, `DesEncrypt failed`)
}
return base64.StdEncoding.EncodeToString(out), nil
}
// DesDecrypt 解密 DesEncrypt 产生的 base64 密文。
func DesDecrypt(cipherText, secret string) (string, error) {
data, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", gerror.Wrap(err, `base64 decode failed`)
}
out, err := gdes.DecryptECB(data, normalizeKey(secret, desKeySize), gdes.PKCS5PADDING)
if err != nil {
return "", gerror.Wrap(err, `DesDecrypt failed`)
}
return string(out), nil
}

24
common/tools/doc.go Normal file
View File

@ -0,0 +1,24 @@
// Package tools 是项目的公共工具集。
//
// 每个子包都是对 GoFrame 内置组件的薄封装,
// gmd5、gaes、gdes、guid、grand、gtime、gconv、gstr、gfile 等),
// 保持小巧且与框架风格一致。
//
// 结构:
//
// common/tools/md5 MD5 摘要工具
// common/tools/cryptox AES/DES 加解密base64 输出)
// common/tools/uuid 唯一 ID 生成
// common/tools/random 随机数与随机字符串
// common/tools/timex 时间格式化与计算
// common/tools/convertx 类型转换(带默认值兜底)
// common/tools/strx 字符串 / 命名 / 脱敏工具
// common/tools/slicex 泛型切片工具
// common/tools/ip IP 地址工具
// common/tools/filex 文件系统工具
//
// 规则:
// - 禁止依赖 internal/ —— 本模块必须保持自包含。
// - 优先复用 GoFrame 内置组件,避免重复造轮子。
// - 每个工具保持小巧,并补充中文文档注释。
package tools

View File

@ -0,0 +1,28 @@
// Package filex 基于 gfile 提供常用文件系统工具。
package filex
import (
"github.com/gogf/gf/v2/os/gfile"
)
// Exists 判断 path 对应的文件或目录是否存在。
func Exists(path string) bool {
return gfile.Exists(path)
}
// IsDir 判断 path 是否为目录。
func IsDir(path string) bool {
return gfile.IsDir(path)
}
// ReadString 以字符串形式返回 path 文件的完整内容,
// 文件不存在时返回空字符串。
func ReadString(path string) string {
return gfile.GetContents(path)
}
// WriteString 将 content 写入 path 文件,必要时自动创建中间目录。
// (续上一行)
func WriteString(path, content string) error {
return gfile.PutContents(path, content)
}

59
common/tools/ip/ip.go Normal file
View File

@ -0,0 +1,59 @@
// Package ip 提供 IP 地址工具。
package ip
import (
"net"
"strconv"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/gipv4"
)
// IsValid 判断 s 是否为合法的 IPv4 地址。
func IsValid(s string) bool {
return gipv4.Validate(s)
}
// LocalIP 返回本机第一个非回环 IPv4 地址。
func LocalIP() (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", gerror.Wrap(err, `net.InterfaceAddrs failed`)
}
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok {
if ipv4 := ipNet.IP.To4(); ipv4 != nil && !ipv4.IsLoopback() {
return ipv4.String(), nil
}
}
}
return "", nil
}
// IsInternal 判断 s 是否为私有/内网 IPv4 地址
// (含私有网段、回环地址、链路本地地址)。
func IsInternal(s string) bool {
parsed := net.ParseIP(s)
if parsed == nil || parsed.To4() == nil {
return false
}
return parsed.IsPrivate() || parsed.IsLoopback() || parsed.IsLinkLocalUnicast()
}
// ToLong 将 IPv4 字符串转为 uint32 数值表示
// (大端序,与 inet_aton 一致)。
func ToLong(s string) (uint32, error) {
ipv4 := net.ParseIP(s).To4()
if ipv4 == nil {
return 0, gerror.Newf(`invalid IPv4 address: %s`, s)
}
return uint32(ipv4[0])<<24 | uint32(ipv4[1])<<16 | uint32(ipv4[2])<<8 | uint32(ipv4[3]), nil
}
// ToString 将 uint32 的 IPv4 值转为点分十进制字符串。
func ToString(v uint32) string {
return strconv.Itoa(int(v>>24)) + "." +
strconv.Itoa(int(v>>16&0xFF)) + "." +
strconv.Itoa(int(v>>8&0xFF)) + "." +
strconv.Itoa(int(v&0xFF))
}

23
common/tools/md5/md5.go Normal file
View File

@ -0,0 +1,23 @@
// Package md5 提供 MD5 摘要工具。
package md5
import (
"github.com/gogf/gf/v2/crypto/gmd5"
)
// Md5Hex 返回 s 的 MD5 摘要(小写十六进制字符串)。
// 底层错误被忽略,因为对内存输入永远不会失败。
func Md5Hex(s string) string {
h, _ := gmd5.EncryptString(s)
return h
}
// Md5Bytes 返回 data 的 MD5 摘要(小写十六进制字符串)。
func Md5Bytes(data []byte) (string, error) {
return gmd5.Encrypt(data)
}
// Md5File 返回 path 本地文件的 MD5 摘要(十六进制字符串)。
func Md5File(path string) (string, error) {
return gmd5.EncryptFile(path)
}

View File

@ -0,0 +1,26 @@
// Package random 提供随机数与随机字符串生成工具。
package random
import (
"github.com/gogf/gf/v2/util/grand"
)
// Int 返回 [min, max] 区间内的随机整数。
func Int(min, max int) int {
return grand.N(min, max)
}
// String 返回长度为 n 的随机字母数字字符串。
func String(n int) string {
return grand.S(n)
}
// Digits 返回长度为 n 的纯数字随机字符串,如短信验证码。
func Digits(n int) string {
return grand.Digits(n)
}
// Letters 返回长度为 n 的纯字母随机字符串。
func Letters(n int) string {
return grand.Letters(n)
}

View File

@ -0,0 +1,64 @@
// Package slicex 基于标准库提供通用切片工具Go 1.23+)。
// (续上一行)
package slicex
import (
"slices"
)
// Contains 判断 items 中是否包含 v。
func Contains[T comparable](items []T, v T) bool {
return slices.Contains(items, v)
}
// Unique 去除 items 中重复元素,保持首次出现顺序。
func Unique[T comparable](items []T) []T {
seen := make(map[T]struct{}, len(items))
out := make([]T, 0, len(items))
for _, v := range items {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
return out
}
// Chunk 将 items 切分为最多 size 个元素的子切片,
// size <= 0 或 items 为空时返回 nil。
func Chunk[T any](items []T, size int) [][]T {
if size <= 0 || len(items) == 0 {
return nil
}
out := make([][]T, 0, (len(items)+size-1)/size)
for len(items) > 0 {
n := size
if len(items) < n {
n = len(items)
}
out = append(out, items[:n])
items = items[n:]
}
return out
}
// Map 对每个元素应用 fn 并返回结果。
func Map[T, R any](items []T, fn func(T) R) []R {
out := make([]R, len(items))
for i, v := range items {
out[i] = fn(v)
}
return out
}
// Filter 返回 fn 为 true 的元素,保持原顺序。
func Filter[T any](items []T, fn func(T) bool) []T {
out := make([]T, 0, len(items))
for _, v := range items {
if fn(v) {
out = append(out, v)
}
}
return out
}

57
common/tools/strx/strx.go Normal file
View File

@ -0,0 +1,57 @@
// Package strx 基于 gstr 提供字符串工具,含命名
// 转换与敏感数据脱敏。
package strx
import (
"strings"
"github.com/gogf/gf/v2/text/gstr"
)
// SnakeCase 将 s 转为 snake_case如 "UserName" -> "user_name"。
func SnakeCase(s string) string {
return gstr.CaseSnake(s)
}
// CamelCase 将 s 转为 CamelCase如 "user_name" -> "UserName"。
func CamelCase(s string) string {
return gstr.CaseCamel(s)
}
// LowerCamelCase 将 s 转为 lowerCamelCase如 "user_name" -> "userName"。
func LowerCamelCase(s string) string {
return gstr.CaseCamelLower(s)
}
// IsEmpty 判断 s 是否为空或仅含空白字符。
func IsEmpty(s string) bool {
return strings.TrimSpace(s) == ""
}
// MaskPhone 对手机号脱敏,保留前 3 位与后 4 位。
// 例如 "13812345678" -> "138****5678"。
func MaskPhone(s string) string {
if len(s) < 7 {
return s
}
return s[:3] + "****" + s[len(s)-4:]
}
// MaskIDCard 对身份证号脱敏,保留前 6 位与后 4 位,
// 例如 "110101199003074512" -> "110101********4512"。
func MaskIDCard(s string) string {
if len(s) < 10 {
return s
}
return s[:6] + "********" + s[len(s)-4:]
}
// MaskName 对中文姓名脱敏,仅保留首字符。
// e.g. "张三丰" -> "张**".
func MaskName(s string) string {
r := []rune(s)
if len(r) <= 1 {
return s
}
return string(r[0]) + strings.Repeat("*", len(r)-1)
}

View File

@ -0,0 +1,46 @@
// Package timex 基于 gtime 提供时间格式化与计算工具。
package timex
import (
"github.com/gogf/gf/v2/os/gtime"
)
const (
// LayoutDateTime 是常用日期时间格式2006-01-02 15:04:05。
LayoutDateTime = "2006-01-02 15:04:05"
// LayoutDate 是常用日期格式2006-01-02。
LayoutDate = "2006-01-02"
)
// Now 返回当前时间。
func Now() *gtime.Time {
return gtime.Now()
}
// Format 按给定 Go 布局格式化 t
// layout 为空时使用 LayoutDateTime。
//
// 注意gtime v2.10.2 的 Format() 接收 PHP 风格格式("Y-m-d H:i:s"
// 因此本工具改用接受 Go 布局的 Layout() 方法。
func Format(t *gtime.Time, layout ...string) string {
ly := LayoutDateTime
if len(layout) > 0 && layout[0] != "" {
ly = layout[0]
}
return t.Layout(ly)
}
// Timestamp 返回当前 Unix 秒级时间戳。
func Timestamp() int64 {
return gtime.Now().Timestamp()
}
// StartOfDay 返回 t 所在日期的零点00:00:00
func StartOfDay(t *gtime.Time) *gtime.Time {
return gtime.NewFromStr(t.Layout(LayoutDate) + " 00:00:00")
}
// EndOfDay 返回 t 所在日期的末尾23:59:59
func EndOfDay(t *gtime.Time) *gtime.Time {
return gtime.NewFromStr(t.Layout(LayoutDate) + " 23:59:59")
}

21
common/tools/uuid/uuid.go Normal file
View File

@ -0,0 +1,21 @@
// Package uuid 提供唯一 ID 生成工具。
package uuid
import (
"github.com/gogf/gf/v2/util/grand"
"github.com/gogf/gf/v2/util/guid"
)
// New 返回不带连字符的 32 位唯一 ID。
func New() string {
return guid.S()
}
// Short 返回长度为 n 的随机字母数字 IDn <= 0 时默认为 8 位),
// 适合短邀请码 / 追踪 ID 场景。
func Short(n int) string {
if n <= 0 {
n = 8
}
return grand.S(n)
}

23
deploy/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
# service.xpcool.com 生产容器镜像alpine 最小运行时
# .gitea/workflows/deploy.yml 构建本地手动构建亦可
# cd <部署目录> && docker build -f deploy/Dockerfile -t service.xpcool.com:latest .
FROM alpine:latest
# 时区与健康检查工具
RUN apk add --no-cache tzdata curl \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
ENV WORKDIR /app
WORKDIR $WORKDIR
# GoFrame 运行所需二进制 + 配置基础 config.yaml + 环境化 config.prod.yaml+ 静态资源
COPY main $WORKDIR/main
COPY manifest/config $WORKDIR/manifest/config
COPY resource/public $WORKDIR/public
COPY resource/template $WORKDIR/template
RUN chmod +x $WORKDIR/main
EXPOSE 10100
CMD ["./main"]

View File

@ -0,0 +1,273 @@
# Change Log 2026-08-24
## 请求
1. 新增一个公共模块模块下存放 `tools` 功能模块`tools` 下再分各种子功能
2. 建立项目上下文记忆体系记录每次请求与更改方案对比后落地要求后续使用其他 CodeBuddy 账号也能无缝衔接
## 变更
新增文件
- `common/doc.go` 公共模块说明不依赖 internal可独立抽取
- `common/tools/doc.go` tools 模块布局清单与封装规则
- `common/tools/md5/md5.go` MD5 摘要Md5Hex / Md5Bytes / Md5File
- `common/tools/cryptox/cryptox.go` AES-128-CBC / DES-ECB 加解密base64 输出密钥任意长度自动规范化
- `common/tools/uuid/uuid.go` 唯一 IDNew 32 / Short 短随机码
- `common/tools/random/random.go` 随机数/随机串Int / String / Digits / Letters
- `common/tools/timex/timex.go` 时间工具Format / Timestamp / StartOfDay / EndOfDay
- `common/tools/convertx/convertx.go` 类型转换带默认值ToInt / ToInt64 / ToFloat64 / ToString / ToBool
- `common/tools/strx/strx.go` 字符串工具命名转换 SnakeCase/CamelCase + 脱敏 MaskPhone/MaskIDCard/MaskName
- `common/tools/slicex/slicex.go` 泛型切片工具Contains / Unique / Chunk / Map / Filter
- `common/tools/ip/ip.go` IP 工具IsValid / LocalIP / IsInternal / ToLong / ToString
- `common/tools/filex/filex.go` 文件工具Exists / IsDir / ReadString / WriteString
- `AGENTS.md` 项目智能体说明书架构规范命令记忆体系索引
- `docs/change-log/2026-08-24.md` 本文档
修改文件
- `.gitignore` 追加 `.workbuddy/`本机记忆不入库
- `PROJECT_STRUCTURE.md` 目录树补充 `common/` `docs/change-log/`见后续提交
验证`go build ./...` `go vet ./common/...` 全部通过
## 决策与理由
- **公共模块放顶层 `common/` 而非 `internal/common/`**Go internal 包无法被外部模块引用放顶层便于未来抽取为独立库/被同仓库其他服务复用
- **tools 一律薄封装 GoFrame 内置组件**v2.10.2 AES/DES 已拆为 `crypto/gaes``crypto/gdes`UUID `util/guid` `gslicer`用标准库 `slices` 替代避免重复造轮子保持与框架一致
- **记忆体系三层方案**对比见下
1. `AGENTS.md`根目录 长期稳定规范跨工具标准Claude Code/CodeBuddy/Codex 等均识别 git
2. `docs/change-log/YYYY-MM-DD.md` 每次请求变更的结构化记录 git **这是跨账号衔接的关键**
3. `.workbuddy/memory/` WorkBuddy 本机增强不入库
- 对比过 `CLAUDE.md`Claude Code 专属已建议统一为 AGENTS.md`.cursor/rules`Cursor 专属`.codebuddy/` CodeBuddy 读取它们都不是最大公约数故不采用
## 待办与风险
- 后续每次任务完成后更新 `docs/change-log/`当日文件追加+ 提交 git确保其他账号 clone 即恢复上下文
- `convertx` 依赖 gconv "转换失败返回零值"行为无法区分"0"与非法输入需要严格转换的场景应在 service 层先校验
- 本次变更尚未 git 提交建议尽快 commit
---
## 追加17:30方向纠正 公共模块实为公共接口
### 请求
用户澄清要新增的是**给前端调用的公共 HTTP 接口**此前误建成 Go 工具包确认应规划到 `api/` 契约层
### 变更
新增文件
- `api/common/v1/tools.go` 公共接口契约`UUIDReq/Res``MD5Req/Res``RandomReq/Res``TimeReq/Res``IPReq/Res`g.Meta 路由元数据
- `internal/controller/common/tools.go` 控制器实现薄适配层复用 `common/tools/*` Go
修改文件
- `internal/cmd/cmd.go` 新增公开分组 `s.Group("/api/common/v1", ...)`Recover+CORS+HandlerResponse**无鉴权**
- `common/tools/timex/timex.go` 修复`Format` 内部改用 `Layout()` 方法见决策
- `internal/controller/common/tools.go` `Time` `now.Layout(...)`
- `AGENTS.md` 目录树与新增公共接口小节注意事项补 gtime 与包名差异
- `PROJECT_STRUCTURE.md` 目录树补充 `api/common/v1`
验证`go build ./...``go vet` 通过**实际启动服务冒烟测试** 5 个端点全部返回正确含修复后 time 格式化
### 决策与理由
- **路由前缀 `/api/common/v1`** `/api/v1`(user)`/admin/v1`(admin) 平级的独立公开前缀天然不套登录鉴权前端三个端mini/h5/app通用
- **契约层放 `api/common/v1`实现放 `internal/controller/common`** user/admin 完全同构公共接口不需要 service 纯工具计算controller 直接复用 `common/tools` 避免过度分层
- **🐛 gtime v2.10.2 大坑已修复**`Time.Format()` 参数是 **PHP 风格**`"Y-m-d H:i:s"` Go layout`"2006-01-02 15:04:05"`会原样输出Go layout 必须用 `Time.Layout()``common/tools/timex` 已统一封装为 `Layout` 语义调用方直接 `timex.Format(t, layout...)` 即可
- **冒烟测试教训**`go run` spawn 子进程`kill %1` 只杀包装进程残留的 `main.exe` 会继续占用 8000 端口导致后续测试打到旧代码杀进程需 `netstat -ano | grep :8000` PID `Stop-Process`
### 待办与风险
- 本次追加变更同样未提交 git与上文合并为一次 commit
- 公共接口已开放无鉴权能力md5/random/uuid 后续新增接口时需评审是否应限流/加签名避免被滥用
---
## 追加18:05开放接口命名决策 + API 目录重构
### 请求
1. 变更 API 目录设计契约层改为 `tools/<子功能>/index.go` 结构示例 `/api/common/v1/tools/ocr/index.go`
2. 同步修改所有涉及处控制器路由
3. 把命名规则记入项目记忆
4. 咨询公共接口一般用 common 命名吗有没有更好的
### 变更
新增文件契约层按子功能拆目录
- `api/open/v1/tools/doc.go` tools 目录结构规则说明
- `api/open/v1/tools/uuid/index.go``md5/index.go``random/index.go``time/index.go``ip/index.go` 每子功能一个目录index.go `package <子功能名>`g.Meta tags `Open/Tools`
- `internal/controller/open/controller.go` Controller 结构 + New()
- `internal/controller/open/uuid.go``md5.go``random.go``time.go``ip.go` 按子功能拆文件import 对应契约子包
删除文件
- `api/common/v1/tools.go``api/common/` 目录
- `internal/controller/common/tools.go``internal/controller/common/` 目录
修改文件
- `internal/cmd/cmd.go` import `commonctl``openctl`路由 `/api/common/v1``/api/open/v1`注释改 Open tools API
- `AGENTS.md` 目录树更新公共接口小节改为开放接口api/open/v1与命名规则记录 open 命名决策与 tools 子功能目录规则
- `PROJECT_STRUCTURE.md` 目录树同步
验证`go build ./...``go vet` 通过冒烟测试旧路由 `/api/common/v1/tools/time` 返回 404新路由 `/api/open/v1/tools/*` 5 端点全部正确
### 决策与理由
- **命名 common open**用户询问"公共接口一般用 common 吗"对比后选 **open**业界开放接口惯例如支付宝 /open/api语义强调对外暴露无鉴权`public` 为并列备选更强调"公开"`common` 偏内部通用语义弃用命名规则已记入 AGENTS.md开放接口小节
- **契约层目录规则**`api/open/v1/tools/<子功能名>/index.go` 每子功能一目录 GoFrame 惯例api 下按功能分包一致用户示例 ocr 即为后续子功能如未来加 OCR 识别接口即建 `tools/ocr/index.go`控制器 `internal/controller/open/<子功能名>.go` 对应拆文件 package open
- **路由绑定不受目录重构影响**GoFrame 通过 controller 方法参数反射定位 g.Meta契约包拆成多个子包uuid/md5/random/time/ip不影响 `group.Bind(openctl.New())` 自动注册cmd.go 只需改前缀
- 包名 `time`api/open/v1/tools/time与标准库 time 潜在同名controller 中统一用 import 别名 `timeapi` 规避
### 待办与风险
- 前端若已联调旧 `/api/common/v1` 路径需同步改 `/api/open/v1`当前无线上前端风险低
- 新增子功能记得更新 `api/open/v1/tools/doc.go` 的示例清单
---
## 追加18:08契约文件 index.go <功能名>.go
### 请求
用户咨询"子功能目录内用 index.go 还是 <功能名>.go 更易扩展维护"确认推荐后执行改动
### 变更
- `git mv` 重命名 5 个契约文件`tools/{uuid,md5,random,time,ip}/index.go` `tools/{uuid,md5,random,time,ip}/{uuid,md5,random,time,ip}.go`包内容不变
- `api/open/v1/tools/doc.go` 规则说明改为目录名=包名=文件名三一致补充"功能变大后目录内加文件"的扩展指引
- `AGENTS.md` / `PROJECT_STRUCTURE.md` 同步 index.go 引用为 <name>.go
验证`go build ./...``go vet` 通过冒烟测试 5 端点全部正常
### 决策与理由
- ** <功能名>.go 而非 index.go** 目录名=包名=文件名三一致导航直观 index.go "入口"语义功能膨胀后出现 `index.go + idcard.go` 混排会失去入口意义 `<name>.go + idcard.go` 自然 符合 Go 生态主流strings/strings.go GoFrame 官方模板api/user/v1/user.go
- **扩展路径已定型**子功能从 1 个端点到多个端点只需在目录内加文件 ocr 目录 `ocr.go → + idcard.go + invoice.go`无需重构文件名
### 待办与风险
- 无新增风险后续新增子功能统一按 `tools/<name>/<name>.go` 建文件
---
## 追加22:40后台管理功能开发后端完成
### 请求
使用 goframe-v2 开发登录菜单按钮级权限等常规后台管理功能前端 vben5 对接backend 动态路由模式登录后台后开发服务器日志管理功能
### 变更commit 8fdb25e35 文件
- `manifest/sql/003_schema_ext.sql`admin_menu `icon/component/hidden`
- `manifest/sql/004_seed.sql`初始 admin/admin123super_admin 角色22 条菜单含按钮权限码角色/用户绑定
- `manifest/sql/005_menu_paths.sql`按钮行 path `"METHOD /路径"` 接口映射{id} 动态段
- auth`/auth/info``/auth/codes`路由拆分公开(NewAuth)/仅登录(NewProfile)/受保护(New)三组新增 `AdminAuthOnly` 中间件
- menu`/menu/routes` 返回 vben backend 动态路由树按角色过滤+排序
- RBAC`/admins``/roles``/menus/tree` CRUD含角色绑定重置密码菜单授权删除校验子节点
- log`/log/files``/log/tail`反向块扫描读尾部+关键词过滤+路径穿越防护
- 安全接口鉴权改为方法+路径权限码自动映射`PermissionForPath`+`matchRoute`**不再信任前端 X-Permission**
- 修复gf v2.10.2 `${ENV}` 不自动替换 cmd.injectEnv 注入MySQL driver blank import `contrib/drivers/mysql/v2`gtime Format(PHP) Layout(Go) 区分再次踩坑CreatedAt 输出 layout 原样
- `log/` 加入 .gitignoreconfig.dev.yaml logger.path=log日志落盘
### 决策与理由
- **接口鉴权用路径映射而非 X-Permission**原实现用户可用自己拥有的任意权限码访问任意受保护接口越权漏洞改为后端按 method+path admin_menu 映射未配置即拒绝
- **受保护接口分三层**公开 loginAdminAuthOnlyinfo/codes/routes登录即可取vben 登录后立即调用AdminAuthRBAC/日志
- **admin_menu 按钮行 path 存接口映射** type=1 菜单行的路由 path 语义区分开避免冲突
- **gf gen dao 不可用**本机 gf CLI 为公司定制版生成 com.lib.gf.v2 import与项目官方 gf 不兼容本次手动补齐 admin_menu 三字段entity/do/table后续换官方 CLI 或脚本化处理
### 待办与风险
- **前端对接vben5**admin.xpcool.com 需配置 accessMode=backend登录/信息/权限码/动态路由对接系统管理三页面+日志监控页面按钮级 v-access:code
- 冒烟测试已建 opuser/op 测试角色可清理
- 菜单管理接口的 assignMenu 权限码暂无独立接口角色授权在 role update 中完成保留扩展位
- 生产环境 JWT_SECRET/DB_DSN 必须通过环境变量提供${ENV} 不会自动替换
---
## 追加23:05前端 vben 对接源码完成本地启动验证受阻
### 请求
继续做 vben 前端admin.xpcool.comvben 5.7.0 monorepo对接后端登录/菜单/按钮级权限/RBAC/日志监控
### 变更前端独立仓库 commit 4615e7c
- `.env.development``VITE_GLOB_API_URL` 置空关闭 mock
- `vite.config.ts`代理 `/admin``/api` `http://localhost:8000`
- `preferences.ts``accessMode: 'backend'`动态路由关闭 token 自动刷新
- `api/core/auth.ts`登录/权限码路径改 `/admin/v1/...`codes 解包 `data.codes`
- `api/core/user.ts``/admin/v1/auth/info` 字段映射adminIduserIdnicknamerealNamehomePath
- `api/core/menu.ts``/admin/v1/menu/routes` 解包 `data.routes`
- 新增 `api/system.ts``api/log.ts`
- 新增页面`views/system/admin|role|menu/index.vue`CRUD+按钮级权限`views/monitor/log/index.vue`文件列表/tail/关键词过滤/自动刷新
### 决策与理由
- **后端 component `system/admin/index` vben 映射**vben `normalizeViewPath` 会去前缀补前导 `/` `/views`最终匹配 `views/**/*.vue`无需 `.vue` 后缀
- **前端请求路径写完整 `/admin/v1/...` + apiURL 置空** user(`/api/v1`)open(`/api/open/v1`)admin(`/admin/v1`) 前缀不同写全路径最清晰避免 proxy rewrite 混乱
- **字段映射在后端/前端约定**后端返回 `adminId/nickname`前端映射为 vben `UserInfo(userId/realName)`
### 待办与风险本地启动验证受阻
- **node 环境**system node 23.0.0 未编译 `node:sqlite``ERR_UNKNOWN_BUILTIN_MODULE`pnpm 11.16.0 依赖它已用 managed node 22.22.2 + corepack wrapper 绕过
- **`pnpm install` 卡住**已配 `.npmrc`npmmirror 镜像 + `node-linker=hoisted` + `store-dir=E:/.pnpm-store` install "added 27→98" 反复循环疑似某 native 依赖 postinstall 失败重试**dev server 未能启动验证**
- 后续步骤 定位卡住的依赖`pnpm install --reporter=append-only` 看具体包 或跳过 postinstall`pnpm install --ignore-scripts` 后手动补 esbuild 等二进制 完成 install `pnpm dev:antd` 启动浏览器验证登录/菜单/权限/日志
---
## 追加23:40API base/system/admin 分组重构
### 请求
优化 API 层设计`/api/admin/v1/base`基础常规`/api/admin/v1/system`menu/role/auth 系统管理`/api/admin/v1/admin`后台管理并同步 service/controller
### 变更
- `api/admin/v1/` 重组为三子包`base/log.go`日志`system/{auth,menu,menu_manage,role}.go`认证+菜单+角色`admin/admin.go`管理员
- 路由前缀变更`/auth/*``/system/auth/*``/menu/routes``/system/menu/routes``/menus``/system/menu``/roles``/system/role``/admins``/admin``/log/*``/base/log/*`
- `internal/controller/admin/*.go` import 对应子包basev1/systemv1/adminv1
- `manifest/sql/006_menu_paths_v2.sql`按钮-接口 path 映射更新`system:role:assignMenu` 无独立接口path 清空
- 冒烟测试全通过新路径 8 接口正常旧路径 `/admins` 返回 Not Found
### 决策与理由
- **权限码与路由分离**permission 保持 `system:admin:list` 等逻辑标识不变只改物理路由 path管理员管理路由在 `/admin/v1/admin` 但权限码仍 `system:admin:*`逻辑归属系统权限体系
- **service/dao 层不硬拆子包**service 保持 `internal/service` 单包按领域接口组织GoFrame 惯例dao 为生成代码按表组织api/controller 体现 base/system/admin 分组即可
- `assignMenu` 权限码保留前端按钮但无独立后端接口授权合并进 role updatepath 置空不映射
### 待办与风险
- 前端 API 路径需同步为 `/admin/v1/{base,system,admin}` 前缀当前前端代码仍是旧路径
---
## 追加23:55前端统一 tdesign UI + 清理无用 app
### 请求
1. 前端 vben5 采用 tdesign UI 检查整个前端项目清理用不到的目录
2. 关联后端 API base/system/admin 分组重构见上一条 43fa796
### 变更前端仓库 commit 6725151
- 移除 `apps/web-antd|web-ele|web-naive|web-antdv-next` `backend-mock`仅保留 `web-tdesign`此前用户已在工作区删除本次一并提交
- `web-tdesign` 重新对接后端`.env.development`apiURL 置空/ mock`vite.config.ts`代理 /admin,/api8000`preferences.ts`accessMode=backend/ token 刷新
- `api/core/{auth,user,menu}.ts` + `api/{system,log}.ts`路径对齐 base/system/admin 分组
- 页面用 tdesign-vue-next 重写`views/system/{admin,role,menu}/index.vue``views/monitor/log/index.vue`
### 决策与理由
- **UI 统一 tdesign**web-tdesign vben 官方 tdesign 应用页面组件从 ant-design-vue 换成 tdesign-vue-nextt-table/t-dialog/t-tree/MessagePlugin/DialogPlugin
- **权限码与路由分离后端**permission 保持 `system:admin:list` 逻辑标识路由改 `/admin/v1/admin`管理员管理在 admin 分组但权限码仍 system:*逻辑归属系统权限体系
### 待办与风险
- `pnpm install` 卡住问题仍未解决node:sqlite 已绕过 install native 依赖 postinstall 反复循环dev server 未启动验证
- 后端已删测试角色 opuser/op 可清理前端路径已同步 base/system/admin dev 启动后联调验证

View File

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

223
docs/house-system-design.md Normal file
View File

@ -0,0 +1,223 @@
# 看房系统House总设计文档
> 版本v1.0 | 日期2026-08-26 | 状态开发中阶段 0/1
> 定位贵阳购房决策辅助系统从公开房源数据中筛选最适合自己的房子
## 1. 项目概述
买房的核心矛盾是信息高度不对称本系统通过 **采集 存储 管理 可视化 推送** 五段闭环把贵阳公开房源数据沉淀成一份可管理可分析可对比的个人决策资产
- **目标城市**贵阳二线/省会数据源以贝壳/安居客/房天下 + 住建局网签为主
- **房源类型**新房 + 二手房都做两套数据模型并存
- **核心诉求**房价波动分析楼盘楼栋级地图多平台报价对比个性化打分推荐Bark 推送
## 2. 总体架构
### 2.1 五层架构
```
应用展示层 房价地图热力 · 楼盘对比看板 · 个性化榜单
分析计算层 价格趋势分析 · 匹配打分排序 · 通勤配套评估
数据治理层 清洗与标准化 · 房源去重对齐(:软关联) · 小区实体映射
数据存储层 MySQL(house_* ) · 价格时序快照
数据采集层 多平台采集器 · 调度与限频 · 代理与反反爬
```
### 2.2 落地拓扑贴合现有项目
```
贵阳公开数据源(贝壳/安居客/房天下/住建局网签)
抓取
Python 采集分析服务(独立进程: 采集/清洗/软关联/打分/调度)
写库
MySQL 共享库(house_* ) service 同库
读写
service.xpcool.com(GoFrame v2 · house 模块 REST API)
REST admin.xpcool.com(Vue3+Vben: 管理列表 + 可视化看板)
推送 Bark(苹果)
```
**关键决策**Python 只负责把数据搞干净写进库 + 算分不对外提供业务 API所有查询/推送由 Go house 模块统一暴露保持单一出口
## 3. 技术选型
| | 选型 | 理由 |
|----|------|------|
| 后端 | GoFrame v2.10现有 service | 复用分层 + RBAC + gf gen dao |
| 数据库 | MySQL 8现有同库 | 贵阳数据量级房源几十万/快照百万单机够用不引 PostGIS/TimescaleDB |
| 前端 | Vben Admin 5web-tdesign+ echarts@6 + vxe-table@4 | 全部现成依赖零新增 |
| 地图 | 腾讯地图 GL JS合规+ DataV GeoJSON | 底图合规支持 MultiMarker/热力/多边形 |
| 采集分析 | PythonScrapy/httpx + pandas | 爬虫与数据分析生态最强 |
| 推送 | Bark自建 Server 到腾讯云 / 官方免费版 | 苹果原生推送一条 HTTP 即可 |
## 4. 数据源规划贵阳
| 类别 | 数据源 | 备注 |
|------|--------|------|
| 二手房挂牌 | 贝壳 gy.ke.com安居客房天下58 | 贝壳最规整MVP 首选 |
| 成交/网签 | 贵阳市住建局网签备案贝壳成交频道 | **成交价是真实价最大风险点** |
| 新房备案价 | 住建局预售许可 + 一房一价备案 | 新房真价格来源 |
| 配套/通勤 | 高德/百度地图 API | 地铁学校商圈 POI + 真实通勤时间 |
| 学区划片 | 贵阳市/各区教育局划片文件 | 年度版本半自动采集 |
## 5. 数据模型9 张表
### 5.1 house_community 小区/楼盘
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT UNSIGNED PK | |
| name | VARCHAR | 小区名 |
| region | VARCHAR | 区县云岩/南明/观山湖/花溪 |
| business_district | VARCHAR | 板块 |
| address / lng / lat | VARCHAR / DECIMAL(10,6) | 定位GCJ-02 |
| build_year / households | INT | 建成年份/户数 |
| plot_ratio / green_rate | DECIMAL | 容积率/绿化率 |
| property_company / property_fee | VARCHAR / DECIMAL | 物业/物业费 |
| developer | VARCHAR | 开发商 |
### 5.2 house_building 楼栋
| 字段 | 说明 |
|------|------|
| community_id | 所属小区 |
| building_no | 栋号 |
| units / total_floors / elevator_count / ladder_ratio | 单元/总层/电梯/梯户比 |
| building_type | 板楼/塔楼 |
| lng / lat | 楼栋级坐标三级落地小区中心楼栋图解析重点盘人工校准 |
### 5.3 house_listing 房源/挂牌核心
| 字段 | 说明 |
|------|------|
| community_id / building_id / house_no | 归属 |
| layout / area / usable_area | 户型 / 建面 / 套内**建面套内要标准化** |
| orientation / floor / total_floors / decoration | 朝向/楼层/总层/装修 |
| total_price / unit_price / list_price | 总价/单价/挂牌价 |
| **source / source_house_id / source_url** | 来源平台**跨平台不去重** |
| **match_group_id** | 疑似同房源软关联对比用 |
| on_market_days / price_change_count | 挂牌天数/调价次数 |
| status | 在售/下架/成交 |
| confidence / is_bargain | 可信度标记 / 笋盘标记 |
> 多平台对比策略同平台 `source+source_house_id` 唯一防重复抓跨平台各存一条小区+楼栋+户型+面积±3%+楼层算相似度打 `match_group_id`用于疑似同房源对比视图不合并
### 5.4 house_price_snapshot 价格快照时序
`listing_id + snap_date` 唯一`list_price` / `deal_price` 分列**趋势分析命脉长期保留 12 **
### 5.5 house_transaction 成交记录
`deal_price` / `deal_unit_price` / `list_days`挂牌到成交天数/ `deal_date`
### 5.6 house_facility 配套 POI
`name` / `type`(地铁/学校/医院/商圈) / `lng/lat` / `line`(地铁线路)
### 5.7 house_community_facility 小区-配套关系
`community_id + facility_id``distance`() + `commute_minutes`(通勤分钟)
### 5.8 house_school_district 学区划片
`school_name` / `community_id` / `district_polygon`(GeoJSON) / `district_year`(划片年度版本化)
### 5.9 house_preference 用户偏好画像
`budget_min/max` / `area_min/max` / `layouts`(JSON) / `subway_lines`(JSON) / `school_required` / `commute_target` / `commute_limit_min` / `weights`(权重 JSON)
## 6. 后端模块设计service.xpcool.com
### 6.1 目录落位
```
api/house/<resource>/<resource>.go # 契约(g.Meta path/method)
internal/controller/house/*.go # 适配层
internal/service/house/<resource>/ # 领域服务(接口+实现+Register)
internal/model/dto/house.go # 服务边界 dto
internal/model/{entity,do} # gf gen dao 生成
manifest/sql/010_house_tables.sql # 建表
manifest/sql/011_house_menu.sql # 菜单+权限种子
```
### 6.2 接口清单 POST 动作式前缀 /api/service/admin/house
**管理 CRUD**
```
/house/community/{list|create|update|delete}
/house/listing/{list|create|update|delete|batch-mark}
/house/snapshot/{list}
/house/transaction/{list}
/house/facility/{list|create|update|delete}
/house/district/{list|create|update|delete}
/house/crawl-task/{list|trigger|log}
```
**看板聚合/筛选**统一 `FilterDto` 入参
```
/house/dashboard/{overview|map-points|price-trend|aggregate-region}
/house/compare
/house/rank
```
### 6.3 权限
`admin_menu`type=1 菜单component 指向前端组件+ type=2 API 权限path=`POST /api/service/admin/house/...``admin_role_menu` 绑定超管role_id=1
## 7. 前端模块设计admin.xpcool.com
### 7.1 页面
| 分组 | 页面 | 组件路径 |
|------|------|---------|
| 数据管理 | 小区/楼盘管理 | house/community/index |
| 数据管理 | 房源管理筛选/标记/批量 | house/listing/index |
| 数据管理 | 价格快照/成交/配套/学区 | house/data/index后续拆分 |
| 可视化看板 | 看板筛选器+图表联动 | house/dashboard/index |
### 7.2 交互
- 管理列表查询表单 + vxe-table + 分页 + 行内操作 + 批量复用 RBAC
- 看板全局筛选器区域/价格/户型/面积/地铁/学区/通勤驱动图表联动筛选器图表图表交叉过滤列表地图双向状态放 Pinia
## 8. Python 采集分析服务
```
house-data/
crawlers/ 各平台采集器
pipeline/ 清洗软关联标准化入库
analysis/ pandas 趋势/议价空间/打分
scheduler/ APScheduler 调度 + 限频
notify/ 触发 Bark
```
只写库不对外 API独立 git 仓库建议 E:\xxcool\project\house-data\
## 9. 可视化设计地图图层
底图腾讯地图 GL JS自下而上叠加区县/板块边界(DataV GeoJSON) 地铁线(1/2/3号线 Polyline) 学区划片(polygon年度版本) 楼盘/楼栋点(MultiMarker) 价格热力(可切换)
## 10. 推送设计Bark
| 触发 | level | group | url |
|------|-------|-------|--------|
| 降价>3% | timeSensitive | 降价 | 跳房源对比页 |
| 新房上架 | active | 新房 | 跳详情 |
| 划片变更 | timeSensitive | 学区 | 跳小区 |
| 每日汇总 | passive | 汇总 | 跳看板 |
## 11. 分阶段路线图
| 阶段 | 周期 | 交付 |
|------|------|------|
| 0 方案定稿 | 12 | 建表 + gf gen dao + Python 骨架 |
| 1 MVP | ~1 | 贝壳二手房挂牌 + 价格快照房源列表/详情 API列表 + 趋势图 |
| 2 分析 | ~1 | 成交/网签 + 软关联对比 + 地图热力 + 楼盘对比 |
| 3 决策 | ~1 | 新房备案价 + 学区/配套 + 偏好打分 |
| 4 自动化 | ~1 | Bark 推送 + 迁腾讯云 7×24 |
## 12. 合规与风险
- 地图仅腾讯/高德/百度/天地图区县边界用 DataV 审图号数据key 走代理不外泄不采集他人个人位置
- 爬虫遵守 robots低频代理池只存公开信息个人自用
- **最大风险**贵阳网签/成交数据公开程度不如一线MVP 用贝壳成交频道兜底

7
go.mod
View File

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

4
go.sum
View File

@ -15,6 +15,10 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2 h1:UdUV+7GhwYLpkwz7VrwIVO/1ZYodyzSL5is25NET24A=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2/go.mod h1:eKc+0i3Il7efS2BBjmpy7T9wvN9NGRd67ZV94r9behA=
github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8= github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
github.com/gogf/gf/v2 v2.10.2/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= github.com/gogf/gf/v2 v2.10.2/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=

View File

@ -4,8 +4,9 @@
gfcli: gfcli:
gen: gen:
dao: dao:
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" - link: "mysql:root:root123@tcp(127.0.0.1:3306)/service_xpcool_com"
descriptionTag: true descriptionTag: true
tables: "house_community,house_building,house_listing,house_price_snapshot,house_transaction,house_facility,house_community_facility,house_school_district,house_preference,house_presale"
docker: docker:
build: "-a amd64 -s linux -p temp -ew" build: "-a amd64 -s linux -p temp -ew"

View File

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

View File

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

View File

@ -5,48 +5,169 @@ import (
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"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/gcmd" "github.com/gogf/gf/v2/os/gcmd"
"github.com/gogf/gf/v2/os/genv"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
adminctl "service.xpcool.com/internal/controller/admin" adminctl "service.xpcool.com/internal/controller/admin"
"service.xpcool.com/internal/controller/hello" housectl "service.xpcool.com/internal/controller/house"
jobctl "service.xpcool.com/internal/controller/job"
noticectl "service.xpcool.com/internal/controller/notice"
openctl "service.xpcool.com/internal/controller/open"
recruitmentctl "service.xpcool.com/internal/controller/recruitment"
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/jwt" "service.xpcool.com/internal/library/jwt"
"service.xpcool.com/internal/middleware" "service.xpcool.com/internal/middleware"
"service.xpcool.com/internal/service" admin "service.xpcool.com/internal/service/admin/admin/admin"
adminauth "service.xpcool.com/internal/service/admin/admin/login"
log "service.xpcool.com/internal/service/admin/base/log"
adminaudit "service.xpcool.com/internal/service/admin/system/log"
adminloginlog "service.xpcool.com/internal/service/admin/system/login_log"
adminmenu "service.xpcool.com/internal/service/admin/system/menu"
menu_manage "service.xpcool.com/internal/service/admin/system/menu_manage"
role "service.xpcool.com/internal/service/admin/system/role"
housecommunity "service.xpcool.com/internal/service/house/community"
housedashboard "service.xpcool.com/internal/service/house/dashboard"
houselisting "service.xpcool.com/internal/service/house/listing"
housepresale "service.xpcool.com/internal/service/house/presale"
housetransaction "service.xpcool.com/internal/service/house/transaction"
jobsvc "service.xpcool.com/internal/service/job"
noticesvc "service.xpcool.com/internal/service/notice"
recruitmentsvc "service.xpcool.com/internal/service/recruitment"
serversecuritysvc "service.xpcool.com/internal/service/serversecurity"
userauth "service.xpcool.com/internal/service/user/auth"
) )
// 开发环境默认值任何启动方式GoLand / 命令行 / go run漏注入环境变量时兜底
// 避免 ${DB_DSN} 占位符原样传入 gdb 导致全站接口 500。
// 生产GF_GCFG_ENV=prod不启用兜底配置缺失应显性报错。
//
// 注意:库名以本机实际建库为准(本项目为 service / recruitment
// 若你的本地库名或账号口令不同,请通过 DB_DSN / RECRUITMENT_DB_DSN 覆盖。
const (
devDefaultDBDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/service?loc=Local"
devDefaultRecruitmentDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/recruitment?loc=Local"
devDefaultJWTSecret = "dev-only-secret-not-for-production"
)
// setIfEmpty 仅当 env 存在时写入配置优先级env > 配置文件默认值)。
func setIfEmpty(adapter *gcfg.AdapterFile, key, envName string) {
if v := genv.Get(envName); !v.IsEmpty() {
_ = adapter.Set(key, v.String())
}
}
// injectEnv 手动把关键环境变量写入配置系统。
// 注意gf v2.10.2 起配置不再自动替换 ${ENV} 占位符,需在此显式注入,
// 否则 config.dev.yaml 中的 ${DB_DSN}/${JWT_SECRET} 会原样传给数据库与 JWT。
// 开发环境下若 env 缺失,回填本地默认值,保证「零配置可启动」。
func injectEnv(ctx context.Context) {
adapter, ok := g.Cfg().GetAdapter().(*gcfg.AdapterFile)
if !ok {
return
}
isProd := genv.Get("GF_GCFG_ENV").String() == "prod"
if !isProd {
// 开发兜底:仅当配置项仍是未解析的 ${...} 占位符时才写默认值,
// 不覆盖 .env.dev / GoLand 运行配置注入的真实值。
if v, _ := adapter.Get(ctx, "database.default.link"); v != nil && gstr.Contains(gconv.String(v), "${") {
_ = adapter.Set("database.default.link", devDefaultDBDSN)
}
if v, _ := adapter.Get(ctx, "database.recruitment.link"); v != nil && gstr.Contains(gconv.String(v), "${") {
_ = adapter.Set("database.recruitment.link", devDefaultRecruitmentDSN)
}
if v, _ := adapter.Get(ctx, "jwt.secret"); v != nil && gstr.Contains(gconv.String(v), "${") {
_ = adapter.Set("jwt.secret", devDefaultJWTSecret)
}
}
setIfEmpty(adapter, "database.default.link", "DB_DSN")
setIfEmpty(adapter, "jwt.secret", "JWT_SECRET")
// 招聘模块独立数据库与 Bark 推送配置(自建 Bark 服务)。
setIfEmpty(adapter, "database.recruitment.link", "RECRUITMENT_DB_DSN")
setIfEmpty(adapter, "bark.baseUrl", "BARK_BASE_URL")
setIfEmpty(adapter, "bark.deviceKey", "BARK_DEVICE_KEY")
setIfEmpty(adapter, "bark.pushTime", "BARK_PUSH_TIME")
// 服务器安全日志上报令牌(宿主机采集脚本携带,接口侧校验)。
setIfEmpty(adapter, "internalToken", "INTERNAL_TOKEN")
// 登录加密开关(直接跑二进制始终加载 config.yaml需 env 显式注入):
// ENCRYPT_FULL_BODY=true 启用全量请求/响应加密生产ENCRYPT_ALLOW_PLAIN=true 仅开发联调。
setIfEmpty(adapter, "encrypt.fullBody", "ENCRYPT_FULL_BODY")
setIfEmpty(adapter, "encrypt.allowPlain", "ENCRYPT_ALLOW_PLAIN")
}
var ( var (
Main = gcmd.Command{ Main = gcmd.Command{
Name: "main", Name: "main",
Usage: "main", Usage: "main",
Brief: "start http server", Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
injectEnv(ctx)
s := g.Server() s := g.Server()
tokens := jwt.New(ctx) tokens := jwt.New(ctx)
service.RegisterUserAuth(service.NewUserAuth(tokens, nil, nil)) // 登录密码混合加密服务RSA + AES-GCM私钥加载/生成失败属致命错误,直接终止启动。
service.RegisterAdminAuth(service.NewAdminAuth(tokens)) cryptoSvc, err := crypto.New(ctx)
service.RegisterAdminAudit(service.NewAdminAudit()) if err != nil {
s.Group("/", func(group *ghttp.RouterGroup) { panic(err)
group.Middleware(middleware.Recover, middleware.CORS) }
group.Middleware(ghttp.MiddlewareHandlerResponse) userauth.RegisterUserAuth(userauth.NewUserAuth(tokens, nil, nil))
group.Bind( adminauth.RegisterAdminAuth(adminauth.NewAdminAuth(tokens, cryptoSvc))
hello.NewV1(), adminmenu.RegisterAdminMenu(adminmenu.NewAdminMenu())
) admin.RegisterAdminManage(admin.NewAdminManage())
}) role.RegisterRoleManage(role.NewRoleManage())
s.Group("/api/v1", func(group *ghttp.RouterGroup) { menu_manage.RegisterMenuManage(menu_manage.NewMenuManage())
log.RegisterLogManage(log.NewLogManage())
adminaudit.RegisterAdminAudit(adminaudit.NewAdminAudit())
adminloginlog.RegisterAdminLoginLog(adminloginlog.NewAdminLoginLog())
housecommunity.RegisterCommunity(housecommunity.NewCommunity())
houselisting.RegisterListing(houselisting.NewListing())
housedashboard.RegisterDashboard(housedashboard.NewDashboard())
housetransaction.RegisterTransaction(housetransaction.NewTransaction())
housepresale.RegisterPresale(housepresale.NewPresale())
recruitmentsvc.RegisterRecruitment(recruitmentsvc.New())
noticesvc.RegisterNotice(noticesvc.New())
jobsvc.RegisterJob(jobsvc.New())
serversecuritysvc.RegisterServerSecurity(serversecuritysvc.New())
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(userctl.New()) // Login and refresh routes are public. group.Bind(openctl.New()) // 开放工具接口(前端调用,免鉴权)。
group.Bind(serversecurityctl.NewReport()) // 安全日志上报(宿主机脚本,内部令牌校验)。
})
s.Group("/api/service/user", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse)
group.Bind(userctl.New()) // 登录与刷新令牌接口公开。
group.Group("/", func(protected *ghttp.RouterGroup) { protected.Middleware(middleware.UserAuth(tokens)) }) group.Group("/", func(protected *ghttp.RouterGroup) { protected.Middleware(middleware.UserAuth(tokens)) })
}) })
s.Group("/admin/v1", func(group *ghttp.RouterGroup) { s.Group("/api/service/admin", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) // 全量请求/响应加密(生产 encrypt.fullBody=trueCORS → APICrypto → Recover → HandlerResponse。
group.Bind(adminctl.New()) // Admin login remains public; protected controllers mount separately. // Recover 置于 APICrypto 内层,使 500 异常响应同样被加密public-key 端点明文豁免。
group.Middleware(middleware.CORS, middleware.APICrypto(cryptoSvc), middleware.Recover, ghttp.MiddlewareHandlerResponse)
group.Bind(adminctl.NewAuth()) // 公开端点:仅登录。
group.Group("/", func(profile *ghttp.RouterGroup) {
// 仅登录端点:个人资料 / 权限码 / 菜单路由。
profile.Middleware(middleware.AdminAuthOnly(tokens))
profile.Bind(adminctl.NewProfile())
})
group.Group("/", func(protected *ghttp.RouterGroup) { group.Group("/", func(protected *ghttp.RouterGroup) {
protected.Middleware(middleware.AdminAuth(tokens, service.AdminAuth().HasPermission, func(ctx context.Context, id uint64, permission, method, path, ip, param string, duration, status int) { // 受权限保护端点RBAC 管理、日志等。
service.AdminAudit().Record(ctx, service.AuditEvent{AdminID: id, Permission: permission, Method: method, Path: path, IP: ip, Param: param, DurationMS: duration, StatusCode: status}) // 权限由后端按「方法+路径」自动匹配,无需前端传 X-Permission。
protected.Middleware(middleware.AdminAuth(tokens, adminauth.AdminAuth().PermissionForPath, adminauth.AdminAuth().HasPermission, func(ctx context.Context, id uint64, permission, method, path, ip, param string, duration, status int, userAgent, errorMessage string) {
adminaudit.AdminAudit().Record(ctx, adminaudit.AuditEvent{AdminID: id, Permission: permission, Method: method, Path: path, IP: ip, Param: param, DurationMS: duration, StatusCode: status, ErrorMessage: errorMessage, UserAgent: userAgent})
})) }))
protected.Bind(adminctl.New())
protected.Bind(housectl.New())
protected.Bind(recruitmentctl.New())
protected.Bind(noticectl.New())
protected.Bind(jobctl.New())
protected.Bind(serversecurityctl.NewManage())
}) })
}) })
// 启动自动任务调度器:招聘模块先把任务注册进来,再由 job 模块按 DB 配置统一调度。
recruitmentsvc.RegisterTasks()
jobsvc.Job().StartScheduler(ctx)
s.Run() s.Run()
return nil return nil
}, },

View File

@ -0,0 +1,93 @@
package admin
import (
"context"
adminv1 "service.xpcool.com/api/admin/admin/admin"
"service.xpcool.com/internal/consts"
cryptolib "service.xpcool.com/internal/library/crypto"
"service.xpcool.com/internal/library/response"
"service.xpcool.com/internal/model/dto"
admin "service.xpcool.com/internal/service/admin/admin/admin"
)
// resolvePassword 解析创建/重置管理员时的密码:
// - 全量加密模式fullBody传输层已整体解密直接使用明文 password
// - 仅密码加密模式密文encryptedKey+encryptedDataRSA+AES 混合加密)优先;明文仅开发 allowPlain。
func resolvePassword(ctx context.Context, encryptedKey, encryptedData, password string) (string, error) {
if cryptolib.Get().FullBody() {
return password, nil
}
if encryptedKey != "" && encryptedData != "" {
v, err := cryptolib.Get().DecryptField(ctx, encryptedKey, encryptedData)
if err != nil {
return "", response.Error(consts.CodeInvalidParam, err.Error())
}
return v, nil
}
if cryptolib.Get().AllowPlain() {
return password, nil
}
return "", response.Error(consts.CodeInvalidParam, "必须提交加密凭据")
}
// AdminList 分页查询管理员。
func (c *Controller) AdminList(ctx context.Context, req *adminv1.AdminListReq) (res *adminv1.AdminListRes, err error) {
items, total, err := admin.AdminManage().List(ctx, dto.PageQuery{Page: req.Page, Size: req.Size, Keyword: req.Keyword})
if err != nil {
return nil, err
}
list := make([]*adminv1.AdminItem, 0, len(items))
for _, it := range items {
list = append(list, &adminv1.AdminItem{
Id: it.Id, Username: it.Username, Nickname: it.Nickname, Status: it.Status,
RoleIds: it.RoleIds, RoleNames: it.RoleNames, CreatedAt: it.CreatedAt,
})
}
return &adminv1.AdminListRes{List: list, Total: total}, nil
}
// AdminCreate 创建管理员。
func (c *Controller) AdminCreate(ctx context.Context, req *adminv1.AdminCreateReq) (res *adminv1.AdminCreateRes, err error) {
password, err := resolvePassword(ctx, req.EncryptedKey, req.EncryptedData, req.Password)
if err != nil {
return nil, err
}
id, err := admin.AdminManage().Create(ctx, dto.AdminCreateInput{
Username: req.Username, Password: password, Nickname: req.Nickname,
BarkDeviceId: req.BarkDeviceId, PushplusToken: req.PushplusToken, RoleIds: req.RoleIds,
})
if err != nil {
return nil, err
}
return &adminv1.AdminCreateRes{Id: id}, nil
}
// AdminUpdate 更新管理员。
func (c *Controller) AdminUpdate(ctx context.Context, req *adminv1.AdminUpdateReq) (res *adminv1.AdminUpdateRes, err error) {
if err = admin.AdminManage().Update(ctx, dto.AdminUpdateInput{Id: req.Id, Nickname: req.Nickname, Status: req.Status,
BarkDeviceId: req.BarkDeviceId, PushplusToken: req.PushplusToken, RoleIds: req.RoleIds}); err != nil {
return nil, err
}
return &adminv1.AdminUpdateRes{}, nil
}
// AdminResetPwd 重置管理员密码。
func (c *Controller) AdminResetPwd(ctx context.Context, req *adminv1.AdminResetPwdReq) (res *adminv1.AdminResetPwdRes, err error) {
password, err := resolvePassword(ctx, req.EncryptedKey, req.EncryptedData, req.Password)
if err != nil {
return nil, err
}
if err = admin.AdminManage().ResetPassword(ctx, req.Id, password); err != nil {
return nil, err
}
return &adminv1.AdminResetPwdRes{}, nil
}
// AdminDelete 删除管理员。
func (c *Controller) AdminDelete(ctx context.Context, req *adminv1.AdminDeleteReq) (res *adminv1.AdminDeleteRes, err error) {
if err = admin.AdminManage().Delete(ctx, req.Id); err != nil {
return nil, err
}
return &adminv1.AdminDeleteRes{}, nil
}

View File

@ -2,18 +2,72 @@ package admin
import ( import (
"context" "context"
adminv1 "service.xpcool.com/api/admin/v1"
"github.com/gogf/gf/v2/net/ghttp"
authv1 "service.xpcool.com/api/admin/admin/login"
"service.xpcool.com/internal/model/dto" "service.xpcool.com/internal/model/dto"
"service.xpcool.com/internal/service" auth "service.xpcool.com/internal/service/admin/admin/login"
loginlog "service.xpcool.com/internal/service/admin/system/login_log"
) )
type Controller struct{} // AuthController 仅暴露公开的登录相关端点。
type AuthController struct{}
func New() *Controller { return &Controller{} } // NewAuth 创建公开的管理端认证控制器(仅登录)。
func (c *Controller) Login(ctx context.Context, req *adminv1.LoginReq) (res *adminv1.LoginRes, err error) { func NewAuth() *AuthController { return &AuthController{} }
p, id, err := service.AdminAuth().Login(ctx, dto.AdminLoginInput{Username: req.Username, Password: req.Password})
// Login 校验管理员身份并签发令牌对,成功与失败均写入登录日志。
// 密码采用「RSA + AES-GCM」混合加密传输先解出明文用户名/密码再走原校验逻辑。
func (c *AuthController) Login(ctx context.Context, req *authv1.LoginReq) (res *authv1.LoginRes, err error) {
// 从请求上下文提取来源信息用于登录审计
r := ghttp.RequestFromCtx(ctx)
ip, ua := "", ""
if r != nil {
ip = r.GetClientIp()
ua = r.Header.Get("User-Agent")
}
// 解析登录凭据密文encryptedKey/encryptedData优先明文仅开发环境 allowPlain 时可用。
username, password, err := auth.AdminAuth().ResolveLogin(ctx, req.EncryptedKey, req.EncryptedData, req.Username, req.Password)
if err != nil {
_ = loginlog.AdminLoginLog().Record(ctx, loginlog.LoginEvent{Username: "(encrypted)", IP: ip, UserAgent: ua, Status: 0, FailReason: err.Error()})
return nil, err
}
p, id, err := auth.AdminAuth().Login(ctx, dto.AdminLoginInput{Username: username, Password: password})
if err != nil {
// 登录失败也落库(含失败原因),便于排查异常登录;日志失败不回传
_ = loginlog.AdminLoginLog().Record(ctx, loginlog.LoginEvent{Username: username, IP: ip, UserAgent: ua, Status: 0, FailReason: err.Error()})
return nil, err
}
// 登录成功落库;日志落库失败不影响登录结果
_ = loginlog.AdminLoginLog().Record(ctx, loginlog.LoginEvent{Username: username, IP: ip, UserAgent: ua, Status: 1})
return &authv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, AdminID: id}, nil
}
// PublicKey 返回登录加密 RSA 公钥(公开端点,前端登录前获取用于混合加密密码)。
func (c *AuthController) PublicKey(ctx context.Context, _ *authv1.PublicKeyReq) (res *authv1.PublicKeyRes, err error) {
pub, err := auth.AdminAuth().PublicKey(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &adminv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, AdminID: id}, nil return &authv1.PublicKeyRes{PublicKey: pub}, nil
}
// Refresh 使用有效刷新令牌轮换管理员令牌对。
func (c *AuthController) Refresh(ctx context.Context, req *authv1.RefreshReq) (res *authv1.RefreshRes, err error) {
p, id, err := auth.AdminAuth().Refresh(ctx, req.RefreshToken)
if err != nil {
return nil, err
}
return &authv1.RefreshRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, AdminID: id}, nil
}
// Logout 结束管理员会话。无状态 JWT 的 access token 无法立即作废(等自然过期),
// 但携带 refreshToken 时可撤销刷新会话,阻止该设备继续续期。
// 幂等设计:即使令牌无效也返回成功,保证前端登出流程不阻塞。
func (c *AuthController) Logout(ctx context.Context, req *authv1.LogoutReq) (res *authv1.LogoutRes, err error) {
if req.RefreshToken != "" {
_ = auth.AdminAuth().Revoke(ctx, req.RefreshToken)
}
return &authv1.LogoutRes{}, nil
} }

View File

@ -0,0 +1,21 @@
package admin
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"service.xpcool.com/internal/middleware"
)
// Controller 实现受权限保护的后台管理端点
// (RBAC management, logs, etc.). Bind behind AdminAuth (X-Permission).
type Controller struct{}
// New 创建受保护的后台管理控制器。
func New() *Controller { return &Controller{} }
// adminID 返回认证中间件写入的管理员 id。
func adminID(ctx context.Context) uint64 {
return g.RequestFromCtx(ctx).GetCtxVar(middleware.AdminIDKey).Uint64()
}

View File

@ -0,0 +1,58 @@
package admin
import (
"context"
logv1 "service.xpcool.com/api/admin/base/log"
systemlogv1 "service.xpcool.com/api/admin/system/log"
"service.xpcool.com/internal/model/dto"
log "service.xpcool.com/internal/service/admin/base/log"
adminlog "service.xpcool.com/internal/service/admin/system/log"
)
// LogFiles 列出服务器日志文件。
func (c *Controller) LogFiles(ctx context.Context, req *logv1.LogFilesReq) (res *logv1.LogFilesRes, err error) {
dir, files, err := log.LogManage().Files(ctx)
if err != nil {
return nil, err
}
list := make([]*logv1.LogFile, 0, len(files))
for _, f := range files {
list = append(list, &logv1.LogFile{Name: f.Name, Path: f.Path, Size: f.Size, ModTime: f.ModTime})
}
return &logv1.LogFilesRes{Dir: dir, Files: list}, nil
}
// LogTail 读取日志文件尾部,支持关键字过滤。
func (c *Controller) LogTail(ctx context.Context, req *logv1.LogTailReq) (res *logv1.LogTailRes, err error) {
lines, err := log.LogManage().Tail(ctx, req.File, req.Lines, req.Keyword)
if err != nil {
return nil, err
}
return &logv1.LogTailRes{Lines: lines}, nil
}
// LogList 分页查询 admin 系统日志(操作审计记录)。
func (c *Controller) LogList(ctx context.Context, req *systemlogv1.LogListReq) (res *systemlogv1.LogListRes, err error) {
items, total, err := adminlog.AdminAudit().List(ctx, dto.LogQuery{
Page: req.Page, Size: req.Size, AdminID: req.AdminID, Username: req.Username,
IP: req.IP, IpLocation: req.IpLocation, Method: req.Method, Status: req.Status,
Keyword: req.Keyword, StartTime: req.StartTime, EndTime: req.EndTime,
MinDuration: req.MinDuration, MaxDuration: req.MaxDuration,
OrderBy: req.OrderBy, OrderDir: req.OrderDir,
})
if err != nil {
return nil, err
}
list := make([]*systemlogv1.LogItem, 0, len(items))
for _, it := range items {
list = append(list, &systemlogv1.LogItem{
Id: it.Id, AdminID: it.AdminID, AdminUsername: it.AdminUsername,
Permission: it.Permission, Method: it.Method, Path: it.Path, IP: it.IP,
IpLocation: it.IpLocation, Param: it.Param, DurationMS: it.DurationMS,
StatusCode: it.StatusCode, ErrorMessage: it.ErrorMessage,
UserAgent: it.UserAgent, CreatedAt: it.CreatedAt,
})
}
return &systemlogv1.LogListRes{List: list, Total: total}, nil
}

View File

@ -0,0 +1,27 @@
package admin
import (
"context"
loginlogv1 "service.xpcool.com/api/admin/system/login_log"
"service.xpcool.com/internal/model/dto"
loginlog "service.xpcool.com/internal/service/admin/system/login_log"
)
// LoginLogList 分页查询管理员登录日志。
func (c *Controller) LoginLogList(ctx context.Context, req *loginlogv1.LoginLogListReq) (res *loginlogv1.LoginLogListRes, err error) {
items, total, err := loginlog.AdminLoginLog().List(ctx, dto.LoginLogQuery{
Page: req.Page, Size: req.Size, Username: req.Username, Status: req.Status,
})
if err != nil {
return nil, err
}
list := make([]*loginlogv1.LoginLogItem, 0, len(items))
for _, it := range items {
list = append(list, &loginlogv1.LoginLogItem{
Id: it.Id, Username: it.Username, IP: it.IP, UserAgent: it.UserAgent,
Status: it.Status, FailReason: it.FailReason, CreatedAt: it.CreatedAt,
})
}
return &loginlogv1.LoginLogListRes{List: list, Total: total}, nil
}

View File

@ -0,0 +1,64 @@
package admin
import (
"context"
menuv1 "service.xpcool.com/api/admin/system/menu_manage"
"service.xpcool.com/internal/model/dto"
menu_manage "service.xpcool.com/internal/service/admin/system/menu_manage"
)
// MenuTree 返回完整菜单树(菜单 + 按钮权限)。
func (c *Controller) MenuTree(ctx context.Context, req *menuv1.MenuTreeReq) (res *menuv1.MenuTreeRes, err error) {
tree, err := menu_manage.MenuManage().Tree(ctx)
if err != nil {
return nil, err
}
out := make([]*menuv1.MenuItem, 0, len(tree))
for _, n := range tree {
out = append(out, menuNodeToV1(n))
}
return &menuv1.MenuTreeRes{Tree: out}, nil
}
// MenuCreate 创建菜单或按钮节点。
func (c *Controller) MenuCreate(ctx context.Context, req *menuv1.MenuCreateReq) (res *menuv1.MenuCreateRes, err error) {
id, err := menu_manage.MenuManage().Create(ctx, dto.MenuCreateInput{
ParentId: req.ParentId, Name: req.Name, Icon: req.Icon, Type: req.Type, Path: req.Path,
Component: req.Component, Permission: req.Permission, Sort: req.Sort, Status: req.Status, Hidden: req.Hidden,
})
if err != nil {
return nil, err
}
return &menuv1.MenuCreateRes{Id: id}, nil
}
// MenuUpdate 更新菜单或按钮节点。
func (c *Controller) MenuUpdate(ctx context.Context, req *menuv1.MenuUpdateReq) (res *menuv1.MenuUpdateRes, err error) {
if err = menu_manage.MenuManage().Update(ctx, dto.MenuUpdateInput{
Id: req.Id, ParentId: req.ParentId, Name: req.Name, Icon: req.Icon, Type: req.Type, Path: req.Path,
Component: req.Component, Permission: req.Permission, Sort: req.Sort, Status: req.Status, Hidden: req.Hidden,
}); err != nil {
return nil, err
}
return &menuv1.MenuUpdateRes{}, nil
}
// MenuDelete 删除菜单节点。
func (c *Controller) MenuDelete(ctx context.Context, req *menuv1.MenuDeleteReq) (res *menuv1.MenuDeleteRes, err error) {
if err = menu_manage.MenuManage().Delete(ctx, req.Id); err != nil {
return nil, err
}
return &menuv1.MenuDeleteRes{}, nil
}
func menuNodeToV1(n *dto.MenuNode) *menuv1.MenuItem {
item := &menuv1.MenuItem{
Id: n.Id, ParentId: n.ParentId, Name: n.Name, Icon: n.Icon, Type: n.Type, Path: n.Path,
Component: n.Component, Permission: n.Permission, Sort: n.Sort, Status: n.Status, Hidden: n.Hidden,
}
for _, c := range n.Children {
item.Children = append(item.Children, menuNodeToV1(c))
}
return item
}

View File

@ -0,0 +1,69 @@
package admin
import (
"context"
authv1 "service.xpcool.com/api/admin/admin/login"
menuv1 "service.xpcool.com/api/admin/system/menu"
"service.xpcool.com/internal/model/dto"
auth "service.xpcool.com/internal/service/admin/admin/login"
menu "service.xpcool.com/internal/service/admin/system/menu"
)
// ProfileController 暴露当前登录管理员的资料端点
// (login-only, no X-Permission required). Bind behind AdminAuthOnly.
type ProfileController struct{}
// NewProfile 创建资料控制器。
func NewProfile() *ProfileController { return &ProfileController{} }
// Info 返回当前管理员资料。
func (c *ProfileController) Info(ctx context.Context, req *authv1.InfoReq) (res *authv1.InfoRes, err error) {
info, err := auth.AdminAuth().Info(ctx, adminID(ctx))
if err != nil {
return nil, err
}
return &authv1.InfoRes{AdminID: info.AdminID, Username: info.Username, Nickname: info.Nickname, Roles: info.Roles}, nil
}
// Codes 返回当前管理员的按钮级权限码。
func (c *ProfileController) Codes(ctx context.Context, req *authv1.CodesReq) (res *authv1.CodesRes, err error) {
codes, err := auth.AdminAuth().Codes(ctx, adminID(ctx))
if err != nil {
return nil, err
}
return &authv1.CodesRes{Codes: codes}, nil
}
// Routes 返回当前管理员的可见菜单树vben 路由)。
func (c *ProfileController) Routes(ctx context.Context, req *menuv1.MenuRoutesReq) (res *menuv1.MenuRoutesRes, err error) {
routes, err := menu.AdminMenu().Routes(ctx, adminID(ctx))
if err != nil {
return nil, err
}
out := make([]*menuv1.RouteItem, 0, len(routes))
for _, r := range routes {
out = append(out, toV1Route(r))
}
return &menuv1.MenuRoutesRes{Routes: out}, nil
}
// toV1Route 将 dto 路由树转换为 API 层结构。
func toV1Route(r *dto.RouteItem) *menuv1.RouteItem {
item := &menuv1.RouteItem{
Name: r.Name,
Path: r.Path,
Component: r.Component,
Meta: menuv1.RouteMeta{
Title: r.Meta.Title,
Icon: r.Meta.Icon,
Order: r.Meta.Order,
Authority: r.Meta.Authority,
HideInMenu: r.Meta.HideInMenu,
},
}
for _, c := range r.Children {
item.Children = append(item.Children, toV1Route(c))
}
return item
}

View File

@ -0,0 +1,49 @@
package admin
import (
"context"
rolev1 "service.xpcool.com/api/admin/system/role"
"service.xpcool.com/internal/model/dto"
role "service.xpcool.com/internal/service/admin/system/role"
)
// RoleList 分页查询角色。
func (c *Controller) RoleList(ctx context.Context, req *rolev1.RoleListReq) (res *rolev1.RoleListRes, err error) {
items, total, err := role.RoleManage().List(ctx, dto.PageQuery{Page: req.Page, Size: req.Size, Keyword: req.Keyword})
if err != nil {
return nil, err
}
list := make([]*rolev1.RoleItem, 0, len(items))
for _, it := range items {
list = append(list, &rolev1.RoleItem{
Id: it.Id, Code: it.Code, Name: it.Name, Status: it.Status, MenuIds: it.MenuIds, CreatedAt: it.CreatedAt,
})
}
return &rolev1.RoleListRes{List: list, Total: total}, nil
}
// RoleCreate 创建角色。
func (c *Controller) RoleCreate(ctx context.Context, req *rolev1.RoleCreateReq) (res *rolev1.RoleCreateRes, err error) {
id, err := role.RoleManage().Create(ctx, dto.RoleCreateInput{Code: req.Code, Name: req.Name, Status: req.Status, MenuIds: req.MenuIds})
if err != nil {
return nil, err
}
return &rolev1.RoleCreateRes{Id: id}, nil
}
// RoleUpdate 更新角色。
func (c *Controller) RoleUpdate(ctx context.Context, req *rolev1.RoleUpdateReq) (res *rolev1.RoleUpdateRes, err error) {
if err = role.RoleManage().Update(ctx, dto.RoleUpdateInput{Id: req.Id, Name: req.Name, Status: req.Status, MenuIds: req.MenuIds}); err != nil {
return nil, err
}
return &rolev1.RoleUpdateRes{}, nil
}
// RoleDelete 删除角色。
func (c *Controller) RoleDelete(ctx context.Context, req *rolev1.RoleDeleteReq) (res *rolev1.RoleDeleteRes, err error) {
if err = role.RoleManage().Delete(ctx, req.Id); err != nil {
return nil, err
}
return &rolev1.RoleDeleteRes{}, nil
}

View File

@ -1,5 +0,0 @@
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package hello

View File

@ -1,15 +0,0 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package hello
import (
"service.xpcool.com/api/hello"
)
type ControllerV1 struct{}
func NewV1() hello.IHelloV1 {
return &ControllerV1{}
}

View File

@ -1,13 +0,0 @@
package hello
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"service.xpcool.com/api/hello/v1"
)
func (c *ControllerV1) Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error) {
g.RequestFromCtx(ctx).Response.Writeln("Hello World!")
return
}

View File

@ -0,0 +1,64 @@
package house
import (
"context"
communityv1 "service.xpcool.com/api/house/community"
"service.xpcool.com/internal/model/dto"
community "service.xpcool.com/internal/service/house/community"
)
// CommunityList 分页查询小区。
func (c *Controller) CommunityList(ctx context.Context, req *communityv1.CommunityListReq) (res *communityv1.CommunityListRes, err error) {
list, total, err := community.Community().List(ctx, req.Page, req.Size, req.Keyword, req.Region)
if err != nil {
return nil, err
}
out := make([]*communityv1.CommunityItem, 0, len(list))
for i := range list {
e := &list[i]
out = append(out, &communityv1.CommunityItem{
Id: e.Id, Name: e.Name, Region: e.Region, BusinessDistrict: e.BusinessDistrict,
Address: e.Address, Lng: e.Lng, Lat: e.Lat, BuildYear: e.BuildYear,
Households: e.Households, PlotRatio: e.PlotRatio, GreenRate: e.GreenRate,
PropertyCompany: e.PropertyCompany, PropertyFee: e.PropertyFee, Developer: e.Developer, Source: e.Source,
})
}
return &communityv1.CommunityListRes{List: out, Total: total}, nil
}
// CommunityCreate 新增小区。
func (c *Controller) CommunityCreate(ctx context.Context, req *communityv1.CommunityCreateReq) (res *communityv1.CommunityCreateRes, err error) {
id, err := community.Community().Create(ctx, dto.HouseCommunityInput{
Name: req.Name, Region: req.Region, BusinessDistrict: req.BusinessDistrict,
Address: req.Address, Lng: req.Lng, Lat: req.Lat, BuildYear: req.BuildYear,
Households: req.Households, PlotRatio: req.PlotRatio, GreenRate: req.GreenRate,
PropertyCompany: req.PropertyCompany, PropertyFee: req.PropertyFee, Developer: req.Developer, Source: req.Source,
})
if err != nil {
return nil, err
}
return &communityv1.CommunityCreateRes{Id: id}, nil
}
// CommunityUpdate 更新小区。
func (c *Controller) CommunityUpdate(ctx context.Context, req *communityv1.CommunityUpdateReq) (res *communityv1.CommunityUpdateRes, err error) {
err = community.Community().Update(ctx, dto.HouseCommunityInput{
Id: req.Id, Name: req.Name, Region: req.Region, BusinessDistrict: req.BusinessDistrict,
Address: req.Address, Lng: req.Lng, Lat: req.Lat, BuildYear: req.BuildYear,
Households: req.Households, PlotRatio: req.PlotRatio, GreenRate: req.GreenRate,
PropertyCompany: req.PropertyCompany, PropertyFee: req.PropertyFee, Developer: req.Developer,
})
if err != nil {
return nil, err
}
return &communityv1.CommunityUpdateRes{}, nil
}
// CommunityDelete 删除小区。
func (c *Controller) CommunityDelete(ctx context.Context, req *communityv1.CommunityDeleteReq) (res *communityv1.CommunityDeleteRes, err error) {
if err = community.Community().Delete(ctx, req.Id); err != nil {
return nil, err
}
return &communityv1.CommunityDeleteRes{}, nil
}

View File

@ -0,0 +1,8 @@
// Package house 实现看房模块的管理端点,绑定在 admin 受权限保护分组下。
package house
// Controller 实现看房模块的所有端点。
type Controller struct{}
// New 创建看房模块控制器。
func New() *Controller { return &Controller{} }

View File

@ -0,0 +1,67 @@
package house
import (
"context"
dashboardv1 "service.xpcool.com/api/house/dashboard"
dashboard "service.xpcool.com/internal/service/house/dashboard"
)
// DashboardOverview 看板统计概览。
func (c *Controller) DashboardOverview(ctx context.Context, req *dashboardv1.OverviewReq) (res *dashboardv1.OverviewRes, err error) {
o, err := dashboard.Dashboard().Overview(ctx, req.Region)
if err != nil {
return nil, err
}
return &dashboardv1.OverviewRes{
CommunityCount: o.CommunityCount, ListingCount: o.ListingCount, BargainCount: o.BargainCount,
LowConfidence: o.LowConfidence, AvgUnitPrice: o.AvgUnitPrice, AvgTotalPrice: o.AvgTotalPrice, AvgListDays: o.AvgListDays,
}, nil
}
// DashboardMapPoints 地图点位聚合。
func (c *Controller) DashboardMapPoints(ctx context.Context, req *dashboardv1.MapPointsReq) (res *dashboardv1.MapPointsRes, err error) {
pts, err := dashboard.Dashboard().MapPoints(ctx, req.Region, req.PriceMin, req.PriceMax, req.Status)
if err != nil {
return nil, err
}
out := make([]*dashboardv1.MapPoint, 0, len(pts))
for i := range pts {
p := &pts[i]
out = append(out, &dashboardv1.MapPoint{
CommunityId: p.CommunityId, Name: p.Name, Region: p.Region, Lng: p.Lng, Lat: p.Lat,
AvgUnitPrice: p.AvgUnitPrice, ListingCount: p.ListingCount, BargainCount: p.BargainCount,
})
}
return &dashboardv1.MapPointsRes{Points: out}, nil
}
// DashboardPriceTrend 价格趋势聚合。
func (c *Controller) DashboardPriceTrend(ctx context.Context, req *dashboardv1.PriceTrendReq) (res *dashboardv1.PriceTrendRes, err error) {
trend, err := dashboard.Dashboard().PriceTrend(ctx, req.CommunityId, req.Region, req.Limit)
if err != nil {
return nil, err
}
out := make([]*dashboardv1.TrendPoint, 0, len(trend))
for i := range trend {
t := &trend[i]
out = append(out, &dashboardv1.TrendPoint{Date: t.Date, AvgListPrice: t.AvgListPrice, AvgDealPrice: t.AvgDealPrice})
}
return &dashboardv1.PriceTrendRes{Trend: out}, nil
}
// DashboardAggregateRegion 区域聚合。
func (c *Controller) DashboardAggregateRegion(ctx context.Context, req *dashboardv1.AggregateRegionReq) (res *dashboardv1.AggregateRegionRes, err error) {
list, err := dashboard.Dashboard().AggregateRegion(ctx)
if err != nil {
return nil, err
}
out := make([]*dashboardv1.RegionAgg, 0, len(list))
for i := range list {
r := &list[i]
out = append(out, &dashboardv1.RegionAgg{
Region: r.Region, AvgUnitPrice: r.AvgUnitPrice, ListingCount: r.ListingCount, BargainCount: r.BargainCount,
})
}
return &dashboardv1.AggregateRegionRes{List: out}, nil
}

View File

@ -0,0 +1,85 @@
package house
import (
"context"
listingv1 "service.xpcool.com/api/house/listing"
"service.xpcool.com/internal/model/dto"
listing "service.xpcool.com/internal/service/house/listing"
)
// ListingList 分页查询房源(带多维筛选)。
func (c *Controller) ListingList(ctx context.Context, req *listingv1.ListingListReq) (res *listingv1.ListingListRes, err error) {
list, total, err := listing.Listing().List(ctx, dto.HouseListingFilter{
Page: req.Page, Size: req.Size, CommunityId: req.CommunityId, Keyword: req.Keyword,
Layout: req.Layout, Region: req.Region, Source: req.Source,
PriceMin: req.PriceMin, PriceMax: req.PriceMax, AreaMin: req.AreaMin, AreaMax: req.AreaMax,
Status: req.Status, IsBargain: req.IsBargain, Confidence: req.Confidence,
})
if err != nil {
return nil, err
}
out := make([]*listingv1.ListingItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &listingv1.ListingItem{
Id: v.Id, CommunityId: v.CommunityId, CommunityName: v.CommunityName, BuildingId: v.BuildingId,
HouseNo: v.HouseNo, Layout: v.Layout, Area: v.Area, UsableArea: v.UsableArea,
Orientation: v.Orientation, Floor: v.Floor, TotalFloors: v.TotalFloors, Decoration: v.Decoration,
TotalPrice: v.TotalPrice, UnitPrice: v.UnitPrice, ListPrice: v.ListPrice,
Source: v.Source, SourceHouseId: v.SourceHouseId, SourceUrl: v.SourceUrl,
MatchGroupId: v.MatchGroupId, OnMarketDays: v.OnMarketDays, PriceChangeCount: v.PriceChangeCount,
Status: v.Status, Confidence: v.Confidence, IsBargain: v.IsBargain,
ListingTime: v.ListingTime, CreatedAt: v.CreatedAt,
})
}
return &listingv1.ListingListRes{List: out, Total: total}, nil
}
// ListingCreate 新增房源。
func (c *Controller) ListingCreate(ctx context.Context, req *listingv1.ListingCreateReq) (res *listingv1.ListingCreateRes, err error) {
id, err := listing.Listing().Create(ctx, dto.HouseListingInput{
CommunityId: req.CommunityId, BuildingId: req.BuildingId, HouseNo: req.HouseNo, Layout: req.Layout,
Area: req.Area, UsableArea: req.UsableArea, Orientation: req.Orientation, Floor: req.Floor,
TotalFloors: req.TotalFloors, Decoration: req.Decoration, TotalPrice: req.TotalPrice, UnitPrice: req.UnitPrice,
ListPrice: req.ListPrice, Source: req.Source, SourceHouseId: req.SourceHouseId, SourceUrl: req.SourceUrl,
MatchGroupId: req.MatchGroupId, OnMarketDays: req.OnMarketDays, PriceChangeCount: req.PriceChangeCount,
Status: req.Status, Confidence: req.Confidence, IsBargain: req.IsBargain,
})
if err != nil {
return nil, err
}
return &listingv1.ListingCreateRes{Id: id}, nil
}
// ListingUpdate 更新房源。
func (c *Controller) ListingUpdate(ctx context.Context, req *listingv1.ListingUpdateReq) (res *listingv1.ListingUpdateRes, err error) {
err = listing.Listing().Update(ctx, dto.HouseListingInput{
Id: req.Id, CommunityId: req.CommunityId, BuildingId: req.BuildingId, HouseNo: req.HouseNo,
Layout: req.Layout, Area: req.Area, UsableArea: req.UsableArea, Orientation: req.Orientation,
Floor: req.Floor, TotalFloors: req.TotalFloors, Decoration: req.Decoration, TotalPrice: req.TotalPrice,
UnitPrice: req.UnitPrice, ListPrice: req.ListPrice, MatchGroupId: req.MatchGroupId,
Status: req.Status, Confidence: req.Confidence, IsBargain: req.IsBargain,
})
if err != nil {
return nil, err
}
return &listingv1.ListingUpdateRes{}, nil
}
// ListingDelete 删除房源。
func (c *Controller) ListingDelete(ctx context.Context, req *listingv1.ListingDeleteReq) (res *listingv1.ListingDeleteRes, err error) {
if err = listing.Listing().Delete(ctx, req.Id); err != nil {
return nil, err
}
return &listingv1.ListingDeleteRes{}, nil
}
// ListingBatchMark 批量标记房源。
func (c *Controller) ListingBatchMark(ctx context.Context, req *listingv1.ListingBatchMarkReq) (res *listingv1.ListingBatchMarkRes, err error) {
affected, err := listing.Listing().BatchMark(ctx, req.Ids, req.Field, req.Value)
if err != nil {
return nil, err
}
return &listingv1.ListingBatchMarkRes{Affected: affected}, nil
}

View File

@ -0,0 +1,36 @@
package house
import (
"context"
presalev1 "service.xpcool.com/api/house/presale"
presale "service.xpcool.com/internal/service/house/presale"
)
// PresaleList 分页查询新房预售证。
func (c *Controller) PresaleList(ctx context.Context, req *presalev1.PresaleListReq) (res *presalev1.PresaleListRes, err error) {
list, total, err := presale.Presale().List(ctx, req.Page, req.Size, req.Region, req.Keyword, req.Purpose)
if err != nil {
return nil, err
}
out := make([]*presalev1.PresaleItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &presalev1.PresaleItem{
Id: v.Id,
PresaleNo: v.PresaleNo,
CommunityName: v.CommunityName,
Developer: v.Developer,
Region: v.Region,
Address: v.Address,
BuildingNo: v.BuildingNo,
HouseCount: v.HouseCount,
Area: v.Area,
Purpose: v.Purpose,
IssueDate: v.IssueDate,
PublishDate: v.PublishDate,
Source: v.Source,
})
}
return &presalev1.PresaleListRes{List: out, Total: total}, nil
}

View File

@ -0,0 +1,26 @@
package house
import (
"context"
transactionv1 "service.xpcool.com/api/house/transaction"
transaction "service.xpcool.com/internal/service/house/transaction"
)
// TransactionList 分页查询成交记录。
func (c *Controller) TransactionList(ctx context.Context, req *transactionv1.TransactionListReq) (res *transactionv1.TransactionListRes, err error) {
list, total, err := transaction.Transaction().List(ctx, req.Page, req.Size, req.Region, req.Keyword)
if err != nil {
return nil, err
}
out := make([]*transactionv1.TransactionItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &transactionv1.TransactionItem{
Id: v.Id, CommunityId: v.CommunityId, CommunityName: v.CommunityName,
Layout: v.Layout, Area: v.Area, DealPrice: v.DealPrice, DealUnitPrice: v.DealUnitPrice,
ListDays: v.ListDays, DealDate: v.DealDate, Source: v.Source,
})
}
return &transactionv1.TransactionListRes{List: out, Total: total}, nil
}

View File

@ -0,0 +1,68 @@
// Package job 自动任务模块控制器(绑定 admin 受权限保护组)。
package job
import (
"context"
jobv1 "service.xpcool.com/api/job"
"service.xpcool.com/internal/model/dto"
"service.xpcool.com/internal/service/job"
)
// Controller 实现自动任务端点。
type Controller struct{}
// New 创建自动任务控制器。
func New() *Controller { return &Controller{} }
// List 自动任务列表。
func (c *Controller) List(ctx context.Context, req *jobv1.AutoJobListReq) (res *jobv1.AutoJobListRes, err error) {
list, err := job.Job().List(ctx)
if err != nil {
return nil, err
}
out := make([]*jobv1.JobItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &jobv1.JobItem{
Id: v.Id, Name: v.Name, Code: v.Code, JobType: v.JobType, CronExpr: v.CronExpr,
Enabled: v.Enabled, Remark: v.Remark, LastRunAt: v.LastRunAt,
LastResult: v.LastResult, LastError: v.LastError,
})
}
return &jobv1.AutoJobListRes{List: out}, nil
}
// Save 保存自动任务cron/启用/备注)。
func (c *Controller) Save(ctx context.Context, req *jobv1.AutoJobSaveReq) (res *jobv1.AutoJobSaveRes, err error) {
if err = job.Job().Save(ctx, dto.JobInput{Id: req.Id, CronExpr: req.CronExpr, Enabled: req.Enabled, Remark: req.Remark}); err != nil {
return nil, err
}
return &jobv1.AutoJobSaveRes{}, nil
}
// Trigger 手动触发任务。
func (c *Controller) Trigger(ctx context.Context, req *jobv1.AutoJobTriggerReq) (res *jobv1.AutoJobTriggerRes, err error) {
summary, ok, err := job.Job().Trigger(ctx, req.Id)
if err != nil {
return nil, err
}
return &jobv1.AutoJobTriggerRes{Summary: summary, Ok: ok}, nil
}
// LogList 任务运行日志分页。
func (c *Controller) LogList(ctx context.Context, req *jobv1.AutoJobLogListReq) (res *jobv1.AutoJobLogListRes, err error) {
list, total, err := job.Job().LogList(ctx, dto.JobLogFilter{Page: req.Page, Size: req.Size, JobId: req.JobId})
if err != nil {
return nil, err
}
out := make([]*jobv1.JobLogItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &jobv1.JobLogItem{
Id: v.Id, JobId: v.JobId, JobName: v.JobName, RunAt: v.RunAt,
Result: v.Result, Error: v.Error, Summary: v.Summary, DurationMs: v.DurationMs,
})
}
return &jobv1.AutoJobLogListRes{List: out, Total: total}, nil
}

View File

@ -0,0 +1,108 @@
// Package notice 通知模块控制器(绑定 admin 受权限保护组)。
package notice
import (
"context"
noticev1 "service.xpcool.com/api/notice"
"service.xpcool.com/internal/model/dto"
"service.xpcool.com/internal/service/notice"
)
// Controller 实现通知模块端点。
type Controller struct{}
// New 创建通知控制器。
func New() *Controller { return &Controller{} }
// ChannelList 通知渠道列表。
func (c *Controller) ChannelList(ctx context.Context, req *noticev1.NoticeChannelListReq) (res *noticev1.NoticeChannelListRes, err error) {
list, err := notice.Notice().ChannelList(ctx)
if err != nil {
return nil, err
}
out := make([]*noticev1.NoticeChannelItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &noticev1.NoticeChannelItem{Id: v.Id, Code: v.Code, Name: v.Name, Enabled: v.Enabled, Config: v.Config, Remark: v.Remark})
}
return &noticev1.NoticeChannelListRes{List: out}, nil
}
// ChannelSave 保存通知渠道。
func (c *Controller) ChannelSave(ctx context.Context, req *noticev1.NoticeChannelSaveReq) (res *noticev1.NoticeChannelSaveRes, err error) {
id, err := notice.Notice().ChannelSave(ctx, dto.NoticeChannelInput{
Id: req.Id, Code: req.Code, Name: req.Name, Enabled: req.Enabled, Config: req.Config, Remark: req.Remark,
})
if err != nil {
return nil, err
}
return &noticev1.NoticeChannelSaveRes{Id: id}, nil
}
// RuleList 通知规则列表。
func (c *Controller) RuleList(ctx context.Context, req *noticev1.NoticeRuleListReq) (res *noticev1.NoticeRuleListRes, err error) {
list, err := notice.Notice().RuleList(ctx, req.EventType)
if err != nil {
return nil, err
}
out := make([]*noticev1.NoticeRuleItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &noticev1.NoticeRuleItem{
Id: v.Id, Name: v.Name, EventType: v.EventType, ChannelCodes: v.ChannelCodes,
UserIds: v.UserIds, TitleTemplate: v.TitleTemplate, BodyTemplate: v.BodyTemplate, Enabled: v.Enabled,
})
}
return &noticev1.NoticeRuleListRes{List: out}, nil
}
// RuleSave 保存通知规则。
func (c *Controller) RuleSave(ctx context.Context, req *noticev1.NoticeRuleSaveReq) (res *noticev1.NoticeRuleSaveRes, err error) {
id, err := notice.Notice().RuleSave(ctx, dto.NoticeRuleInput{
Id: req.Id, Name: req.Name, EventType: req.EventType, ChannelCodes: req.ChannelCodes,
UserIds: req.UserIds, TitleTemplate: req.TitleTemplate, BodyTemplate: req.BodyTemplate, Enabled: req.Enabled,
})
if err != nil {
return nil, err
}
return &noticev1.NoticeRuleSaveRes{Id: id}, nil
}
// RuleDelete 删除通知规则。
func (c *Controller) RuleDelete(ctx context.Context, req *noticev1.NoticeRuleDeleteReq) (res *noticev1.NoticeRuleDeleteRes, err error) {
if err = notice.Notice().RuleDelete(ctx, req.Id); err != nil {
return nil, err
}
return &noticev1.NoticeRuleDeleteRes{}, nil
}
// LogList 通知发送记录分页。
func (c *Controller) LogList(ctx context.Context, req *noticev1.NoticeLogListReq) (res *noticev1.NoticeLogListRes, err error) {
list, total, err := notice.Notice().LogList(ctx, dto.NoticeLogFilter{
Page: req.Page, Size: req.Size, EventType: req.EventType,
ChannelCode: req.ChannelCode, Result: req.Result, DateFrom: req.DateFrom, DateTo: req.DateTo,
})
if err != nil {
return nil, err
}
out := make([]*noticev1.NoticeLogItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &noticev1.NoticeLogItem{
Id: v.Id, RuleId: v.RuleId, EventType: v.EventType, ChannelCode: v.ChannelCode,
UserId: v.UserId, Target: v.Target, Title: v.Title, Body: v.Body,
Result: v.Result, Error: v.Error, CreatedAt: v.CreatedAt,
})
}
return &noticev1.NoticeLogListRes{List: out, Total: total}, nil
}
// Test 测试通知发送。
func (c *Controller) Test(ctx context.Context, req *noticev1.NoticeTestReq) (res *noticev1.NoticeTestRes, err error) {
ok, msg, err := notice.Notice().Test(ctx, req.RuleId, req.ChannelCode, req.Target, req.UserIds, req.Title, req.Body)
if err != nil {
return nil, err
}
return &noticev1.NoticeTestRes{Result: ok, Message: msg}, nil
}

View File

@ -0,0 +1,11 @@
// Package open 实现公开的开放接口(/api/open
// 这些控制器是 common/tools Go 包的薄适配层,
// 无需认证。每个子功能位于本目录下的独立文件,
// 与 api/open/tools/<name>/index.go 一一对应。
package open
// Controller 实现 /api/open 端点。
type Controller struct{}
// New 创建开放接口控制器。
func New() *Controller { return &Controller{} }

View File

@ -0,0 +1,16 @@
package open
import (
"context"
"github.com/gogf/gf/v2/frame/g"
ipapi "service.xpcool.com/api/open/tools/ip"
"service.xpcool.com/common/tools/ip"
)
// IP 返回调用方 IP 及是否为内网地址。
func (c *Controller) IP(ctx context.Context, req *ipapi.IPReq) (res *ipapi.IPRes, err error) {
clientIP := g.RequestFromCtx(ctx).GetClientIp()
return &ipapi.IPRes{IP: clientIP, Internal: ip.IsInternal(clientIP)}, nil
}

View File

@ -0,0 +1,13 @@
package open
import (
"context"
md5api "service.xpcool.com/api/open/tools/md5"
"service.xpcool.com/common/tools/md5"
)
// MD5 计算给定文本的 MD5 摘要。
func (c *Controller) MD5(ctx context.Context, req *md5api.MD5Req) (res *md5api.MD5Res, err error) {
return &md5api.MD5Res{MD5: md5.Md5Hex(req.Text)}, nil
}

View File

@ -0,0 +1,22 @@
package open
import (
"context"
randomapi "service.xpcool.com/api/open/tools/random"
"service.xpcool.com/common/tools/random"
)
// Random 生成指定类型和长度的随机字符串。
func (c *Controller) Random(ctx context.Context, req *randomapi.RandomReq) (res *randomapi.RandomRes, err error) {
var value string
switch req.Type {
case "digits":
value = random.Digits(req.Length)
case "letters":
value = random.Letters(req.Length)
default:
value = random.String(req.Length)
}
return &randomapi.RandomRes{Value: value}, nil
}

View File

@ -0,0 +1,18 @@
package open
import (
"context"
timeapi "service.xpcool.com/api/open/tools/time"
"service.xpcool.com/common/tools/timex"
)
// Time 返回当前服务器时间戳与格式化时间。
func (c *Controller) Time(ctx context.Context, req *timeapi.TimeReq) (res *timeapi.TimeRes, err error) {
now := timex.Now()
return &timeapi.TimeRes{
Timestamp: now.Timestamp(),
DateTime: now.Layout(timex.LayoutDateTime),
Date: now.Layout(timex.LayoutDate),
}, nil
}

View File

@ -0,0 +1,16 @@
package open
import (
"context"
uuidapi "service.xpcool.com/api/open/tools/uuid"
"service.xpcool.com/common/tools/uuid"
)
// UUID 生成唯一 ID默认 32 位short=true 时为 8 位)。
func (c *Controller) UUID(ctx context.Context, req *uuidapi.UUIDReq) (res *uuidapi.UUIDRes, err error) {
if req.Short {
return &uuidapi.UUIDRes{UUID: uuid.Short(8)}, nil
}
return &uuidapi.UUIDRes{UUID: uuid.New()}, nil
}

View File

@ -0,0 +1,8 @@
// Package recruitment 实现招聘考试聚合模块管理端点,绑定在 admin 受权限保护分组下。
package recruitment
// Controller 实现招聘考试聚合模块的所有端点。
type Controller struct{}
// New 创建招聘模块控制器。
func New() *Controller { return &Controller{} }

View File

@ -0,0 +1,159 @@
package recruitment
import (
"context"
recruitmentv1 "service.xpcool.com/api/recruitment"
"service.xpcool.com/internal/model/dto"
"service.xpcool.com/internal/service/recruitment"
)
// toItem 将服务层 VO 映射为 API 出参项。
func toItem(v *dto.RecruitmentInfoVO) *recruitmentv1.RecruitmentItem {
if v == nil {
return nil
}
return &recruitmentv1.RecruitmentItem{
Id: v.Id, Title: v.Title, SourceId: v.SourceId, SourceName: v.SourceName,
OrgId: v.OrgId, OrgName: v.OrgName, Category: v.Category, CategoryName: v.CategoryName,
Region: v.Region, PublishDate: v.PublishDate, Deadline: v.Deadline, ExamDate: v.ExamDate,
Url: v.Url, Content: v.Content, Attachments: v.Attachments, Status: v.Status,
StatusName: v.StatusName, GroupKey: v.GroupKey, CreatedAt: v.CreatedAt,
}
}
// InfoList 招聘公告列表(多维筛选)。
func (c *Controller) InfoList(ctx context.Context, req *recruitmentv1.RecruitmentListReq) (res *recruitmentv1.RecruitmentListRes, err error) {
list, total, err := recruitment.Recruitment().List(ctx, dto.RecruitmentFilter{
Page: req.Page, Size: req.Size, Region: req.Region, Category: req.Category,
Keyword: req.Keyword, Status: req.Status, DateFrom: req.DateFrom, DateTo: req.DateTo,
OrgName: req.OrgName, SourceId: req.SourceId, OnlyNewToday: req.OnlyNewToday,
})
if err != nil {
return nil, err
}
out := make([]*recruitmentv1.RecruitmentItem, 0, len(list))
for i := range list {
out = append(out, toItem(&list[i]))
}
return &recruitmentv1.RecruitmentListRes{List: out, Total: total}, nil
}
// InfoDetail 招聘公告详情。
func (c *Controller) InfoDetail(ctx context.Context, req *recruitmentv1.RecruitmentDetailReq) (res *recruitmentv1.RecruitmentDetailRes, err error) {
info, err := recruitment.Recruitment().Detail(ctx, req.Id)
if err != nil {
return nil, err
}
return &recruitmentv1.RecruitmentDetailRes{Info: toItem(info)}, nil
}
// Stats 招聘数据看板统计。
func (c *Controller) Stats(ctx context.Context, req *recruitmentv1.RecruitmentStatsReq) (res *recruitmentv1.RecruitmentStatsRes, err error) {
s, err := recruitment.Recruitment().Stats(ctx, req.Region)
if err != nil {
return nil, err
}
byCat := make([]recruitmentv1.CategoryAggItem, 0, len(s.ByCategory))
for _, v := range s.ByCategory {
byCat = append(byCat, recruitmentv1.CategoryAggItem{Category: v.Category, CategoryName: v.CategoryName, Count: v.Count})
}
byRegion := make([]recruitmentv1.RegionAggItem, 0, len(s.ByRegion))
for _, v := range s.ByRegion {
byRegion = append(byRegion, recruitmentv1.RegionAggItem{Region: v.Region, Count: v.Count})
}
trend := make([]recruitmentv1.TrendPointItem, 0, len(s.RecentTrend))
for _, v := range s.RecentTrend {
trend = append(trend, recruitmentv1.TrendPointItem{Date: v.Date, Count: v.Count})
}
return &recruitmentv1.RecruitmentStatsRes{
Total: s.Total, TodayNew: s.TodayNew, WeekNew: s.WeekNew,
ByCategory: byCat, ByRegion: byRegion, RecentTrend: trend,
}, nil
}
// Trend 招聘公告趋势(按日)。
func (c *Controller) Trend(ctx context.Context, req *recruitmentv1.RecruitmentTrendReq) (res *recruitmentv1.RecruitmentTrendRes, err error) {
pts, err := recruitment.Recruitment().Trend(ctx, req.Region, req.Category, req.Days)
if err != nil {
return nil, err
}
out := make([]recruitmentv1.TrendPointItem, 0, len(pts))
for _, v := range pts {
out = append(out, recruitmentv1.TrendPointItem{Date: v.Date, Count: v.Count})
}
return &recruitmentv1.RecruitmentTrendRes{Trend: out}, nil
}
// SourceList 数据源列表与运行状态。
func (c *Controller) SourceList(ctx context.Context, req *recruitmentv1.RecruitmentSourcesReq) (res *recruitmentv1.RecruitmentSourcesRes, err error) {
list, err := recruitment.Recruitment().Sources(ctx)
if err != nil {
return nil, err
}
out := make([]*recruitmentv1.SourceItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &recruitmentv1.SourceItem{
Id: v.Id, Name: v.Name, BaseUrl: v.BaseUrl, SourceType: v.SourceType,
Category: v.Category, Region: v.Region, Enabled: v.Enabled,
LastSuccessAt: v.LastSuccessAt, FailCount: v.FailCount, LastSummary: v.LastSummary,
})
}
return &recruitmentv1.RecruitmentSourcesRes{List: out}, nil
}
// CrawlTrigger 手动触发抓取(单源或全量)。
func (c *Controller) CrawlTrigger(ctx context.Context, req *recruitmentv1.RecruitmentTriggerReq) (res *recruitmentv1.RecruitmentTriggerRes, err error) {
n, summary, err := recruitment.Recruitment().Trigger(ctx, req.SourceId, req.Force)
if err != nil {
return nil, err
}
return &recruitmentv1.RecruitmentTriggerRes{Triggered: n, Summary: summary}, nil
}
// PushTest Bark 推送测试。
func (c *Controller) PushTest(ctx context.Context, req *recruitmentv1.RecruitmentPushTestReq) (res *recruitmentv1.RecruitmentPushTestRes, err error) {
ok, msg, err := recruitment.Recruitment().PushTest(ctx, req.SubscriptionId, req.Title, req.Body)
if err != nil {
return nil, err
}
return &recruitmentv1.RecruitmentPushTestRes{Result: ok, Message: msg}, nil
}
// SubscriptionList 推送订阅列表。
func (c *Controller) SubscriptionList(ctx context.Context, req *recruitmentv1.RecruitmentSubscriptionListReq) (res *recruitmentv1.RecruitmentSubscriptionListRes, err error) {
list, err := recruitment.Recruitment().SubscriptionList(ctx)
if err != nil {
return nil, err
}
out := make([]*recruitmentv1.SubscriptionItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &recruitmentv1.SubscriptionItem{
Id: v.Id, Name: v.Name, DeviceKey: v.DeviceKey, Regions: v.Regions,
Categories: v.Categories, OnlyNew: v.OnlyNew, PushTime: v.PushTime, Enabled: v.Enabled,
})
}
return &recruitmentv1.RecruitmentSubscriptionListRes{List: out}, nil
}
// SubscriptionSave 保存推送订阅(新增/更新)。
func (c *Controller) SubscriptionSave(ctx context.Context, req *recruitmentv1.RecruitmentSubscriptionSaveReq) (res *recruitmentv1.RecruitmentSubscriptionSaveRes, err error) {
id, err := recruitment.Recruitment().SubscriptionSave(ctx, dto.SubscriptionInput{
Id: req.Id, Name: req.Name, DeviceKey: req.DeviceKey, Regions: req.Regions,
Categories: req.Categories, OnlyNew: req.OnlyNew, PushTime: req.PushTime, Enabled: req.Enabled,
})
if err != nil {
return nil, err
}
return &recruitmentv1.RecruitmentSubscriptionSaveRes{Id: id}, nil
}
// SubscriptionDelete 删除推送订阅。
func (c *Controller) SubscriptionDelete(ctx context.Context, req *recruitmentv1.RecruitmentSubscriptionDeleteReq) (res *recruitmentv1.RecruitmentSubscriptionDeleteRes, err error) {
if err = recruitment.Recruitment().SubscriptionDelete(ctx, req.Id); err != nil {
return nil, err
}
return &recruitmentv1.RecruitmentSubscriptionDeleteRes{}, nil
}

View File

@ -0,0 +1,21 @@
// Package serversecurity 服务器安全日志模块控制器。
// 分为两个 ControllerReportController 绑定 open 组(宿主机脚本上报,内部令牌鉴权);
// ManageController 绑定 admin 受权限保护组(查询/统计)。
package serversecurity
import (
serversecurityv1 "service.xpcool.com/api/serversecurity"
"service.xpcool.com/internal/model/dto"
)
// toInput 将 API 上报项转为服务层入参。
func toInput(items []serversecurityv1.SecurityLogItem) []dto.SecurityLogInput {
out := make([]dto.SecurityLogInput, 0, len(items))
for _, it := range items {
out = append(out, dto.SecurityLogInput{
LogTime: it.LogTime, SrcIp: it.SrcIp, SrcPort: it.SrcPort,
DestPort: it.DestPort, EventType: it.EventType, Detail: it.Detail,
})
}
return out
}

View File

@ -0,0 +1,57 @@
// Package serversecurity 管理控制器:绑定 admin 受权限保护组(查询/统计)。
package serversecurity
import (
"context"
serversecurityv1 "service.xpcool.com/api/serversecurity"
"service.xpcool.com/internal/model/dto"
"service.xpcool.com/internal/service/serversecurity"
)
// ManageController 实现安全日志查询与统计端点。
type ManageController struct{}
// NewManage 创建管理控制器。
func NewManage() *ManageController { return &ManageController{} }
// ListLog 安全日志分页查询(时间倒序)。
func (c *ManageController) ListLog(ctx context.Context, req *serversecurityv1.SecurityLogListReq) (res *serversecurityv1.SecurityLogListRes, err error) {
list, total, err := serversecurity.Security().List(ctx, dto.SecurityLogFilter{
Page: req.Page, Size: req.Size, SrcIp: req.SrcIp, EventType: req.EventType,
DestPort: req.DestPort, DateFrom: req.DateFrom, DateTo: req.DateTo,
})
if err != nil {
return nil, err
}
out := make([]*serversecurityv1.SecurityLogListItem, 0, len(list))
for i := range list {
v := &list[i]
out = append(out, &serversecurityv1.SecurityLogListItem{
Id: v.Id, LogTime: v.LogTime, SrcIp: v.SrcIp, SrcPort: v.SrcPort,
DestPort: v.DestPort, EventType: v.EventType, EventName: v.EventName,
Detail: v.Detail, CreatedAt: v.CreatedAt,
})
}
return &serversecurityv1.SecurityLogListRes{List: out, Total: total}, nil
}
// Stats 安全日志统计默认最近24小时
func (c *ManageController) Stats(ctx context.Context, req *serversecurityv1.SecurityLogStatsReq) (res *serversecurityv1.SecurityLogStatsRes, err error) {
st, err := serversecurity.Security().Stats(ctx, req.DateFrom, req.DateTo)
if err != nil {
return nil, err
}
topIps := make([]serversecurityv1.SecurityTopIp, 0, len(st.TopIps))
for _, v := range st.TopIps {
topIps = append(topIps, serversecurityv1.SecurityTopIp{SrcIp: v.SrcIp, Count: v.Count})
}
byType := make([]serversecurityv1.SecurityByType, 0, len(st.ByType))
for _, v := range st.ByType {
byType = append(byType, serversecurityv1.SecurityByType{EventType: v.EventType, EventName: v.EventName, Count: v.Count})
}
return &serversecurityv1.SecurityLogStatsRes{
Total: st.Total, Failed: st.Failed, Banned: st.Banned, Accepted: st.Accepted,
TopIps: topIps, ByType: byType,
}, nil
}

View File

@ -0,0 +1,24 @@
// Package serversecurity 上报控制器:绑定 open 组(无登录态),仅宿主机采集脚本调用。
package serversecurity
import (
"context"
serversecurityv1 "service.xpcool.com/api/serversecurity"
"service.xpcool.com/internal/service/serversecurity"
)
// ReportController 实现安全日志上报端点。
type ReportController struct{}
// NewReport 创建上报控制器。
func NewReport() *ReportController { return &ReportController{} }
// ReportLog 接收宿主机采集脚本上报的安全日志并入库(内部令牌鉴权)。
func (c *ReportController) ReportLog(ctx context.Context, req *serversecurityv1.SecurityLogReportReq) (res *serversecurityv1.SecurityLogReportRes, err error) {
n, err := serversecurity.Security().Report(ctx, req.Token, toInput(req.List))
if err != nil {
return nil, err
}
return &serversecurityv1.SecurityLogReportRes{Accepted: n}, nil
}

View File

@ -2,25 +2,25 @@ package user
import ( import (
"context" "context"
userv1 "service.xpcool.com/api/user/v1" authv1 "service.xpcool.com/api/user/auth"
"service.xpcool.com/internal/model/dto" "service.xpcool.com/internal/model/dto"
"service.xpcool.com/internal/service" auth "service.xpcool.com/internal/service/user/auth"
) )
type Controller struct{} type Controller struct{}
func New() *Controller { return &Controller{} } func New() *Controller { return &Controller{} }
func (c *Controller) Login(ctx context.Context, req *userv1.LoginReq) (res *userv1.LoginRes, err error) { func (c *Controller) Login(ctx context.Context, req *authv1.LoginReq) (res *authv1.LoginRes, err error) {
p, id, err := service.UserAuth().Login(ctx, dto.UserLoginInput{LoginType: req.LoginType, Code: req.Code, Mobile: req.Mobile, VerifyCode: req.VerifyCode, Account: req.Account, Password: req.Password, Terminal: req.Terminal}) p, id, err := auth.UserAuth().Login(ctx, dto.UserLoginInput{LoginType: req.LoginType, Code: req.Code, Mobile: req.Mobile, VerifyCode: req.VerifyCode, Account: req.Account, Password: req.Password, Terminal: req.Terminal})
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &userv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil return &authv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil
} }
func (c *Controller) Refresh(ctx context.Context, req *userv1.RefreshReq) (res *userv1.RefreshRes, err error) { func (c *Controller) Refresh(ctx context.Context, req *authv1.RefreshReq) (res *authv1.RefreshRes, err error) {
p, id, err := service.UserAuth().Refresh(ctx, req.RefreshToken) p, id, err := auth.UserAuth().Refresh(ctx, req.RefreshToken)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &userv1.RefreshRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil return &authv1.RefreshRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil
} }

View File

@ -0,0 +1,22 @@
// =================================================================================
// 本文件由 GoFrame CLI 工具自动生成,可按需修改。
// =================================================================================
package dao
import (
"service.xpcool.com/internal/dao/internal"
)
// adminLoginLogDao 是表 admin_login_log 的数据访问对象。
// 可在其上定义自定义方法以扩展其功能。
type adminLoginLogDao struct {
*internal.AdminLoginLogDao
}
var (
// AdminLoginLog 是表 admin_login_log 的全局可访问操作对象。
AdminLoginLog = adminLoginLogDao{internal.NewAdminLoginLogDao()}
)
// 在下方添加你的自定义方法。

View File

@ -1,5 +1,5 @@
// ================================================================================= // =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. // 本文件由 GoFrame CLI 工具自动生成,可按需修改。
// ================================================================================= // =================================================================================
package dao package dao
@ -8,15 +8,15 @@ import (
"service.xpcool.com/internal/dao/internal" "service.xpcool.com/internal/dao/internal"
) )
// adminMenuDao is the data access object for the table admin_menu. // adminMenuDao 是表 admin_menu 的数据访问对象。
// You can define custom methods on it to extend its functionality as needed. // 可在其上定义自定义方法以扩展其功能。
type adminMenuDao struct { type adminMenuDao struct {
*internal.AdminMenuDao *internal.AdminMenuDao
} }
var ( var (
// AdminMenu is a globally accessible object for table admin_menu operations. // AdminMenu 是表 admin_menu 的全局可访问操作对象。
AdminMenu = adminMenuDao{internal.NewAdminMenuDao()} AdminMenu = adminMenuDao{internal.NewAdminMenuDao()}
) )
// Add your custom methods and functionality below. // 在下方添加你的自定义方法。

View File

@ -1,5 +1,5 @@
// ================================================================================= // =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. // 本文件由 GoFrame CLI 工具自动生成,可按需修改。
// ================================================================================= // =================================================================================
package dao package dao
@ -8,15 +8,15 @@ import (
"service.xpcool.com/internal/dao/internal" "service.xpcool.com/internal/dao/internal"
) )
// adminOperationLogDao is the data access object for the table admin_operation_log. // adminOperationLogDao 是表 admin_operation_log 的数据访问对象。
// You can define custom methods on it to extend its functionality as needed. // 可在其上定义自定义方法以扩展其功能。
type adminOperationLogDao struct { type adminOperationLogDao struct {
*internal.AdminOperationLogDao *internal.AdminOperationLogDao
} }
var ( var (
// AdminOperationLog is a globally accessible object for table admin_operation_log operations. // AdminOperationLog 是表 admin_operation_log 的全局可访问操作对象。
AdminOperationLog = adminOperationLogDao{internal.NewAdminOperationLogDao()} AdminOperationLog = adminOperationLogDao{internal.NewAdminOperationLogDao()}
) )
// Add your custom methods and functionality below. // 在下方添加你的自定义方法。

View File

@ -1,5 +1,5 @@
// ================================================================================= // =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. // 本文件由 GoFrame CLI 工具自动生成,可按需修改。
// ================================================================================= // =================================================================================
package dao package dao
@ -8,15 +8,15 @@ import (
"service.xpcool.com/internal/dao/internal" "service.xpcool.com/internal/dao/internal"
) )
// adminRoleDao is the data access object for the table admin_role. // adminRoleDao 是表 admin_role 的数据访问对象。
// You can define custom methods on it to extend its functionality as needed. // 可在其上定义自定义方法以扩展其功能。
type adminRoleDao struct { type adminRoleDao struct {
*internal.AdminRoleDao *internal.AdminRoleDao
} }
var ( var (
// AdminRole is a globally accessible object for table admin_role operations. // AdminRole 是表 admin_role 的全局可访问操作对象。
AdminRole = adminRoleDao{internal.NewAdminRoleDao()} AdminRole = adminRoleDao{internal.NewAdminRoleDao()}
) )
// Add your custom methods and functionality below. // 在下方添加你的自定义方法。

View File

@ -1,5 +1,5 @@
// ================================================================================= // =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. // 本文件由 GoFrame CLI 工具自动生成,可按需修改。
// ================================================================================= // =================================================================================
package dao package dao
@ -8,15 +8,15 @@ import (
"service.xpcool.com/internal/dao/internal" "service.xpcool.com/internal/dao/internal"
) )
// adminRoleMenuDao is the data access object for the table admin_role_menu. // adminRoleMenuDao 是表 admin_role_menu 的数据访问对象。
// You can define custom methods on it to extend its functionality as needed. // 可在其上定义自定义方法以扩展其功能。
type adminRoleMenuDao struct { type adminRoleMenuDao struct {
*internal.AdminRoleMenuDao *internal.AdminRoleMenuDao
} }
var ( var (
// AdminRoleMenu is a globally accessible object for table admin_role_menu operations. // AdminRoleMenu 是表 admin_role_menu 的全局可访问操作对象。
AdminRoleMenu = adminRoleMenuDao{internal.NewAdminRoleMenuDao()} AdminRoleMenu = adminRoleMenuDao{internal.NewAdminRoleMenuDao()}
) )
// Add your custom methods and functionality below. // 在下方添加你的自定义方法。

View File

@ -1,5 +1,5 @@
// ================================================================================= // =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. // 本文件由 GoFrame CLI 工具自动生成,可按需修改。
// ================================================================================= // =================================================================================
package dao package dao
@ -8,15 +8,15 @@ import (
"service.xpcool.com/internal/dao/internal" "service.xpcool.com/internal/dao/internal"
) )
// adminUserDao is the data access object for the table admin_user. // adminUserDao 是表 admin_user 的数据访问对象。
// You can define custom methods on it to extend its functionality as needed. // 可在其上定义自定义方法以扩展其功能。
type adminUserDao struct { type adminUserDao struct {
*internal.AdminUserDao *internal.AdminUserDao
} }
var ( var (
// AdminUser is a globally accessible object for table admin_user operations. // AdminUser 是表 admin_user 的全局可访问操作对象。
AdminUser = adminUserDao{internal.NewAdminUserDao()} AdminUser = adminUserDao{internal.NewAdminUserDao()}
) )
// Add your custom methods and functionality below. // 在下方添加你的自定义方法。

View File

@ -1,5 +1,5 @@
// ================================================================================= // =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. // 本文件由 GoFrame CLI 工具自动生成,可按需修改。
// ================================================================================= // =================================================================================
package dao package dao
@ -8,15 +8,15 @@ import (
"service.xpcool.com/internal/dao/internal" "service.xpcool.com/internal/dao/internal"
) )
// adminUserRoleDao is the data access object for the table admin_user_role. // adminUserRoleDao 是表 admin_user_role 的数据访问对象。
// You can define custom methods on it to extend its functionality as needed. // 可在其上定义自定义方法以扩展其功能。
type adminUserRoleDao struct { type adminUserRoleDao struct {
*internal.AdminUserRoleDao *internal.AdminUserRoleDao
} }
var ( var (
// AdminUserRole is a globally accessible object for table admin_user_role operations. // AdminUserRole 是表 admin_user_role 的全局可访问操作对象。
AdminUserRole = adminUserRoleDao{internal.NewAdminUserRoleDao()} AdminUserRole = adminUserRoleDao{internal.NewAdminUserRoleDao()}
) )
// Add your custom methods and functionality below. // 在下方添加你的自定义方法。

View File

@ -1,5 +1,5 @@
// ================================================================================= // =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. // 本文件由 GoFrame CLI 工具自动生成,可按需修改。
// ================================================================================= // =================================================================================
package dao package dao
@ -8,15 +8,15 @@ import (
"service.xpcool.com/internal/dao/internal" "service.xpcool.com/internal/dao/internal"
) )
// authRefreshSessionDao is the data access object for the table auth_refresh_session. // authRefreshSessionDao 是表 auth_refresh_session 的数据访问对象。
// You can define custom methods on it to extend its functionality as needed. // 可在其上定义自定义方法以扩展其功能。
type authRefreshSessionDao struct { type authRefreshSessionDao struct {
*internal.AuthRefreshSessionDao *internal.AuthRefreshSessionDao
} }
var ( var (
// AuthRefreshSession is a globally accessible object for table auth_refresh_session operations. // AuthRefreshSession 是表 auth_refresh_session 的全局可访问操作对象。
AuthRefreshSession = authRefreshSessionDao{internal.NewAuthRefreshSessionDao()} AuthRefreshSession = authRefreshSessionDao{internal.NewAuthRefreshSessionDao()}
) )
// Add your custom methods and functionality below. // 在下方添加你的自定义方法。

View File

@ -1,5 +1,5 @@
// ================================================================================= // =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. // 本文件由 GoFrame CLI 工具自动生成,可按需修改。
// ================================================================================= // =================================================================================
package dao package dao
@ -8,15 +8,15 @@ import (
"service.xpcool.com/internal/dao/internal" "service.xpcool.com/internal/dao/internal"
) )
// contentDao is the data access object for the table content. // contentDao 是表 content 的数据访问对象。
// You can define custom methods on it to extend its functionality as needed. // 可在其上定义自定义方法以扩展其功能。
type contentDao struct { type contentDao struct {
*internal.ContentDao *internal.ContentDao
} }
var ( var (
// Content is a globally accessible object for table content operations. // Content 是表 content 的全局可访问操作对象。
Content = contentDao{internal.NewContentDao()} Content = contentDao{internal.NewContentDao()}
) )
// Add your custom methods and functionality below. // 在下方添加你的自定义方法。

View File

@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"service.xpcool.com/internal/dao/internal"
)
// houseBuildingDao is the data access object for the table house_building.
// You can define custom methods on it to extend its functionality as needed.
type houseBuildingDao struct {
*internal.HouseBuildingDao
}
var (
// HouseBuilding is a globally accessible object for table house_building operations.
HouseBuilding = houseBuildingDao{internal.NewHouseBuildingDao()}
)
// Add your custom methods and functionality below.

View File

@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"service.xpcool.com/internal/dao/internal"
)
// houseCommunityDao is the data access object for the table house_community.
// You can define custom methods on it to extend its functionality as needed.
type houseCommunityDao struct {
*internal.HouseCommunityDao
}
var (
// HouseCommunity is a globally accessible object for table house_community operations.
HouseCommunity = houseCommunityDao{internal.NewHouseCommunityDao()}
)
// Add your custom methods and functionality below.

View File

@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"service.xpcool.com/internal/dao/internal"
)
// houseCommunityFacilityDao is the data access object for the table house_community_facility.
// You can define custom methods on it to extend its functionality as needed.
type houseCommunityFacilityDao struct {
*internal.HouseCommunityFacilityDao
}
var (
// HouseCommunityFacility is a globally accessible object for table house_community_facility operations.
HouseCommunityFacility = houseCommunityFacilityDao{internal.NewHouseCommunityFacilityDao()}
)
// Add your custom methods and functionality below.

View File

@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"service.xpcool.com/internal/dao/internal"
)
// houseFacilityDao is the data access object for the table house_facility.
// You can define custom methods on it to extend its functionality as needed.
type houseFacilityDao struct {
*internal.HouseFacilityDao
}
var (
// HouseFacility is a globally accessible object for table house_facility operations.
HouseFacility = houseFacilityDao{internal.NewHouseFacilityDao()}
)
// Add your custom methods and functionality below.

View File

@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"service.xpcool.com/internal/dao/internal"
)
// houseListingDao is the data access object for the table house_listing.
// You can define custom methods on it to extend its functionality as needed.
type houseListingDao struct {
*internal.HouseListingDao
}
var (
// HouseListing is a globally accessible object for table house_listing operations.
HouseListing = houseListingDao{internal.NewHouseListingDao()}
)
// Add your custom methods and functionality below.

Some files were not shown because too many files have changed in this diff Show More