Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05bb399321 | ||
|
|
2c8d7f362e | ||
|
|
364d9feeb1 | ||
|
|
eb519e3265 | ||
|
|
fda44c1025 | ||
|
|
f318d0d35b | ||
|
|
2637e62f3f |
@ -1,9 +1,4 @@
|
||||
# service.xpcool.com 自动部署:Gitea act_runner(push 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/ 下脚本(003→004→004b→007),workflow 不做自动 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.yaml(GF_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"}'
|
||||
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
@ -17,22 +17,3 @@ 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*
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
# service.xpcool.com 变更记录
|
||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
||||
|
||||
2026-09-13 | DOC | 项目整理与全局中文化:①重写 README.MD(原英文且路由陈旧 /api/v1 → 实际三分组 /api/service/{open,user,admin},补分层约定/RBAC 权限映射/gen dao 流程/本地启动);②重写 PROJECT_STRUCTURE.md(原英文目录树缺 house/recruitment/notice/job/serversecurity,标注手写 entity/do/dao 风险);③hack/hack.mk + hack-cli.mk 注释中文化;④common/doc.go + common/tools/doc.go 工具清单中文化,ip/main.go/cmd.go 英文注释中文化;⑤**校验提示与服务层错误信息中文化**(19 文件逾 90 处):api 层 v:"#提示" 中文(user/auth、tools/md5、tools/random、menu_manage、admin)、service 层 gerror.Wrap/response.Error 全部中文(admin/admin、admin/login、system/{role,menu,menu_manage,login_log}、user/auth、house/{community,listing,dashboard,transaction,presale}、notice、job、serversecurity、recruitment/{recruitment,crawler})、jwt.go、controller/admin/admin.go、各 service 的 panic("xxx implementation not registered")→"xxx 实现未注册";⑥api/user/login/login.go 补中文包文档。生成代码(entity/do/dao)英文注释按约定保持原样(DO NOT EDIT,重生成会覆盖)。仅注释/文档/字符串改动,无业务逻辑变更;read_lints 全绿,go build 因 proxy.golang.org 网络超时未跑通。详见 docs/change-log/2026-09-13.md
|
||||
|
||||
2026-08-27 | CHG | 登录接口从全量加密豁免,永远只加密密码字段:APICrypto plainRoutes 增 /system/auth/login(登录请求不整体加密,handler 直接收 {username, encryptedKey, encryptedData},登录响应因无会话密钥保持明文);ResolveLogin 分支顺序调整——密码字段密文优先解密,其次 fullBody 明文、开发 allowPlain。全量模式验证 5 项(登录明文响应/info 密文/解密成功/未加密拒绝/登出)+ dev 回归 15 项全过
|
||||
|
||||
2026-08-27 | FEAT | 全量请求/响应加密(生产 encrypt.fullBody=true / env ENCRYPT_FULL_BODY):①crypto.Service 增 fullBody 开关 + DecryptRequest(RSA 解 AES 会话密钥 + AES-GCM 解 body,返回会话密钥)+ EncryptResponse(用同一会话密钥 AES-GCM 加密响应回传,请求结束即销毁);②新增 middleware/APICrypto——请求整体解密(io.ReadAll 直接读 r.Request.Body 勿用 GetBody,否则缓存密文致 handler Parse 读到密文)+ 响应加密 buffer(HandlerResponse 外层、Recover 内层,500 也加密),public-key 明文豁免,未加密业务请求一律拒绝;③admin 组中间件链改 CORS→APICrypto→Recover→HandlerResponse;④ResolveLogin/resolvePassword 增 fullBody 分支(传输层已整体解密,直接信任明文 username/password);⑤injectEnv 支持 ENCRYPT_FULL_BODY/ENCRYPT_ALLOW_PLAIN(直接跑二进制恒加载 config.yaml,GF_GCFG_ENV 仅 gf run 认,必须 env 注入)。⚠️GF 坑:r.GetBody() 缓存密文到 bodyContent,handler Parse 走缓存 → 中间件必须 io.ReadAll(r.Request.Body)。dev(仅密码加密)E2E 15 项、prod(全量加密)E2E 9 项全过
|
||||
|
||||
2026-08-27 | FEAT | 登录密码「RSA + AES-GCM」混合加密传输(对称+非对称结合)+ 创建/重置密码加密字段:①新建 internal/library/crypto——密钥优先级 配置 encrypt.privateKey(PEM) > data/crypto/rsa_private.pem > 自动生成落盘(data/ 已 gitignore);公钥输出 SPKI(x509.MarshalPKIXPublicKey,前端 WebCrypto importKey('spki'),⚠️PKCS#1 会 ASN.1 wrong tag);DecryptLogin 解密 {username,password,ts}(ts 5 分钟窗口防重放)、DecryptField 解密 {value,ts}(创建/重置密码复用);进程级 SetDefault/Get 单例。②公开接口 POST /system/auth/public-key 返回 publicKey;LoginReq 改 encryptedKey+encryptedData(明文字段仅 encrypt.allowPlain=true 时可用,config.yaml/prod 默认 false、dev true);controller.ResolveLogin 统一解析凭据。③AdminCreate/AdminResetPwd 同样支持加密 password(resolvePassword 复用 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 均为启动漏 env),prod 不兜底。端到端 8 项 curl 全过:错密码 30002/登录/伪造 token HTTP401/轮换/旧令牌重放拒绝/登出撤销/登出后拒绝/有效访问 200。注:并行 notice/job 会话提交的 admin_user entity 为 bool/string 映射(公司 gen 模板),已按旧风格手工恢复 int/gtime 并补两列
|
||||
2026-08-27 | CHG | 通知模块+自动任务+用户渠道字段:016 SQL(admin_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/job(11 接口全 POST 无 URL 参数);service/notice(Send 统一发送:Bark 路径式 POST /{key} + pushplus 官方接口;渠道/规则/日志/测试);service/job(DB 驱动 gcron:List/Save/Trigger/LogList + Register 业务函数注册 + 完成/失败自动通知 job_done/job_fail);recruitment scheduler 改 RegisterTasks 纳管(原 StartScheduler 废弃);用户 admin 接口支持渠道字段;已部署:容器重建 api.json 200、11 路由全注册、调度器无 handler 缺失告警、种子数据验证 OK。
|
||||
2026-08-27 | FIX | 菜单种子主键冲突修复(743683e):recruitment/003_menu.sql 招聘中心一级菜单 id=95 与 014_house_presale_menu.sql 新房预售 id=95(挂看房中心 90)ON DUPLICATE 互相覆盖、后导者赢,本地实际发生(新房预售菜单被顶掉致 /house/presale 404、生产库同样风险);改为 id=96 后发现 015_server_security.sql(并行会话,生产已部署)也用 96=安全日志(挂系统监控5),再改 **97**:95=新房预售(挂90)、96=安全日志(挂5)、97=招聘中心,三模块错开;本地按 014→015→003 重放,routes 接口 22 节点验证全部正确(e18b25f)。⚠️ 生产库需按 014→015→003 顺序重放修复(当前生产 95=招聘中心、新房预售菜单丢失)
|
||||
|
||||
2026-08-27 | CHG | 服务器安全日志模块:015 SQL 建 server_security_log 表 + 菜单(96安全日志/960查询/961统计);api/serversecurity/v1 三接口全 POST——open 上报 /api/service/open/security/log/report(body token+list,校验 internalToken=INTERNAL_TOKEN env)+ admin 查询 /server-security/log/list、统计 /server-security/log/stats(时间/IP/类型/端口筛选+TOP攻击源);entity/do/dao(主库)/dto/service/controller(ReportController+ManageController 双控制器);cmd.go 注入 INTERNAL_TOKEN→internalToken、open 组绑 NewReport、protected 组绑 NewManage;go build 通过,交叉编译上传 + 服务器 docker build + 容器重建(复用 env 追加 INTERNAL_TOKEN),路由/上报验证 OK(错误 token 返回 10002)
|
||||
2026-08-27 | FIX | 本地登录 500 修复:根因=config.dev.yaml 的 ${RECRUITMENT_DB_DSN} 占位符在本地无对应 env(injectEnv 只回填已有变量),gdb 配置解析失败致 g.DB() 全局报 invalid link configuration;本地补建 recruitment 库(导入 001_init+002_seed_sources,6 表 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/service(POST /api/service/admin/house/presale/list,筛选 region/purpose/keyword,id 倒序);dto 加 HousePresaleVO;house_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 加 AvgPrice);go build 通过
|
||||
2026-08-27 | CHG | 服务器日志(操作审计 admin_operation_log)全面增强为综合分页列表:① 新增字段 admin_username(冗余账号)、ip_location(IP归属地)、error_message(失败原因)、user_agent;② 自建纯 Go 的 xdb v4 离线解析器 internal/library/iploc(go:embed 嵌入 ip2region.xdb,无外部依赖,单次内存二分),审计写入时解析账号+归属地、记录 UA 与错误摘要;③ 查询筛选覆盖时间范围/账号/IP/归属地/HTTP方法/结果(成功2xx·失败非2xx)/关键字(权限码·路径·IP)/耗时上下限/排序(时间·耗时);④ 接口 POST /api/service/admin/system/log(body 入参,遵守全 POST 无参约定),dto.LogQuery/LogItem 同步;⑤ SQL:001_core.sql 建表补列 + 新增 010_server_log_enhance.sql 增量迁移(须手动导库,DB 结构变更不自动 DDL);go 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 加 HouseTransactionVO,cmd.go 注册;012_house_transaction_menu.sql 权限种子(house:data:transaction id=941 挂数据明细 94);go build 通过
|
||||
2026-08-26 | CFG | 新增 Gitea act_runner 自动部署:.gitea/workflows/deploy.yml(push 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:10100(xpcool-net,GF_GCFG_ENV=prod,DB_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 个 RegisterService;go build 通过。坑①gf CLI 为 com.lib.gf.v2 分支,gen dao 产物 import 需 sed 回 github.com/gogf/gf/v2(18 文件);②mysql 客户端须 --default-character-set=utf8mb4 否则中文 COMMENT/INSERT 乱码;③TINYINT 字段 comment 含 0/1 被 gf 映射 bool(status 三值改 INT);hack/config.yaml 的 link+tables 已指向本地库
|
||||
2026-08-26 | CHG | 生产超级管理员账号替换:删除 admin/admin123(用户/绑定/refresh 会话全清),新增 xxcool/xxCool@2026(bcrypt $2b$10$,绑定 super_admin);脚本 manifest/sql/009_admin_account_v2.sql(008 编号已被 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),原按钮 63(system: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=1,opuser 联调验证 codes 空/routes 空/接口 403,admin 全链路回归通过
|
||||
|
||||
2026-08-26 | REQ | 看房方案补充「后台管理 + 可视化看板」两层:数据管理用 vben 标准列表 CRUD(查询表单 + vxe-table + 分页 + 批量,页面含小区/房源/快照/成交/配套学区/采集任务),可视化看板用全局筛选器驱动图表联动(筛选器→图表、图表交叉过滤、列表↔地图双向,状态放 Pinia);Go 接口分管理 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/score),admin 新增看房页面(echarts@6 已内置),另立独立 Python 采集分析服务(采集/清洗/去重/打分,只写库不对外 API);决策:目标城市贵阳、新房+二手房都做、先本地后上云;核心风险为贵阳网签/成交数据公开程度不如一线
|
||||
2026-08-26 | DEP | 生产部署上线(193.112.118.168):交叉编译 linux/amd64,重建容器 service.xpcool.com:new(127.0.0.1:10100,GF_GCFG_ENV=prod,DB_DSN 补 mysql: 前缀,沿用 JWT_SECRET);nginx 改 HTTPS(443+80→301,证书 /data/nginx/ssl/service.xpcool.com/)反代 10100;DB service 库补种 003→004→004b→007,权限映射对齐 /api/service 路由,admin/admin123 登录验证通过
|
||||
2026-08-26 | CFG | 新增 manifest/sql/007_menu_permissions_v3.sql(type=2 权限路径映射最新 /api/service 全 POST 路由,含 system:log:list id=63)与 004b_fill_seed.sql(004 中断后的幂等补齐脚本);部署要点:容器内需 config.yaml(基础配置)否则 GoFrame 启动报找不到 config
|
||||
|
||||
2026-08-26 | FIX | RBAC 安全漏洞修复:禁用角色(status=0)绑定的权限仍生效。根因为 Codes/HasPermission/Routes 三处联表查询未过滤 admin_role 启用状态,仅 roleCodes 过滤。修复:三处统一 LeftJoin admin_role 并加 r.status=1,opuser 联调验证 codes 空/routes 空/接口 403,admin 全链路回归通过
|
||||
|
||||
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 200,dev server(20100/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/admin、user→/api/service/user、open→/api/service/open);服务端口统一 10100(dev/prod/test);DB admin_menu 权限映射前缀同步;前端 vite 代理与 api 文件同步适配(全 POST + 动作式路径),路由回归 29 条一致
|
||||
2026-08-26 | CHG | 遗留 /v1 全移除(URL 前缀 /admin/v1→/admin、/api/v1→/api、/api/open/v1→/api/open);全部接口改 POST(5 组 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/log(package 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/login(api+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/hello、api/open、common/tools 及 controller/hello、controller/open;API 目录去掉 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
126
AGENTS.md
@ -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 |
|
||||
| 数据库 | MySQL(ORM 由 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 管理端 API(AdminAuth + X-Permission)
|
||||
│ └── open/v1/ # /api/open/v1 开放接口(给前端调用,公开无鉴权)
|
||||
│ └── tools/ # 工具子功能契约,每子功能一目录:tools/<name>/<name>.go
|
||||
├── common/ # 公共可复用模块(不依赖 internal,可独立抽取成库)
|
||||
│ └── tools/ # 工具模块,按子功能分包(见下文)
|
||||
├── internal/
|
||||
│ ├── cmd/ # 启动引导(路由分组注册在此)
|
||||
│ ├── consts/ # 错误码与常量
|
||||
│ ├── controller/ # API→service 适配层(不写业务;含 open/ 开放接口实现)
|
||||
│ ├── service/ # 领域用例(业务逻辑直接写这里,不用 logic/)
|
||||
│ ├── dao/ # gf gen dao 生成,禁止手改
|
||||
│ ├── model/ # entity/ do/ dto/ vo/(entity、do 生成,禁止手改)
|
||||
│ ├── middleware/ # 路由中间件
|
||||
│ ├── library/ # jwt / page / response 等内部基础件
|
||||
│ └── table/ # 表列名常量
|
||||
├── manifest/ # config.dev/test/prod.yaml、sql 迁移
|
||||
├── docs/change-log/ # 每次请求与变更的记录(重要!见「上下文记忆」)
|
||||
└── utility/ # (预留)跨切面辅助
|
||||
```
|
||||
|
||||
## 后台管理 API(api/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/*`(uuid、md5、random、time、ip),底层复用 `common/tools` Go 包。
|
||||
- **新增子功能三步**:① `api/open/v1/tools/<name>/<name>.go` 写 Req/Res(g.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`(无 gcrypto);UUID 在 `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. **中文优先**:与用户的思考、输出、交流,能中文尽量中文。
|
||||
@ -1,55 +1,27 @@
|
||||
# 生产目录结构
|
||||
# Production layout
|
||||
|
||||
```text
|
||||
service.xpcool.com/
|
||||
├── api/ # HTTP 契约与 Swagger 元数据(按业务分组)
|
||||
│ ├── admin/ # /api/service/admin - 管理端 API(AdminAuth + 权限码)
|
||||
│ │ ├── admin/ # 管理员账号 CRUD、登录/资料/权限码/刷新/登出
|
||||
│ │ └── system/ # 登录日志、菜单路由、菜单管理、角色
|
||||
│ ├── house/ # /api/service/admin - 看房模块(小区/房源/看板/成交/预售证)
|
||||
│ ├── recruitment/ # /api/service/admin - 招聘考试聚合模块
|
||||
│ ├── notice/ # /api/service/admin - 站内通知(渠道/规则/日志)
|
||||
│ ├── job/ # /api/service/admin - 自动任务(任务/日志)
|
||||
│ ├── serversecurity/ # 服务器安全日志(open 上报 + admin 查询/统计)
|
||||
│ ├── user/ # /api/service/user - 用户端认证(登录/刷新)
|
||||
│ └── open/ # /api/service/open - 开放接口(公开、无鉴权)
|
||||
│ └── tools/ # 工具子功能契约,每子功能一目录:tools/<name>/<name>.go
|
||||
├── common/ # 公共可复用模块(不依赖 internal/)
|
||||
│ └── tools/ # 工具集:md5、cryptox、uuid、random、timex、
|
||||
│ # convertx、strx、slicex、ip、filex
|
||||
├── api/ # HTTP contracts and Swagger metadata
|
||||
│ ├── user/v1/ # /api/v1 - client-facing API
|
||||
│ └── admin/v1/ # /admin/v1 - administration API
|
||||
├── internal/
|
||||
│ ├── cmd/ # 启动引导、环境变量注入、路由分组注册
|
||||
│ ├── consts/ # 业务错误码与常量
|
||||
│ ├── controller/ # API→service 适配层(不写业务;含 open/ 开放接口实现)
|
||||
│ ├── service/ # 领域用例(业务逻辑直接写这里,不用 logic/)
|
||||
│ ├── dao/ # 数据访问(gf gen dao 生成 + 部分手写,禁止手改生成物)
|
||||
│ │ └── internal/ # 生成代码内部实现
|
||||
│ ├── cmd/ # application bootstrap and route isolation
|
||||
│ ├── consts/ # application error codes and constants
|
||||
│ ├── controller/ # API-to-service adapters only
|
||||
│ ├── service/ # domain use cases and provider interfaces
|
||||
│ ├── dao/ # generated by gf gen dao; never hand edited
|
||||
│ ├── model/
|
||||
│ │ ├── entity/ # 数据库实体(生成 + 部分手写)
|
||||
│ │ ├── do/ # Data Object(写库必须用 DO)
|
||||
│ │ ├── dto/ # 服务边界入参/出参
|
||||
│ │ └── vo/ # API 视图模型
|
||||
│ ├── middleware/ # 路由中间件:Recover、CORS、认证、全量加密
|
||||
│ ├── library/ # jwt / crypto(RSA+AES) / iploc / page / response 基础件
|
||||
│ └── table/ # 表列名常量(手写维护,供代码引用列名)
|
||||
├── docs/
|
||||
│ ├── house-system-design.md # 看房系统总设计文档
|
||||
│ └── change-log/ # 每次请求与变更的记录(AI 上下文记忆,随 git 提交)
|
||||
│ │ ├── entity/ # generated database entities
|
||||
│ │ ├── do/ # generated Data Objects
|
||||
│ │ ├── dto/ # service boundary input/output
|
||||
│ │ └── vo/ # API view models
|
||||
│ ├── middleware/ # configurable route-group middleware
|
||||
│ └── library/ # JWT, response, pagination primitives
|
||||
├── manifest/
|
||||
│ ├── config/ # config.dev/test/prod.yaml(config.yaml 不入库)
|
||||
│ ├── sql/ # 有序 MySQL 迁移脚本(含 recruitment/ 子模块)
|
||||
│ ├── deploy/ # kustomize 部署清单
|
||||
│ ├── docker/ # Docker 构建上下文
|
||||
│ ├── i18n/ # 国际化资源
|
||||
│ └── protobuf/ # protobuf 定义
|
||||
├── deploy/
|
||||
│ └── Dockerfile # 生产容器镜像(alpine 最小运行时)
|
||||
├── hack/ # gf CLI 配置与 Makefile 片段
|
||||
└── utility/ # (预留)跨切面辅助
|
||||
│ ├── config/ # config.dev/test/prod.yaml
|
||||
│ └── sql/ # ordered MySQL migrations
|
||||
└── utility/ # optional cross-cutting helpers
|
||||
```
|
||||
|
||||
`dao`、`model/do`、`model/entity` 由迁移脚本生成,保证与 MySQL 结构不漂移;
|
||||
但招聘 / 通知 / 自动任务 / 服务器安全日志等模块的部分结构为手写,改动时勿被生成命令覆盖。
|
||||
控制器不访问 DAO,只有 service 可以。
|
||||
|
||||
面向 AI 助手的约定与上下文记忆体系见 [`AGENTS.md`](./AGENTS.md)。
|
||||
`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.
|
||||
|
||||
62
README.MD
62
README.MD
@ -1,64 +1,22 @@
|
||||
# service.xpcool.com — 个人多客户端后端服务
|
||||
# Personal multi-client service
|
||||
|
||||
GoFrame v2 单体应用,为 mini / h5 / app 多端提供后端服务,并附带完整的后台管理体系。
|
||||
## Layout
|
||||
|
||||
核心业务:用户认证(JWT)、看房数据(小区/房源/成交/预售证)、招聘考试聚合、
|
||||
站内通知、服务器安全日志;后台管理(RBAC 菜单权限 + 操作审计)。
|
||||
`api → internal/controller → internal/service → internal/dao → internal/model(entity/do)` is enforced. DTO and VO live in `internal/model/dto` and `internal/model/vo`; neither API types nor entities cross that boundary.
|
||||
|
||||
> 面向 AI 助手的项目上下文见 [`AGENTS.md`](./AGENTS.md);每次变更的结构化记录见
|
||||
> [`docs/change-log/`](./docs/change-log/);目录树见 [`PROJECT_STRUCTURE.md`](./PROJECT_STRUCTURE.md)。
|
||||
## Database model generation
|
||||
|
||||
## 分层约定
|
||||
|
||||
调用链强制为 `api → internal/controller → internal/service → internal/dao → internal/model(entity/do)`。
|
||||
|
||||
- `controller` 只做 API 到 service 的适配,**不碰 dao**;
|
||||
- 跨层出入参走 `internal/model/dto` 与 `internal/model/vo`,API 类型与 entity 不得越界;
|
||||
- 数据库操作必须用 DO 对象(`internal/model/do`),禁止 `g.Map`;
|
||||
- 错误一律用 `gerror`(保留堆栈),响应统一走 `internal/library/response`。
|
||||
|
||||
## 路由分组
|
||||
|
||||
三组路由在 `internal/cmd/cmd.go` 中注册:
|
||||
|
||||
| 前缀 | 用途 | 鉴权 |
|
||||
|---|---|---|
|
||||
| `/api/service/open` | 开放接口(前端直接调用) | 公开,无鉴权 |
|
||||
| `/api/service/user` | 用户端接口 | 登录公开,其余走 `UserAuth` |
|
||||
| `/api/service/admin` | 管理端接口 | `AdminAuth` + 按「方法+路径」自动匹配权限码 |
|
||||
|
||||
管理端权限由后端从 `admin_menu`(type=2 行,`path` 存 `"METHOD /路径"`)反查,
|
||||
**前端无需传 `X-Permission`**;未配置映射的接口一律拒绝。
|
||||
|
||||
## 数据库模型生成
|
||||
|
||||
先在 MySQL 上执行 `manifest/sql/` 下的迁移脚本,再设置 `DB_DSN`,然后运行:
|
||||
Run `manifest/sql/001_core.sql` on MySQL, set `DB_DSN`, then run:
|
||||
|
||||
```powershell
|
||||
gf gen dao -p internal -g default -gt -c
|
||||
```
|
||||
|
||||
该命令是 `internal/dao`、`internal/model/do`、`internal/model/entity` 的**唯一来源**,生成物禁止手改。
|
||||
The command is intentionally the only source of `internal/dao`, `internal/model/do`, and `internal/model/entity`.
|
||||
|
||||
注意:部分模块(招聘、通知、自动任务、服务器安全日志)的表结构为**手写** entity/do/dao,
|
||||
未走 `gf gen`,改动时请勿用生成命令覆盖。
|
||||
## API isolation
|
||||
|
||||
## 本地启动
|
||||
- `/api/v1/*`: user API, terminal header/body supports `mini`, `h5`, and `app`.
|
||||
- `/admin/v1/*`: admin API. Protected endpoints require both an admin access token and an `X-Permission` identifier.
|
||||
|
||||
环境变量按 `GF_GCFG_FILE` 切换配置文件(`config.dev.yaml` / `config.test.yaml` / `config.prod.yaml`)。
|
||||
|
||||
- **终端**:`source .env.dev && go run main.go`
|
||||
(`.env.dev` 不入库,保存本地 DSN 与密钥)。
|
||||
- **GoLand**:运行配置 `.run/service-dev.run.xml`(项目级,重载项目后自动识别)
|
||||
已注入 `GF_GCFG_FILE` / `DB_DSN` / `JWT_SECRET`,在运行下拉框中选择即可。
|
||||
该文件含数据库口令,已加入 `.gitignore`。
|
||||
|
||||
开发环境下,若 `${DB_DSN}` / `${RECRUITMENT_DB_DSN}` / `${JWT_SECRET}` 占位符未被环境变量注入,
|
||||
`injectEnv` 会回填本地默认值,保证「零配置可启动」;生产(`GF_GCFG_ENV=prod`)不兜底,配置缺失显性报错。
|
||||
|
||||
## 构建与测试
|
||||
|
||||
```powershell
|
||||
go build ./...
|
||||
go test ./...
|
||||
```
|
||||
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.
|
||||
|
||||
@ -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#密码长度不能少于 6 位"`
|
||||
EncryptedKey string `json:"encryptedKey"`
|
||||
EncryptedData string `json:"encryptedData"`
|
||||
Nickname string `json:"nickname"`
|
||||
BarkDeviceId string `json:"barkDeviceId"`
|
||||
PushplusToken string `json:"pushplusToken"`
|
||||
RoleIds []uint64 `json:"roleIds"`
|
||||
}
|
||||
|
||||
// AdminCreateRes 是 AdminCreateReq 的响应。
|
||||
type AdminCreateRes struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
// AdminUpdateReq 更新管理员的资料、状态与角色。
|
||||
type AdminUpdateReq struct {
|
||||
g.Meta `path:"/admin/update/{id}" method:"post" tags:"Admin/Admin" summary:"更新管理员"`
|
||||
Id uint64 `json:"id" in:"path" v:"required"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status int `json:"status"`
|
||||
BarkDeviceId string `json:"barkDeviceId"`
|
||||
PushplusToken string `json:"pushplusToken"`
|
||||
RoleIds []uint64 `json:"roleIds"`
|
||||
}
|
||||
|
||||
// AdminUpdateRes 是 AdminUpdateReq 的响应。
|
||||
type AdminUpdateRes struct{}
|
||||
|
||||
// AdminResetPwdReq 重置管理员密码。
|
||||
// 密码采用「RSA + AES-GCM」混合加密传输:encryptedKey/encryptedData 必填(生产强制),
|
||||
// password 明文字段仅当服务端 encrypt.allowPlain=true(本地联调)时可用。
|
||||
type AdminResetPwdReq struct {
|
||||
g.Meta `path:"/admin/resetPwd/{id}" method:"post" tags:"Admin/Admin" summary:"重置管理员密码"`
|
||||
Id uint64 `json:"id" in:"path" v:"required"`
|
||||
Password string `json:"password" v:"min-length:6#密码长度不能少于 6 位"`
|
||||
EncryptedKey string `json:"encryptedKey"`
|
||||
EncryptedData string `json:"encryptedData"`
|
||||
}
|
||||
|
||||
// AdminResetPwdRes 是 AdminResetPwdReq 的响应。
|
||||
type AdminResetPwdRes struct{}
|
||||
|
||||
// AdminDeleteReq 删除管理员(软删除)。
|
||||
type AdminDeleteReq struct {
|
||||
g.Meta `path:"/admin/delete/{id}" method:"post" tags:"Admin/Admin" summary:"删除管理员"`
|
||||
Id uint64 `json:"id" in:"path" v:"required"`
|
||||
}
|
||||
|
||||
// AdminDeleteRes 是 AdminDeleteReq 的响应。
|
||||
type AdminDeleteRes struct{}
|
||||
@ -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 必填,生产强制):
|
||||
// - encryptedKey:RSA-OAEP 加密的 AES-256 会话密钥(base64)
|
||||
// - encryptedData:AES-GCM 加密的明文载荷 {username,password,ts}(base64,nonce 前缀)
|
||||
//
|
||||
// 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{}
|
||||
@ -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"`
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
@ -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#类型必须为 1 或 2"`
|
||||
Path string `json:"path"`
|
||||
Component string `json:"component"`
|
||||
Permission string `json:"permission" v:"required"`
|
||||
Sort int `json:"sort"`
|
||||
Status int `json:"status"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
// MenuCreateRes 是 MenuCreateReq 的响应。
|
||||
type MenuCreateRes struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
// MenuUpdateReq 更新菜单或按钮节点。
|
||||
type MenuUpdateReq struct {
|
||||
g.Meta `path:"/system/menu/update/{id}" method:"post" tags:"Admin/System/Menu" summary:"更新菜单"`
|
||||
Id uint64 `json:"id" in:"path" v:"required"`
|
||||
ParentId uint64 `json:"parentId"`
|
||||
Name string `json:"name" v:"required"`
|
||||
Icon string `json:"icon"`
|
||||
Type int `json:"type" v:"in:1,2#类型必须为 1 或 2"`
|
||||
Path string `json:"path"`
|
||||
Component string `json:"component"`
|
||||
Permission string `json:"permission" v:"required"`
|
||||
Sort int `json:"sort"`
|
||||
Status int `json:"status"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
// MenuUpdateRes 是 MenuUpdateReq 的响应。
|
||||
type MenuUpdateRes struct{}
|
||||
|
||||
// MenuDeleteReq 删除菜单节点(软删除)。
|
||||
type MenuDeleteReq struct {
|
||||
g.Meta `path:"/system/menu/delete/{id}" method:"post" tags:"Admin/System/Menu" summary:"删除菜单"`
|
||||
Id uint64 `json:"id" in:"path" v:"required"`
|
||||
}
|
||||
|
||||
// MenuDeleteRes 是 MenuDeleteReq 的响应。
|
||||
type MenuDeleteRes struct{}
|
||||
@ -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
15
api/admin/v1/auth.go
Normal 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
15
api/hello/hello.go
Normal 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
12
api/hello/v1/hello.go
Normal 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"`
|
||||
}
|
||||
@ -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{}
|
||||
@ -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"`
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
// Package job_v1 自动任务管理模块接口契约(DB 驱动的 gcron 调度 + 运行记录)。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含参数;入参一律 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"`
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
// Package notice_v1 通知模块接口契约(统一推送管理:渠道/规则/日志/测试)。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含参数;入参一律 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"`
|
||||
}
|
||||
@ -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
|
||||
@ -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"` // 是否为内网/私网地址
|
||||
}
|
||||
@ -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#待摘要文本不能为空"`
|
||||
}
|
||||
|
||||
// MD5Res 是 MD5Req 的响应。
|
||||
type MD5Res struct {
|
||||
MD5 string `json:"md5"`
|
||||
}
|
||||
@ -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#长度必须在 1-128 之间"`
|
||||
Type string `json:"type" d:"alnum" v:"in:alnum,digits,letters#不支持的随机类型"`
|
||||
}
|
||||
|
||||
// RandomRes 是 RandomReq 的响应。
|
||||
type RandomRes struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
@ -1,191 +0,0 @@
|
||||
// Package recruitment_v1 招聘考试聚合模块接口契约。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含任何参数(查询/路径参数均禁止);入参一律走 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{}
|
||||
@ -1,91 +0,0 @@
|
||||
// Package serversecurity_v1 服务器安全监控日志模块接口契约。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含任何参数(查询/路径参数均禁止);入参一律走 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"` // 按类型分布
|
||||
}
|
||||
@ -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#登录方式不能为空|不支持的登录方式"`
|
||||
Code string `json:"code"` // 微信授权码
|
||||
Mobile string `json:"mobile"` // 手机号(mobile 登录方式)
|
||||
VerifyCode string `json:"verifyCode"` // 短信验证码
|
||||
Account string `json:"account"` // 账号(password 登录方式)
|
||||
Password string `json:"password"` // 密码
|
||||
Terminal string `json:"terminal" v:"required|in:mini,h5,app#终端类型不能为空|无效的终端类型"`
|
||||
}
|
||||
|
||||
// LoginRes 用户登录响应。
|
||||
type LoginRes struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
ExpiresIn int64 `json:"expiresIn"`
|
||||
UserID uint64 `json:"userId"`
|
||||
}
|
||||
|
||||
// RefreshReq 使用刷新令牌轮换用户令牌对。
|
||||
type RefreshReq struct {
|
||||
g.Meta `path:"/auth/refresh" method:"post" tags:"User/Auth" summary:"轮换用户令牌"`
|
||||
RefreshToken string `json:"refreshToken" v:"required"`
|
||||
}
|
||||
|
||||
// RefreshRes 是 RefreshReq 的响应。
|
||||
type RefreshRes LoginRes
|
||||
@ -1,6 +0,0 @@
|
||||
// Package user_login 用户端登录契约。
|
||||
//
|
||||
// 说明:当前用户端登录已统一收敛到 `api/user/auth`(支持微信 / 手机号 / 密码三种方式),
|
||||
// user 路由组直接绑定 Auth 控制器即可,因此本包暂无端点定义。
|
||||
// 保留该包是为了维持目录结构一致性,后续如需拆分「扫码登录」「短信登录」等独立契约,可在此扩展。
|
||||
package user_login
|
||||
25
api/user/v1/auth.go
Normal file
25
api/user/v1/auth.go
Normal 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
|
||||
@ -1,10 +0,0 @@
|
||||
// Package common 是 service.xpcool.com 的公共可复用模块。
|
||||
//
|
||||
// 仅包含与 internal/ 无关的代码——此处的内容可以
|
||||
// 在仓库内跨服务共享,或日后抽取为独立库,
|
||||
// 均无需改动业务代码。
|
||||
//
|
||||
// 当前结构:
|
||||
//
|
||||
// common/tools 公共工具集(md5、cryptox、uuid 等)
|
||||
package common
|
||||
@ -1,64 +0,0 @@
|
||||
// Package convertx 提供带默认值兜底的类型转换工具,
|
||||
// 基于 gconv 构建。
|
||||
//
|
||||
// 注意:gconv 转换失败时静默返回零值,
|
||||
// 因此本工具仅对 nil / 空字符串输入回退到默认值。
|
||||
// 需要严格转换时请传入已校验的数据。
|
||||
package convertx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// ToInt 将 v 转为 int,v 为 nil 或空字符串时返回 def。
|
||||
func ToInt(v any, def int) int {
|
||||
if isEmpty(v) {
|
||||
return def
|
||||
}
|
||||
return gconv.Int(v)
|
||||
}
|
||||
|
||||
// ToInt64 将 v 转为 int64,v 为 nil 或空字符串时返回 def。
|
||||
func ToInt64(v any, def int64) int64 {
|
||||
if isEmpty(v) {
|
||||
return def
|
||||
}
|
||||
return gconv.Int64(v)
|
||||
}
|
||||
|
||||
// ToFloat64 将 v 转为 float64,v 为 nil 或空字符串时返回 def。
|
||||
func ToFloat64(v any, def float64) float64 {
|
||||
if isEmpty(v) {
|
||||
return def
|
||||
}
|
||||
return gconv.Float64(v)
|
||||
}
|
||||
|
||||
// ToString 将 v 转为 string,v 为 nil 时返回 def。
|
||||
func ToString(v any, def string) string {
|
||||
if v == nil {
|
||||
return def
|
||||
}
|
||||
return gconv.String(v)
|
||||
}
|
||||
|
||||
// ToBool 将 v 转为 bool,v 为 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
|
||||
}
|
||||
@ -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-ECB(PKCS5 填充)加密,
|
||||
// 返回 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
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
// Package tools 是项目的公共工具集。
|
||||
//
|
||||
// 每个子包都是对 GoFrame 内置组件的薄封装,
|
||||
// (gmd5、gaes、gdes、guid、grand、gtime、gconv、gstr、gfile 等),
|
||||
// 保持小巧且与框架风格一致。
|
||||
//
|
||||
// 结构:
|
||||
//
|
||||
// common/tools/md5 MD5 摘要工具
|
||||
// common/tools/cryptox AES/DES 加解密(base64 输出)
|
||||
// common/tools/uuid 唯一 ID 生成
|
||||
// common/tools/random 随机数与随机字符串
|
||||
// common/tools/timex 时间格式化与计算
|
||||
// common/tools/convertx 类型转换(带默认值兜底)
|
||||
// common/tools/strx 字符串 / 命名 / 脱敏工具
|
||||
// common/tools/slicex 泛型切片工具
|
||||
// common/tools/ip IP 地址工具
|
||||
// common/tools/filex 文件系统工具
|
||||
//
|
||||
// 规则:
|
||||
// - 禁止依赖 internal/ —— 本模块必须保持自包含。
|
||||
// - 优先复用 GoFrame 内置组件,避免重复造轮子。
|
||||
// - 每个工具保持小巧,并补充中文文档注释。
|
||||
package tools
|
||||
@ -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)
|
||||
}
|
||||
@ -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 地址
|
||||
// (含私有网段、回环地址、链路本地地址)。
|
||||
func IsInternal(s string) bool {
|
||||
parsed := net.ParseIP(s)
|
||||
if parsed == nil || parsed.To4() == nil {
|
||||
return false
|
||||
}
|
||||
return parsed.IsPrivate() || parsed.IsLoopback() || parsed.IsLinkLocalUnicast()
|
||||
}
|
||||
|
||||
// ToLong 将 IPv4 字符串转为 uint32 数值表示
|
||||
// (大端序,与 inet_aton 一致)。
|
||||
func ToLong(s string) (uint32, error) {
|
||||
ipv4 := net.ParseIP(s).To4()
|
||||
if ipv4 == nil {
|
||||
return 0, gerror.Newf(`invalid IPv4 address: %s`, s)
|
||||
}
|
||||
return uint32(ipv4[0])<<24 | uint32(ipv4[1])<<16 | uint32(ipv4[2])<<8 | uint32(ipv4[3]), nil
|
||||
}
|
||||
|
||||
// ToString 将 uint32 的 IPv4 值转为点分十进制字符串。
|
||||
func ToString(v uint32) string {
|
||||
return strconv.Itoa(int(v>>24)) + "." +
|
||||
strconv.Itoa(int(v>>16&0xFF)) + "." +
|
||||
strconv.Itoa(int(v>>8&0xFF)) + "." +
|
||||
strconv.Itoa(int(v&0xFF))
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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 的随机字母数字 ID(n <= 0 时默认为 8 位),
|
||||
// 适合短邀请码 / 追踪 ID 场景。
|
||||
func Short(n int) string {
|
||||
if n <= 0 {
|
||||
n = 8
|
||||
}
|
||||
return grand.S(n)
|
||||
}
|
||||
@ -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"]
|
||||
@ -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` — 唯一 ID(New 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 8fdb25e,35 文件)
|
||||
|
||||
- `manifest/sql/003_schema_ext.sql`:admin_menu 加 `icon/component/hidden` 列
|
||||
- `manifest/sql/004_seed.sql`:初始 admin/admin123、super_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/` 加入 .gitignore;config.dev.yaml logger.path=log(日志落盘)
|
||||
|
||||
### 决策与理由
|
||||
|
||||
- **接口鉴权用路径映射而非 X-Permission**:原实现用户可用自己拥有的任意权限码访问任意受保护接口(越权漏洞);改为后端按 method+path 查 admin_menu 映射,未配置即拒绝。
|
||||
- **受保护接口分三层**:公开 login;AdminAuthOnly(info/codes/routes,登录即可取,vben 登录后立即调用);AdminAuth(RBAC/日志)。
|
||||
- **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.com,vben 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` 字段映射(adminId→userId、nickname→realName、homePath)
|
||||
- `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:40):API 层 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 update),path 置空不映射。
|
||||
|
||||
### 待办与风险
|
||||
|
||||
- 前端 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,/api→8000)、`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-next(t-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 启动后联调验证。
|
||||
@ -1,69 +0,0 @@
|
||||
# 变更日志 — 2026-09-13
|
||||
|
||||
## 请求
|
||||
|
||||
对整个项目做一次整理:更新陈旧的文档,凡能用中文的地方(注释、文档、提示语、错误信息)一律中文化。
|
||||
|
||||
## 变更
|
||||
|
||||
### 文档
|
||||
|
||||
- `README.MD` — **重写**。原文为英文且路由信息陈旧(写的是 `/api/v1`)。现改为中文,并修正为实际的三组路由
|
||||
(`/api/service/open`、`/api/service/user`、`/api/service/admin`),补充分层约定、RBAC 权限映射机制、
|
||||
`gf gen dao` 生成流程、本地启动方式(`.env.dev` / GoLand 运行配置)、构建测试命令。
|
||||
- `PROJECT_STRUCTURE.md` — **重写**。原文为英文且目录树过时(缺 house / recruitment / notice / job / serversecurity
|
||||
等模块)。现改为中文,并按实际结构补全 `api/`、`internal/`、`docs/`、`manifest/` 各层说明,
|
||||
标注「部分模块 entity/do/dao 为手写,勿被生成命令覆盖」这一关键事实。
|
||||
- `hack/hack.mk`、`hack/hack-cli.mk` — 构建脚本注释全部中文化。
|
||||
|
||||
### 源码注释与提示
|
||||
|
||||
- `common/doc.go`、`common/tools/doc.go` — 工具清单由英文改为中文,规则说明中文化。
|
||||
- `common/tools/ip/ip.go` — `IsInternal` / `ToLong` 注释中文化。
|
||||
- `main.go` — MySQL 驱动引入注释中文化。
|
||||
- `internal/cmd/cmd.go` — 路由分组注册处的英文注释中文化(open / user / admin 三处)。
|
||||
- `api/user/login/login.go` — 补充中文包文档,说明该包当前无端点、登录已收敛至 `api/user/auth`。
|
||||
|
||||
### 校验与错误提示中文化
|
||||
|
||||
- **API 层校验消息**(`v:"...#提示"` 中文提示):
|
||||
`api/user/auth/auth.go`(登录方式、终端类型)、`api/open/tools/md5`、`api/open/tools/random`、
|
||||
`api/admin/system/menu_manage`(type)、`api/admin/admin/admin`(密码长度)。
|
||||
- **service 层错误上下文与用户提示**(`gerror.Wrap` / `response.Error`),共涉及 19 个文件、逾 90 处:
|
||||
- `service/admin/admin/admin`、`service/admin/admin/login`、`service/admin/system/{role,menu,menu_manage,login_log}`
|
||||
- `service/user/auth`
|
||||
- `service/house/{community,listing,dashboard,transaction,presale}`
|
||||
- `service/notice`、`service/job`、`service/serversecurity`
|
||||
- `service/recruitment/{recruitment,crawler}`
|
||||
- `internal/library/jwt/jwt.go`、`internal/controller/admin/admin.go`
|
||||
- 模式统一:`gerror.Wrap(err, "query list")` → `gerror.Wrap(err, "查询列表失败")`;
|
||||
用户可见提示如 `"username or password incorrect"` → `"用户名或密码错误"`。
|
||||
- 各 service 的 `panic("Xxx implementation not registered")` → `panic("Xxx 实现未注册")`。
|
||||
|
||||
### 变更记录
|
||||
|
||||
- 新增 `docs/change-log/2026-09-13.md`(本文)。
|
||||
- `.workbuddy/memory/CHANGELOG.md` 顶部追加当日条目。
|
||||
|
||||
## 决策与理由
|
||||
|
||||
- **`README.MD` / `PROJECT_STRUCTURE.md` 采用重写而非增量修补**:两份文档原文均为英文,
|
||||
且路由前缀、目录树与当前代码差异过大(路由从 `/api/v1` 演进为 `/api/service/*` 三分组;
|
||||
新增 house/recruitment/notice/job/serversecurity 五大模块),增量改会造成前后矛盾,故整体重写。
|
||||
- **生成代码(`internal/model/entity`、`internal/model/do`、`internal/dao`)的英文注释保持原样**:
|
||||
这些文件由 `gf gen dao` 生成,头部含 `DO NOT EDIT` 标记,手工中文化会在下次生成时被覆盖,
|
||||
反而制造噪音。符合 AGENTS.md「生成代码禁止手改」的约定。
|
||||
- **错误信息中文化的范围界定**:
|
||||
- 用户可见提示(`response.Error`)必须中文——直接展示给前端/用户;
|
||||
- 内部错误上下文(`gerror.Wrap` 的 message)也一并中文——便于运维/日志排查时统一语义;
|
||||
- 日志文案(`g.Log().Errorf` 等)同步中文,保持日志可读性一致。
|
||||
- **保留英文的技术标识不动**:HTTP 方法名、SQL 关键字、表名/列名、字段名、`cron` 表达式、
|
||||
第三方接口名(Bark / pushplus)、`panic` 中的类型名等属于技术符号,翻译反而降低可检索性。
|
||||
|
||||
## 待办与风险
|
||||
|
||||
- 本次仅做「文档整理 + 中文化」,**未改变任何业务逻辑**;所有改动均为注释、文档与字符串字面量。
|
||||
- `go build ./...` 因当前环境无法访问 `proxy.golang.org`(模块下载超时)未能完整跑通;
|
||||
已用 `read_lints` 对改动目录做静态检查,未发现语法/类型错误。
|
||||
建议在有网络的环境执行一次 `go build ./...` 做最终确认。
|
||||
- 后续新增代码请遵循「中文注释」约定;`gf gen dao` 重新生成后,entity/do/dao 的英文注释属预期现象,无需处理。
|
||||
@ -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 5(web-tdesign)+ echarts@6 + vxe-table@4 | 全部现成依赖,零新增 |
|
||||
| 地图 | 腾讯地图 GL JS(合规)+ DataV GeoJSON | 底图合规、支持 MultiMarker/热力/多边形 |
|
||||
| 采集分析 | Python(Scrapy/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` 分列。**趋势分析命脉,长期保留 1–2 年**。
|
||||
|
||||
### 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 方案定稿 | 1–2 天 | 建表 + gf gen dao + Python 骨架 |
|
||||
| 1 MVP | ~1 周 | 贝壳二手房挂牌 + 价格快照;房源列表/详情 API;列表 + 趋势图 |
|
||||
| 2 分析 | ~1 周 | 成交/网签 + 软关联对比 + 地图热力 + 楼盘对比 |
|
||||
| 3 决策 | ~1 周 | 新房备案价 + 学区/配套 + 偏好打分 |
|
||||
| 4 自动化 | ~1 周 | Bark 推送 + 迁腾讯云 7×24 |
|
||||
|
||||
## 12. 合规与风险
|
||||
|
||||
- 地图:仅腾讯/高德/百度/天地图;区县边界用 DataV 审图号数据;key 走代理不外泄;不采集他人个人位置
|
||||
- 爬虫:遵守 robots、低频、代理池、只存公开信息、个人自用
|
||||
- **最大风险**:贵阳网签/成交数据公开程度不如一线,MVP 用贝壳成交频道兜底
|
||||
7
go.mod
7
go.mod
@ -2,12 +2,7 @@ module service.xpcool.com
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
)
|
||||
|
||||
require github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
require github.com/gogf/gf/v2 v2.10.2
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@ -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=
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
|
||||
# 安装/更新到最新的 CLI 工具。
|
||||
# Install/Update to the latest CLI tool.
|
||||
.PHONY: cli
|
||||
cli:
|
||||
@set -e; \
|
||||
echo "go install github.com/gogf/gf/cmd/gf/v2@latest"; \
|
||||
go install github.com/gogf/gf/cmd/gf/v2@latest; \
|
||||
echo "GoFrame CLI 安装成功!"
|
||||
echo "GoFame CLI installed successfully!"
|
||||
|
||||
|
||||
# 检查并安装 CLI 工具。
|
||||
# Check and install CLI tool.
|
||||
.PHONY: cli.install
|
||||
cli.install:
|
||||
@set -e; \
|
||||
gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \
|
||||
echo "GoFrame CLI 未安装,开始自动安装..."; \
|
||||
echo "GoFame CLI is not installed, start proceeding auto installation..."; \
|
||||
make cli; \
|
||||
fi;
|
||||
22
hack/hack.mk
22
hack/hack.mk
@ -1,37 +1,37 @@
|
||||
.DEFAULT_GOAL := build
|
||||
|
||||
# 更新 GoFrame 及 CLI 到最新稳定版。
|
||||
# Update GoFrame and its CLI to latest stable version.
|
||||
.PHONY: up
|
||||
up: cli.install
|
||||
@gf up -a
|
||||
|
||||
# 使用 hack/config.yaml 中的配置构建二进制。
|
||||
# Build binary using configuration from hack/config.yaml.
|
||||
.PHONY: build
|
||||
build: cli.install
|
||||
@gf build -ew
|
||||
|
||||
# 解析 api 目录并生成 controller/sdk。
|
||||
# Parse api and generate controller/sdk.
|
||||
.PHONY: ctrl
|
||||
ctrl: cli.install
|
||||
@gf gen ctrl
|
||||
|
||||
# 生成 DAO/DO/Entity 的 Go 代码。
|
||||
# Generate Go files for DAO/DO/Entity.
|
||||
.PHONY: dao
|
||||
dao: cli.install
|
||||
@gf gen dao
|
||||
|
||||
# 解析当前项目 Go 文件并生成枚举文件。
|
||||
# Parse current project go files and generate enums go file.
|
||||
.PHONY: enums
|
||||
enums: cli.install
|
||||
@gf gen enums
|
||||
|
||||
# 生成 Service 接口与实现代码。
|
||||
# Generate Go files for Service.
|
||||
.PHONY: service
|
||||
service: cli.install
|
||||
@gf gen service
|
||||
|
||||
|
||||
# 构建 Docker 镜像。
|
||||
# Build docker image.
|
||||
.PHONY: image
|
||||
image: cli.install
|
||||
$(eval _TAG = $(shell git rev-parse --short HEAD))
|
||||
@ -43,13 +43,13 @@ endif
|
||||
@gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG};
|
||||
|
||||
|
||||
# 构建 Docker 镜像并自动推送到镜像仓库。
|
||||
# Build docker image and automatically push to docker repo.
|
||||
.PHONY: image.push
|
||||
image.push: cli.install
|
||||
@make image PUSH=-p;
|
||||
|
||||
|
||||
# 部署镜像与 yaml 到当前 kubectl 环境。
|
||||
# Deploy image and yaml to current kubectl environment.
|
||||
.PHONY: deploy
|
||||
deploy: cli.install
|
||||
$(eval _TAG = $(if ${TAG}, ${TAG}, develop))
|
||||
@ -64,12 +64,12 @@ deploy: cli.install
|
||||
fi;
|
||||
|
||||
|
||||
# 解析 protobuf 文件并生成 Go 代码。
|
||||
# Parsing protobuf files and generating go files.
|
||||
.PHONY: pb
|
||||
pb: cli.install
|
||||
@gf gen pb
|
||||
|
||||
# 根据数据库表生成 protobuf 文件。
|
||||
# Generate protobuf files for database tables.
|
||||
.PHONY: pbentity
|
||||
pbentity: cli.install
|
||||
@gf gen pbentity
|
||||
@ -5,169 +5,48 @@ import (
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
"github.com/gogf/gf/v2/os/gcmd"
|
||||
"github.com/gogf/gf/v2/os/genv"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
adminctl "service.xpcool.com/internal/controller/admin"
|
||||
housectl "service.xpcool.com/internal/controller/house"
|
||||
jobctl "service.xpcool.com/internal/controller/job"
|
||||
noticectl "service.xpcool.com/internal/controller/notice"
|
||||
openctl "service.xpcool.com/internal/controller/open"
|
||||
recruitmentctl "service.xpcool.com/internal/controller/recruitment"
|
||||
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"
|
||||
housecommunity "service.xpcool.com/internal/service/house/community"
|
||||
housedashboard "service.xpcool.com/internal/service/house/dashboard"
|
||||
houselisting "service.xpcool.com/internal/service/house/listing"
|
||||
housepresale "service.xpcool.com/internal/service/house/presale"
|
||||
housetransaction "service.xpcool.com/internal/service/house/transaction"
|
||||
jobsvc "service.xpcool.com/internal/service/job"
|
||||
noticesvc "service.xpcool.com/internal/service/notice"
|
||||
recruitmentsvc "service.xpcool.com/internal/service/recruitment"
|
||||
serversecuritysvc "service.xpcool.com/internal/service/serversecurity"
|
||||
userauth "service.xpcool.com/internal/service/user/auth"
|
||||
"service.xpcool.com/internal/service"
|
||||
)
|
||||
|
||||
// 开发环境默认值:任何启动方式(GoLand / 命令行 / go run)漏注入环境变量时兜底,
|
||||
// 避免 ${DB_DSN} 占位符原样传入 gdb 导致全站接口 500。
|
||||
// 生产(GF_GCFG_ENV=prod)不启用兜底,配置缺失应显性报错。
|
||||
//
|
||||
// 注意:库名以本机实际建库为准(本项目为 service / recruitment)。
|
||||
// 若你的本地库名或账号口令不同,请通过 DB_DSN / RECRUITMENT_DB_DSN 覆盖。
|
||||
const (
|
||||
devDefaultDBDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/service?loc=Local"
|
||||
devDefaultRecruitmentDSN = "mysql:service:3103ed27e98fc13b6d60e6571c4a8f76@tcp(127.0.0.1:3306)/recruitment?loc=Local"
|
||||
devDefaultJWTSecret = "dev-only-secret-not-for-production"
|
||||
)
|
||||
|
||||
// setIfEmpty 仅当 env 存在时写入配置(优先级:env > 配置文件默认值)。
|
||||
func setIfEmpty(adapter *gcfg.AdapterFile, key, envName string) {
|
||||
if v := genv.Get(envName); !v.IsEmpty() {
|
||||
_ = adapter.Set(key, v.String())
|
||||
}
|
||||
}
|
||||
|
||||
// injectEnv 手动把关键环境变量写入配置系统。
|
||||
// 注意:gf v2.10.2 起配置不再自动替换 ${ENV} 占位符,需在此显式注入,
|
||||
// 否则 config.dev.yaml 中的 ${DB_DSN}/${JWT_SECRET} 会原样传给数据库与 JWT。
|
||||
// 开发环境下若 env 缺失,回填本地默认值,保证「零配置可启动」。
|
||||
func injectEnv(ctx context.Context) {
|
||||
adapter, ok := g.Cfg().GetAdapter().(*gcfg.AdapterFile)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
isProd := genv.Get("GF_GCFG_ENV").String() == "prod"
|
||||
if !isProd {
|
||||
// 开发兜底:仅当配置项仍是未解析的 ${...} 占位符时才写默认值,
|
||||
// 不覆盖 .env.dev / GoLand 运行配置注入的真实值。
|
||||
if v, _ := adapter.Get(ctx, "database.default.link"); v != nil && gstr.Contains(gconv.String(v), "${") {
|
||||
_ = adapter.Set("database.default.link", devDefaultDBDSN)
|
||||
}
|
||||
if v, _ := adapter.Get(ctx, "database.recruitment.link"); v != nil && gstr.Contains(gconv.String(v), "${") {
|
||||
_ = adapter.Set("database.recruitment.link", devDefaultRecruitmentDSN)
|
||||
}
|
||||
if v, _ := adapter.Get(ctx, "jwt.secret"); v != nil && gstr.Contains(gconv.String(v), "${") {
|
||||
_ = adapter.Set("jwt.secret", devDefaultJWTSecret)
|
||||
}
|
||||
}
|
||||
setIfEmpty(adapter, "database.default.link", "DB_DSN")
|
||||
setIfEmpty(adapter, "jwt.secret", "JWT_SECRET")
|
||||
// 招聘模块独立数据库与 Bark 推送配置(自建 Bark 服务)。
|
||||
setIfEmpty(adapter, "database.recruitment.link", "RECRUITMENT_DB_DSN")
|
||||
setIfEmpty(adapter, "bark.baseUrl", "BARK_BASE_URL")
|
||||
setIfEmpty(adapter, "bark.deviceKey", "BARK_DEVICE_KEY")
|
||||
setIfEmpty(adapter, "bark.pushTime", "BARK_PUSH_TIME")
|
||||
// 服务器安全日志上报令牌(宿主机采集脚本携带,接口侧校验)。
|
||||
setIfEmpty(adapter, "internalToken", "INTERNAL_TOKEN")
|
||||
// 登录加密开关(直接跑二进制始终加载 config.yaml,需 env 显式注入):
|
||||
// ENCRYPT_FULL_BODY=true 启用全量请求/响应加密(生产);ENCRYPT_ALLOW_PLAIN=true 仅开发联调。
|
||||
setIfEmpty(adapter, "encrypt.fullBody", "ENCRYPT_FULL_BODY")
|
||||
setIfEmpty(adapter, "encrypt.allowPlain", "ENCRYPT_ALLOW_PLAIN")
|
||||
}
|
||||
|
||||
var (
|
||||
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()) // 开放工具接口(前端调用,免鉴权)。
|
||||
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()) // 登录与刷新令牌接口公开。
|
||||
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=true):CORS → APICrypto → Recover → HandlerResponse。
|
||||
// Recover 置于 APICrypto 内层,使 500 异常响应同样被加密;public-key 端点明文豁免。
|
||||
group.Middleware(middleware.CORS, middleware.APICrypto(cryptoSvc), middleware.Recover, ghttp.MiddlewareHandlerResponse)
|
||||
group.Bind(adminctl.NewAuth()) // 公开端点:仅登录。
|
||||
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
|
||||
},
|
||||
|
||||
@ -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+encryptedData,RSA+AES 混合加密)优先;明文仅开发 allowPlain。
|
||||
func resolvePassword(ctx context.Context, encryptedKey, encryptedData, password string) (string, error) {
|
||||
if cryptolib.Get().FullBody() {
|
||||
return password, nil
|
||||
}
|
||||
if encryptedKey != "" && encryptedData != "" {
|
||||
v, err := cryptolib.Get().DecryptField(ctx, encryptedKey, encryptedData)
|
||||
if err != nil {
|
||||
return "", response.Error(consts.CodeInvalidParam, err.Error())
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
if cryptolib.Get().AllowPlain() {
|
||||
return password, nil
|
||||
}
|
||||
return "", response.Error(consts.CodeInvalidParam, "必须提交加密凭据")
|
||||
}
|
||||
|
||||
// AdminList 分页查询管理员。
|
||||
func (c *Controller) AdminList(ctx context.Context, req *adminv1.AdminListReq) (res *adminv1.AdminListRes, err error) {
|
||||
items, total, err := admin.AdminManage().List(ctx, dto.PageQuery{Page: req.Page, Size: req.Size, Keyword: req.Keyword})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]*adminv1.AdminItem, 0, len(items))
|
||||
for _, it := range items {
|
||||
list = append(list, &adminv1.AdminItem{
|
||||
Id: it.Id, Username: it.Username, Nickname: it.Nickname, Status: it.Status,
|
||||
RoleIds: it.RoleIds, RoleNames: it.RoleNames, CreatedAt: it.CreatedAt,
|
||||
})
|
||||
}
|
||||
return &adminv1.AdminListRes{List: list, Total: total}, nil
|
||||
}
|
||||
|
||||
// AdminCreate 创建管理员。
|
||||
func (c *Controller) AdminCreate(ctx context.Context, req *adminv1.AdminCreateReq) (res *adminv1.AdminCreateRes, err error) {
|
||||
password, err := resolvePassword(ctx, req.EncryptedKey, req.EncryptedData, req.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := admin.AdminManage().Create(ctx, dto.AdminCreateInput{
|
||||
Username: req.Username, Password: password, Nickname: req.Nickname,
|
||||
BarkDeviceId: req.BarkDeviceId, PushplusToken: req.PushplusToken, RoleIds: req.RoleIds,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &adminv1.AdminCreateRes{Id: id}, nil
|
||||
}
|
||||
|
||||
// AdminUpdate 更新管理员。
|
||||
func (c *Controller) AdminUpdate(ctx context.Context, req *adminv1.AdminUpdateReq) (res *adminv1.AdminUpdateRes, err error) {
|
||||
if err = admin.AdminManage().Update(ctx, dto.AdminUpdateInput{Id: req.Id, Nickname: req.Nickname, Status: req.Status,
|
||||
BarkDeviceId: req.BarkDeviceId, PushplusToken: req.PushplusToken, RoleIds: req.RoleIds}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &adminv1.AdminUpdateRes{}, nil
|
||||
}
|
||||
|
||||
// AdminResetPwd 重置管理员密码。
|
||||
func (c *Controller) AdminResetPwd(ctx context.Context, req *adminv1.AdminResetPwdReq) (res *adminv1.AdminResetPwdRes, err error) {
|
||||
password, err := resolvePassword(ctx, req.EncryptedKey, req.EncryptedData, req.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = admin.AdminManage().ResetPassword(ctx, req.Id, password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &adminv1.AdminResetPwdRes{}, nil
|
||||
}
|
||||
|
||||
// AdminDelete 删除管理员。
|
||||
func (c *Controller) AdminDelete(ctx context.Context, req *adminv1.AdminDeleteReq) (res *adminv1.AdminDeleteRes, err error) {
|
||||
if err = admin.AdminManage().Delete(ctx, req.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &adminv1.AdminDeleteRes{}, nil
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
5
internal/controller/hello/hello.go
Normal file
5
internal/controller/hello/hello.go
Normal file
@ -0,0 +1,5 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package hello
|
||||
15
internal/controller/hello/hello_new.go
Normal file
15
internal/controller/hello/hello_new.go
Normal 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{}
|
||||
}
|
||||
13
internal/controller/hello/hello_v1_hello.go
Normal file
13
internal/controller/hello/hello_v1_hello.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
// Package house 实现看房模块的管理端点,绑定在 admin 受权限保护分组下。
|
||||
package house
|
||||
|
||||
// Controller 实现看房模块的所有端点。
|
||||
type Controller struct{}
|
||||
|
||||
// New 创建看房模块控制器。
|
||||
func New() *Controller { return &Controller{} }
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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, ¬icev1.NoticeChannelItem{Id: v.Id, Code: v.Code, Name: v.Name, Enabled: v.Enabled, Config: v.Config, Remark: v.Remark})
|
||||
}
|
||||
return ¬icev1.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 ¬icev1.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, ¬icev1.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 ¬icev1.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 ¬icev1.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 ¬icev1.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, ¬icev1.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 ¬icev1.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 ¬icev1.NoticeTestRes{Result: ok, Message: msg}, nil
|
||||
}
|
||||
@ -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{} }
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
// Package recruitment 实现招聘考试聚合模块管理端点,绑定在 admin 受权限保护分组下。
|
||||
package recruitment
|
||||
|
||||
// Controller 实现招聘考试聚合模块的所有端点。
|
||||
type Controller struct{}
|
||||
|
||||
// New 创建招聘模块控制器。
|
||||
func New() *Controller { return &Controller{} }
|
||||
@ -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
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
// Package serversecurity 服务器安全日志模块控制器。
|
||||
// 分为两个 Controller:ReportController 绑定 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()}
|
||||
)
|
||||
|
||||
// 在下方添加你的自定义方法。
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user