Compare commits

..

7 Commits
main ... dev

Author SHA1 Message Date
xpcool-bot
05bb399321 fix(docker): bundle manifest/config into image
All checks were successful
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Successful in 56s
2026-08-25 00:09:44 +08:00
xpcool-bot
2c8d7f362e ci: retry deploy
All checks were successful
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Successful in 1m0s
2026-08-25 00:06:57 +08:00
xpcool-bot
364d9feeb1 ci: trigger first deploy
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 0s
2026-08-25 00:02:07 +08:00
xpcool-bot
eb519e3265 ci: tweak comment v3 2026-08-24 23:56:45 +08:00
xpcool-bot
fda44c1025 ci: tweak comment v2 2026-08-24 23:55:28 +08:00
xpcool-bot
f318d0d35b ci: tweak workflow comment 2026-08-24 23:54:19 +08:00
xpcool-bot
2637e62f3f ci: add build & docker deploy workflow with notification 2026-08-24 23:49:02 +08:00
261 changed files with 724 additions and 12712 deletions

View File

@ -1,9 +1,4 @@
# 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
# deployed by gitea act_runner - trigger on push v5
name: Build and Deploy (service.xpcool.com)
on:
@ -15,71 +10,54 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Install git & download Go toolchain
- name: Install git & docker CLI
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.d/debian.sources 2>/dev/null || true
apt-get update -qq
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
apt-get update
apt-get install -y git docker.io
- name: Checkout (local Gitea)
run: |
git clone --depth 1 --branch "${{ github.ref_name }}" https://oauth2:${{ github.token }}@git.xpcool.com/${{ github.repository }}.git .
git checkout ${{ github.sha }}
- name: Cross compile (linux/amd64, CGO disabled)
- name: Install Go 1.23
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:
GOPROXY: https://goproxy.cn,direct
GOFLAGS: -mod=mod
run: |
export PATH="/usr/local/go/bin:$PATH"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tmp/deploy/main main.go
ls -lh /tmp/deploy/main
export PATH=$PATH:/usr/local/go/bin
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o temp/linux_amd64/main .
- name: Assemble deploy dir (binary + config + resource + Dockerfile)
- name: Docker build & deploy
run: |
set -e
mkdir -p /tmp/deploy/manifest/config /tmp/deploy/resource
# 配置基础 config.yaml + 环境化 config.prod.yamlGF_GCFG_ENV=prod 时生效
cp manifest/config/config.prod.yaml /tmp/deploy/manifest/config/config.prod.yaml
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 \
-p 127.0.0.1:10100:10100 \
-e GF_GCFG_ENV=prod \
-e "DB_DSN=$DB_DSN" \
-e "JWT_SECRET=$JWT_SECRET" \
docker build -f manifest/docker/Dockerfile -t service.xpcool.com:latest .
docker rm -f service.xpcool.com 2>/dev/null || true
docker run -d \
--name service.xpcool.com \
--restart unless-stopped \
--network xpcool-net \
-p 127.0.0.1:8000:8000 \
-e DB_DSN="${{ secrets.DB_DSN }}" \
-e JWT_SECRET="${{ secrets.JWT_SECRET }}" \
service.xpcool.com:latest
# 冒烟等待启动并验证 OpenAPI
sleep 5
curl -fsS -m 10 http://127.0.0.1:10100/api.json | head -c 120 || { echo "SMOKE TEST FAILED"; exit 1; }
echo
echo "Deployed service.xpcool.com (container restarted, 127.0.0.1:10100)"
- name: Notify success (PushPlus)
if: success()
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://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"}'

21
.gitignore vendored
View File

@ -16,23 +16,4 @@ manifest/output/
temp/
temp.yaml
bin
**/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*
**/config/config.yaml

View File

@ -1,55 +0,0 @@
# service.xpcool.com 变更记录
> 倒序最新在上格式YYYY-MM-DD | 类型 | 摘要
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
View File

@ -1,126 +0,0 @@
# 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

@ -4,16 +4,11 @@
service.xpcool.com/
api/ # HTTP contracts and Swagger metadata
user/v1/ # /api/v1 - client-facing API
admin/v1/ # /admin/v1 - administration API
open/v1/ # /api/open/v1 - open API for frontends (no auth)
tools/ # one dir per sub-feature: tools/<name>/<name>.go
common/ # public reusable module (no internal/ deps)
tools/ # utility toolbox: md5, cryptox, uuid, random,
# timex, convertx, strx, slicex, ip, filex
admin/v1/ # /admin/v1 - administration API
internal/
cmd/ # application bootstrap and route isolation
consts/ # application error codes and constants
controller/ # API-to-service adapters only (incl. open/ open API)
controller/ # API-to-service adapters only
service/ # domain use cases and provider interfaces
dao/ # generated by gf gen dao; never hand edited
model/
@ -23,7 +18,6 @@ service.xpcool.com/
vo/ # API view models
middleware/ # configurable route-group middleware
library/ # JWT, response, pagination primitives
docs/change-log/ # per-request change records (agent context)
manifest/
config/ # config.dev/test/prod.yaml
sql/ # ordered MySQL migrations
@ -31,5 +25,3 @@ service.xpcool.com/
```
`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.
See `AGENTS.md` for the agent-facing conventions and the context-memory system.

View File

@ -20,9 +20,3 @@ The command is intentionally the only source of `internal/dao`, `internal/model/
- `/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.
## Local startup
- **Terminal**: `source .env.dev && go run main.go` (`.env.dev` is gitignored, holds local DSN + secret).
- **GoLand**: run configuration `.run/service-dev.run.xml` (project-level, auto-detected after reloading the project) already injects `GF_GCFG_FILE` / `DB_DSN` / `JWT_SECRET`; pick it in the run dropdown. The file is gitignored because it contains the DB password.

View File

@ -1,88 +0,0 @@
// 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#password too short"`
EncryptedKey string `json:"encryptedKey"`
EncryptedData string `json:"encryptedData"`
Nickname string `json:"nickname"`
BarkDeviceId string `json:"barkDeviceId"`
PushplusToken string `json:"pushplusToken"`
RoleIds []uint64 `json:"roleIds"`
}
// 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#password too short"`
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

@ -1,79 +0,0 @@
// 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

@ -1,30 +0,0 @@
// 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

@ -1,33 +0,0 @@
// 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

@ -1,78 +0,0 @@
// 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#type must be 1 or 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#type must be 1 or 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

@ -1,63 +0,0 @@
// 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{}

15
api/admin/v1/auth.go Normal file
View File

@ -0,0 +1,15 @@
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"`
}

15
api/hello/hello.go Normal file
View File

@ -0,0 +1,15 @@
// =================================================================================
// 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)
}

12
api/hello/v1/hello.go Normal file
View File

@ -0,0 +1,12 @@
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

@ -1,93 +0,0 @@
// 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

@ -1,85 +0,0 @@
// 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

@ -1,139 +0,0 @@
// 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

@ -1,37 +0,0 @@
// 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

@ -1,33 +0,0 @@
// 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"`
}

View File

@ -1,73 +0,0 @@
// 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"`
}

View File

@ -1,128 +0,0 @@
// 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"`
}

View File

@ -1,15 +0,0 @@
// 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

View File

@ -1,15 +0,0 @@
// 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"` // 是否为内网/私网地址
}

View File

@ -1,15 +0,0 @@
// 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#text required"`
}
// MD5Res 是 MD5Req 的响应。
type MD5Res struct {
MD5 string `json:"md5"`
}

View File

@ -1,16 +0,0 @@
// 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#length must be 1-128"`
Type string `json:"type" d:"alnum" v:"in:alnum,digits,letters#unsupported type"`
}
// RandomRes 是 RandomReq 的响应。
type RandomRes struct {
Value string `json:"value"`
}

View File

@ -1,16 +0,0 @@
// 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

@ -1,15 +0,0 @@
// 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

@ -1,191 +0,0 @@
// 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

@ -1,91 +0,0 @@
// 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"` // 按类型分布
}

View File

@ -1,33 +0,0 @@
// 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#login type required|unsupported login type"`
Code string `json:"code"` // 微信授权码
Mobile string `json:"mobile"` // 手机号mobile 登录方式)
VerifyCode string `json:"verifyCode"` // 短信验证码
Account string `json:"account"` // 账号password 登录方式)
Password string `json:"password"` // 密码
Terminal string `json:"terminal" v:"required|in:mini,h5,app#terminal required|invalid terminal"`
}
// 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

View File

@ -1 +0,0 @@
package user_login

25
api/user/v1/auth.go Normal file
View File

@ -0,0 +1,25 @@
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

View File

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

View File

@ -1,64 +0,0 @@
// 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

@ -1,80 +0,0 @@
// 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
}

View File

@ -1,24 +0,0 @@
// Package tools 是项目的公共工具集。
//
// 每个子包都是对 GoFrame 内置组件的薄封装,
// gmd5、gaes、gdes、guid、grand、gtime、gconv、gstr、gfile 等),
// 保持小巧且与框架风格一致。
//
// 结构:
//
// common/tools/md5 MD5 digest helpers
// common/tools/cryptox AES/DES encryption with base64 output
// common/tools/uuid unique ID generation
// common/tools/random random numbers and strings
// common/tools/timex time formatting and computation
// common/tools/convertx type conversion with default fallback
// common/tools/strx string / naming / masking helpers
// common/tools/slicex generic slice utilities
// common/tools/ip IP address helpers
// common/tools/filex file system helpers
//
// 规则:
// - Never depend on internal/ — this module must stay self-contained.
// - Prefer reusing GoFrame built-in components over re-implementing.
// - Keep each helper small and add a Chinese doc comment.
package tools

View File

@ -1,28 +0,0 @@
// 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)
}

View File

@ -1,59 +0,0 @@
// 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 地址
// (private ranges, loopback or link-local).
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 数值表示
// (big-endian, same as 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))
}

View File

@ -1,23 +0,0 @@
// 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

@ -1,26 +0,0 @@
// 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

@ -1,64 +0,0 @@
// 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
}

View File

@ -1,57 +0,0 @@
// 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

@ -1,46 +0,0 @@
// 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")
}

View File

@ -1,21 +0,0 @@
// 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)
}

View File

@ -1,23 +0,0 @@
# 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

@ -1,273 +0,0 @@
# 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

@ -1,223 +0,0 @@
# 看房系统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 用贝壳成交频道兜底

5
go.mod
View File

@ -4,11 +4,6 @@ go 1.23.0
require github.com/gogf/gf/v2 v2.10.2
require (
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2 // indirect
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect

6
go.sum
View File

@ -15,10 +15,6 @@ 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
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/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@ -33,8 +29,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lionsoul2014/ip2region v3.17.0+incompatible h1:91PbwotwoEMPV6nfAALL+WTmYHmTuRNFgJX6T8uPUNY=
github.com/lionsoul2014/ip2region v3.17.0+incompatible/go.mod h1:+ZBN7PBoh5gG6/y0ZQ85vJDBe21WnfbRrQQwTfliJJI=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=

View File

@ -4,9 +4,8 @@
gfcli:
gen:
dao:
- link: "mysql:root:root123@tcp(127.0.0.1:3306)/service_xpcool_com"
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
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:
build: "-a amd64 -s linux -p temp -ew"

View File

@ -4,167 +4,49 @@ import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gcmd"
"github.com/gogf/gf/v2/os/genv"
"github.com/gogf/gf/v2/text/gstr"
adminctl "service.xpcool.com/internal/controller/admin"
housectl "service.xpcool.com/internal/controller/house"
openctl "service.xpcool.com/internal/controller/open"
recruitmentctl "service.xpcool.com/internal/controller/recruitment"
noticectl "service.xpcool.com/internal/controller/notice"
jobctl "service.xpcool.com/internal/controller/job"
serversecurityctl "service.xpcool.com/internal/controller/serversecurity"
"service.xpcool.com/internal/controller/hello"
userctl "service.xpcool.com/internal/controller/user"
"service.xpcool.com/internal/library/crypto"
"service.xpcool.com/internal/library/jwt"
"service.xpcool.com/internal/middleware"
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"
userauth "service.xpcool.com/internal/service/user/auth"
housecommunity "service.xpcool.com/internal/service/house/community"
housedashboard "service.xpcool.com/internal/service/house/dashboard"
houselisting "service.xpcool.com/internal/service/house/listing"
housetransaction "service.xpcool.com/internal/service/house/transaction"
housepresale "service.xpcool.com/internal/service/house/presale"
recruitmentsvc "service.xpcool.com/internal/service/recruitment"
noticesvc "service.xpcool.com/internal/service/notice"
jobsvc "service.xpcool.com/internal/service/job"
serversecuritysvc "service.xpcool.com/internal/service/serversecurity"
"service.xpcool.com/internal/service"
)
// 开发环境默认值任何启动方式GoLand / 命令行 / go run漏注入环境变量时兜底
// 避免 ${DB_DSN} 占位符原样传入 gdb 导致全站接口 500。
// 生产GF_GCFG_ENV=prod不启用兜底配置缺失应显性报错。
const (
devDefaultDBDSN = "mysql:root:root123@tcp(127.0.0.1:3306)/service_xpcool_com?loc=Local"
devDefaultRecruitmentDSN = "mysql:root:root123@tcp(127.0.0.1:3306)/recruitment?loc=Local"
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 (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
injectEnv(ctx)
s := g.Server()
tokens := jwt.New(ctx)
// 登录密码混合加密服务RSA + AES-GCM私钥加载/生成失败属致命错误,直接终止启动。
cryptoSvc, err := crypto.New(ctx)
if err != nil {
panic(err)
}
userauth.RegisterUserAuth(userauth.NewUserAuth(tokens, nil, nil))
adminauth.RegisterAdminAuth(adminauth.NewAdminAuth(tokens, cryptoSvc))
adminmenu.RegisterAdminMenu(adminmenu.NewAdminMenu())
admin.RegisterAdminManage(admin.NewAdminManage())
role.RegisterRoleManage(role.NewRoleManage())
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.Bind(openctl.New()) // Open tools API for frontends, no auth required.
group.Bind(serversecurityctl.NewReport()) // 安全日志上报(宿主机脚本,内部令牌校验)。
service.RegisterUserAuth(service.NewUserAuth(tokens, nil, nil))
service.RegisterAdminAuth(service.NewAdminAuth(tokens))
service.RegisterAdminAudit(service.NewAdminAudit())
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Recover, middleware.CORS)
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(
hello.NewV1(),
)
})
s.Group("/api/service/user", func(group *ghttp.RouterGroup) {
s.Group("/api/v1", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse)
group.Bind(userctl.New()) // Login and refresh routes are public.
group.Group("/", func(protected *ghttp.RouterGroup) { protected.Middleware(middleware.UserAuth(tokens)) })
})
s.Group("/api/service/admin", func(group *ghttp.RouterGroup) {
// 全量请求/响应加密(生产 encrypt.fullBody=trueCORS → APICrypto → Recover → HandlerResponse。
// Recover 置于 APICrypto 内层,使 500 异常响应同样被加密public-key 端点明文豁免。
group.Middleware(middleware.CORS, middleware.APICrypto(cryptoSvc), middleware.Recover, ghttp.MiddlewareHandlerResponse)
group.Bind(adminctl.NewAuth()) // Public: login only.
group.Group("/", func(profile *ghttp.RouterGroup) {
// 仅登录端点:个人资料 / 权限码 / 菜单路由。
profile.Middleware(middleware.AdminAuthOnly(tokens))
profile.Bind(adminctl.NewProfile())
})
s.Group("/admin/v1", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse)
group.Bind(adminctl.New()) // Admin login remains public; protected controllers mount separately.
group.Group("/", func(protected *ghttp.RouterGroup) {
// 受权限保护端点RBAC 管理、日志等。
// 权限由后端按「方法+路径」自动匹配,无需前端传 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.Middleware(middleware.AdminAuth(tokens, service.AdminAuth().HasPermission, func(ctx context.Context, id uint64, permission, method, path, ip, param string, duration, status int) {
service.AdminAudit().Record(ctx, service.AuditEvent{AdminID: id, Permission: permission, Method: method, Path: path, IP: ip, Param: param, DurationMS: duration, StatusCode: status})
}))
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()
return nil
},

View File

@ -1,93 +0,0 @@
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, "encrypted payload required")
}
// 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,72 +2,18 @@ package admin
import (
"context"
"github.com/gogf/gf/v2/net/ghttp"
authv1 "service.xpcool.com/api/admin/admin/login"
adminv1 "service.xpcool.com/api/admin/v1"
"service.xpcool.com/internal/model/dto"
auth "service.xpcool.com/internal/service/admin/admin/login"
loginlog "service.xpcool.com/internal/service/admin/system/login_log"
"service.xpcool.com/internal/service"
)
// AuthController 仅暴露公开的登录相关端点。
type AuthController struct{}
type Controller struct{}
// NewAuth 创建公开的管理端认证控制器(仅登录)。
func NewAuth() *AuthController { return &AuthController{} }
// 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)
func New() *Controller { return &Controller{} }
func (c *Controller) Login(ctx context.Context, req *adminv1.LoginReq) (res *adminv1.LoginRes, err error) {
p, id, err := service.AdminAuth().Login(ctx, dto.AdminLoginInput{Username: req.Username, Password: req.Password})
if err != nil {
return nil, err
}
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
return &adminv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, AdminID: id}, nil
}

View File

@ -1,21 +0,0 @@
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

@ -1,58 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,64 +0,0 @@
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

@ -1,69 +0,0 @@
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

@ -1,49 +0,0 @@
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

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

View File

@ -0,0 +1,15 @@
// =================================================================================
// 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

@ -0,0 +1,13 @@
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

@ -1,64 +0,0 @@
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

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

View File

@ -1,67 +0,0 @@
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

@ -1,85 +0,0 @@
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

@ -1,36 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,68 +0,0 @@
// 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

@ -1,108 +0,0 @@
// 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

@ -1,11 +0,0 @@
// 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

@ -1,16 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,22 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,16 +0,0 @@
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

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

View File

@ -1,159 +0,0 @@
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

@ -1,21 +0,0 @@
// 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

@ -1,57 +0,0 @@
// 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

@ -1,24 +0,0 @@
// 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 (
"context"
authv1 "service.xpcool.com/api/user/auth"
userv1 "service.xpcool.com/api/user/v1"
"service.xpcool.com/internal/model/dto"
auth "service.xpcool.com/internal/service/user/auth"
"service.xpcool.com/internal/service"
)
type Controller struct{}
func New() *Controller { return &Controller{} }
func (c *Controller) Login(ctx context.Context, req *authv1.LoginReq) (res *authv1.LoginRes, err error) {
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})
func (c *Controller) Login(ctx context.Context, req *userv1.LoginReq) (res *userv1.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})
if err != nil {
return nil, err
}
return &authv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil
return &userv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil
}
func (c *Controller) Refresh(ctx context.Context, req *authv1.RefreshReq) (res *authv1.RefreshRes, err error) {
p, id, err := auth.UserAuth().Refresh(ctx, req.RefreshToken)
func (c *Controller) Refresh(ctx context.Context, req *userv1.RefreshReq) (res *userv1.RefreshRes, err error) {
p, id, err := service.UserAuth().Refresh(ctx, req.RefreshToken)
if err != nil {
return nil, err
}
return &authv1.RefreshRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil
return &userv1.RefreshRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +0,0 @@
// =================================================================================
// 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

@ -1,22 +0,0 @@
// =================================================================================
// 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

@ -1,22 +0,0 @@
// =================================================================================
// 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

@ -1,22 +0,0 @@
// =================================================================================
// 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

@ -1,22 +0,0 @@
// =================================================================================
// 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.

View File

@ -1,22 +0,0 @@
// =================================================================================
// 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"
)
// housePreferenceDao is the data access object for the table house_preference.
// You can define custom methods on it to extend its functionality as needed.
type housePreferenceDao struct {
*internal.HousePreferenceDao
}
var (
// HousePreference is a globally accessible object for table house_preference operations.
HousePreference = housePreferenceDao{internal.NewHousePreferenceDao()}
)
// Add your custom methods and functionality below.

View File

@ -1,22 +0,0 @@
// =================================================================================
// 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"
)
// housePresaleDao is the data access object for the table house_presale.
// You can define custom methods on it to extend its functionality as needed.
type housePresaleDao struct {
*internal.HousePresaleDao
}
var (
// HousePresale is a globally accessible object for table house_presale operations.
HousePresale = housePresaleDao{internal.NewHousePresaleDao()}
)
// Add your custom methods and functionality below.

View File

@ -1,22 +0,0 @@
// =================================================================================
// 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"
)
// housePriceSnapshotDao is the data access object for the table house_price_snapshot.
// You can define custom methods on it to extend its functionality as needed.
type housePriceSnapshotDao struct {
*internal.HousePriceSnapshotDao
}
var (
// HousePriceSnapshot is a globally accessible object for table house_price_snapshot operations.
HousePriceSnapshot = housePriceSnapshotDao{internal.NewHousePriceSnapshotDao()}
)
// Add your custom methods and functionality below.

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